package user import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "errors" "fmt" "log" "net/http" "sort" "strings" "time" "golang.org/x/crypto/bcrypt" "github.com/bouncepaw/mycorrhiza/cfg" "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 { return EmptyUser() } return ByToken(cookie.Value) } // 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) } } // Register registers the given user. If it fails, a non-nil error is returned. func Register(username, password, group, source string, force bool) error { if !IsValidUsername(username) { return fmt.Errorf("illegal username ‘%s’", username) } username = util.CanonicalName(username) switch { case !IsValidUsername(username): return fmt.Errorf("illegal username ‘%s’", username) case !ValidGroup(group): return fmt.Errorf("invalid group ‘%s’", group) case !ValidSource(source): return fmt.Errorf("invalid source ‘%s’", source) case HasUsername(username): return fmt.Errorf("username ‘%s’ is already taken", username) case !force && cfg.RegistrationLimit > 0 && Count() >= cfg.RegistrationLimit: return fmt.Errorf("reached the limit of registered users (%d)", cfg.RegistrationLimit) case password == "" && source != "telegram": return fmt.Errorf("password must not be empty") } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) if err != nil { return err } u := User{ Name: username, Group: group, Source: source, Password: string(hash), RegisteredAt: time.Now(), } users.Store(username, &u) return SaveUserDatabase() } // LoginDataHTTP logs such user in and returns string representation of an error if there is any. // // The HTTP parameters are used for setting header status (bad request, if it is bad) and saving a cookie. func LoginDataHTTP(w http.ResponseWriter, username, password string) error { w.Header().Set("Content-Type", "text/html;charset=utf-8") if !HasUsername(username) { w.WriteHeader(http.StatusBadRequest) log.Println("Unknown username", username, "was entered") return errors.New("unknown username") } if !CredentialsOK(username, password) { w.WriteHeader(http.StatusBadRequest) log.Println("A wrong password was entered for username", username) return errors.New("wrong password") } token, err := AddSession(username) if err != nil { log.Println(err) w.WriteHeader(http.StatusBadRequest) return err } http.SetCookie(w, cookie("token", token, time.Now().Add(365*24*time.Hour))) return nil } // 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 func cookie(nameSuffix, val string, t time.Time) *http.Cookie { return &http.Cookie{ Name: "mycorrhiza_" + nameSuffix, Value: val, Expires: t, Path: "/", } } // 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= with a line feed character ('\n', 0x0A) used as separator – e.g., 'auth_date=\nfirst_name=\nid=\nusername='. // // 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") }