// Copyright 2019 Parminder Singh <parmsingh129@gmail.com> // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package routing import ( "fmt" "html/template" "net/http" "github.com/matrix-org/dendrite/clientapi/auth/authtypes" "github.com/matrix-org/dendrite/setup/config" "github.com/matrix-org/util" ) // recaptchaTemplate is an HTML webpage template for recaptcha auth const recaptchaTemplate = ` <html> <head> <title>Authentication</title> <meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'> <script src="{{.apiJsUrl}}" async defer></script> <script src="//code.jquery.com/jquery-1.11.2.min.js"></script> <script> function captchaDone() { $('#registrationForm').submit(); } </script> </head> <body> <form id="registrationForm" method="post" action="{{.myUrl}}"> <div> <p> Hello! We need to prevent computer programs and other automated things from creating accounts on this server. </p> <p> Please verify that you're not a robot. </p> <input type="hidden" name="session" value="{{.session}}" /> <div class="{{.sitekeyClass}}" data-sitekey="{{.sitekey}}" data-callback="captchaDone"> </div> <noscript> <input type="submit" value="All Done" /> </noscript> </div> </div> </form> </body> </html> ` // successTemplate is an HTML template presented to the user after successful // recaptcha completion const successTemplate = ` <html> <head> <title>Success!</title> <meta name='viewport' content='width=device-width, initial-scale=1, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0'> <script> if (window.onAuthDone) { window.onAuthDone(); } else if (window.opener && window.opener.postMessage) { window.opener.postMessage("authDone", "*"); } </script> </head> <body> <div> <p>Thank you!</p> <p>You may now close this window and return to the application.</p> </div> </body> </html> ` // serveTemplate fills template data and serves it using http.ResponseWriter func serveTemplate(w http.ResponseWriter, templateHTML string, data map[string]string) { t := template.Must(template.New("response").Parse(templateHTML)) if err := t.Execute(w, data); err != nil { panic(err) } } // AuthFallback implements GET and POST /auth/{authType}/fallback/web?session={sessionID} func AuthFallback( w http.ResponseWriter, req *http.Request, authType string, cfg *config.ClientAPI, ) { // We currently only support "m.login.recaptcha", so fail early if that's not requested if authType == authtypes.LoginTypeRecaptcha { if !cfg.RecaptchaEnabled { writeHTTPMessage(w, req, "Recaptcha login is disabled on this Homeserver", http.StatusBadRequest, ) return } } else { writeHTTPMessage(w, req, fmt.Sprintf("Unknown authtype %q", authType), http.StatusNotImplemented) return } sessionID := req.URL.Query().Get("session") if sessionID == "" { writeHTTPMessage(w, req, "Session ID not provided", http.StatusBadRequest, ) return } serveRecaptcha := func() { data := map[string]string{ "myUrl": req.URL.String(), "session": sessionID, "apiJsUrl": cfg.RecaptchaApiJsUrl, "sitekey": cfg.RecaptchaPublicKey, "sitekeyClass": cfg.RecaptchaSitekeyClass, "formField": cfg.RecaptchaFormField, } serveTemplate(w, recaptchaTemplate, data) } serveSuccess := func() { data := map[string]string{} serveTemplate(w, successTemplate, data) } if req.Method == http.MethodGet { // Handle Recaptcha serveRecaptcha() return } else if req.Method == http.MethodPost { // Handle Recaptcha clientIP := req.RemoteAddr err := req.ParseForm() if err != nil { util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed") w.WriteHeader(http.StatusBadRequest) serveRecaptcha() return } response := req.Form.Get(cfg.RecaptchaFormField) err = validateRecaptcha(cfg, response, clientIP) switch err { case ErrMissingResponse: w.WriteHeader(http.StatusBadRequest) serveRecaptcha() // serve the initial page again, instead of nothing return case ErrInvalidCaptcha: w.WriteHeader(http.StatusUnauthorized) serveRecaptcha() return case nil: default: // something else failed util.GetLogger(req.Context()).WithError(err).Error("failed to validate recaptcha") serveRecaptcha() return } // Success. Add recaptcha as a completed login flow sessions.addCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha) serveSuccess() return } writeHTTPMessage(w, req, "Bad method", http.StatusMethodNotAllowed) } // writeHTTPMessage writes the given header and message to the HTTP response writer. // Returns an error JSONResponse obtained through httputil.LogThenError if the writing failed, otherwise nil. func writeHTTPMessage( w http.ResponseWriter, req *http.Request, message string, header int, ) { w.WriteHeader(header) _, err := w.Write([]byte(message)) if err != nil { util.GetLogger(req.Context()).WithError(err).Error("w.Write failed") } }