212 lines
5.7 KiB
Go
212 lines
5.7 KiB
Go
// 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 (
|
|
"html/template"
|
|
"net/http"
|
|
|
|
"github.com/matrix-org/dendrite/clientapi/auth/authtypes"
|
|
"github.com/matrix-org/dendrite/clientapi/jsonerror"
|
|
"github.com/matrix-org/dendrite/internal/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="https://www.google.com/recaptcha/api.js"
|
|
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="g-recaptcha"
|
|
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,
|
|
) *util.JSONResponse {
|
|
sessionID := req.URL.Query().Get("session")
|
|
|
|
if sessionID == "" {
|
|
return writeHTTPMessage(w, req,
|
|
"Session ID not provided",
|
|
http.StatusBadRequest,
|
|
)
|
|
}
|
|
|
|
serveRecaptcha := func() {
|
|
data := map[string]string{
|
|
"myUrl": req.URL.String(),
|
|
"session": sessionID,
|
|
"siteKey": cfg.RecaptchaPublicKey,
|
|
}
|
|
serveTemplate(w, recaptchaTemplate, data)
|
|
}
|
|
|
|
serveSuccess := func() {
|
|
data := map[string]string{}
|
|
serveTemplate(w, successTemplate, data)
|
|
}
|
|
|
|
if req.Method == http.MethodGet {
|
|
// Handle Recaptcha
|
|
if authType == authtypes.LoginTypeRecaptcha {
|
|
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
|
return err
|
|
}
|
|
|
|
serveRecaptcha()
|
|
return nil
|
|
}
|
|
return &util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: jsonerror.NotFound("Unknown auth stage type"),
|
|
}
|
|
} else if req.Method == http.MethodPost {
|
|
// Handle Recaptcha
|
|
if authType == authtypes.LoginTypeRecaptcha {
|
|
if err := checkRecaptchaEnabled(cfg, w, req); err != nil {
|
|
return err
|
|
}
|
|
|
|
clientIP := req.RemoteAddr
|
|
err := req.ParseForm()
|
|
if err != nil {
|
|
util.GetLogger(req.Context()).WithError(err).Error("req.ParseForm failed")
|
|
res := jsonerror.InternalServerError()
|
|
return &res
|
|
}
|
|
|
|
response := req.Form.Get("g-recaptcha-response")
|
|
if err := validateRecaptcha(cfg, response, clientIP); err != nil {
|
|
util.GetLogger(req.Context()).Error(err)
|
|
return err
|
|
}
|
|
|
|
// Success. Add recaptcha as a completed login flow
|
|
AddCompletedSessionStage(sessionID, authtypes.LoginTypeRecaptcha)
|
|
|
|
serveSuccess()
|
|
return nil
|
|
}
|
|
|
|
return &util.JSONResponse{
|
|
Code: http.StatusNotFound,
|
|
JSON: jsonerror.NotFound("Unknown auth stage type"),
|
|
}
|
|
}
|
|
return &util.JSONResponse{
|
|
Code: http.StatusMethodNotAllowed,
|
|
JSON: jsonerror.NotFound("Bad method"),
|
|
}
|
|
}
|
|
|
|
// checkRecaptchaEnabled creates an error response if recaptcha is not usable on homeserver.
|
|
func checkRecaptchaEnabled(
|
|
cfg *config.ClientAPI,
|
|
w http.ResponseWriter,
|
|
req *http.Request,
|
|
) *util.JSONResponse {
|
|
if !cfg.RecaptchaEnabled {
|
|
return writeHTTPMessage(w, req,
|
|
"Recaptcha login is disabled on this Homeserver",
|
|
http.StatusBadRequest,
|
|
)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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,
|
|
) *util.JSONResponse {
|
|
w.WriteHeader(header)
|
|
_, err := w.Write([]byte(message))
|
|
if err != nil {
|
|
util.GetLogger(req.Context()).WithError(err).Error("w.Write failed")
|
|
res := jsonerror.InternalServerError()
|
|
return &res
|
|
}
|
|
return nil
|
|
}
|