diff options
Diffstat (limited to 'web/web.go')
-rw-r--r-- | web/web.go | 366 |
1 files changed, 324 insertions, 42 deletions
diff --git a/web/web.go b/web/web.go index c79815a35..da7eff13d 100644 --- a/web/web.go +++ b/web/web.go @@ -4,19 +4,20 @@ package web import ( - "fmt" - "html/template" - "net/http" - "strconv" - "strings" - l4g "code.google.com/p/log4go" + "fmt" "github.com/gorilla/mux" "github.com/mattermost/platform/api" "github.com/mattermost/platform/model" + "github.com/mattermost/platform/store" "github.com/mattermost/platform/utils" "github.com/mssola/user_agent" "gopkg.in/fsnotify.v1" + "html/template" + "net/http" + "regexp" + "strconv" + "strings" ) var Templates *template.Template @@ -26,14 +27,12 @@ type HtmlTemplatePage api.Page func NewHtmlTemplatePage(templateName string, title string) *HtmlTemplatePage { if len(title) > 0 { - title = utils.Cfg.ServiceSettings.SiteName + " - " + title + title = utils.Cfg.TeamSettings.SiteName + " - " + title } props := make(map[string]string) - props["AnalyticsUrl"] = utils.Cfg.ServiceSettings.AnalyticsUrl - props["ProfileHeight"] = fmt.Sprintf("%v", utils.Cfg.ImageSettings.ProfileHeight) - props["ProfileWidth"] = fmt.Sprintf("%v", utils.Cfg.ImageSettings.ProfileWidth) - return &HtmlTemplatePage{TemplateName: templateName, Title: title, SiteName: utils.Cfg.ServiceSettings.SiteName, Props: props} + props["Title"] = title + return &HtmlTemplatePage{TemplateName: templateName, Props: props, ClientProps: utils.ClientProperties} } func (me *HtmlTemplatePage) Render(c *api.Context, w http.ResponseWriter) { @@ -52,31 +51,34 @@ func InitWeb() { mainrouter.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(staticDir)))) mainrouter.Handle("/", api.AppHandlerIndependent(root)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") + mainrouter.Handle("/oauth/authorize", api.UserRequired(authorizeOAuth)).Methods("GET") + mainrouter.Handle("/oauth/access_token", api.ApiAppHandler(getAccessToken)).Methods("POST") - // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") - mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(loginCompleteOAuth)).Methods("GET") - - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") - mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") - // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/channels/{channelname}", api.UserRequired(getChannel)).Methods("GET") - - // Anything added here must have an _ in it so it does not conflict with team names mainrouter.Handle("/signup_team_complete/", api.AppHandlerIndependent(signupTeamComplete)).Methods("GET") mainrouter.Handle("/signup_user_complete/", api.AppHandlerIndependent(signupUserComplete)).Methods("GET") mainrouter.Handle("/signup_team_confirm/", api.AppHandlerIndependent(signupTeamConfirm)).Methods("GET") - - // Bug in gorilla.mux prevents us from using regex here. - mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") - mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(signupCompleteOAuth)).Methods("GET") - mainrouter.Handle("/verify_email", api.AppHandlerIndependent(verifyEmail)).Methods("GET") mainrouter.Handle("/find_team", api.AppHandlerIndependent(findTeam)).Methods("GET") mainrouter.Handle("/signup_team", api.AppHandlerIndependent(signup)).Methods("GET") + mainrouter.Handle("/login/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(loginCompleteOAuth)).Methods("GET") + mainrouter.Handle("/signup/{service:[A-Za-z]+}/complete", api.AppHandlerIndependent(signupCompleteOAuth)).Methods("GET") + + mainrouter.Handle("/admin_console", api.UserRequired(adminConsole)).Methods("GET") + + mainrouter.Handle("/hooks/{id:[A-Za-z0-9]+}", api.ApiAppHandler(incomingWebhook)).Methods("POST") + + // ---------------------------------------------------------------------------------------------- + // *ANYTHING* team specific should go below this line + // ---------------------------------------------------------------------------------------------- + + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}", api.AppHandler(login)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/", api.AppHandler(login)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/login", api.AppHandler(login)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/logout", api.AppHandler(logout)).Methods("GET") + mainrouter.Handle("/{team:[A-Za-z0-9-]+(__)?[A-Za-z0-9-]+}/reset_password", api.AppHandler(resetPassword)).Methods("GET") + mainrouter.Handle("/{team}/login/{service}", api.AppHandler(loginWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. + mainrouter.Handle("/{team}/channels/{channelname}", api.UserRequired(getChannel)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. + mainrouter.Handle("/{team}/signup/{service}", api.AppHandler(signupWithOAuth)).Methods("GET") // Bug in gorilla.mux prevents us from using regex here. watchAndParseTemplates() } @@ -145,7 +147,6 @@ func root(c *api.Context, w http.ResponseWriter, r *http.Request) { if len(c.Session.UserId) == 0 { page := NewHtmlTemplatePage("signup_team", "Signup") - page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } else { page := NewHtmlTemplatePage("home", "Home") @@ -161,7 +162,6 @@ func signup(c *api.Context, w http.ResponseWriter, r *http.Request) { } page := NewHtmlTemplatePage("signup_team", "Signup") - page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } @@ -175,8 +175,7 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) { var team *model.Team if tResult := <-api.Srv.Store.Team().GetByName(teamName); tResult.Err != nil { l4g.Error("Couldn't find team name=%v, teamURL=%v, err=%v", teamName, c.GetTeamURL(), tResult.Err.Message) - // This should probably do somthing nicer - http.Redirect(w, r, "http://"+r.Host, http.StatusTemporaryRedirect) + http.Redirect(w, r, api.GetProtocol(r)+"://"+r.Host, http.StatusTemporaryRedirect) return } else { team = tResult.Data.(*model.Team) @@ -193,7 +192,6 @@ func login(c *api.Context, w http.ResponseWriter, r *http.Request) { page := NewHtmlTemplatePage("login", "Login") page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamName"] = teamName - page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } @@ -209,7 +207,7 @@ func signupTeamComplete(c *api.Context, w http.ResponseWriter, r *http.Request) data := r.FormValue("d") hash := r.FormValue("h") - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) { + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { c.Err = model.NewAppError("signupTeamComplete", "The signup link does not appear to be valid", "") return } @@ -258,7 +256,7 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) } } else { - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) { + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { c.Err = model.NewAppError("signupTeamComplete", "The signup link does not appear to be valid", "") return } @@ -279,7 +277,6 @@ func signupUserComplete(c *api.Context, w http.ResponseWriter, r *http.Request) page.Props["TeamId"] = props["id"] page.Props["Data"] = data page.Props["Hash"] = hash - page.Props["AuthServices"] = model.ArrayToJson(utils.GetAllowedAuthServices()) page.Render(c, w) } @@ -345,7 +342,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { } page := NewHtmlTemplatePage("channel", "") - page.Title = name + " - " + team.DisplayName + " " + page.SiteName + page.Props["Title"] = name + " - " + team.DisplayName + " " + page.ClientProps["SiteName"] page.Props["TeamDisplayName"] = team.DisplayName page.Props["TeamType"] = team.Type page.Props["TeamId"] = team.Id @@ -357,6 +354,7 @@ func getChannel(c *api.Context, w http.ResponseWriter, r *http.Request) { func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { resend := r.URL.Query().Get("resend") + resendSuccess := r.URL.Query().Get("resend_success") name := r.URL.Query().Get("teamname") email := r.URL.Query().Get("email") hashedId := r.URL.Query().Get("hid") @@ -377,7 +375,9 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { } else { user := result.Data.(*model.User) api.FireAndForgetVerifyEmail(user.Id, user.Email, team.Name, team.DisplayName, c.GetSiteURL(), c.GetTeamURLFromTeam(team)) - http.Redirect(w, r, "/", http.StatusFound) + + newAddress := strings.Replace(r.URL.String(), "&resend=true", "&resend_success=true", -1) + http.Redirect(w, r, newAddress, http.StatusFound) return } } @@ -402,6 +402,7 @@ func verifyEmail(c *api.Context, w http.ResponseWriter, r *http.Request) { page.Props["IsVerified"] = isVerified page.Props["TeamURL"] = c.GetTeamURLFromTeam(team) page.Props["UserEmail"] = email + page.Props["ResendSuccess"] = resendSuccess page.Render(c, w) } @@ -420,7 +421,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { if len(hash) == 0 || len(data) == 0 { isResetLink = false } else { - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.ResetSalt)) { + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.PasswordResetSalt)) { c.Err = model.NewAppError("resetPassword", "The reset link does not appear to be valid", "") return } @@ -448,7 +449,7 @@ func resetPassword(c *api.Context, w http.ResponseWriter, r *http.Request) { } page := NewHtmlTemplatePage("password_reset", "") - page.Title = "Reset Password - " + page.SiteName + page.Props["Title"] = "Reset Password " + page.ClientProps["SiteName"] page.Props["TeamDisplayName"] = teamDisplayName page.Props["Hash"] = hash page.Props["Data"] = data @@ -482,7 +483,7 @@ func signupWithOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { data := r.URL.Query().Get("d") props := model.MapFromJson(strings.NewReader(data)) - if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.ServiceSettings.InviteSalt)) { + if !model.ComparePassword(hash, fmt.Sprintf("%v:%v", data, utils.Cfg.EmailSettings.InviteSalt)) { c.Err = model.NewAppError("signupWithOAuth", "The signup link does not appear to be valid", "") return } @@ -561,6 +562,7 @@ func signupCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) } user.TeamId = team.Id + user.EmailVerified = true ruser := api.CreateUser(c, team, user) if c.Err != nil { @@ -640,3 +642,283 @@ func loginCompleteOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) } } } + +func adminConsole(c *api.Context, w http.ResponseWriter, r *http.Request) { + + if !c.HasSystemAdminPermissions("adminConsole") { + return + } + + page := NewHtmlTemplatePage("admin_console", "Admin Console") + page.Render(c, w) +} + +func authorizeOAuth(c *api.Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewAppError("authorizeOAuth", "The system admin has turned off OAuth service providing.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + if !CheckBrowserCompatability(c, r) { + return + } + + responseType := r.URL.Query().Get("response_type") + clientId := r.URL.Query().Get("client_id") + redirect := r.URL.Query().Get("redirect_uri") + scope := r.URL.Query().Get("scope") + state := r.URL.Query().Get("state") + + if len(responseType) == 0 || len(clientId) == 0 || len(redirect) == 0 { + c.Err = model.NewAppError("authorizeOAuth", "Missing one or more of response_type, client_id, or redirect_uri", "") + return + } + + var app *model.OAuthApp + if result := <-api.Srv.Store.OAuth().GetApp(clientId); result.Err != nil { + c.Err = result.Err + return + } else { + app = result.Data.(*model.OAuthApp) + } + + var team *model.Team + if result := <-api.Srv.Store.Team().Get(c.Session.TeamId); result.Err != nil { + c.Err = result.Err + return + } else { + team = result.Data.(*model.Team) + } + + page := NewHtmlTemplatePage("authorize", "Authorize Application") + page.Props["TeamName"] = team.Name + page.Props["AppName"] = app.Name + page.Props["ResponseType"] = responseType + page.Props["ClientId"] = clientId + page.Props["RedirectUri"] = redirect + page.Props["Scope"] = scope + page.Props["State"] = state + page.Render(c, w) +} + +func getAccessToken(c *api.Context, w http.ResponseWriter, r *http.Request) { + if !utils.Cfg.ServiceSettings.EnableOAuthServiceProvider { + c.Err = model.NewAppError("getAccessToken", "The system admin has turned off OAuth service providing.", "") + c.Err.StatusCode = http.StatusNotImplemented + return + } + + c.LogAudit("attempt") + + r.ParseForm() + + grantType := r.FormValue("grant_type") + if grantType != model.ACCESS_TOKEN_GRANT_TYPE { + c.Err = model.NewAppError("getAccessToken", "invalid_request: Bad grant_type", "") + return + } + + clientId := r.FormValue("client_id") + if len(clientId) != 26 { + c.Err = model.NewAppError("getAccessToken", "invalid_request: Bad client_id", "") + return + } + + secret := r.FormValue("client_secret") + if len(secret) == 0 { + c.Err = model.NewAppError("getAccessToken", "invalid_request: Missing client_secret", "") + return + } + + code := r.FormValue("code") + if len(code) == 0 { + c.Err = model.NewAppError("getAccessToken", "invalid_request: Missing code", "") + return + } + + redirectUri := r.FormValue("redirect_uri") + + achan := api.Srv.Store.OAuth().GetApp(clientId) + tchan := api.Srv.Store.OAuth().GetAccessDataByAuthCode(code) + + authData := api.GetAuthData(code) + + if authData == nil { + c.LogAudit("fail - invalid auth code") + c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "") + return + } + + uchan := api.Srv.Store.User().Get(authData.UserId) + + if authData.IsExpired() { + c.LogAudit("fail - auth code expired") + c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "") + return + } + + if authData.RedirectUri != redirectUri { + c.LogAudit("fail - redirect uri provided did not match previous redirect uri") + c.Err = model.NewAppError("getAccessToken", "invalid_request: Supplied redirect_uri does not match authorization code redirect_uri", "") + return + } + + if !model.ComparePassword(code, fmt.Sprintf("%v:%v:%v:%v", clientId, redirectUri, authData.CreateAt, authData.UserId)) { + c.LogAudit("fail - auth code is invalid") + c.Err = model.NewAppError("getAccessToken", "invalid_grant: Invalid or expired authorization code", "") + return + } + + var app *model.OAuthApp + if result := <-achan; result.Err != nil { + c.Err = model.NewAppError("getAccessToken", "invalid_client: Invalid client credentials", "") + return + } else { + app = result.Data.(*model.OAuthApp) + } + + if !model.ComparePassword(app.ClientSecret, secret) { + c.LogAudit("fail - invalid client credentials") + c.Err = model.NewAppError("getAccessToken", "invalid_client: Invalid client credentials", "") + return + } + + callback := redirectUri + if len(callback) == 0 { + callback = app.CallbackUrls[0] + } + + if result := <-tchan; result.Err != nil { + c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while accessing database", "") + return + } else if result.Data != nil { + c.LogAudit("fail - auth code has been used previously") + accessData := result.Data.(*model.AccessData) + + // Revoke access token, related auth code, and session from DB as well as from cache + if err := api.RevokeAccessToken(accessData.Token); err != nil { + l4g.Error("Encountered an error revoking an access token, err=" + err.Message) + } + + c.Err = model.NewAppError("getAccessToken", "invalid_grant: Authorization code already exchanged for an access token", "") + return + } + + var user *model.User + if result := <-uchan; result.Err != nil { + c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while pulling user from database", "") + return + } else { + user = result.Data.(*model.User) + } + + session := &model.Session{UserId: user.Id, TeamId: user.TeamId, Roles: user.Roles, IsOAuth: true} + + if result := <-api.Srv.Store.Session().Save(session); result.Err != nil { + c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while saving session to database", "") + return + } else { + session = result.Data.(*model.Session) + api.AddSessionToCache(session) + } + + accessData := &model.AccessData{AuthCode: authData.Code, Token: session.Token, RedirectUri: callback} + + if result := <-api.Srv.Store.OAuth().SaveAccessData(accessData); result.Err != nil { + l4g.Error(result.Err) + c.Err = model.NewAppError("getAccessToken", "server_error: Encountered internal server error while saving access token to database", "") + return + } + + accessRsp := &model.AccessResponse{AccessToken: session.Token, TokenType: model.ACCESS_TOKEN_TYPE, ExpiresIn: model.SESSION_TIME_OAUTH_IN_SECS} + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Cache-Control", "no-store") + w.Header().Set("Pragma", "no-cache") + + c.LogAuditWithUserId(user.Id, "success") + + w.Write([]byte(accessRsp.ToJson())) +} + +func incomingWebhook(c *api.Context, w http.ResponseWriter, r *http.Request) { + params := mux.Vars(r) + id := params["id"] + + hchan := api.Srv.Store.Webhook().GetIncoming(id) + + r.ParseForm() + + props := model.MapFromJson(strings.NewReader(r.FormValue("payload"))) + + text := props["text"] + if len(text) == 0 { + c.Err = model.NewAppError("incomingWebhook", "No text specified", "") + return + } + + channelName := props["channel"] + + var hook *model.IncomingWebhook + if result := <-hchan; result.Err != nil { + c.Err = model.NewAppError("incomingWebhook", "Invalid webhook", "err="+result.Err.Message) + return + } else { + hook = result.Data.(*model.IncomingWebhook) + } + + var channel *model.Channel + var cchan store.StoreChannel + + if len(channelName) != 0 { + if channelName[0] == '@' { + if result := <-api.Srv.Store.User().GetByUsername(hook.TeamId, channelName[1:]); result.Err != nil { + c.Err = model.NewAppError("incomingWebhook", "Couldn't find the user", "err="+result.Err.Message) + return + } else { + channelName = model.GetDMNameFromIds(result.Data.(*model.User).Id, hook.UserId) + } + } else if channelName[0] == '#' { + channelName = channelName[1:] + } + + cchan = api.Srv.Store.Channel().GetByName(hook.TeamId, channelName) + } else { + cchan = api.Srv.Store.Channel().Get(hook.ChannelId) + } + + // parse links into Markdown format + linkWithTextRegex := regexp.MustCompile(`<([^<\|]+)\|([^>]+)>`) + text = linkWithTextRegex.ReplaceAllString(text, "[${2}](${1})") + + linkRegex := regexp.MustCompile(`<\s*(\S*)\s*>`) + text = linkRegex.ReplaceAllString(text, "${1}") + + if result := <-cchan; result.Err != nil { + c.Err = model.NewAppError("incomingWebhook", "Couldn't find the channel", "err="+result.Err.Message) + return + } else { + channel = result.Data.(*model.Channel) + } + + pchan := api.Srv.Store.Channel().CheckPermissionsTo(hook.TeamId, channel.Id, hook.UserId) + + post := &model.Post{UserId: hook.UserId, ChannelId: channel.Id, Message: text} + + if !c.HasPermissionsToChannel(pchan, "createIncomingHook") && channel.Type != model.CHANNEL_OPEN { + c.Err = model.NewAppError("incomingWebhook", "Inappropriate channel permissions", "") + return + } + + // create a mock session + c.Session = model.Session{UserId: hook.UserId, TeamId: hook.TeamId, IsOAuth: false} + + if _, err := api.CreatePost(c, post, false); err != nil { + c.Err = model.NewAppError("incomingWebhook", "Error creating post", "err="+err.Message) + return + } + + w.Header().Set("Content-Type", "text/plain") + w.Write([]byte("ok")) +} |