From df78f75efb34ce9776dab5451ac11edc6ff9c297 Mon Sep 17 00:00:00 2001 From: bouncepaw Date: Wed, 14 Jul 2021 19:51:55 +0000 Subject: [PATCH] Implement initial Telegram integration --- cfg/config.go | 19 +++ user/net.go | 39 +++++++ user/user.go | 2 + views/auth.qtpl | 11 +- views/auth.qtpl.go | 281 ++++++++++++++++++++++++++++----------------- web/auth.go | 67 +++++++++++ 6 files changed, 310 insertions(+), 109 deletions(-) diff --git a/cfg/config.go b/cfg/config.go index fd784a6..37c565f 100644 --- a/cfg/config.go +++ b/cfg/config.go @@ -34,6 +34,11 @@ var ( CommonScripts []string ViewScripts []string EditScripts []string + + // TelegramEnabled if both TelegramBotToken and TelegramBotName are not empty strings. + TelegramEnabled bool + TelegramBotToken string + TelegramBotName string ) // WikiDir is a full path to the wiki storage directory, which also must be a @@ -49,6 +54,7 @@ type Config struct { Network Authorization CustomScripts `comment:"You can specify additional scripts to load on different kinds of pages, delimited by a comma ',' sign."` + Telegram `comment:"You can enable Telegram authorization. Follow these instructions: https://core.telegram.org/widgets/login#setting-up-a-bot"` } // Hyphae is a section of Config which has fields related to special hyphae. @@ -85,6 +91,12 @@ type Authorization struct { Locked bool `comment:"Set if users have to authorize to see anything on the wiki."` } +// Telegram is the section of Config that sets Telegram authorization. +type Telegram struct { + TelegramBotToken string `comment:"Token of your bot.` + TelegramBotName string `comment:"Username of your bot, sans @.` +} + // ReadConfigFile reads a config on the given path and stores the // configuration. Call it sometime during the initialization. func ReadConfigFile(path string) error { @@ -111,6 +123,10 @@ func ReadConfigFile(path string) error { ViewScripts: []string{}, EditScripts: []string{}, }, + Telegram: Telegram{ + TelegramBotToken: "", + TelegramBotName: "", + }, } f, err := ini.Load(path) @@ -158,6 +174,9 @@ func ReadConfigFile(path string) error { CommonScripts = cfg.CommonScripts ViewScripts = cfg.ViewScripts EditScripts = cfg.EditScripts + TelegramBotToken = cfg.TelegramBotToken + TelegramBotName = cfg.TelegramBotName + TelegramEnabled = (TelegramBotToken != "") && (TelegramBotName != "") // This URL makes much more sense. if URL == "" { diff --git a/user/net.go b/user/net.go index c90dfa8..1d6d510 100644 --- a/user/net.go +++ b/user/net.go @@ -5,6 +5,11 @@ import ( "log" "net/http" "time" + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "sort" + "strings" "github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/util" @@ -65,6 +70,8 @@ func Register(username, password, group string, force bool) error { } // 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, rq *http.Request, username, password string) string { w.Header().Set("Content-Type", "text/html;charset=utf-8") if !HasUsername(username) { @@ -106,3 +113,35 @@ func cookie(name_suffix, val string, t time.Time) *http.Cookie { 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") +} diff --git a/user/user.go b/user/user.go index d9cbfac..81d04a3 100644 --- a/user/user.go +++ b/user/user.go @@ -44,6 +44,7 @@ var groups = []string{ "anon", "editor", "trusted", + "telegram", "moderator", "admin", } @@ -53,6 +54,7 @@ var groupRight = map[string]int{ "anon": 0, "editor": 1, "trusted": 2, + "telegram": 2, "moderator": 3, "admin": 4, } diff --git a/views/auth.qtpl b/views/auth.qtpl index a952635..0f16696 100644 --- a/views/auth.qtpl +++ b/views/auth.qtpl @@ -23,6 +23,7 @@ Cancel + {%= telegramWidgetHTML() %} {% elseif cfg.UseAuth %}

Registrations are currently closed. Administrators can make an account for you by hand; contact them.

← Go back

@@ -55,6 +56,7 @@ Cancel + {%= telegramWidgetHTML() %} {% else %}

Authentication is disabled. You can make edits anonymously.

← Go home

@@ -64,6 +66,13 @@ {% endfunc %} +Telegram auth widget was requested by Yogurt. As you can see, we don't offer user administrators control over it. Of course we don't. +{% func telegramWidgetHTML() %} +{% if cfg.TelegramEnabled %} + +{% endif %} +{% endfunc %} + {% func LoginErrorHTML(err string) %}
@@ -130,4 +139,4 @@
-{% endfunc %} \ No newline at end of file +{% endfunc %} diff --git a/views/auth.qtpl.go b/views/auth.qtpl.go index 8b0658a..dc024dc 100644 --- a/views/auth.qtpl.go +++ b/views/auth.qtpl.go @@ -64,84 +64,89 @@ func StreamRegisterHTML(qw422016 *qt422016.Writer, rq *http.Request) { qw422016.N().S(`">Cancel + `) +//line views/auth.qtpl:26 + streamtelegramWidgetHTML(qw422016) +//line views/auth.qtpl:26 + qw422016.N().S(` `) -//line views/auth.qtpl:26 +//line views/auth.qtpl:27 } else if cfg.UseAuth { -//line views/auth.qtpl:26 +//line views/auth.qtpl:27 qw422016.N().S(`

Registrations are currently closed. Administrators can make an account for you by hand; contact them.

← Go back

`) -//line views/auth.qtpl:29 +//line views/auth.qtpl:30 } else { -//line views/auth.qtpl:29 +//line views/auth.qtpl:30 qw422016.N().S(`

Authentication is disabled. You can make edits anonymously.

← Go back

`) -//line views/auth.qtpl:32 +//line views/auth.qtpl:33 } -//line views/auth.qtpl:32 +//line views/auth.qtpl:33 qw422016.N().S(`
`) -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 } -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 func WriteRegisterHTML(qq422016 qtio422016.Writer, rq *http.Request) { -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 qw422016 := qt422016.AcquireWriter(qq422016) -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 StreamRegisterHTML(qw422016, rq) -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 qt422016.ReleaseWriter(qw422016) -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 } -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 func RegisterHTML(rq *http.Request) string { -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 qb422016 := qt422016.AcquireByteBuffer() -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 WriteRegisterHTML(qb422016, rq) -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 qs422016 := string(qb422016.B) -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 qt422016.ReleaseByteBuffer(qb422016) -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 return qs422016 -//line views/auth.qtpl:36 +//line views/auth.qtpl:37 } -//line views/auth.qtpl:38 +//line views/auth.qtpl:39 func StreamLoginHTML(qw422016 *qt422016.Writer) { -//line views/auth.qtpl:38 +//line views/auth.qtpl:39 qw422016.N().S(`
`) -//line views/auth.qtpl:42 +//line views/auth.qtpl:43 if cfg.UseAuth { -//line views/auth.qtpl:42 +//line views/auth.qtpl:43 qw422016.N().S(` + `) +//line views/auth.qtpl:59 + streamtelegramWidgetHTML(qw422016) +//line views/auth.qtpl:59 + qw422016.N().S(` `) -//line views/auth.qtpl:58 +//line views/auth.qtpl:60 } else { -//line views/auth.qtpl:58 +//line views/auth.qtpl:60 qw422016.N().S(`

Authentication is disabled. You can make edits anonymously.

← Go home

`) -//line views/auth.qtpl:61 +//line views/auth.qtpl:63 } -//line views/auth.qtpl:61 +//line views/auth.qtpl:63 qw422016.N().S(`
`) -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 } -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 func WriteLoginHTML(qq422016 qtio422016.Writer) { -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 qw422016 := qt422016.AcquireWriter(qq422016) -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 StreamLoginHTML(qw422016) -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 qt422016.ReleaseWriter(qw422016) -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 } -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 func LoginHTML() string { -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 qb422016 := qt422016.AcquireByteBuffer() -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 WriteLoginHTML(qb422016) -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 qs422016 := string(qb422016.B) -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 qt422016.ReleaseByteBuffer(qb422016) -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 return qs422016 -//line views/auth.qtpl:65 +//line views/auth.qtpl:67 } -//line views/auth.qtpl:67 +// Telegram auth widget was requested by Yogurt. As you can see, we don't offer user administrators control over it. Of course we don't. + +//line views/auth.qtpl:70 +func streamtelegramWidgetHTML(qw422016 *qt422016.Writer) { +//line views/auth.qtpl:70 + qw422016.N().S(` +`) +//line views/auth.qtpl:71 + if cfg.TelegramEnabled { +//line views/auth.qtpl:71 + qw422016.N().S(` + +`) +//line views/auth.qtpl:73 + } +//line views/auth.qtpl:73 + qw422016.N().S(` +`) +//line views/auth.qtpl:74 +} + +//line views/auth.qtpl:74 +func writetelegramWidgetHTML(qq422016 qtio422016.Writer) { +//line views/auth.qtpl:74 + qw422016 := qt422016.AcquireWriter(qq422016) +//line views/auth.qtpl:74 + streamtelegramWidgetHTML(qw422016) +//line views/auth.qtpl:74 + qt422016.ReleaseWriter(qw422016) +//line views/auth.qtpl:74 +} + +//line views/auth.qtpl:74 +func telegramWidgetHTML() string { +//line views/auth.qtpl:74 + qb422016 := qt422016.AcquireByteBuffer() +//line views/auth.qtpl:74 + writetelegramWidgetHTML(qb422016) +//line views/auth.qtpl:74 + qs422016 := string(qb422016.B) +//line views/auth.qtpl:74 + qt422016.ReleaseByteBuffer(qb422016) +//line views/auth.qtpl:74 + return qs422016 +//line views/auth.qtpl:74 +} + +//line views/auth.qtpl:76 func StreamLoginErrorHTML(qw422016 *qt422016.Writer, err string) { -//line views/auth.qtpl:67 +//line views/auth.qtpl:76 qw422016.N().S(`
`) -//line views/auth.qtpl:71 +//line views/auth.qtpl:80 switch err { -//line views/auth.qtpl:72 +//line views/auth.qtpl:81 case "unknown username": -//line views/auth.qtpl:72 +//line views/auth.qtpl:81 qw422016.N().S(`

Unknown username.

`) -//line views/auth.qtpl:74 +//line views/auth.qtpl:83 case "wrong password": -//line views/auth.qtpl:74 +//line views/auth.qtpl:83 qw422016.N().S(`

Wrong password.

`) -//line views/auth.qtpl:76 +//line views/auth.qtpl:85 default: -//line views/auth.qtpl:76 +//line views/auth.qtpl:85 qw422016.N().S(`

`) -//line views/auth.qtpl:77 +//line views/auth.qtpl:86 qw422016.E().S(err) -//line views/auth.qtpl:77 +//line views/auth.qtpl:86 qw422016.N().S(`

`) -//line views/auth.qtpl:78 +//line views/auth.qtpl:87 } -//line views/auth.qtpl:78 +//line views/auth.qtpl:87 qw422016.N().S(`

← Try again

`) -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 } -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 func WriteLoginErrorHTML(qq422016 qtio422016.Writer, err string) { -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 qw422016 := qt422016.AcquireWriter(qq422016) -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 StreamLoginErrorHTML(qw422016, err) -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 qt422016.ReleaseWriter(qw422016) -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 } -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 func LoginErrorHTML(err string) string { -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 qb422016 := qt422016.AcquireByteBuffer() -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 WriteLoginErrorHTML(qb422016, err) -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 qs422016 := string(qb422016.B) -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 qt422016.ReleaseByteBuffer(qb422016) -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 return qs422016 -//line views/auth.qtpl:83 +//line views/auth.qtpl:92 } -//line views/auth.qtpl:85 +//line views/auth.qtpl:94 func StreamLogoutHTML(qw422016 *qt422016.Writer, can bool) { -//line views/auth.qtpl:85 +//line views/auth.qtpl:94 qw422016.N().S(`
`) -//line views/auth.qtpl:89 +//line views/auth.qtpl:98 if can { -//line views/auth.qtpl:89 +//line views/auth.qtpl:98 qw422016.N().S(`

Log out?

Confirm

Cancel

`) -//line views/auth.qtpl:93 +//line views/auth.qtpl:102 } else { -//line views/auth.qtpl:93 +//line views/auth.qtpl:102 qw422016.N().S(`

You cannot log out because you are not logged in.

Login

← Home

`) -//line views/auth.qtpl:97 +//line views/auth.qtpl:106 } -//line views/auth.qtpl:97 +//line views/auth.qtpl:106 qw422016.N().S(`
`) -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 } -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 func WriteLogoutHTML(qq422016 qtio422016.Writer, can bool) { -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 qw422016 := qt422016.AcquireWriter(qq422016) -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 StreamLogoutHTML(qw422016, can) -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 qt422016.ReleaseWriter(qw422016) -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 } -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 func LogoutHTML(can bool) string { -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 qb422016 := qt422016.AcquireByteBuffer() -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 WriteLogoutHTML(qb422016, can) -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 qs422016 := string(qb422016.B) -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 qt422016.ReleaseByteBuffer(qb422016) -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 return qs422016 -//line views/auth.qtpl:101 +//line views/auth.qtpl:110 } -//line views/auth.qtpl:103 +//line views/auth.qtpl:112 func StreamLockHTML(qw422016 *qt422016.Writer) { -//line views/auth.qtpl:103 +//line views/auth.qtpl:112 qw422016.N().S(` @@ -365,31 +430,31 @@ func StreamLockHTML(qw422016 *qt422016.Writer) { `) -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 } -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 func WriteLockHTML(qq422016 qtio422016.Writer) { -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 qw422016 := qt422016.AcquireWriter(qq422016) -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 StreamLockHTML(qw422016) -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 qt422016.ReleaseWriter(qw422016) -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 } -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 func LockHTML() string { -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 qb422016 := qt422016.AcquireByteBuffer() -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 WriteLockHTML(qb422016) -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 qs422016 := string(qb422016.B) -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 qt422016.ReleaseByteBuffer(qb422016) -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 return qs422016 -//line views/auth.qtpl:133 +//line views/auth.qtpl:142 } diff --git a/web/auth.go b/web/auth.go index e74e174..8b77b4a 100644 --- a/web/auth.go +++ b/web/auth.go @@ -1,11 +1,13 @@ package web import ( + "errors" "fmt" "io" "log" "mime" "net/http" + "strings" "github.com/bouncepaw/mycorrhiza/cfg" "github.com/bouncepaw/mycorrhiza/user" @@ -21,6 +23,9 @@ func initAuth() { if cfg.AllowRegistration { http.HandleFunc("/register", handlerRegister) } + if cfg.TelegramEnabled { + http.HandleFunc("/telegram-login", handlerTelegramLogin) + } http.HandleFunc("/login", handlerLogin) http.HandleFunc("/login-data", handlerLoginData) http.HandleFunc("/logout", handlerLogout) @@ -118,6 +123,68 @@ func handlerLogin(w http.ResponseWriter, rq *http.Request) { w.Write([]byte(views.BaseHTML("Login", views.LoginHTML(), user.EmptyUser()))) } +func handlerTelegramLogin(w http.ResponseWriter, rq *http.Request) { + // Note there is no lock here. + w.Header().Set("Content-Type", "text/plain;charset=utf-8") + rq.ParseForm() + var ( + values = rq.URL.Query() + username = strings.ToLower(values.Get("username")) + seemsValid = user.TelegramAuthParamsAreValid(values) + err = user.Register( + username, + "", // Password matters not + "telegram", + false, + ) + ) + if user.HasUsername(username) && user.UserByName(username).Group == "telegram" { + // Problems is something we put blankets on. + err = nil + } + + if !seemsValid { + err = errors.New("Wrong parameters") + } + + if err != nil { + log.Printf("Failed to register ‘%s’ using Telegram: %s", username, err.Error()) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint( + w, + views.BaseHTML( + "Error", + fmt.Sprintf( + `

Could not authorize using Telegram.

%s

Go to the login page

`, + err.Error(), + ), + user.FromRequest(rq), + ), + ) + return + } + + errmsg := user.LoginDataHTTP(w, rq, username, "") + if errmsg != "" { + log.Printf("Failed to login ‘%s’ using Telegram: %s", username, err.Error()) + w.WriteHeader(http.StatusBadRequest) + fmt.Fprint( + w, + views.BaseHTML( + "Error", + fmt.Sprintf( + `

Could not authorize using Telegram.

%s

Go to the login page

`, + err.Error(), + ), + user.FromRequest(rq), + ), + ) + return + } + log.Printf("Authorize ‘%s’ from Telegram", username) + http.Redirect(w, rq, "/", http.StatusSeeOther) +} + // handlerLoginData logs the user in. // // TODO: merge into handlerLogin as POST method.