1package api 2 3import ( 4 "fmt" 5 "net/http" 6 "os" 7 "strings" 8 9 "github.com/ansible-semaphore/semaphore/api/projects" 10 "github.com/ansible-semaphore/semaphore/api/sockets" 11 "github.com/ansible-semaphore/semaphore/api/tasks" 12 13 "github.com/ansible-semaphore/semaphore/util" 14 "github.com/gobuffalo/packr" 15 "github.com/gorilla/mux" 16 "github.com/russross/blackfriday" 17) 18 19var publicAssets = packr.NewBox("../web/public") 20var publicAssets2 = packr.NewBox("../web2/dist") 21 22//JSONMiddleware ensures that all the routes respond with Json, this is added by default to all routes 23func JSONMiddleware(next http.Handler) http.Handler { 24 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 25 w.Header().Set("content-type", "application/json") 26 next.ServeHTTP(w, r) 27 }) 28} 29 30//plainTextMiddleware resets headers to Plain Text if needed 31func plainTextMiddleware(next http.Handler) http.Handler { 32 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 33 w.Header().Set("content-type", "text/plain; charset=utf-8") 34 next.ServeHTTP(w, r) 35 }) 36} 37 38func pongHandler(w http.ResponseWriter, r *http.Request) { 39 //nolint: errcheck 40 w.Write([]byte("pong")) 41} 42 43func notFoundHandler(w http.ResponseWriter, r *http.Request) { 44 w.Header().Set("Access-Control-Allow-Origin", r.Header.Get("Origin")) 45 w.Header().Set("Access-Control-Allow-Credentials", "true") 46 w.WriteHeader(http.StatusNotFound) 47 //nolint: errcheck 48 w.Write([]byte("404 not found")) 49 fmt.Println(r.Method, ":", r.URL.String(), "--> 404 Not Found") 50} 51 52// Route declares all routes 53func Route() *mux.Router { 54 r := mux.NewRouter() 55 r.NotFoundHandler = http.HandlerFunc(servePublic) 56 57 webPath := "/" 58 if util.WebHostURL != nil { 59 webPath = util.WebHostURL.Path 60 if !strings.HasSuffix(webPath, "/") { 61 webPath += "/" 62 } 63 } 64 65 r.Use(mux.CORSMethodMiddleware(r)) 66 67 pingRouter := r.Path(webPath + "api/ping").Subrouter() 68 pingRouter.Use(plainTextMiddleware) 69 pingRouter.Methods("GET", "HEAD").HandlerFunc(pongHandler) 70 71 publicAPIRouter := r.PathPrefix(webPath + "api").Subrouter() 72 publicAPIRouter.Use(JSONMiddleware) 73 74 publicAPIRouter.HandleFunc("/auth/login", login).Methods("POST") 75 publicAPIRouter.HandleFunc("/auth/logout", logout).Methods("POST") 76 77 authenticatedAPI := r.PathPrefix(webPath + "api").Subrouter() 78 authenticatedAPI.Use(JSONMiddleware, authentication) 79 80 authenticatedAPI.Path("/ws").HandlerFunc(sockets.Handler).Methods("GET", "HEAD") 81 authenticatedAPI.Path("/info").HandlerFunc(getSystemInfo).Methods("GET", "HEAD") 82 authenticatedAPI.Path("/upgrade").HandlerFunc(checkUpgrade).Methods("GET", "HEAD") 83 authenticatedAPI.Path("/upgrade").HandlerFunc(doUpgrade).Methods("POST") 84 85 authenticatedAPI.Path("/projects").HandlerFunc(projects.GetProjects).Methods("GET", "HEAD") 86 authenticatedAPI.Path("/projects").HandlerFunc(projects.AddProject).Methods("POST") 87 authenticatedAPI.Path("/events").HandlerFunc(getAllEvents).Methods("GET", "HEAD") 88 authenticatedAPI.HandleFunc("/events/last", getLastEvents).Methods("GET", "HEAD") 89 90 authenticatedAPI.Path("/users").HandlerFunc(getUsers).Methods("GET", "HEAD") 91 authenticatedAPI.Path("/users").HandlerFunc(addUser).Methods("POST") 92 93 authenticatedAPI.Path("/user").HandlerFunc(getUser).Methods("GET", "HEAD") 94 95 tokenAPI := authenticatedAPI.PathPrefix("/user").Subrouter() 96 tokenAPI.Path("/tokens").HandlerFunc(getAPITokens).Methods("GET", "HEAD") 97 tokenAPI.Path("/tokens").HandlerFunc(createAPIToken).Methods("POST") 98 tokenAPI.HandleFunc("/tokens/{token_id}", expireAPIToken).Methods("DELETE") 99 100 userAPI := authenticatedAPI.Path("/users/{user_id}").Subrouter() 101 userAPI.Use(getUserMiddleware) 102 103 userAPI.Methods("GET", "HEAD").HandlerFunc(getUser) 104 userAPI.Methods("PUT").HandlerFunc(updateUser) 105 userAPI.Methods("DELETE").HandlerFunc(deleteUser) 106 107 userPasswordAPI := authenticatedAPI.PathPrefix("/users/{user_id}").Subrouter() 108 userPasswordAPI.Use(getUserMiddleware) 109 userPasswordAPI.Path("/password").HandlerFunc(updateUserPassword).Methods("POST") 110 111 projectGet := authenticatedAPI.Path("/project/{project_id}").Subrouter() 112 projectGet.Use(projects.ProjectMiddleware) 113 projectGet.Methods("GET", "HEAD").HandlerFunc(projects.GetProject) 114 115 projectUserAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() 116 projectUserAPI.Use(projects.ProjectMiddleware) 117 118 projectUserAPI.Path("/events").HandlerFunc(getAllEvents).Methods("GET", "HEAD") 119 projectUserAPI.HandleFunc("/events/last", getLastEvents).Methods("GET", "HEAD") 120 121 projectUserAPI.Path("/users").HandlerFunc(projects.GetUsers).Methods("GET", "HEAD") 122 123 projectUserAPI.Path("/keys").HandlerFunc(projects.GetKeys).Methods("GET", "HEAD") 124 projectUserAPI.Path("/keys").HandlerFunc(projects.AddKey).Methods("POST") 125 126 projectUserAPI.Path("/repositories").HandlerFunc(projects.GetRepositories).Methods("GET", "HEAD") 127 projectUserAPI.Path("/repositories").HandlerFunc(projects.AddRepository).Methods("POST") 128 129 projectUserAPI.Path("/inventory").HandlerFunc(projects.GetInventory).Methods("GET", "HEAD") 130 projectUserAPI.Path("/inventory").HandlerFunc(projects.AddInventory).Methods("POST") 131 132 projectUserAPI.Path("/environment").HandlerFunc(projects.GetEnvironment).Methods("GET", "HEAD") 133 projectUserAPI.Path("/environment").HandlerFunc(projects.AddEnvironment).Methods("POST") 134 135 projectUserAPI.Path("/tasks").HandlerFunc(tasks.GetAllTasks).Methods("GET", "HEAD") 136 projectUserAPI.HandleFunc("/tasks/last", tasks.GetLastTasks).Methods("GET", "HEAD") 137 projectUserAPI.Path("/tasks").HandlerFunc(tasks.AddTask).Methods("POST") 138 139 projectUserAPI.Path("/templates").HandlerFunc(projects.GetTemplates).Methods("GET", "HEAD") 140 projectUserAPI.Path("/templates").HandlerFunc(projects.AddTemplate).Methods("POST") 141 142 projectAdminAPI := authenticatedAPI.Path("/project/{project_id}").Subrouter() 143 projectAdminAPI.Use(projects.ProjectMiddleware, projects.MustBeAdmin) 144 projectAdminAPI.Methods("PUT").HandlerFunc(projects.UpdateProject) 145 projectAdminAPI.Methods("DELETE").HandlerFunc(projects.DeleteProject) 146 147 projectAdminUsersAPI := authenticatedAPI.PathPrefix("/project/{project_id}").Subrouter() 148 projectAdminUsersAPI.Use(projects.ProjectMiddleware, projects.MustBeAdmin) 149 projectAdminUsersAPI.Path("/users").HandlerFunc(projects.AddUser).Methods("POST") 150 151 projectUserManagement := projectAdminUsersAPI.PathPrefix("/users").Subrouter() 152 projectUserManagement.Use(projects.UserMiddleware) 153 154 projectUserManagement.HandleFunc("/{user_id}", projects.GetUsers).Methods("GET", "HEAD") 155 projectUserManagement.HandleFunc("/{user_id}/admin", projects.MakeUserAdmin).Methods("POST") 156 projectUserManagement.HandleFunc("/{user_id}/admin", projects.MakeUserAdmin).Methods("DELETE") 157 projectUserManagement.HandleFunc("/{user_id}", projects.RemoveUser).Methods("DELETE") 158 159 projectKeyManagement := projectAdminUsersAPI.PathPrefix("/keys").Subrouter() 160 projectKeyManagement.Use(projects.KeyMiddleware) 161 162 projectKeyManagement.HandleFunc("/{key_id}", projects.GetKeys).Methods("GET", "HEAD") 163 projectKeyManagement.HandleFunc("/{key_id}", projects.UpdateKey).Methods("PUT") 164 projectKeyManagement.HandleFunc("/{key_id}", projects.RemoveKey).Methods("DELETE") 165 166 projectRepoManagement := projectUserAPI.PathPrefix("/repositories").Subrouter() 167 projectRepoManagement.Use(projects.RepositoryMiddleware) 168 169 projectRepoManagement.HandleFunc("/{repository_id}", projects.GetRepositories).Methods("GET", "HEAD") 170 projectRepoManagement.HandleFunc("/{repository_id}", projects.UpdateRepository).Methods("PUT") 171 projectRepoManagement.HandleFunc("/{repository_id}", projects.RemoveRepository).Methods("DELETE") 172 173 projectInventoryManagement := projectUserAPI.PathPrefix("/inventory").Subrouter() 174 projectInventoryManagement.Use(projects.InventoryMiddleware) 175 176 projectInventoryManagement.HandleFunc("/{inventory_id}", projects.GetInventory).Methods("GET", "HEAD") 177 projectInventoryManagement.HandleFunc("/{inventory_id}", projects.UpdateInventory).Methods("PUT") 178 projectInventoryManagement.HandleFunc("/{inventory_id}", projects.RemoveInventory).Methods("DELETE") 179 180 projectEnvManagement := projectUserAPI.PathPrefix("/environment").Subrouter() 181 projectEnvManagement.Use(projects.EnvironmentMiddleware) 182 183 projectEnvManagement.HandleFunc("/{environment_id}", projects.GetEnvironment).Methods("GET", "HEAD") 184 projectEnvManagement.HandleFunc("/{environment_id}", projects.UpdateEnvironment).Methods("PUT") 185 projectEnvManagement.HandleFunc("/{environment_id}", projects.RemoveEnvironment).Methods("DELETE") 186 187 projectTmplManagement := projectUserAPI.PathPrefix("/templates").Subrouter() 188 projectTmplManagement.Use(projects.TemplatesMiddleware) 189 190 projectTmplManagement.HandleFunc("/{template_id}", projects.UpdateTemplate).Methods("PUT") 191 projectTmplManagement.HandleFunc("/{template_id}", projects.RemoveTemplate).Methods("DELETE") 192 projectTmplManagement.HandleFunc("/{template_id}", projects.GetTemplate).Methods("GET") 193 projectTmplManagement.HandleFunc("/{template_id}/tasks", tasks.GetAllTasks).Methods("GET") 194 projectTmplManagement.HandleFunc("/{template_id}/tasks/last", tasks.GetLastTasks).Methods("GET") 195 196 projectTaskManagement := projectUserAPI.PathPrefix("/tasks").Subrouter() 197 projectTaskManagement.Use(tasks.GetTaskMiddleware) 198 199 projectTaskManagement.HandleFunc("/{task_id}/output", tasks.GetTaskOutput).Methods("GET", "HEAD") 200 projectTaskManagement.HandleFunc("/{task_id}", tasks.GetTask).Methods("GET", "HEAD") 201 projectTaskManagement.HandleFunc("/{task_id}", tasks.RemoveTask).Methods("DELETE") 202 203 if os.Getenv("DEBUG") == "1" { 204 defer debugPrintRoutes(r) 205 } 206 207 return r 208} 209 210func debugPrintRoutes(r *mux.Router) { 211 err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { 212 pathTemplate, err := route.GetPathTemplate() 213 if err == nil { 214 fmt.Println("ROUTE:", pathTemplate) 215 } 216 pathRegexp, err := route.GetPathRegexp() 217 if err == nil { 218 fmt.Println("Path regexp:", pathRegexp) 219 } 220 queriesTemplates, err := route.GetQueriesTemplates() 221 if err == nil { 222 fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) 223 } 224 queriesRegexps, err := route.GetQueriesRegexp() 225 if err == nil { 226 fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) 227 } 228 methods, err := route.GetMethods() 229 if err == nil { 230 fmt.Println("Methods:", strings.Join(methods, ",")) 231 } 232 fmt.Println() 233 return nil 234 }) 235 236 if err != nil { 237 fmt.Println(err) 238 } 239} 240 241//nolint: gocyclo 242func servePublic(w http.ResponseWriter, r *http.Request) { 243 webPath := "/" 244 if util.WebHostURL != nil { 245 webPath = util.WebHostURL.RequestURI() 246 } 247 248 path := r.URL.Path 249 250 if path == webPath + "api" || strings.HasPrefix(path, webPath + "api/") { 251 w.WriteHeader(http.StatusNotFound) 252 return 253 } 254 255 htmlPrefix := "" 256 publicAssetsPrefix := "" 257 if util.Config.OldFrontend { 258 htmlPrefix = "/html" 259 publicAssetsPrefix = "public" 260 } 261 262 if publicAssetsPrefix != "" && !strings.HasPrefix(path, webPath+publicAssetsPrefix) { 263 if strings.Contains(path, ".") { 264 w.WriteHeader(http.StatusNotFound) 265 return 266 } 267 268 path = htmlPrefix+"/index.html" 269 } else if !strings.Contains(path, ".") { 270 path = htmlPrefix+"/index.html" 271 } 272 273 path = strings.Replace(path, webPath+publicAssetsPrefix+"/", "", 1) 274 split := strings.Split(path, ".") 275 suffix := split[len(split)-1] 276 277 var res []byte 278 var err error 279 280 if util.Config.OldFrontend { 281 res, err = publicAssets.MustBytes(path) 282 } else { 283 res, err = publicAssets2.MustBytes(path) 284 } 285 286 if err != nil { 287 notFoundHandler(w, r) 288 return 289 } 290 291 // replace base path 292 if util.WebHostURL != nil && path == htmlPrefix+"/index.html" { 293 baseURL := util.WebHostURL.String() 294 if !strings.HasSuffix(baseURL, "/") { 295 baseURL += "/" 296 } 297 res = []byte(strings.Replace(string(res), 298 "<base href=\"/\">", 299 "<base href=\""+baseURL+"\">", 300 1)) 301 } 302 303 contentType := "text/plain" 304 switch suffix { 305 case "png": 306 contentType = "image/png" 307 case "jpg", "jpeg": 308 contentType = "image/jpeg" 309 case "gif": 310 contentType = "image/gif" 311 case "js": 312 contentType = "application/javascript" 313 case "css": 314 contentType = "text/css" 315 case "woff": 316 contentType = "application/x-font-woff" 317 case "ttf": 318 contentType = "application/x-font-ttf" 319 case "otf": 320 contentType = "application/x-font-otf" 321 case "html": 322 contentType = "text/html" 323 } 324 325 w.Header().Set("content-type", contentType) 326 _, err = w.Write(res) 327 util.LogWarning(err) 328} 329 330func getSystemInfo(w http.ResponseWriter, r *http.Request) { 331 body := map[string]interface{}{ 332 "version": util.Version, 333 "update": util.UpdateAvailable, 334 "config": map[string]string{ 335 "dbHost": util.Config.MySQL.Hostname, 336 "dbName": util.Config.MySQL.DbName, 337 "dbUser": util.Config.MySQL.Username, 338 "path": util.Config.TmpPath, 339 "cmdPath": util.FindSemaphore(), 340 }, 341 } 342 343 if util.UpdateAvailable != nil { 344 body["updateBody"] = string(blackfriday.MarkdownCommon([]byte(*util.UpdateAvailable.Body))) 345 } 346 347 util.WriteJSON(w, http.StatusOK, body) 348} 349 350func checkUpgrade(w http.ResponseWriter, r *http.Request) { 351 if err := util.CheckUpdate(util.Version); err != nil { 352 util.WriteJSON(w, 500, err) 353 return 354 } 355 356 if util.UpdateAvailable != nil { 357 getSystemInfo(w, r) 358 return 359 } 360 361 w.WriteHeader(http.StatusNoContent) 362} 363 364func doUpgrade(w http.ResponseWriter, r *http.Request) { 365 util.LogError(util.DoUpgrade(util.Version)) 366} 367