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