1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2025-01-06 10:00:26 +00:00

Implement initial Telegram integration

This commit is contained in:
bouncepaw 2021-07-14 19:51:55 +00:00
parent 9ad9db9825
commit df78f75efb
6 changed files with 310 additions and 109 deletions

View File

@ -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 == "" {

View File

@ -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=<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")
}

View File

@ -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,
}

View File

@ -23,6 +23,7 @@
<a class="btn btn_weak" href="/{%s rq.URL.RawQuery %}">Cancel</a>
</fieldset>
</form>
{%= telegramWidgetHTML() %}
{% elseif cfg.UseAuth %}
<p>Registrations are currently closed. Administrators can make an account for you by hand; contact them.</p>
<p><a href="/{%s rq.URL.RawQuery %}">← Go back</a></p>
@ -55,6 +56,7 @@
<a class="btn btn_weak" href="/">Cancel</a>
</fieldset>
</form>
{%= telegramWidgetHTML() %}
{% else %}
<p>Authentication is disabled. You can make edits anonymously.</p>
<p><a class="btn btn_weak" href="/">← Go home</a></p>
@ -64,6 +66,13 @@
</div>
{% 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 %}
<script async src="https://telegram.org/js/telegram-widget.js?15" data-telegram-login="{%s cfg.TelegramBotName %}" data-size="medium" data-userpic="false" data-auth-url="{%s cfg.URL %}/telegram-login"></script>
{% endif %}
{% endfunc %}
{% func LoginErrorHTML(err string) %}
<div class="layout">
<main class="main-width">
@ -130,4 +139,4 @@
</main>
</body>
</html>
{% endfunc %}
{% endfunc %}

View File

@ -64,84 +64,89 @@ func StreamRegisterHTML(qw422016 *qt422016.Writer, rq *http.Request) {
qw422016.N().S(`">Cancel</a>
</fieldset>
</form>
`)
//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(`
<p>Registrations are currently closed. Administrators can make an account for you by hand; contact them.</p>
<p><a href="/`)
//line views/auth.qtpl:28
//line views/auth.qtpl:29
qw422016.E().S(rq.URL.RawQuery)
//line views/auth.qtpl:28
//line views/auth.qtpl:29
qw422016.N().S(`"> Go back</a></p>
`)
//line views/auth.qtpl:29
//line views/auth.qtpl:30
} else {
//line views/auth.qtpl:29
//line views/auth.qtpl:30
qw422016.N().S(`
<p>Authentication is disabled. You can make edits anonymously.</p>
<p><a href="/`)
//line views/auth.qtpl:31
//line views/auth.qtpl:32
qw422016.E().S(rq.URL.RawQuery)
//line views/auth.qtpl:31
//line views/auth.qtpl:32
qw422016.N().S(`"> Go back</a></p>
`)
//line views/auth.qtpl:32
//line views/auth.qtpl:33
}
//line views/auth.qtpl:32
//line views/auth.qtpl:33
qw422016.N().S(`
</section>
</main>
</div>
`)
//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(`
<div class="layout">
<main class="main-width">
<section>
`)
//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(`
<form class="modal" method="post" action="/login-data" id="login-form" enctype="multipart/form-data" autocomplete="on">
<fieldset class="modal__fieldset">
<legend class="modal__title">Log in to `)
//line views/auth.qtpl:45
//line views/auth.qtpl:46
qw422016.E().S(cfg.WikiName)
//line views/auth.qtpl:45
//line views/auth.qtpl:46
qw422016.N().S(`</legend>
<label for="login-form__username">Username</label>
<br>
@ -155,185 +160,245 @@ func StreamLoginHTML(qw422016 *qt422016.Writer) {
<a class="btn btn_weak" href="/">Cancel</a>
</fieldset>
</form>
`)
//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(`
<p>Authentication is disabled. You can make edits anonymously.</p>
<p><a class="btn btn_weak" href="/"> Go home</a></p>
`)
//line views/auth.qtpl:61
//line views/auth.qtpl:63
}
//line views/auth.qtpl:61
//line views/auth.qtpl:63
qw422016.N().S(`
</section>
</main>
</div>
`)
//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(`
<script async src="https://telegram.org/js/telegram-widget.js?15" data-telegram-login="`)
//line views/auth.qtpl:72
qw422016.E().S(cfg.TelegramBotName)
//line views/auth.qtpl:72
qw422016.N().S(`" data-size="medium" data-userpic="false" data-auth-url="`)
//line views/auth.qtpl:72
qw422016.E().S(cfg.URL)
//line views/auth.qtpl:72
qw422016.N().S(`/telegram-login"></script>
`)
//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(`
<div class="layout">
<main class="main-width">
<section>
`)
//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(`
<p class="error">Unknown username.</p>
`)
//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(`
<p class="error">Wrong password.</p>
`)
//line views/auth.qtpl:76
//line views/auth.qtpl:85
default:
//line views/auth.qtpl:76
//line views/auth.qtpl:85
qw422016.N().S(`
<p class="error">`)
//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(`</p>
`)
//line views/auth.qtpl:78
//line views/auth.qtpl:87
}
//line views/auth.qtpl:78
//line views/auth.qtpl:87
qw422016.N().S(`
<p><a href="/login"> Try again</a></p>
</section>
</main>
</div>
`)
//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(`
<div class="layout">
<main class="main-width">
<section>
`)
//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(`
<h1>Log out?</h1>
<p><a href="/logout-confirm"><strong>Confirm</strong></a></p>
<p><a href="/">Cancel</a></p>
`)
//line views/auth.qtpl:93
//line views/auth.qtpl:102
} else {
//line views/auth.qtpl:93
//line views/auth.qtpl:102
qw422016.N().S(`
<p>You cannot log out because you are not logged in.</p>
<p><a href="/login">Login</a></p>
<p><a href="/login"> Home</a></p>
`)
//line views/auth.qtpl:97
//line views/auth.qtpl:106
}
//line views/auth.qtpl:97
//line views/auth.qtpl:106
qw422016.N().S(`
</section>
</main>
</div>
`)
//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(`
<!doctype html>
<html>
@ -365,31 +430,31 @@ func StreamLockHTML(qw422016 *qt422016.Writer) {
</body>
</html>
`)
//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
}

View File

@ -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(
`<main class="main-width"><p>Could not authorize using Telegram.</p><p>%s</p><p><a href="/login">Go to the login page<a></p></main>`,
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(
`<main class="main-width"><p>Could not authorize using Telegram.</p><p>%s</p><p><a href="/login">Go to the login page<a></p></main>`,
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.