package api import ( "encoding/json" "errors" "fmt" "net/http" "runtime/debug" "github.com/gorilla/mux" "github.com/mattermost/focalboard/server/app" "github.com/mattermost/focalboard/server/model" "github.com/mattermost/focalboard/server/services/audit" "github.com/mattermost/focalboard/server/services/permissions" "github.com/mattermost/mattermost-server/v6/shared/mlog" ) const ( HeaderRequestedWith = "X-Requested-With" HeaderRequestedWithXML = "XMLHttpRequest" UploadFormFileKey = "file" True = "true" ErrorNoTeamCode = 1000 ErrorNoTeamMessage = "No team" ) var ( ErrHandlerPanic = errors.New("http handler panic") ) // ---------------------------------------------------------------------------------------------------- // REST APIs type API struct { app *app.App authService string permissions permissions.PermissionsService singleUserToken string MattermostAuth bool logger mlog.LoggerIFace audit *audit.Audit isPlugin bool } func NewAPI( app *app.App, singleUserToken string, authService string, permissions permissions.PermissionsService, logger mlog.LoggerIFace, audit *audit.Audit, isPlugin bool, ) *API { return &API{ app: app, singleUserToken: singleUserToken, authService: authService, permissions: permissions, logger: logger, audit: audit, isPlugin: isPlugin, } } func (a *API) RegisterRoutes(r *mux.Router) { apiv2 := r.PathPrefix("/api/v2").Subrouter() apiv2.Use(a.panicHandler) apiv2.Use(a.requireCSRFToken) /* ToDo: apiv3 := r.PathPrefix("/api/v3").Subrouter() apiv3.Use(a.panicHandler) apiv3.Use(a.requireCSRFToken) */ // V2 routes (ToDo: migrate these to V3 when ready to ship V3) a.registerUsersRoutes(apiv2) a.registerAuthRoutes(apiv2) a.registerMembersRoutes(apiv2) a.registerCategoriesRoutes(apiv2) a.registerSharingRoutes(apiv2) a.registerTeamsRoutes(apiv2) a.registerAchivesRoutes(apiv2) a.registerSubscriptionsRoutes(apiv2) a.registerFilesRoutes(apiv2) a.registerLimitsRoutes(apiv2) a.registerInsightsRoutes(apiv2) a.registerOnboardingRoutes(apiv2) a.registerSearchRoutes(apiv2) a.registerConfigRoutes(apiv2) a.registerBoardsAndBlocksRoutes(apiv2) a.registerChannelsRoutes(apiv2) a.registerTemplatesRoutes(apiv2) a.registerBoardsRoutes(apiv2) a.registerBlocksRoutes(apiv2) a.registerStatisticsRoutes(apiv2) // V3 routes a.registerCardsRoutes(apiv2) // System routes are outside the /api/v2 path a.registerSystemRoutes(r) } func (a *API) RegisterAdminRoutes(r *mux.Router) { r.HandleFunc("/api/v2/admin/users/{username}/password", a.adminRequired(a.handleAdminSetPassword)).Methods("POST") } func getUserID(r *http.Request) string { ctx := r.Context() session, ok := ctx.Value(sessionContextKey).(*model.Session) if !ok { return "" } return session.UserID } func (a *API) panicHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { defer func() { if p := recover(); p != nil { a.logger.Error("Http handler panic", mlog.Any("panic", p), mlog.String("stack", string(debug.Stack())), mlog.String("uri", r.URL.Path), ) a.errorResponse(w, r, ErrHandlerPanic) } }() next.ServeHTTP(w, r) }) } func (a *API) requireCSRFToken(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !a.checkCSRFToken(r) { a.logger.Error("checkCSRFToken FAILED") a.errorResponse(w, r, model.NewErrBadRequest("checkCSRFToken FAILED")) return } next.ServeHTTP(w, r) }) } func (a *API) checkCSRFToken(r *http.Request) bool { token := r.Header.Get(HeaderRequestedWith) return token == HeaderRequestedWithXML } func (a *API) hasValidReadTokenForBoard(r *http.Request, boardID string) bool { query := r.URL.Query() readToken := query.Get("read_token") if len(readToken) < 1 { return false } isValid, err := a.app.IsValidReadToken(boardID, readToken) if err != nil { a.logger.Error("IsValidReadTokenForBoard ERROR", mlog.Err(err)) return false } return isValid } func (a *API) userIsGuest(userID string) (bool, error) { if a.singleUserToken != "" { return false, nil } return a.app.UserIsGuest(userID) } // Response helpers func (a *API) errorResponse(w http.ResponseWriter, r *http.Request, err error) { errorResponse := model.ErrorResponse{Error: err.Error()} switch { case model.IsErrBadRequest(err): errorResponse.ErrorCode = http.StatusBadRequest case model.IsErrUnauthorized(err): errorResponse.ErrorCode = http.StatusUnauthorized case model.IsErrForbidden(err): errorResponse.ErrorCode = http.StatusForbidden case model.IsErrNotFound(err): errorResponse.ErrorCode = http.StatusNotFound case model.IsErrRequestEntityTooLarge(err): errorResponse.ErrorCode = http.StatusRequestEntityTooLarge case model.IsErrNotImplemented(err): errorResponse.ErrorCode = http.StatusNotImplemented default: a.logger.Error("API ERROR", mlog.Int("code", http.StatusInternalServerError), mlog.Err(err), mlog.String("api", r.URL.Path), ) errorResponse.Error = "internal server error" errorResponse.ErrorCode = http.StatusInternalServerError } setResponseHeader(w, "Content-Type", "application/json") data, err := json.Marshal(errorResponse) if err != nil { data = []byte("{}") } w.WriteHeader(errorResponse.ErrorCode) _, _ = w.Write(data) } func stringResponse(w http.ResponseWriter, message string) { setResponseHeader(w, "Content-Type", "text/plain") _, _ = fmt.Fprint(w, message) } func jsonStringResponse(w http.ResponseWriter, code int, message string) { setResponseHeader(w, "Content-Type", "application/json") w.WriteHeader(code) fmt.Fprint(w, message) } func jsonBytesResponse(w http.ResponseWriter, code int, json []byte) { setResponseHeader(w, "Content-Type", "application/json") w.WriteHeader(code) _, _ = w.Write(json) } func setResponseHeader(w http.ResponseWriter, key string, value string) { header := w.Header() if header == nil { return } header.Set(key, value) }