2021-01-09 20:49:48 +00:00
package user
import (
2021-07-14 19:51:55 +00:00
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
2022-05-17 13:31:12 +00:00
"errors"
2021-07-15 17:46:35 +00:00
"fmt"
"log"
"net/http"
2021-07-14 19:51:55 +00:00
"sort"
"strings"
2021-07-15 17:46:35 +00:00
"time"
2021-01-09 20:49:48 +00:00
2022-08-20 12:33:05 +00:00
"golang.org/x/crypto/bcrypt"
2021-07-01 08:45:03 +00:00
"github.com/bouncepaw/mycorrhiza/cfg"
2021-01-09 20:49:48 +00:00
"github.com/bouncepaw/mycorrhiza/util"
)
// CanProceed returns `true` if the user in `rq` has enough rights to access `route`.
func CanProceed ( rq * http . Request , route string ) bool {
return FromRequest ( rq ) . CanProceed ( route )
}
// FromRequest returns user from `rq`. If there is no user, an anon user is returned instead.
func FromRequest ( rq * http . Request ) * User {
cookie , err := rq . Cookie ( "mycorrhiza_token" )
if err != nil {
2021-01-24 07:30:14 +00:00
return EmptyUser ( )
2021-01-09 20:49:48 +00:00
}
2021-10-01 17:12:16 +00:00
return ByToken ( cookie . Value )
2021-01-09 20:49:48 +00:00
}
// LogoutFromRequest logs the user in `rq` out and rewrites the cookie in `w`.
func LogoutFromRequest ( w http . ResponseWriter , rq * http . Request ) {
cookieFromUser , err := rq . Cookie ( "mycorrhiza_token" )
if err == nil {
http . SetCookie ( w , cookie ( "token" , "" , time . Unix ( 0 , 0 ) ) )
terminateSession ( cookieFromUser . Value )
}
}
2021-04-19 16:39:25 +00:00
// Register registers the given user. If it fails, a non-nil error is returned.
2021-07-14 21:00:35 +00:00
func Register ( username , password , group , source string , force bool ) error {
2022-05-17 13:31:12 +00:00
if ! IsValidUsername ( username ) {
return fmt . Errorf ( "illegal username ‘ %s’ " , username )
}
2021-04-19 16:39:25 +00:00
username = util . CanonicalName ( username )
2021-07-02 10:25:13 +00:00
2021-04-19 16:39:25 +00:00
switch {
2021-10-27 06:43:01 +00:00
case ! IsValidUsername ( username ) :
2021-08-12 12:12:53 +00:00
return fmt . Errorf ( "illegal username ‘ %s’ " , username )
2021-07-02 12:02:42 +00:00
case ! ValidGroup ( group ) :
2021-08-12 12:12:53 +00:00
return fmt . Errorf ( "invalid group ‘ %s’ " , group )
2021-07-14 21:00:35 +00:00
case ! ValidSource ( source ) :
2021-08-12 12:12:53 +00:00
return fmt . Errorf ( "invalid source ‘ %s’ " , source )
2021-07-02 12:02:42 +00:00
case HasUsername ( username ) :
2021-08-12 12:12:53 +00:00
return fmt . Errorf ( "username ‘ %s’ is already taken" , username )
2021-07-02 10:25:13 +00:00
case ! force && cfg . RegistrationLimit > 0 && Count ( ) >= cfg . RegistrationLimit :
return fmt . Errorf ( "reached the limit of registered users (%d)" , cfg . RegistrationLimit )
2022-08-20 12:33:05 +00:00
case password == "" :
return fmt . Errorf ( "password must not be empty" )
2021-04-19 16:39:25 +00:00
}
2021-07-02 10:25:13 +00:00
2021-04-19 16:39:25 +00:00
hash , err := bcrypt . GenerateFromPassword ( [ ] byte ( password ) , bcrypt . DefaultCost )
if err != nil {
return err
}
2021-07-02 10:25:13 +00:00
2021-04-19 16:39:25 +00:00
u := User {
2021-07-02 08:20:03 +00:00
Name : username ,
2021-07-02 10:25:13 +00:00
Group : group ,
2021-07-15 17:46:35 +00:00
Source : source ,
2021-07-02 08:20:03 +00:00
Password : string ( hash ) ,
RegisteredAt : time . Now ( ) ,
2021-04-19 16:39:25 +00:00
}
users . Store ( username , & u )
2021-07-02 10:25:13 +00:00
return SaveUserDatabase ( )
2021-04-19 16:39:25 +00:00
}
2021-01-09 20:49:48 +00:00
// LoginDataHTTP logs such user in and returns string representation of an error if there is any.
2021-07-14 19:51:55 +00:00
//
// The HTTP parameters are used for setting header status (bad request, if it is bad) and saving a cookie.
2022-05-17 13:31:12 +00:00
func LoginDataHTTP ( w http . ResponseWriter , username , password string ) error {
2021-01-09 20:49:48 +00:00
w . Header ( ) . Set ( "Content-Type" , "text/html;charset=utf-8" )
if ! HasUsername ( username ) {
w . WriteHeader ( http . StatusBadRequest )
log . Println ( "Unknown username" , username , "was entered" )
2022-05-17 13:31:12 +00:00
return errors . New ( "unknown username" )
2021-01-09 20:49:48 +00:00
}
if ! CredentialsOK ( username , password ) {
w . WriteHeader ( http . StatusBadRequest )
log . Println ( "A wrong password was entered for username" , username )
2022-05-17 13:31:12 +00:00
return errors . New ( "wrong password" )
2021-01-09 20:49:48 +00:00
}
token , err := AddSession ( username )
if err != nil {
log . Println ( err )
w . WriteHeader ( http . StatusBadRequest )
2022-05-17 13:31:12 +00:00
return err
2021-01-09 20:49:48 +00:00
}
http . SetCookie ( w , cookie ( "token" , token , time . Now ( ) . Add ( 365 * 24 * time . Hour ) ) )
2022-05-17 13:31:12 +00:00
return nil
2021-01-09 20:49:48 +00:00
}
// AddSession saves a session for `username` and returns a token to use.
func AddSession ( username string ) ( string , error ) {
token , err := util . RandomString ( 16 )
if err == nil {
commenceSession ( username , token )
log . Println ( "New token for" , username , "is" , token )
}
return token , err
}
// A handy cookie constructor
2021-10-01 17:12:16 +00:00
func cookie ( nameSuffix , val string , t time . Time ) * http . Cookie {
2021-01-09 20:49:48 +00:00
return & http . Cookie {
2021-10-01 17:12:16 +00:00
Name : "mycorrhiza_" + nameSuffix ,
2021-01-09 20:49:48 +00:00
Value : val ,
Expires : t ,
Path : "/" ,
}
}
2021-07-14 19:51:55 +00:00
// TelegramAuthParamsAreValid is true if the given params are ok.
func TelegramAuthParamsAreValid ( params map [ string ] [ ] string ) bool {
// According to the Telegram documentation,
// > You can verify the authentication and the integrity of the data received by comparing the received hash parameter with the hexadecimal representation of the HMAC-SHA-256 signature of the data-check-string with the SHA256 hash of the bot's token used as a secret key.
tokenHash := sha256 . New ( )
tokenHash . Write ( [ ] byte ( cfg . TelegramBotToken ) )
secretKey := tokenHash . Sum ( nil )
hash := hmac . New ( sha256 . New , secretKey )
hash . Write ( [ ] byte ( telegramDataCheckString ( params ) ) )
hexHash := hex . EncodeToString ( hash . Sum ( nil ) )
passedHash := params [ "hash" ] [ 0 ]
return passedHash == hexHash
}
// According to the Telegram documentation,
// > Data-check-string is a concatenation of all received fields, sorted in alphabetical order, in the format key=<value> with a line feed character ('\n', 0x0A) used as separator – e.g., 'auth_date=<auth_date>\nfirst_name=<first_name>\nid=<id>\nusername=<username>'.
//
// Note that hash is not used here.
func telegramDataCheckString ( params map [ string ] [ ] string ) string {
var lines [ ] string
for key , value := range params {
if key == "hash" {
continue
}
lines = append ( lines , fmt . Sprintf ( "%s=%s" , key , value [ 0 ] ) )
}
sort . Strings ( lines )
return strings . Join ( lines , "\n" )
}