package main import ( "fmt" "time" ) func logMessage(severity int, message string) { var moment = time.Now() if severity <= logVerbosity { fmt.Printf("[%s] %02d:%02d:%02d %04d-%02d-%02d %s\n", sevMap[severity], moment.Hour(), moment.Minute(), moment.Second(), moment.Year(), moment.Month(), moment.Day(), message) } }
package main import ( "bytes" "crypto/tls" "database/sql" "fmt" "io" "net/http" "net/smtp" "regexp" "strings" "unicode" ) func AuthenticateRequestor(response http.ResponseWriter, request *http.Request, userID string) { token, err := GenerateSessionToken() if err != nil { ServeErrorPage(response, err) return } logMessage(4, fmt.Sprintf("User %s (%s) authenticated", userID, request.RemoteAddr)) _, err = db.Exec(fmt.Sprintf("INSERT INTO %s_sessions (session_token, user_id) VALUES (?, ?)", tablePrefix), token, userID) if err != nil { ServeErrorPage(response, err) return } cookie := http.Cookie{Name: sessionCookieName, Value: token, SameSite: http.SameSiteStrictMode, Secure: false, Path: "/"} http.SetCookie(response, &cookie) if enableLoginAlerts { var ( username string email string emailConfirmed bool ) err = db.QueryRow(fmt.Sprintf("SELECT email, username, email_confirmed FROM %s_accounts WHERE id = ?", tablePrefix), userID).Scan(&email, &username, &emailConfirmed) if err != nil { ServeErrorPage(response, err) return } else if emailConfirmed { err = sendMail(email, "Sign In - New Device", username, "A new device has signed in to your account.<br><br>If this was you, there's nothing you need to do. Otherwise, please change your password immediately.", "", "") if err != nil { logMessage(3, fmt.Sprintf("User %s was not notified of new device sign in.", username)) } } } http.Redirect(response, request, "/", http.StatusSeeOther) } func IsValidSession(sessionToken string) (bool, error) { var userId string err := db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_sessions INNER JOIN %s_accounts ON id = user_id WHERE session_token = ? AND critical = 0", tablePrefix, tablePrefix), sessionToken).Scan(&userId) if err == sql.ErrNoRows { logMessage(5, "Invalid session token presented") return false, nil } else if err != nil { logMessage(1, err.Error()) return false, err } else { return true, nil } } func IsValidSessionWithInfo(sessionToken string) (bool, string, string, bool, error) { var ( userID string userEmail string emailConfirmed bool ) err := db.QueryRow(fmt.Sprintf("SELECT user_id, email, email_confirmed FROM %s_sessions INNER JOIN %s_accounts ON id = user_id WHERE session_token = ? AND critical = 0", tablePrefix, tablePrefix), sessionToken).Scan(&userID, &userEmail, &emailConfirmed) if err == sql.ErrNoRows { logMessage(5, "Invalid session token presented") return false, "", "", false, nil } else if err != nil { return false, "", "", false, err } else { return true, userID, userEmail, emailConfirmed, nil } } func IsValidCriticalSession(sessionToken string) (bool, error) { var userID string err := db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_sessions INNER JOIN %s_accounts ON id = user_id WHERE session_token = ? AND critical = 1 AND created > NOW() - INTERVAL 1 HOUR", tablePrefix, tablePrefix), sessionToken).Scan(&userID) if err == sql.ErrNoRows { logMessage(5, "Invalid critical session token presented") return false, nil } else if err != nil { return false, err } else { return true, nil } } func PendingEmailApproval(sessionToken string) (bool, error) { var emailConfirmed bool err := db.QueryRow(fmt.Sprintf("SELECT email_confirmed FROM %s_accounts INNER JOIN %s_sessions ON id = user_id WHERE session_token = ?", tablePrefix, tablePrefix), sessionToken).Scan(&emailConfirmed) if err == sql.ErrNoRows || emailConfirmed { return false, nil } else if err != nil { return false, err } else { return true, nil } } func ConfirmEmailCode(code string) (bool, error) { var isTokenUsed bool err := db.QueryRow(fmt.Sprintf("SELECT used FROM %s_confirmations WHERE confirmation_token = ?", tablePrefix), code).Scan(&isTokenUsed) if err == sql.ErrNoRows { return false, nil } else if err != nil { return false, err } else if isTokenUsed { return false, nil } _, err = db.Exec(fmt.Sprintf("UPDATE %s_accounts INNER JOIN %s_confirmations ON user_id = id SET email_confirmed = 1, used = 1 WHERE confirmation_token = ?", tablePrefix, tablePrefix), code) if err != nil { return false, err } return true, nil } func SendEmailConfirmationCode(userID string, email string, username string) error { code, err := GenerateEmailConfirmationToken() if err == nil { _, err = db.Exec(fmt.Sprintf("INSERT INTO %s_confirmations (confirmation_token, user_id) VALUES (?, ?)", tablePrefix), code, userID) } if err == nil { // Email code err = sendMail(email, "Confirm your Email Address", username, fmt.Sprintf("Thank you for signing up to %s! Please confirm your email by clicking the link below:", appName), fmt.Sprintf("%s/%s/confirmcode?c=%s", webDomain, functionalPath, code), "If you did not request this action, please ignore this email.") } return err } func sendMail(to string, subject string, recipient string, message string, link string, closingStatement string) error { var ( client *smtp.Client err error body bytes.Buffer hasLink bool = false hasCloser bool = false content []byte data io.WriteCloser conn *tls.Conn ) // Create a custom tls.Config with InsecureSkipVerify set to true if smtpTLS { tlsConfig := &tls.Config{ InsecureSkipVerify: smtpTLSSkipVerify, /* #nosec G402 */ ServerName: smtpHost, } conn, err = tls.Dial("tcp", smtpHost+":"+smtpPort, tlsConfig) if err == nil { client, err = smtp.NewClient(conn, smtpHost) } } else { // Don't use TLS encryption client, err = smtp.Dial(smtpHost + ":" + smtpPort) } // Authenticate with the server if needed if err == nil && smtpUser != "" && smtpPass != "" { auth := smtp.PlainAuth("", smtpUser, smtpPass, smtpHost) err = client.Auth(auth) } if err == nil { // Compose the email message if link != "" { hasLink = true } if closingStatement != "" { hasCloser = true } err = emailTemplate.Execute(&body, struct { Title string Recipient string Message string HasLink bool Link string HasCloser bool ClosingStatement string AppName string }{ Title: subject, Recipient: recipient, Message: message, HasLink: hasLink, Link: link, HasCloser: hasCloser, ClosingStatement: closingStatement, AppName: appName, }) } if err == nil { // Set the sender and recipient from := senderAddress content = []byte("To: " + to + "\r\n" + "From: " + appName + " <" + from + ">\r\n" + "Subject: " + subject + "\r\n" + "MIME-version: 1.0;\r\n" + "Content-Type: text/html; charset=\"UTF-8\";\r\n" + "\r\n" + body.String() + "\r\n") // Send the email message err = client.Mail(senderAddress) } if err == nil { err = client.Rcpt(to) } if err == nil { data, err = client.Data() } if err == nil { _, err = data.Write(content) } // Close the connection if err == nil { err = data.Close() } if err == nil { err = client.Quit() } if err != nil { logMessage(2, fmt.Sprintf("Error sending '%s' email to '%s': %s", subject, to, err.Error())) } else { logMessage(4, fmt.Sprintf("Email '%s' to %s via %s:%s", subject, to, smtpHost, smtpPort)) } return err } func ResetPasswordRequest(email string) (bool, error) { var userID string var username string err := db.QueryRow(fmt.Sprintf("SELECT id, username FROM %s_accounts WHERE email = ?", tablePrefix), strings.ToLower(email)).Scan(&userID, &username) if err == sql.ErrNoRows { return false, nil } else if err != nil { return false, err } // Reset password resetCode, err := GenerateResetToken() if err != nil { return false, err } _, err = db.Exec(fmt.Sprintf("INSERT INTO %s_resets (user_id, reset_token) VALUES (?, ?)", tablePrefix), userID, resetCode) if err != nil { return false, err } err = sendMail(strings.ToLower(email), "Password Reset Request", username, fmt.Sprintf("Click the link below to reset your %s password:", appName), fmt.Sprintf("%s/%s/resetpassword?c=%s", webDomain, functionalPath, resetCode), "If you did not request this action, please disregard this email.") if err != nil { logMessage(2, fmt.Sprintf("Password reset request for %s was not sent.", username)) return false, err } return true, nil } func IsValidResetCode(code string) (bool, error) { var userID string err := db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_resets WHERE reset_token = ? AND used = 0", tablePrefix), code).Scan(&userID) if err == sql.ErrNoRows { return false, nil } else if err != nil { return false, err } return true, nil } func ResendConfirmationEmail(sessionToken string) (bool, error) { var userID string var username string var email string err := db.QueryRow(fmt.Sprintf("SELECT user_id, email, username FROM %s_sessions INNER JOIN %s_accounts ON id = user_id WHERE session_token = ? AND email_resent = 0", tablePrefix, tablePrefix), sessionToken).Scan(&userID, &email, &username) if err == sql.ErrNoRows { return false, nil } else if err != nil { return false, err } err = SendEmailConfirmationCode(userID, email, username) if err != nil { return false, err } _, err = db.Exec(fmt.Sprintf("UPDATE %s_accounts SET email_resent = 1 WHERE id = ?", tablePrefix), userID) if err != nil { return false, err } return true, nil } func IsValidNewUsername(username string) (bool, error) { match, _ := regexp.MatchString("^[a-zA-Z0-9\\-_]{1,32}$", username) if !match { return false, nil } var userID string err := db.QueryRow(fmt.Sprintf("SELECT id FROM %s_accounts WHERE username = ?", tablePrefix), username).Scan(&userID) if err == sql.ErrNoRows { return true, nil } else if err != nil { return false, err } return false, nil } func IsValidNewEmail(email string) (bool, error) { match, _ := regexp.MatchString(`^[\w!#$%&'*+/=?^_{|}~-]+(\.[\w!#$%&'*+/=?^_{|}~-]+)*@[a-zA-Z0-9]+(\.[a-zA-Z0-9-]+)*(\.[a-zA-Z]{2,})$`, email) if !match { return false, nil } var userID string err := db.QueryRow(fmt.Sprintf("SELECT id FROM %s_accounts WHERE email = ?", tablePrefix), strings.ToLower(email)).Scan(&userID) if err == sql.ErrNoRows { return true, nil } else if err != nil { return false, err } return false, nil } func IsValidPassword(password string) bool { // Check length if len(password) < 8 { return false } // Check for uppercase letter hasUppercase := false for _, char := range password { if unicode.IsUpper(char) { hasUppercase = true break } } if !hasUppercase { return false } // Check for numeric digit hasDigit := false for _, char := range password { if unicode.IsDigit(char) { hasDigit = true break } } return hasDigit }
package main import ( "crypto/hmac" "crypto/rand" "crypto/sha1" /* #nosec G505 */ "crypto/sha256" "database/sql" "encoding/base32" "encoding/base64" "encoding/json" "fmt" "math" "math/big" "os" "strconv" "strings" "time" "golang.org/x/crypto/bcrypt" ) func HashPassword(password string) string { bytes, err := bcrypt.GenerateFromPassword([]byte(password), 14) if err != nil { logMessage(0, fmt.Sprintf("Failed to generate BCRYPT hash of password string: %s", err.Error())) logMessage(5, fmt.Sprintf("Password string: %s", password)) os.Exit(1) } return string(bytes) } func CheckPasswordHash(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) return err == nil } func GenerateRandomString(length int) string { charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" charsetLength := big.NewInt(int64(len(charset))) bytes := make([]byte, length) for i := 0; i < length; i++ { num, err := rand.Int(rand.Reader, charsetLength) if err != nil { logMessage(0, fmt.Sprintf("Failed to generate random number: %s", err.Error())) os.Exit(1) } bytes[i] = charset[num.Int64()] } return string(bytes) } func GenerateRandomNumbers(length int) string { charset := "0123456789" charsetLength := big.NewInt(int64(len(charset))) bytes := make([]byte, length) for i := 0; i < length; i++ { num, err := rand.Int(rand.Reader, charsetLength) if err != nil { logMessage(0, fmt.Sprintf("Failed to generate random number: %s", err.Error())) os.Exit(1) } bytes[i] = charset[num.Int64()] } return string(bytes) } func GenerateOTP(secret string, timestep int64) (string, error) { // decode the base32 secret into bytes key, err := base32.StdEncoding.DecodeString(secret) if err != nil { return "", err } // calculate the number of time steps since Unix epoch (Jan 1 1970 00:00:00 UTC) steps := time.Now().Unix() / timestep // convert the steps to a byte array in big-endian format msg := make([]byte, 8) for i := 7; i >= 0; i-- { msg[i] = byte(steps & 0xff) steps >>= 8 } // calculate the HMAC-SHA1 hash of the message using the secret key h := hmac.New(sha1.New, key) h.Write(msg) hash := h.Sum(nil) // truncate the hash to a 4-byte value offset := hash[len(hash)-1] & 0xf code := (int(hash[offset])&0x7f)<<24 | (int(hash[offset+1])&0xff)<<16 | (int(hash[offset+2])&0xff)<<8 | (int(hash[offset+3]) & 0xff) code = int(math.Mod(float64(code), math.Pow10(6))) // convert the code to a string with leading zeros if necessary codeStr := strconv.Itoa(code) for len(codeStr) < 6 { codeStr = "0" + codeStr } return codeStr, nil } func GenerateOTPSecret() string { // Generate a random secret key secret := make([]byte, 10) _, err := rand.Read(secret) if err != nil { logMessage(0, fmt.Sprintf("Failed to read bytes: %s", err.Error())) os.Exit(1) } secretBase32 := base32.StdEncoding.EncodeToString(secret) return secretBase32 } func GenerateUserID() (string, error) { newID := GenerateRandomString(8) var userID string err := db.QueryRow(fmt.Sprintf("SELECT id FROM %s_accounts WHERE id = ?", tablePrefix), strings.ToLower(newID)).Scan(&userID) if err == sql.ErrNoRows { return newID, nil } else if err != nil { return "", err } else { newID, err = GenerateUserID() return newID, err } } func GenerateAvatarID() (string, error) { newID := GenerateRandomString(16) var avatarID string err := db.QueryRow(fmt.Sprintf("SELECT avatar_id FROM %s_avatars WHERE avatar_id = ?", tablePrefix), strings.ToLower(newID)).Scan(&avatarID) if err == sql.ErrNoRows { return newID, nil } else if err != nil { return "", err } else { newID, err = GenerateAvatarID() return newID, err } } func GenerateSessionToken() (string, error) { newToken := GenerateRandomString(64) var userID string err := db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_sessions WHERE session_token = ?", tablePrefix), newToken).Scan(&userID) if err == sql.ErrNoRows { return newToken, nil } else if err != nil { logMessage(1, err.Error()) return "", err } else { newToken, err = GenerateSessionToken() return newToken, err } } func GenerateResetToken() (string, error) { newToken := GenerateRandomString(32) var userID string err := db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_resets WHERE reset_token = ?", tablePrefix), newToken).Scan(&userID) if err == sql.ErrNoRows { return newToken, nil } else if err != nil { return "", err } else { newToken, err = GenerateResetToken() return newToken, err } } func GenerateMfaSessionToken() (string, error) { newToken := GenerateRandomString(32) var userID string err := db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_mfa WHERE mfa_session = ?", tablePrefix), newToken).Scan(&userID) if err == sql.ErrNoRows { return newToken, nil } else if err != nil { return "", err } else { newToken, err = GenerateMfaSessionToken() return newToken, err } } func GenerateEmailConfirmationToken() (string, error) { newToken := GenerateRandomString(32) var userID string err := db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_confirmations WHERE confirmation_token = ?", tablePrefix), newToken).Scan(&userID) if err == sql.ErrNoRows { return strings.ToLower(newToken), nil } else if err != nil { return "", err } else { newToken, err = GenerateEmailConfirmationToken() return strings.ToLower(newToken), err } } func CreateJWT(userId string) (string, error) { var ( username string email string emailConfirmed bool avatarURL string ) err := db.QueryRow(fmt.Sprintf("SELECT username, email, email_confirmed, avatar_url FROM %s_accounts WHERE id = ?", tablePrefix), userId).Scan(&username, &email, &emailConfirmed, &avatarURL) if err != nil { return "", err } var header = map[string]string{ "alg": "HS256", "typ": "JWT", } headerBytes, err := json.Marshal(header) if err != nil { return "", err } emailConfirmedString := "false" if emailConfirmed { emailConfirmedString = "true" } var payload = map[string]string{ "iss": "Gatehouse", "sub": userId, "name": username, "nickname": username, "iat": "now", "email": email, "email_verified": emailConfirmedString, "picture": avatarURL, } payloadBytes, err := json.Marshal(payload) if err != nil { return "", err } headerString := base64.RawURLEncoding.EncodeToString(headerBytes) payloadString := base64.RawURLEncoding.EncodeToString(payloadBytes) signature := CreateHMAC256(headerString+"."+payloadString, jwtSecret) return fmt.Sprintf("%s.%s.%s", headerString, payloadString, signature), nil } func CreateHMAC256(message, key string) string { keyBytes := []byte(key) messageBytes := []byte(message) h := hmac.New(sha256.New, keyBytes) h.Write(messageBytes) hashBytes := h.Sum(nil) return base64.RawURLEncoding.EncodeToString(hashBytes) }
package main type GatehouseForm struct { TabTitle string FormTitle string FormAction string FormMethod string FormElements []GatehouseFormElement OIDCOptions []OIDCButton FunctionalPath string } type GatehouseFormElement struct { Class string InnerText string IsLink bool LinkURI string IsInput bool IsImage bool InputType string InputName string InputPlaceholder string } type OIDCButton struct { Text string ImageURI string BackgroundColor string TextColor string URI string } func FormCreateCheckboxInput(name string) GatehouseFormElement { return GatehouseFormElement{ "gh_inp_checkbox", "", false, "", true, false, "checkbox", name, "", } } func FormCreateTextInput(name string, placeholder string) GatehouseFormElement { return GatehouseFormElement{ "gh_inp_text", "", false, "", true, false, "text", name, placeholder, } } func FormCreatePasswordInput(name string, placeholder string) GatehouseFormElement { return GatehouseFormElement{ "gh_inp_text", "", false, "", true, false, "password", name, placeholder, } } func FormCreateSubmitInput(name string, text string) GatehouseFormElement { return GatehouseFormElement{ "gh_a_primary_button gh_inp_button", text, false, "", true, false, "submit", name, "", } } func FormCreateDangerSubmitInput(name string, text string) GatehouseFormElement { return GatehouseFormElement{ "gh_a_danger_button gh_inp_button", text, false, "", true, false, "submit", name, "", } } func FormCreateButtonLink(linkUrl string, text string) GatehouseFormElement { return GatehouseFormElement{ "gh_a_button", text, true, linkUrl, false, false, "", "", "", } } func FormCreateDangerButtonLink(linkUrl string, text string) GatehouseFormElement { return GatehouseFormElement{ "gh_a_danger_button gh_a_button", text, true, linkUrl, false, false, "", "", "", } } func FormCreateSmallLink(linkUrl string, text string) GatehouseFormElement { return GatehouseFormElement{ "gh_a_small", text, true, linkUrl, false, false, "", "", "", } } func FormCreateDivider() GatehouseFormElement { return GatehouseFormElement{ "gh_div_divider", "", false, "", false, false, "", "", "", } } func FormCreateHint(text string) GatehouseFormElement { return GatehouseFormElement{ "gh_div_hint", text, false, "", false, false, "", "", "", } } func FormCreateSmallHint(text string) GatehouseFormElement { return GatehouseFormElement{ "gh_div_smallhint", text, false, "", false, false, "", "", "", } } func FormCreateQR(b64Data string) GatehouseFormElement { return GatehouseFormElement{ "gh_img_qr", b64Data, false, "", false, true, "", "", "", } } func FormCreateUploadInput(name string, text string) GatehouseFormElement { return GatehouseFormElement{ "gh_inp_upload", text, false, "", true, false, "file", name, "", } } var ( registrationPage GatehouseForm = GatehouseForm{ // Define registration page appName + " - Create Account", "Create an Account", "/" + functionalPath + "/submit/register", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateTextInput("newUsername", "Username"), FormCreateTextInput("email", "Email Address"), FormCreatePasswordInput("password", "Password"), FormCreatePasswordInput("passwordConfirm", "Confirm Password"), FormCreateSubmitInput("register", "Create Account"), FormCreateDivider(), FormCreateHint("Already have an account?"), FormCreateButtonLink("/"+functionalPath+"/login", "Sign In"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } forgotPasswordPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Reset Password", "Reset Password", "/" + functionalPath + "/submit/resetrequest", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateTextInput("email", "Email Address"), FormCreateSubmitInput("register", "Send Reset Email"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } confirmEmailPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Confirm Email Address", "Confirmation Required", "/submit", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("A confirmation email has been sent to your registered email address."), FormCreateDivider(), FormCreateHint("Didn't receive an email?"), FormCreateButtonLink("/"+functionalPath+"/resendconfirmation", "Resend Confirmation Email"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } confirmedEmailPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Confirmed Email Address", "Email Confirmed", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Thank you for confirming your email address."), FormCreateSmallLink("/", "Back to site"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } confirmedUsernameChangePage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Confirmed New Username", "Username Changed", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Username has been changed successfully."), FormCreateSmallLink("/"+functionalPath+"/manage", "Back to site"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } linkExpired GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Expired", "Link Expired", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("This link is no longer valid."), FormCreateSmallLink("/", "Back to site"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } resetSentPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Reset Sent", "Reset Email Sent", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("A reset email has been sent to your email address."), FormCreateSmallLink("/", "Back to site"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } resetNotSentPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Reset Not Sent", "Account Not Found", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("There is no account registered with this email address."), FormCreateSmallLink("/", "Back to site"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } resetPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Reset Password", "Reset Password", "/" + functionalPath + "/submit/reset", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreatePasswordInput("password", "Password"), FormCreatePasswordInput("passwordConfirm", "Confirm Password"), FormCreateSubmitInput("submit", "Set Password"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } resetSuccessPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Reset Success", "Reset Success", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Reset request successful."), FormCreateButtonLink("/"+functionalPath+"/login", "Sign In"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } resendConfirmationPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Second Confirmation Sent", "Second Confirmation Sent", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Second confirmation email sent. If the problem persists please contact your system administrator."), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } emailTakenPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Email Already Registered", "Email Already Registered", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("This email has already been registered."), FormCreateDivider(), FormCreateButtonLink("/"+functionalPath+"/login", "Sign In"), FormCreateHint("Forgotten your details?"), FormCreateButtonLink("/"+functionalPath+"/forgot", "Reset Password"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } emailChangePage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Change Your Email", "Change Your Email", "/" + functionalPath + "/submit/changeemail", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Enter your new email address:"), FormCreateTextInput("newemail", "name@example.com"), FormCreateSubmitInput("submit", "Change Email"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } usernameChangePage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Change Your Username", "Change Your Username", "/" + functionalPath + "/submit/changeusername", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Choose your new username:"), FormCreateTextInput("newUsername", "JohnSmith1234"), FormCreateCheckboxInput("confirmed"), FormCreateHint("I understand I can only change username once per 30 days."), FormCreateDangerSubmitInput("submit", "Change Username"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } usernameChangeBlockedPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Change Your Username", "Change Your Username", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("You have already changed your username recently."), FormCreateButtonLink("/"+functionalPath+"/manage", "Back"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } avatarChangePage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Change Your Avatar", "Change Your Avatar", "/" + functionalPath + "/submit/changeavatar", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Upload your new avatar:"), FormCreateUploadInput("avatarupload", "Select Image"), FormCreateSmallHint("JPG or PNG formats supported. Max 5MB"), FormCreateDivider(), FormCreateSubmitInput("submit", "Upload"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } avatarChangedPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Success", "Avatar changed", "", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Your avatar has been changed successfully."), FormCreateButtonLink("/"+functionalPath+"/manage", "Manage Account"), FormCreateDivider(), FormCreateButtonLink("/"+functionalPath+"/", "Back To Site"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } avatarInvalidPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Invalid Image", "Invalid Image", "", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Your uploaded image was not supported."), FormCreateHint("Make sure the image is a supported format and below 5MB in size."), FormCreateButtonLink("/"+functionalPath+"/changeavatar", "Try Again"), FormCreateDivider(), FormCreateButtonLink("/"+functionalPath+"/manage", "Manage Account"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } mfaEmailPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - MFA", "MFA Code Sent", "/" + functionalPath + "/submit/mfa", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("An MFA code has been sent to your email address."), FormCreateHint("Enter the code below:"), FormCreateTextInput("token", "000000"), FormCreateSubmitInput("submit", "Submit"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } mfaTokenPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - MFA", "Enter TOTP", "/" + functionalPath + "/submit/mfa", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("A timed-based one-time password is needed from your registered two-factor device."), FormCreateHint("Enter the code below:"), FormCreateTextInput("token", "000000"), FormCreateSubmitInput("submit", "Submit"), FormCreateDivider(), FormCreateHint("Can't access TOTP?"), FormCreateSmallLink("/gatehouse/recoverycode", "Use recovery code"), }, []OIDCButton{}, functionalPath, } mfaRecoveryCodePage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - MFA", "Enter Recovery Code", "/" + functionalPath + "/submit/recoverycode", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Enter one of your saved recovery codes to sign in. The codes are single use and are deactivated once entered."), FormCreateHint("Enter the code below:"), FormCreateTextInput("token", "00000000"), FormCreateSubmitInput("submit", "Submit"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } elevateSessionPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Reauthenticate", "Confirm Password", "/" + functionalPath + "/submit/elevate", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("You must reauthenticate to perform this action."), FormCreatePasswordInput("password", "Password"), FormCreateSubmitInput("submit", "Submit"), FormCreateDivider(), FormCreateHint("Changed your mind?"), FormCreateButtonLink("/"+functionalPath+"/manage", "Cancel"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } mfaValidatedPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - MFA Validated", "Success", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Your OTP code was validated successfully! You are now able to sign in with your authenticator OTP in the future."), FormCreateButtonLink("/"+functionalPath+"/manage", "Back to Dashboard"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } mfaFailedPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - MFA Failed", "OTP Incorrect", "/" + functionalPath + "/addmfa", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Your OTP code was not valid, please try adding you MFA device again."), FormCreateButtonLink("/"+functionalPath+"/addmfa", "Try Again"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } mfaRemovePage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Remove MFA", "Remove MFA", "/" + functionalPath + "/submit/removemfa", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("This will remove your registered MFA device. Two-factor OTP codes will instead be sent by email."), FormCreateHint("Are you sure you wish to proceed?"), FormCreateSubmitInput("submit", "Yes"), FormCreateDivider(), FormCreateButtonLink("/"+functionalPath+"/manage", "No, take me back!"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } mfaRemovedPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - MFA Removed", "MFA Removed", "/" + functionalPath + "/manage", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("MFA device removed. Two-factor OTP codes will now be sent by email."), FormCreateButtonLink("/"+functionalPath+"/manage", "OK"), }, []OIDCButton{}, functionalPath, } deleteAccountPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Delete Account", "Delete Account", "/" + functionalPath + "/submit/deleteaccount", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Are you sure you wish to delete your account?"), FormCreateDivider(), FormCreateCheckboxInput("confirmed"), FormCreateHint("I understand this action is permanent and cannot be reversed."), FormCreateDangerSubmitInput("submit", "Delete Account"), FormCreateDivider(), FormCreateHint("Changed your mind?"), FormCreateButtonLink("/"+functionalPath+"/manage", "Cancel"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } deletedAccountPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Account Deleted", "Account Deleted", "/", "GET", []GatehouseFormElement{ FormCreateHint("Your account has been deleted."), FormCreateButtonLink("/"+functionalPath+"/manage", "Back to Site"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } disabledFeaturePage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Feature Disabled", "Feature Disabled", "", "", []GatehouseFormElement{ FormCreateHint("This feature is disabled."), FormCreateButtonLink("/", "Back to Site"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } )
package main import ( "bytes" "database/sql" "encoding/base64" "fmt" "image" "image/jpeg" "image/png" "net/http" "path" "strings" "github.com/skip2/go-qrcode" ) func HandleMain(response http.ResponseWriter, request *http.Request) { // Create main listener function var ( validSession bool = false userId string = "Unauth" userEmail string = "-" emailConfirmed bool = false proxied string = "Proxied" ) sessionCookie, err := request.Cookie(sessionCookieName) if err != nil { validSession = false } else { validSession, userId, userEmail, emailConfirmed, err = IsValidSessionWithInfo(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } handler := functionalURIs[request.Method][strings.ToLower(request.URL.Path)] // Load handler associated with URI from functionalURIs map if handler != nil { response.Header().Set("Cache-Control", "no-store") handler.(func(http.ResponseWriter, *http.Request))(response, request) // If handler function set, use it to handle http request proxied = "Served" } else if strings.HasPrefix(request.URL.Path, "/"+functionalPath+"/avatar/") { HandleAvatar(response, request) proxied = "Served" } else if requireEmailConfirm && validSession && !emailConfirmed { http.Redirect(response, request, "/"+functionalPath+"/confirmemail", http.StatusSeeOther) proxied = "Redirected" } else if requireAuthentication && !validSession && !sliceContainsPath(publicPageList, request.URL.Path) && !(request.URL.Path == "/" && sliceContainsPath(publicPageList, "//")) { http.Redirect(response, request, path.Join("/", functionalPath, "login"), http.StatusSeeOther) proxied = "Redirected" } else { if jwtSecret != "" { jwt, err := CreateJWT(userId) if err != nil { ServeErrorPage(response, err) return } request.Header.Add("Authorization", "Bearer "+jwt) } proxy.ServeHTTP(response, request) } logMessage(4, fmt.Sprintf("%s(%s) (%s) %s %s %d %s %s", userId, userEmail, request.RemoteAddr, proxied, request.Proto, request.ContentLength, request.Method, request.RequestURI)) } func sliceContainsPath(slice []string, path string) bool { for _, val := range slice { if val == path { return true } else if len(val) > 2 && string(val[len(val)-1]) == "/" && len(path) > len(val) && path[0:len(val)] == val { return true } } return false } func HandleLogin(response http.ResponseWriter, request *http.Request) { var validSession = false sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) } } if validSession { http.Redirect(response, request, path.Join("/", functionalPath, "manage"), http.StatusSeeOther) return } var innerForm = []GatehouseFormElement{} if allowUsernameLogin { innerForm = append( innerForm, FormCreateDivider(), FormCreateTextInput("username", "Username"), FormCreatePasswordInput("password", "Password"), ) } if allowPasswordReset && allowUsernameLogin { innerForm = append(innerForm, FormCreateSmallLink("/"+functionalPath+"/forgot", "Forgot my password..."), ) } if allowUsernameLogin { innerForm = append( innerForm, FormCreateSubmitInput("signin", "Sign In"), FormCreateDivider(), ) } if allowRegistration { innerForm = append(innerForm, FormCreateButtonLink("/"+functionalPath+"/register", "Create an Account"), FormCreateDivider(), ) } var loginPage GatehouseForm = GatehouseForm{ // Define login page appName + " - Sign in", "Sign In", "/" + functionalPath + "/submit/login", "POST", innerForm, []OIDCButton{ // {"Sign In with Google", "/" + functionalPath + "/static/icons/google.png", "#fff", "#000", "/" + functionalPath + "/auth/google"}, // {"Sign In with Microsoft Account", "/" + functionalPath + "/static/icons/microsoft.png", "#fff", "#000", "/" + functionalPath + "/auth/microsoft"}, // {"Sign In with Apple ID", "/" + functionalPath + "/static/icons/apple.png", "#fff", "#000", "/" + functionalPath + "/auth/apple"}, }, functionalPath, } ServePage(response, loginPage) } func HandleLogout(response http.ResponseWriter, request *http.Request) { sessionToken, err := request.Cookie(sessionCookieName) if err != nil { response.WriteHeader(410) ServePage(response, linkExpired) return } _, err = db.Exec(fmt.Sprintf("DELETE FROM %s_sessions WHERE session_token = ?", tablePrefix), sessionToken.Value) if err != nil { ServeErrorPage(response, err) return } http.SetCookie(response, &http.Cookie{Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1}) var innerform = []GatehouseFormElement{} innerform = append( innerform, FormCreateDivider(), FormCreateHint("You have signed out."), FormCreateButtonLink("/", "Back to site"), FormCreateDivider(), FormCreateButtonLink("/"+functionalPath+"/login", "Sign In"), ) if allowRegistration { innerform = append(innerform, FormCreateButtonLink("/"+functionalPath+"/register", "Create an Account")) } var logoutPage = GatehouseForm{ appName + " - Sign Out", "Goodbye", "", "", innerform, []OIDCButton{}, functionalPath, } ServePage(response, logoutPage) } func HandleForgotPassword(response http.ResponseWriter, request *http.Request) { if !allowPasswordReset { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } ServePage(response, forgotPasswordPage) } func HandleRecoveryCode(response http.ResponseWriter, request *http.Request) { if !allowMobileMFA { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } var ( userID string ) mfaCookie, err := request.Cookie(mfaCookieName) if err != nil { http.Redirect(response, request, "/"+functionalPath+"/login", http.StatusSeeOther) return } err = db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_mfa WHERE mfa_session = ? AND used = 0 AND type = 'totp'", tablePrefix), mfaCookie.Value).Scan(&userID) if err == sql.ErrNoRows { http.Redirect(response, request, "/"+functionalPath+"/login", http.StatusSeeOther) } else if err != nil { ServeErrorPage(response, err) } else { ServePage(response, mfaRecoveryCodePage) } } func HandleConfirmEmail(response http.ResponseWriter, request *http.Request) { ServePage(response, confirmEmailPage) } func HandleRegister(response http.ResponseWriter, request *http.Request) { if !allowRegistration { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } ServePage(response, registrationPage) } func HandleConfirmEmailCode(response http.ResponseWriter, request *http.Request) { emailCode := request.URL.Query().Get("c") validCode, err := ConfirmEmailCode(emailCode) if err != nil { ServeErrorPage(response, err) return } if !validCode { response.WriteHeader(410) ServePage(response, linkExpired) return } ServePage(response, confirmedEmailPage) } func HandlePasswordResetCode(response http.ResponseWriter, request *http.Request) { resetCode := request.URL.Query().Get("c") validResetCode, err := IsValidResetCode(resetCode) if err != nil { ServeErrorPage(response, err) return } if !validResetCode { response.WriteHeader(410) ServePage(response, linkExpired) return } customResetPage := resetPage customResetPage.FormAction += fmt.Sprintf("?c=%s", resetCode) ServePage(response, customResetPage) } func HandleResendConfirmation(response http.ResponseWriter, request *http.Request) { tokenCookie, err := request.Cookie(sessionCookieName) if err != nil { http.Redirect(response, request, path.Join("/", functionalPath, "login"), http.StatusSeeOther) return } var ( validSession bool = false pendingEmail bool = false ) validSession, err = IsValidSession(tokenCookie.Value) if err != nil { ServeErrorPage(response, err) return } pendingEmail, err = PendingEmailApproval(tokenCookie.Value) if err != nil { ServeErrorPage(response, err) return } if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "login"), http.StatusSeeOther) return } if !pendingEmail { response.WriteHeader(410) ServePage(response, linkExpired) return } emailSent, err := ResendConfirmationEmail(tokenCookie.Value) if err != nil { ServeErrorPage(response, err) return } if emailSent { ServePage(response, resendConfirmationPage) } else { response.WriteHeader(410) ServePage(response, linkExpired) } } func HandleIsUsernameTaken(response http.ResponseWriter, request *http.Request) { isValid, err := IsValidNewUsername(strings.ToLower(request.URL.Query().Get("u"))) if err != nil { ServeErrorPage(response, err) return } if !isValid { response.WriteHeader(400) fmt.Fprint(response, `Username taken.`) } else { response.WriteHeader(200) fmt.Fprint(response, `Username available.`) } } func HandleAddMFA(response http.ResponseWriter, request *http.Request) { if !allowMobileMFA { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } validSession := false sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "/login"), http.StatusSeeOther) return } var ( userID string username string email string mfaType string mfaStoredSecret *string mfaSecret string png []byte ) err = db.QueryRow(fmt.Sprintf("SELECT user_id, email, username, mfa_type, mfa_secret FROM %s_sessions INNER JOIN %s_accounts ON id = user_id WHERE session_token = ?", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&userID, &email, &username, &mfaType, &mfaStoredSecret) if err == sql.ErrNoRows { http.Redirect(response, request, path.Join("/", functionalPath, "/login"), http.StatusSeeOther) return } else if err != nil { ServeErrorPage(response, err) logMessage(1, err.Error()) return } else if mfaType == "token" { response.WriteHeader(400) fmt.Fprint(response, `Token MFA already configured.`) return } if mfaStoredSecret == nil { mfaSecret = GenerateOTPSecret() _, err = db.Exec(fmt.Sprintf("UPDATE %s_accounts SET mfa_secret = ? WHERE id = ?", tablePrefix), mfaSecret, userID) if err != nil { ServeErrorPage(response, err) return } } else { mfaSecret = *mfaStoredSecret } otpUrl := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30", appName, email, mfaSecret, appName) png, err = qrcode.Encode(otpUrl, qrcode.Medium, 256) if err != nil { ServeErrorPage(response, err) logMessage(1, fmt.Sprintf("Failed to encode QRCode: %s", otpUrl)) return } png64 := base64.StdEncoding.EncodeToString(png) var enrolPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - OTP Token", "Setup Authenticator", "/" + functionalPath + "/submit/validatemfa", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("To set up a OTP token, scan this QR code with a compatible authenticator app."), FormCreateQR(png64), FormCreateHint("Once added, enter the current code into the text box and confirm."), FormCreateTextInput("otp", "123456"), FormCreateSubmitInput("submit", "Confirm"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } ServePage(response, enrolPage) } func HandleElevateSession(response http.ResponseWriter, request *http.Request) { sessionCookie, err := request.Cookie(sessionCookieName) validSession := false if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } target := request.URL.Query().Get("t") if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "/login"), http.StatusSeeOther) return } if target == "" { response.WriteHeader(400) fmt.Fprintf(response, "Target required.") return } if !listContains(elevatedRedirectPages, target) { response.WriteHeader(400) fmt.Fprintf(response, "Invalid target.") return } var editedPage GatehouseForm = elevateSessionPage editedPage.FormAction = path.Join("/", functionalPath, fmt.Sprintf("/submit/elevate?t=%s", target)) ServePage(response, editedPage) } func HandleRemoveMFA(response http.ResponseWriter, request *http.Request) { if !allowMobileMFA { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } var ( validSession bool = false validCriticalSession bool = false ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } critialSessionCookie, err := request.Cookie(criticalCookieName) if err == nil { validCriticalSession, err = IsValidCriticalSession(critialSessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "/login"), http.StatusSeeOther) } else if !validCriticalSession { http.Redirect(response, request, path.Join("/", functionalPath, "elevate?t=removemfa"), http.StatusSeeOther) } else { ServePage(response, mfaRemovePage) } } func HandleManage(response http.ResponseWriter, request *http.Request) { var validSession bool = false sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "login"), http.StatusSeeOther) return } var ( userID string = "" email string = "" emailConfirmed bool = false mfaType string = "" dashButtons []GatehouseFormElement ) err = db.QueryRow(fmt.Sprintf("SELECT user_id, email, email_confirmed, mfa_type FROM %s_sessions INNER JOIN %s_accounts ON id = user_id WHERE session_token = ? AND critical = 0", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&userID, &email, &emailConfirmed, &mfaType) if err == sql.ErrNoRows { http.Redirect(response, request, path.Join("/", functionalPath, "login"), http.StatusSeeOther) return } else if err != nil { ServeErrorPage(response, err) return } // Account info options if allowEmailChange || allowUsernameChange { dashButtons = append( dashButtons, FormCreateDivider(), FormCreateHint("Account Details"), ) } if email == "" && allowEmailChange { dashButtons = append(dashButtons, FormCreateButtonLink(path.Join("/", functionalPath, "changeemail"), "Add Email Address")) } else if allowEmailChange { dashButtons = append(dashButtons, FormCreateButtonLink(path.Join("/", functionalPath, "changeemail"), "Change Email Address")) } if allowUsernameChange { dashButtons = append(dashButtons, FormCreateButtonLink(path.Join("/", functionalPath, "changeusername"), "Change Username")) } if allowAvatarChange { dashButtons = append(dashButtons, FormCreateButtonLink(path.Join("/", functionalPath, "changeavatar"), "Change Avatar")) } // Security options if allowMobileMFA || allowSessionRevoke { dashButtons = append( dashButtons, FormCreateDivider(), FormCreateHint("Account Security"), ) } if mfaType == "email" && allowMobileMFA { dashButtons = append(dashButtons, FormCreateButtonLink(path.Join("/", functionalPath, "addmfa"), "Add MFA Device")) } else if mfaType == "token" { dashButtons = append(dashButtons, FormCreateButtonLink(path.Join("/", functionalPath, "removemfa"), "Remove MFA Device")) } dashButtons = append( dashButtons, FormCreateButtonLink(path.Join("/", functionalPath, "logout"), "Sign Out"), ) if allowSessionRevoke { dashButtons = append( dashButtons, FormCreateButtonLink(path.Join("/", functionalPath, "revokesessions"), "Sign Out All Devices"), ) } if allowDeleteAccount { dashButtons = append( dashButtons, FormCreateDivider(), FormCreateHint("Danger Area"), FormCreateDangerButtonLink(path.Join("/", functionalPath, "deleteaccount"), "Delete Account"), ) } var dashboardPage GatehouseForm = GatehouseForm{ appName + " - Manage Account", "Manage Account", "/", "GET", dashButtons, []OIDCButton{}, functionalPath, } err = dashTemplate.Execute(response, dashboardPage) if err != nil { logMessage(1, fmt.Sprintf("Error rendering dashboard page: %s", err.Error())) ServeErrorPage(response, err) } } func HandleChangeEmail(response http.ResponseWriter, request *http.Request) { if !allowEmailChange { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } var validSession bool = false var validCriticalSession bool = false sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "/login"), http.StatusSeeOther) return } critialSessionCookie, err := request.Cookie(criticalCookieName) if err == nil { validCriticalSession, err = IsValidCriticalSession(critialSessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validCriticalSession { http.Redirect(response, request, path.Join("/", functionalPath, "elevate?t=changeemail"), http.StatusSeeOther) return } ServePage(response, emailChangePage) } func HandleChangeUsername(response http.ResponseWriter, request *http.Request) { if !allowUsernameChange { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } var ( userId string validSession bool = false validCriticalSession bool = false ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "/login"), http.StatusSeeOther) return } critialSessionCookie, err := request.Cookie(criticalCookieName) if err == nil { validCriticalSession, err = IsValidCriticalSession(critialSessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validCriticalSession { http.Redirect(response, request, path.Join("/", functionalPath, "elevate?t=changeusername"), http.StatusSeeOther) return } err = db.QueryRow(fmt.Sprintf("SELECT id FROM %s_accounts INNER JOIN %s_sessions ON user_id = id WHERE session_token = ? AND username_changed < CURRENT_TIMESTAMP - INTERVAL 30 DAY", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&userId) if err == sql.ErrNoRows { ServePage(response, usernameChangeBlockedPage) } else if err != nil { ServeErrorPage(response, err) return } else { ServePage(response, usernameChangePage) } } func HandleChangeAvatar(response http.ResponseWriter, request *http.Request) { if !allowAvatarChange { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } var ( validSession bool = false ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "/login"), http.StatusSeeOther) return } ServePage(response, avatarChangePage) } func HandleDeleteAccount(response http.ResponseWriter, request *http.Request) { if !allowDeleteAccount { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } var ( validSession bool = false validCriticalSession bool = false ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "/login"), http.StatusSeeOther) return } critialSessionCookie, err := request.Cookie(criticalCookieName) if err == nil { validCriticalSession, err = IsValidCriticalSession(critialSessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validCriticalSession { http.Redirect(response, request, path.Join("/", functionalPath, "elevate?t=deleteaccount"), http.StatusSeeOther) return } ServePage(response, deleteAccountPage) } func HandleSessionRevoke(response http.ResponseWriter, request *http.Request) { if !allowSessionRevoke { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } var validSession bool = false sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { http.Redirect(response, request, path.Join("/", functionalPath, "/login"), http.StatusSeeOther) return } var sessionRevokePage = GatehouseForm{ appName + " - Sign Out All Devices", "Sign Out All Devices", "/" + functionalPath + "/submit/revokesessions", "POST", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Are you sure you wish to sign out all devices?"), FormCreateDivider(), FormCreateDangerSubmitInput("submit", "Sign Out"), FormCreateDivider(), FormCreateHint("Changed your mind?"), FormCreateButtonLink("/"+functionalPath+"/manage", "Cancel"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } ServePage(response, sessionRevokePage) } func HandleAvatar(response http.ResponseWriter, request *http.Request) { pathSplit := strings.Split(request.URL.Path, "/") avatarID := strings.Split(pathSplit[len(pathSplit)-1], ".")[0] var data []byte err := db.QueryRow(fmt.Sprintf("SELECT data FROM %s_avatars WHERE avatar_id = ?", tablePrefix), avatarID).Scan(&data) if err == sql.ErrNoRows { response.WriteHeader(404) fmt.Fprint(response, `Not Found.`) return } else if err != nil { ServeErrorPage(response, err) return } _, err = response.Write(data) if err != nil { ServeErrorPage(response, err) return } } func HandleMyAvatar(response http.ResponseWriter, request *http.Request) { validSession := false var userID string sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, userID, _, _, err = IsValidSessionWithInfo(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { response.WriteHeader(404) fmt.Fprint(response, `Not Found.`) return } var avatarURL string err = db.QueryRow(fmt.Sprintf("SELECT avatar_url FROM %s_accounts WHERE id = ?", tablePrefix), userID).Scan(&avatarURL) if err != nil { ServeErrorPage(response, err) return } http.Redirect(response, request, avatarURL, http.StatusSeeOther) } func HandleMyUsername(response http.ResponseWriter, request *http.Request) { validSession := false var userID string sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, userID, _, _, err = IsValidSessionWithInfo(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { response.WriteHeader(404) fmt.Fprint(response, `Not Found.`) return } var username string err = db.QueryRow(fmt.Sprintf("SELECT username FROM %s_accounts WHERE id = ?", tablePrefix), userID).Scan(&username) if err != nil { ServeErrorPage(response, err) return } response.WriteHeader(200) fmt.Fprint(response, username) } ////////////////////////////////////////////////////////////////////////////// // Form Submissions func HandleSubLogin(response http.ResponseWriter, request *http.Request) { var ( username string = strings.ToLower(request.FormValue("username")) password string = request.FormValue("password") userID string email string emailConfirmed bool passwordHash string mfaType string ) if !allowUsernameLogin { response.WriteHeader(410) fmt.Fprint(response, `410 - Feature Disabled.`) return } if username == "" || password == "" { response.WriteHeader(400) fmt.Fprint(response, `400 - Credentials Not Provided.`) return } err := db.QueryRow(fmt.Sprintf("SELECT id, email, email_confirmed, password, mfa_type FROM %s_accounts WHERE username = ?", tablePrefix), username).Scan(&userID, &email, &emailConfirmed, &passwordHash, &mfaType) if err == sql.ErrNoRows { http.Redirect(response, request, path.Join("/", functionalPath, "/login?error=invalid"), http.StatusSeeOther) return } else if err != nil { ServeErrorPage(response, err) return } if !CheckPasswordHash(password, passwordHash) { http.Redirect(response, request, path.Join("/", functionalPath, "/login?error=invalid"), http.StatusSeeOther) return } if !allowMobileMFA { AuthenticateRequestor(response, request, userID) return } if mfaType == "email" && !emailConfirmed { AuthenticateRequestor(response, request, userID) return } // Begin multifactor session sessionToken, err := GenerateMfaSessionToken() if err != nil { ServeErrorPage(response, err) return } cookie := http.Cookie{Name: mfaCookieName, Value: sessionToken, SameSite: http.SameSiteStrictMode, Secure: false, Path: "/"} http.SetCookie(response, &cookie) if mfaType == "email" { mfaToken := GenerateRandomNumbers(6) _, err = db.Exec(fmt.Sprintf("INSERT INTO %s_mfa (mfa_session, type, user_id, token) VALUES (?, ?, ?, ?)", tablePrefix), sessionToken, "email", userID, mfaToken) if err != nil { ServeErrorPage(response, err) return } err = sendMail( strings.ToLower(email), "Sign In - OTP", username, fmt.Sprintf("Your OTP code for %s is: %s", appName, mfaToken), "", "<b>If you did not request this action, please change your password immediately.</b>", ) if err != nil { ServeErrorPage(response, err) logMessage(2, fmt.Sprintf("User %s was not sent their email MFA code: %s", userID, err.Error())) } else { ServePage(response, mfaEmailPage) } } else if mfaType == "token" { _, err := db.Exec(fmt.Sprintf("INSERT INTO %s_mfa (mfa_session, type, user_id, token) VALUES (?, ?, ?, ?)", tablePrefix), sessionToken, "totp", userID, "") if err != nil { ServeErrorPage(response, err) return } ServePage(response, mfaTokenPage) } else { ServeErrorPage(response, nil) logMessage(1, fmt.Sprintf("Unrecognised MFA type in database: %s", mfaType)) } } func HandleSubRegister(response http.ResponseWriter, request *http.Request) { username := strings.ToLower(request.FormValue("newUsername")) email := strings.ToLower(request.FormValue("email")) password := request.FormValue("password") passwordConfirm := request.FormValue("passwordConfirm") if !allowRegistration { response.WriteHeader(410) fmt.Fprint(response, `410 - Feature Disabled.`) return } validUsername, err := IsValidNewUsername(username) if err != nil { ServeErrorPage(response, err) return } if !validUsername { response.WriteHeader(400) fmt.Fprint(response, `400 - Invalid Username.`) return } validEmail, err := IsValidNewEmail(email) if err != nil { ServeErrorPage(response, err) return } if !validEmail { response.WriteHeader(400) fmt.Fprint(response, `400 - Invalid email.`) return } if !IsValidPassword(password) { response.WriteHeader(400) fmt.Fprint(response, `400 - Invalid Password.`) return } if password != passwordConfirm { response.WriteHeader(400) fmt.Fprint(response, `400 - Passwords did not match.`) return } userID, err := GenerateUserID() if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("INSERT INTO %s_accounts (id, username, email, password) VALUES (?, ?, ?, ?)", tablePrefix), userID, username, email, HashPassword(password)) if err != nil { ServeErrorPage(response, err) return } err = SendEmailConfirmationCode(userID, email, username) if err != nil { ServeErrorPage(response, err) return } AuthenticateRequestor(response, request, userID) } func HandleSubResetRequest(response http.ResponseWriter, request *http.Request) { email := request.FormValue("email") if !allowPasswordReset { response.WriteHeader(410) fmt.Fprint(response, `410 - Feature Disabled.`) return } emailExists, err := ResetPasswordRequest(email) if err != nil { ServeErrorPage(response, err) return } if !emailExists { ServePage(response, resetNotSentPage) } ServePage(response, resetSentPage) } func HandleSubReset(response http.ResponseWriter, request *http.Request) { if !allowPasswordReset { response.WriteHeader(410) fmt.Fprint(response, `410 - Feature Disabled.`) return } code := request.URL.Query().Get("c") password := request.FormValue("password") passwordConfirm := request.FormValue("passwordConfirm") if code == "" || !IsValidPassword(password) || password != passwordConfirm { response.WriteHeader(400) fmt.Fprint(response, `400 - Invalid request.`) return } var userID string err := db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_resets WHERE reset_token = ? AND used = 0", tablePrefix), code).Scan(&userID) if err == sql.ErrNoRows { response.WriteHeader(403) fmt.Fprint(response, `403 - Unauthorized.`) return } else if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("UPDATE %s_resets INNER JOIN %s_accounts ON user_id = id SET password = ?, used = 1 WHERE reset_token = ?", tablePrefix, tablePrefix), HashPassword(password), code) if err != nil { ServeErrorPage(response, err) return } ServePage(response, resetSuccessPage) } func HandleSubOTP(response http.ResponseWriter, request *http.Request) { var ( otpInput string = request.FormValue("token") mfaType string mfaStoredToken string mfaSecret *string userID string ) mfaSession, err := request.Cookie(mfaCookieName) if err != nil { response.WriteHeader(400) fmt.Fprint(response, `Invalid request.`) return } err = db.QueryRow(fmt.Sprintf("SELECT user_id, token, mfa_type, mfa_secret FROM %s_mfa INNER JOIN %s_accounts ON user_id = id WHERE mfa_session = ? AND created > CURRENT_TIMESTAMP - INTERVAL 1 HOUR AND used = 0", tablePrefix, tablePrefix), mfaSession.Value).Scan(&userID, &mfaStoredToken, &mfaType, &mfaSecret) if err == sql.ErrNoRows { response.WriteHeader(400) fmt.Fprint(response, `Invalid request.`) return } else if err != nil { ServeErrorPage(response, err) return } if mfaType == "email" && mfaStoredToken == otpInput { AuthenticateRequestor(response, request, userID) } else if mfaType == "token" { otp, _ := GenerateOTP(*mfaSecret, 30) if otp == otpInput { AuthenticateRequestor(response, request, userID) } else { http.Redirect(response, request, "/"+functionalPath+"/login?error=invalid", http.StatusSeeOther) } } else { http.Redirect(response, request, "/"+functionalPath+"/login?error=invalid", http.StatusSeeOther) } _, err = db.Exec(fmt.Sprintf("UPDATE %s_mfa SET used = 1 WHERE mfa_session = ?", tablePrefix), mfaSession.Value) if err != nil { logMessage(1, err.Error()) } } func HandleSubMFAValidate(response http.ResponseWriter, request *http.Request) { if !allowMobileMFA { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } var ( submitOtp string = request.FormValue("otp") mfaSecret string userID string email string username string validSession bool = false ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { response.WriteHeader(403) fmt.Fprint(response, `Unauthorized.`) return } err = db.QueryRow(fmt.Sprintf("SELECT id, mfa_secret, email, username FROM %s_accounts INNER JOIN %s_sessions ON id = user_id WHERE session_token = ? AND mfa_type = 'email' AND mfa_secret IS NOT NULL", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&userID, &mfaSecret, &email, &username) if err == sql.ErrNoRows { response.WriteHeader(400) fmt.Fprint(response, `Invalid Request.`) return } else if err != nil { ServeErrorPage(response, err) return } otp, err := GenerateOTP(mfaSecret, 30) if err != nil { ServeErrorPage(response, err) return } if otp != submitOtp { response.WriteHeader(400) ServePage(response, mfaFailedPage) return } _, err = db.Exec(fmt.Sprintf("UPDATE %s_accounts SET mfa_type = 'token' WHERE id = ?", tablePrefix), userID) if err != nil { ServeErrorPage(response, err) return } var recoveryCodes string = "" for i := 0; i < 12; i++ { recoveryCode := GenerateRandomNumbers(8) recoveryCodes = recoveryCodes + "<br>" + recoveryCode _, err = db.Exec(fmt.Sprintf("INSERT IGNORE INTO %s_recovery (user_id, code) VALUES (?, ?)", tablePrefix), userID, recoveryCode) if err != nil { ServeErrorPage(response, err) return } } var mfaValidatedPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - MFA Validated", "Success", "/", "GET", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("Your MFA device was successfully registered. You are now able to sign in with your authenticator OTP in the future."), FormCreateDivider(), FormCreateHint("In the event you lose your MFA device, a recovery code can be used instead."), FormCreateHint("Your recovery codes:"), FormCreateHint(recoveryCodes), FormCreateHint("Ensure these are recorded somewhere safe."), FormCreateDivider(), FormCreateButtonLink("/"+functionalPath+"/manage", "Back to Dashboard"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } ServePage(response, mfaValidatedPage) if enableMFAAlerts { err = sendMail(email, "MFA Device Added", username, "You have successfully added an MFA device to your account.", "", "") if err != nil { logMessage(3, fmt.Sprintf("User %s was not sent MFA added email.", userID)) } } } func HandleSubElevate(response http.ResponseWriter, request *http.Request) { var ( password string = request.FormValue("password") passwordHash string userID string validSession bool = false ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { response.WriteHeader(403) fmt.Fprint(response, `Unauthorized.`) return } target := request.URL.Query().Get("t") if target == "" { response.WriteHeader(400) fmt.Fprintf(response, "Target required.") return } if !listContains(elevatedRedirectPages, target) { response.WriteHeader(400) fmt.Fprintf(response, "Invalid target.") return } err = db.QueryRow(fmt.Sprintf("SELECT id, password FROM %s_accounts INNER JOIN %s_sessions ON id = user_id WHERE session_token = ?", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&userID, &passwordHash) if err != nil { ServeErrorPage(response, err) return } if !CheckPasswordHash(password, passwordHash) { http.Redirect(response, request, "/"+functionalPath+fmt.Sprintf("/elevate?error=invalid&t=%s", target), http.StatusSeeOther) return } elevatedSessionToken, err := GenerateSessionToken() if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("INSERT INTO %s_sessions (session_token, user_id, critical) VALUES (?, ?, 1)", tablePrefix), elevatedSessionToken, userID) if err != nil { ServeErrorPage(response, err) return } cookie := http.Cookie{Name: criticalCookieName, Value: elevatedSessionToken, SameSite: http.SameSiteStrictMode, Secure: false, Path: "/"} http.SetCookie(response, &cookie) http.Redirect(response, request, path.Join("/", functionalPath, target), http.StatusSeeOther) } func HandleSubRemoveMFA(response http.ResponseWriter, request *http.Request) { var ( validSession bool = false validCriticalSession bool = false mfaType string username string email string sessionUserID string criticalUserID string ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } critialSessionCookie, err := request.Cookie(criticalCookieName) if err == nil { validCriticalSession, err = IsValidCriticalSession(critialSessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { response.WriteHeader(403) fmt.Fprint(response, `Unauthorized.`) return } if !validCriticalSession { http.Redirect(response, request, path.Join("/", functionalPath, "elevate?t=removemfa"), http.StatusSeeOther) return } err = db.QueryRow(fmt.Sprintf("SELECT id, mfa_type FROM %s_accounts INNER JOIN %s_sessions ON id = user_id WHERE session_token = ? AND critical = 0", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&sessionUserID, &mfaType) if err != nil { ServeErrorPage(response, err) return } err = db.QueryRow(fmt.Sprintf("SELECT id, email, username FROM %s_accounts INNER JOIN %s_sessions ON id = user_id WHERE session_token = ? AND critical = 1", tablePrefix, tablePrefix), critialSessionCookie.Value).Scan(&criticalUserID, &email, &username) if err != nil { ServeErrorPage(response, err) return } if sessionUserID != criticalUserID { response.WriteHeader(500) fmt.Fprint(response, `Undefined error.`) logMessage(3, fmt.Sprintf("User (%s) attempted to use another user's (%s) elevated session token.", sessionUserID, criticalUserID)) return } if mfaType != "token" { response.WriteHeader(400) fmt.Fprint(response, `MFA Device not registered`) return } _, err = db.Exec(fmt.Sprintf("UPDATE %s_accounts SET mfa_type = 'email', mfa_secret = NULL WHERE id = ?", tablePrefix), sessionUserID) if err != nil { ServeErrorPage(response, err) return } ServePage(response, mfaRemovedPage) if enableMFAAlerts { err = sendMail(email, "MFA Device Removed", username, "You have successfully removed an MFA device to your account.", "", "If you did not request this action, change your password immediately.") if err != nil { logMessage(3, fmt.Sprintf("User %s was not sent MFA removed email.", sessionUserID)) } } } func HandleSubEmailChange(response http.ResponseWriter, request *http.Request) { if !allowEmailChange { response.WriteHeader(410) fmt.Fprint(response, `Feature disabled.`) return } var ( validSession bool = false validCriticalSession bool = false userID string username string email string ) email = strings.ToLower(request.FormValue("newemail")) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } critialSessionCookie, err := request.Cookie(criticalCookieName) if err == nil { validCriticalSession, err = IsValidCriticalSession(critialSessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession || !validCriticalSession { response.WriteHeader(403) fmt.Fprint(response, `Unauthorized.`) return } validEmail, err := IsValidNewEmail(email) if err != nil { ServeErrorPage(response, err) return } if !validEmail { response.WriteHeader(400) fmt.Fprint(response, `400 - Invalid email.`) return } err = db.QueryRow(fmt.Sprintf("SELECT user_id, username FROM %s_accounts INNER JOIN %s_sessions ON user_id = id WHERE session_token = ?", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&userID, &username) if err == sql.ErrNoRows { response.WriteHeader(400) fmt.Fprint(response, `Invalid request.`) return } else if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("UPDATE %s_accounts SET email = ?, email_confirmed = 0, email_resent = 0 WHERE id = ?", tablePrefix), email, userID) if err != nil { ServeErrorPage(response, err) return } err = SendEmailConfirmationCode(userID, email, username) if err != nil { ServeErrorPage(response, err) return } ServePage(response, confirmEmailPage) } func HandleSubUsernameChange(response http.ResponseWriter, request *http.Request) { if !allowUsernameChange { response.WriteHeader(410) fmt.Fprint(response, `Feature disabled.`) return } var ( validSession bool = false validCriticalSession bool = false userID string username string email string ) username = strings.ToLower(request.FormValue("newUsername")) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } critialSessionCookie, err := request.Cookie(criticalCookieName) if err == nil { validCriticalSession, err = IsValidCriticalSession(critialSessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession || !validCriticalSession { response.WriteHeader(403) fmt.Fprint(response, `Unauthorized.`) return } validUsername, err := IsValidNewUsername(username) if err != nil { ServeErrorPage(response, err) return } if !validUsername { response.WriteHeader(400) fmt.Fprint(response, `400 - Invalid username.`) return } err = db.QueryRow(fmt.Sprintf("SELECT user_id, email FROM %s_accounts INNER JOIN %s_sessions ON user_id = id WHERE session_token = ? AND username_changed < CURRENT_TIMESTAMP - INTERVAL 30 DAY", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&userID, &email) if err == sql.ErrNoRows { response.WriteHeader(400) fmt.Fprint(response, `Invalid request.`) return } else if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("UPDATE %s_accounts SET username = ?, username_changed = CURRENT_TIMESTAMP WHERE id = ? AND username_changed < CURRENT_TIMESTAMP - INTERVAL 30 DAY", tablePrefix), username, userID) if err != nil { ServeErrorPage(response, err) return } ServePage(response, confirmedUsernameChangePage) err = sendMail(email, "Username Changed", username, "Your username has been changed successfully. You will be able to change your username again after 30 days.", "", "If you did not perform this action, please change your password immediately.") if err != nil { logMessage(3, fmt.Sprintf("User %s was not notified of username change.", username)) } } func HandleSubAvatarChange(response http.ResponseWriter, request *http.Request) { if !allowAvatarChange { response.WriteHeader(410) fmt.Fprint(response, `Feature Disabled.`) return } var ( validSession bool = false image image.Image imageBytes []byte userID string ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, userID, _, _, err = IsValidSessionWithInfo(sessionCookie.Value) if err != nil { response.WriteHeader(403) fmt.Fprint(response, `Unauthorized.`) return } } if !validSession { response.WriteHeader(403) fmt.Fprint(response, `Unauthorized.`) return } err = request.ParseMultipartForm(6 << 20) if err != nil { ServePage(response, avatarInvalidPage) return } file, handler, err := request.FormFile("avatarupload") if err != nil { ServeErrorPage(response, err) return } defer file.Close() logMessage(5, fmt.Sprintf("User %s uploaded file %s: %d %s", userID, handler.Filename, handler.Size, handler.Header["Content-Type"])) if handler.Header["Content-Type"][0] == "image/png" { image, err = png.Decode(file) if err != nil { ServeErrorPage(response, err) return } } else if handler.Header["Content-Type"][0] == "image/jpeg" { image, err = jpeg.Decode(file) if err != nil { ServeErrorPage(response, err) return } } else { ServePage(response, avatarInvalidPage) return } var options = jpeg.Options{ Quality: 70, } imageBuffer := new(bytes.Buffer) err = jpeg.Encode(imageBuffer, image, &options) if err != nil { ServeErrorPage(response, err) return } imageBytes = imageBuffer.Bytes() avatarID, err := GenerateAvatarID() if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("INSERT INTO %s_avatars (avatar_id, format, data) VALUES (?, ?, ?)", tablePrefix), avatarID, "jpeg", imageBytes) if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("UPDATE %s_accounts SET avatar_url = ? WHERE id = ?", tablePrefix), fmt.Sprintf("/%s/avatar/%s.jpg", functionalPath, avatarID), userID) if err != nil { ServeErrorPage(response, err) return } ServePage(response, avatarChangedPage) } func HandleSubDeleteAccount(response http.ResponseWriter, request *http.Request) { if !allowDeleteAccount { response.WriteHeader(410) fmt.Fprint(response, `Feature Disabled.`) return } var ( validSession bool = false validCriticalSession bool = false userID string username string ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } critialSessionCookie, err := request.Cookie(criticalCookieName) if err == nil { validCriticalSession, err = IsValidCriticalSession(critialSessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession || !validCriticalSession { response.WriteHeader(403) fmt.Fprint(response, `Unauthorized.`) return } err = db.QueryRow(fmt.Sprintf("SELECT user_id, username FROM %s_accounts INNER JOIN %s_sessions ON user_id = id WHERE session_token = ?", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&userID, &username) if err == sql.ErrNoRows { response.WriteHeader(400) fmt.Fprint(response, `Invalid request.`) } else if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("DELETE FROM %s_accounts WHERE id = ?", tablePrefix), userID) if err != nil { ServeErrorPage(response, err) return } ServePage(response, deletedAccountPage) } func HandleSubRecoveryCode(response http.ResponseWriter, request *http.Request) { if !allowMobileMFA { response.WriteHeader(410) ServePage(response, disabledFeaturePage) return } var ( userID string recoveryToken string = request.FormValue("token") ) mfaCookie, err := request.Cookie(mfaCookieName) if err != nil { http.Redirect(response, request, "/"+functionalPath+"/login", http.StatusSeeOther) return } err = db.QueryRow(fmt.Sprintf("SELECT id FROM %s_mfa INNER JOIN %s_recovery ON %s_mfa.user_id = %s_recovery.user_id INNER JOIN %s_accounts ON id = %s_recovery.user_id WHERE mfa_session = ? AND %s_mfa.used = 0 AND type = 'totp' AND %s_mfa.created > CURRENT_TIMESTAMP - INTERVAL 1 HOUR AND code = ?", tablePrefix, tablePrefix, tablePrefix, tablePrefix, tablePrefix, tablePrefix, tablePrefix, tablePrefix), mfaCookie.Value, recoveryToken).Scan(&userID) if err == sql.ErrNoRows { http.Redirect(response, request, "/"+functionalPath+"/login?error=invalid", http.StatusSeeOther) return } else if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("UPDATE %s_recovery SET used = 1 WHERE user_id = ? AND code = ?", tablePrefix), userID, recoveryToken) if err != nil { ServeErrorPage(response, err) return } AuthenticateRequestor(response, request, userID) } func HandleSubSessionRevoke(response http.ResponseWriter, request *http.Request) { if !allowSessionRevoke { response.WriteHeader(410) fmt.Fprint(response, `Feature Disabled.`) return } var ( validSession bool = false userID string ) sessionCookie, err := request.Cookie(sessionCookieName) if err == nil { validSession, err = IsValidSession(sessionCookie.Value) if err != nil { ServeErrorPage(response, err) return } } if !validSession { response.WriteHeader(403) fmt.Fprint(response, `Unauthorized.`) return } err = db.QueryRow(fmt.Sprintf("SELECT user_id FROM %s_accounts INNER JOIN %s_sessions ON user_id = id WHERE session_token = ?", tablePrefix, tablePrefix), sessionCookie.Value).Scan(&userID) if err == sql.ErrNoRows { response.WriteHeader(400) fmt.Fprint(response, `Invalid request.`) return } else if err != nil { ServeErrorPage(response, err) return } _, err = db.Exec(fmt.Sprintf("DELETE FROM %s_sessions WHERE user_id = ?", tablePrefix), userID) if err != nil { ServeErrorPage(response, err) return } var logoutPage = GatehouseForm{ appName + " - Sign Out", "Signed Out All Devices", "", "", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("You have signed out all devices."), FormCreateButtonLink("/", "Back to site"), FormCreateDivider(), FormCreateButtonLink("/"+functionalPath+"/login", "Sign In"), }, []OIDCButton{}, functionalPath, } http.SetCookie(response, &http.Cookie{Name: sessionCookieName, Value: "", Path: "/", MaxAge: -1}) ServePage(response, logoutPage) }
package main import ( "database/sql" "fmt" "net/http" "net/http/httputil" "net/url" "os" "strconv" "strings" "text/template" "time" _ "github.com/go-sql-driver/mysql" ) ///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // LOAD ENVIRONMENT VARIABLES var ( logVerbosity int = envWithDefaultInt("LOG_LEVEL", 4) backendServerAddr string = envWithDefault("BACKEND_SERVER", "127.0.0.1") // Load configuration from environment or set defaults backendServerPort string = envWithDefault("BACKEND_PORT", "9000") listenPort string = envWithDefault("LISTEN_PORT", "8080") functionalPath string = envWithDefault("GATEHOUSE_PATH", "gatehouse") appName string = envWithDefault("APP_NAME", "Gatehouse") mysqlHost string = envWithDefault("MYSQL_HOST", "127.0.0.1") mysqlPort string = envWithDefault("MYSQL_PORT", "3306") mysqlUser string = envWithDefault("MYSQL_USER", "gatehouse") mysqlPassword string = envWithDefault("MYSQL_PASS", "password") mysqlDatabase string = envWithDefault("MYSQL_DATABASE", "gatehouse") tablePrefix string = envWithDefault("TABLE_PREFIX", "gatehouse") sessionCookieName string = envWithDefault("SESSION_COOKIE", "gatehouse-session") mfaCookieName string = envWithDefault("MFA_COOKIE", "gatehouse-mfa") criticalCookieName string = envWithDefault("CRITICAL_COOKIE", "gatehouse-crit") requireAuthentication bool = envWithDefaultBool("REQUIRE_AUTH", true) requireEmailConfirm bool = envWithDefaultBool("REQUIRE_EMAIL_CONFIRM", true) smtpHost string = envWithDefault("SMTP_HOST", "127.0.0.1") smtpPort string = envWithDefault("SMTP_PORT", "25") smtpUser string = envWithDefault("SMTP_USER", "") smtpPass string = envWithDefault("SMTP_PASS", "") smtpTLS bool = envWithDefaultBool("SMTP_TLS", false) smtpTLSSkipVerify bool = envWithDefaultBool("SMTP_TLS_SKIP", false) senderAddress string = envWithDefault("MAIL_ADDRESS", "gatehouse@mydomain.local") webDomain string = envWithDefault("WEB_DOMAIN", "http://localhost:8080") formTemplate *template.Template emailTemplate *template.Template dashTemplate *template.Template functionalURIs map[string]map[string]interface{} proxy *httputil.ReverseProxy db *sql.DB elevatedRedirectPages = []string{"removemfa", "changeemail", "deleteaccount", "changeusername"} sevMap = [6]string{"FATAL", "CRITICAL", "ERROR", "WARNING", "INFO", "DEBUG"} gatehouseVersion string = "%VERSION%" allowRegistration bool = envWithDefaultBool("ALLOW_REGISTRATION", true) allowUsernameLogin bool = envWithDefaultBool("ALLOW_USERNAME_LOGIN", true) allowPasswordReset bool = envWithDefaultBool("ALLOW_PASSWORD_RESET", true) allowMobileMFA bool = envWithDefaultBool("ALLOW_MOBILE_MFA", true) allowUsernameChange bool = envWithDefaultBool("ALLOW_USERNAME_CHANGE", true) allowEmailChange bool = envWithDefaultBool("ALLOW_EMAIL_CHANGE", true) allowDeleteAccount bool = envWithDefaultBool("ALLOW_DELETE_ACCOUNT", true) allowSessionRevoke bool = envWithDefaultBool("ALLOW_SESSION_REVOKE", true) allowAvatarChange bool = envWithDefaultBool("ALLOW_AVATAR_CHANGE", true) enableLoginAlerts bool = envWithDefaultBool("ENABLE_LOGIN_ALERTS", true) enableMFAAlerts bool = envWithDefaultBool("ENABLE_MFA_ALERTS", true) publicPages string = envWithDefault("PUBLIC_PAGES", "") jwtSecret string = envWithDefault("JWT_SECRET", "") publicPageList []string = strings.Split(publicPages, ",") ) func main() { printBanner() InitDatabase(10) defer db.Close() LoadTemplates() LoadFuncionalURIs() initProxy() staticFiles := http.StripPrefix("/"+functionalPath+"/static/", http.FileServer(http.Dir("./assets/static/"))) http.Handle("/"+functionalPath+"/static/", staticFiles) // If /gatehouse/static, use static assets http.HandleFunc("/", HandleMain) server := &http.Server{ Addr: ":" + listenPort, ReadHeaderTimeout: 10 * time.Second, } logMessage(4, fmt.Sprintf("Listening for incoming requests on %s", server.Addr)) err := server.ListenAndServe() if err != nil { logMessage(0, fmt.Sprintf("Server error: %s", err.Error())) } } func printBanner() { fmt.Println(" _____ _ _ ") fmt.Println(" / ____| | | | | ") fmt.Println(" | | __ __ _| |_ ___| |__ ___ _ _ ___ ___ ") fmt.Println(" | | |_ |/ _\\ | __/ _ \\ _ \\ / _ \\| | | / __|/ _ \\") fmt.Println(" | |__| | (_| | || __/ | | | (_) | |_| \\__ \\ __/") fmt.Println(" \\_____|\\__,_|\\__\\___|_| |_|\\___/ \\__,_|___/\\___|") fmt.Println(" ") fmt.Println("Version " + gatehouseVersion) } func envWithDefault(variableName string, defaultString string) string { val := os.Getenv(variableName) if len(val) == 0 { return defaultString } else { logMessage(5, fmt.Sprintf("Loaded %s value '%s'", variableName, val)) return val } } func envWithDefaultInt(variableName string, defaultInt int) int { val := os.Getenv(variableName) if len(val) == 0 { return defaultInt } else { i, err := strconv.Atoi(val) if err != nil { fmt.Printf("[CRITICAL] Integer parameter %s is not valid\n", val) os.Exit(1) } return i } } func envWithDefaultBool(variableName string, defaultBool bool) bool { var ( trueValues []string = []string{"true", "yes", "on"} falseValues []string = []string{"false", "no", "off"} ) val := os.Getenv(variableName) if len(val) == 0 { return defaultBool } else if listContains(trueValues, strings.ToLower(val)) { logMessage(5, fmt.Sprintf("Loaded %s value 'true'", variableName)) return true } else if listContains(falseValues, strings.ToLower(val)) { logMessage(5, fmt.Sprintf("Loaded %s value 'false'", variableName)) return false } else { logMessage(3, fmt.Sprintf("Invalid true/false value set for %s\n", variableName)) os.Exit(1) return false } } func listContains(s []string, str string) bool { for _, v := range s { if v == str { return true } } return false } func LoadTemplates() { var err error formTemplate, err = template.ParseFiles("assets/form.html") // Preload form page template into memory if err != nil { logMessage(0, fmt.Sprintf("Unable to load HTML template from assets/form.html: %s", err.Error())) os.Exit(1) } emailTemplate, err = template.ParseFiles("assets/email.html") // Preload email template into memory if err != nil { logMessage(0, fmt.Sprintf("Unable to load HTML template from assets/email.html: %s", err.Error())) os.Exit(1) } dashTemplate, err = template.ParseFiles("assets/dashboard.html") // Preload dashboard template into memory if err != nil { logMessage(0, fmt.Sprintf("Unable to load HTML template from assets/dashboard.html: %s", err.Error())) os.Exit(1) } } func LoadFuncionalURIs() { functionalURIs = map[string]map[string]interface{}{ "GET": { "/" + functionalPath + "/login": HandleLogin, "/" + functionalPath + "/logout": HandleLogout, "/" + functionalPath + "/register": HandleRegister, "/" + functionalPath + "/forgot": HandleForgotPassword, "/" + functionalPath + "/confirmemail": HandleConfirmEmail, "/" + functionalPath + "/confirmcode": HandleConfirmEmailCode, "/" + functionalPath + "/resetpassword": HandlePasswordResetCode, "/" + functionalPath + "/resendconfirmation": HandleResendConfirmation, "/" + functionalPath + "/usernametaken": HandleIsUsernameTaken, "/" + functionalPath + "/addmfa": HandleAddMFA, "/" + functionalPath + "/removemfa": HandleRemoveMFA, "/" + functionalPath + "/elevate": HandleElevateSession, "/" + functionalPath + "/manage": HandleManage, "/" + functionalPath + "/changeemail": HandleChangeEmail, "/" + functionalPath + "/changeusername": HandleChangeUsername, "/" + functionalPath + "/changeavatar": HandleChangeAvatar, "/" + functionalPath + "/myavatar": HandleMyAvatar, "/" + functionalPath + "/myusername": HandleMyUsername, "/" + functionalPath + "/deleteaccount": HandleDeleteAccount, "/" + functionalPath + "/recoverycode": HandleRecoveryCode, "/" + functionalPath + "/revokesessions": HandleSessionRevoke, }, "POST": { "/" + functionalPath + "/submit/register": HandleSubRegister, "/" + functionalPath + "/submit/login": HandleSubLogin, "/" + functionalPath + "/submit/resetrequest": HandleSubResetRequest, "/" + functionalPath + "/submit/reset": HandleSubReset, "/" + functionalPath + "/submit/mfa": HandleSubOTP, "/" + functionalPath + "/submit/validatemfa": HandleSubMFAValidate, "/" + functionalPath + "/submit/elevate": HandleSubElevate, "/" + functionalPath + "/submit/removemfa": HandleSubRemoveMFA, "/" + functionalPath + "/submit/changeemail": HandleSubEmailChange, "/" + functionalPath + "/submit/changeusername": HandleSubUsernameChange, "/" + functionalPath + "/submit/changeavatar": HandleSubAvatarChange, "/" + functionalPath + "/submit/deleteaccount": HandleSubDeleteAccount, "/" + functionalPath + "/submit/recoverycode": HandleSubRecoveryCode, "/" + functionalPath + "/submit/revokesessions": HandleSubSessionRevoke, }, } } func InitDatabase(n int) { var err error db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/", mysqlUser, mysqlPassword, mysqlHost, mysqlPort)) if err != nil { logMessage(0, fmt.Sprintf("Failed to connect to create database connection: %s", err.Error())) os.Exit(1) } _, err = db.Exec(fmt.Sprintf("CREATE SCHEMA IF NOT EXISTS %s", mysqlDatabase)) if err != nil { if n > 1 { logMessage(2, "Failed to connect to database! Trying again in 5 seconds...") err = db.Close() if err != nil { logMessage(4, fmt.Sprintf("Error closing connection: %s"+err.Error())) } time.Sleep(5 * time.Second) InitDatabase(n - 1) } else { logMessage(0, "Failed to connect to database. Exiting...") os.Exit(1) } } else { err = db.Close() if err != nil { logMessage(4, fmt.Sprintf("Error closing connection: %s"+err.Error())) } db, err = sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", mysqlUser, mysqlPassword, mysqlHost, mysqlPort, mysqlDatabase)) if err != nil { logMessage(0, "Failed to open newly created database. Exiting...") os.Exit(1) } logMessage(4, "Creating database tables") db.SetConnMaxIdleTime(10 * time.Second) CreateDatabaseTable(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s`.`%s_accounts` (`id` VARCHAR(8) NOT NULL,`username` VARCHAR(32) NULL,`email` VARCHAR(255) NOT NULL DEFAULT '',`email_confirmed` TINYINT(1) NULL DEFAULT 0, `email_resent` TINYINT(1) NULL DEFAULT 0,`password` VARCHAR(64) NULL,`avatar_url` VARCHAR(128) NOT NULL DEFAULT '/gatehouse/static/icons/user.png', `tos` TINYINT(1) NULL DEFAULT 0,`locked` TINYINT(1) NULL DEFAULT 0, `mfa_type` VARCHAR(8) NOT NULL DEFAULT 'email', `mfa_secret` VARCHAR(16) NULL, `username_changed` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; ", mysqlDatabase, tablePrefix)) CreateDatabaseTable(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s`.`%s_sessions` (`session_token` VARCHAR(64) NOT NULL, `user_id` VARCHAR(8) NOT NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `critical` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`session_token`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; ", mysqlDatabase, tablePrefix)) CreateDatabaseTable(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s`.`%s_confirmations` (`confirmation_token` VARCHAR(32) NOT NULL, `user_id` VARCHAR(8) NOT NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `used` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`confirmation_token`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; ", mysqlDatabase, tablePrefix)) CreateDatabaseTable(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s`.`%s_resets` (`reset_token` VARCHAR(32) NOT NULL, `user_id` VARCHAR(8) NOT NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `used` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`reset_token`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; ", mysqlDatabase, tablePrefix)) CreateDatabaseTable(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s`.`%s_mfa` (`mfa_session` VARCHAR(32) NOT NULL, `user_id` VARCHAR(8) NOT NULL, `type` VARCHAR(8) NOT NULL, `token` VARCHAR(6) NOT NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `used` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`mfa_session`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; ", mysqlDatabase, tablePrefix)) CreateDatabaseTable(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s`.`%s_recovery` (`user_id` VARCHAR(8) NOT NULL, `code` VARCHAR(8) NOT NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `used` TINYINT(1) NOT NULL DEFAULT 0, PRIMARY KEY (`user_id`, `code`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; ", mysqlDatabase, tablePrefix)) CreateDatabaseTable(fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%s`.`%s_avatars` (`avatar_id` VARCHAR(16) NOT NULL, `format` VARCHAR(8) NOT NULL, `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `data` LONGBLOB NOT NULL, PRIMARY KEY (`avatar_id`)) ENGINE = InnoDB DEFAULT CHARACTER SET = utf8 COLLATE = utf8_bin; ", mysqlDatabase, tablePrefix)) } } func initProxy() { url, err := url.Parse("http://" + backendServerAddr + ":" + backendServerPort) // Validate backend URL if err != nil { logMessage(0, fmt.Sprintf("Unable to start listening: %s", err.Error())) os.Exit(1) } proxy = httputil.NewSingleHostReverseProxy(url) } func CreateDatabaseTable(tableSql string) { _, err := db.Exec(tableSql) if err != nil { logMessage(0, fmt.Sprintf("Failed to create required table: %s", err.Error())) os.Exit(1) } } func ServePage(response http.ResponseWriter, pageStruct GatehouseForm) { err := formTemplate.Execute(response, pageStruct) if err != nil { ServeErrorPage(response, err) } } func ServeErrorPage(response http.ResponseWriter, err error) { if err != nil { logMessage(1, fmt.Sprintf("An internal error occurred: %s", err.Error())) } var errorPage GatehouseForm = GatehouseForm{ // Define forgot password page appName + " - Error Occurred", "Error Occurred", "", "", []GatehouseFormElement{ FormCreateDivider(), FormCreateHint("We're currently experiencing issues. Please try again later."), FormCreateButtonLink("/", "Back to site"), FormCreateDivider(), }, []OIDCButton{}, functionalPath, } response.WriteHeader(500) err = formTemplate.Execute(response, errorPage) if err != nil { logMessage(1, fmt.Sprintf("Error rendering error page: %s", err.Error())) fmt.Fprint(response, `Internal Error.`) } }