From 21499a592badf01c96a70d772bd9733ef21969c6 Mon Sep 17 00:00:00 2001 From: Nicolas MASSE Date: Fri, 8 May 2020 11:53:20 +0200 Subject: [PATCH] reorganize code --- bot.go | 147 +++++++++++++++++++++++++--------------------------- http.go | 33 ++---------- main.go | 109 ++++++++++++++++++++------------------ security.go | 17 ++++++ web.go | 81 ++++++++++++++++++----------- 5 files changed, 200 insertions(+), 187 deletions(-) diff --git a/bot.go b/bot.go index 1297b28..e85e223 100644 --- a/bot.go +++ b/bot.go @@ -13,13 +13,8 @@ import ( tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" ) -type PhotoBot struct { - Telegram TelegramBackend - MediaStore *MediaStore - WebInterface WebInterface -} - -type TelegramBackend struct { +type TelegramBot struct { + MediaStore *MediaStore TokenGenerator *TokenGenerator GlobalTokenValidity int PerAlbumTokenValidity int @@ -57,42 +52,40 @@ type TelegramMessages struct { SharedGlobal string } -func InitBot(targetDir string) *PhotoBot { - return &PhotoBot{ - Telegram: TelegramBackend{ - AuthorizedUsers: make(map[string]bool), - RetryDelay: time.Duration(30) * time.Second, - }, - } +func NewTelegramBot() *TelegramBot { + bot := TelegramBot{} + bot.AuthorizedUsers = make(map[string]bool) + return &bot } -func (bot *PhotoBot) StartBot(token string) { +func (bot *TelegramBot) StartBot(token string, debug bool) { var telegramBot *tgbotapi.BotAPI var err error for tryAgain := true; tryAgain; tryAgain = (err != nil) { telegramBot, err = tgbotapi.NewBotAPI(token) if err != nil { - log.Printf("Cannot start the Telegram Bot because of '%s'. Retrying in %d seconds...", err, bot.Telegram.RetryDelay/time.Second) - time.Sleep(bot.Telegram.RetryDelay) + log.Printf("Cannot start the Telegram Bot because of '%s'. Retrying in %d seconds...", err, bot.RetryDelay/time.Second) + time.Sleep(bot.RetryDelay) } } log.Printf("Authorized on account %s", telegramBot.Self.UserName) - bot.Telegram.API = telegramBot + bot.API = telegramBot + bot.API.Debug = debug } -func (bot *PhotoBot) Process() { +func (bot *TelegramBot) Process() { u := tgbotapi.NewUpdate(0) - u.Timeout = bot.Telegram.NewUpdateTimeout - updates, _ := bot.Telegram.API.GetUpdatesChan(u) + u.Timeout = bot.NewUpdateTimeout + updates, _ := bot.API.GetUpdatesChan(u) for update := range updates { bot.ProcessUpdate(update) } } -func (bot *PhotoBot) ProcessUpdate(update tgbotapi.Update) { +func (bot *TelegramBot) ProcessUpdate(update tgbotapi.Update) { if update.Message == nil || update.Message.From == nil { return } @@ -105,29 +98,29 @@ func (bot *PhotoBot) ProcessUpdate(update tgbotapi.Update) { username := update.Message.From.UserName if username == "" { - bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.NoUsername) + bot.replyToCommandWithMessage(update.Message, bot.Messages.NoUsername) return } - if !bot.Telegram.AuthorizedUsers[username] { + if !bot.AuthorizedUsers[username] { log.Printf("[%s] unauthorized user", username) - bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.Forbidden) + bot.replyToCommandWithMessage(update.Message, bot.Messages.Forbidden) return } - err := bot.Telegram.ChatDB.UpdateWith(username, update.Message.Chat.ID) + err := bot.ChatDB.UpdateWith(username, update.Message.Chat.ID) if err != nil { log.Printf("[%s] cannot update chat db: %s", username, err) } if update.Message.ReplyToMessage != nil { // Only deal with forced replies (reply to bot's messages) - if update.Message.ReplyToMessage.From == nil || update.Message.ReplyToMessage.From.UserName != bot.Telegram.API.Self.UserName { + if update.Message.ReplyToMessage.From == nil || update.Message.ReplyToMessage.From.UserName != bot.API.Self.UserName { return } if update.Message.ReplyToMessage.Text != "" { - if update.Message.ReplyToMessage.Text == bot.Telegram.Messages.MissingAlbumName { - log.Printf("[%s] reply to previous command /%s: %s", username, bot.Telegram.Commands.NewAlbum, text) + if update.Message.ReplyToMessage.Text == bot.Messages.MissingAlbumName { + log.Printf("[%s] reply to previous command /%s: %s", username, bot.Commands.NewAlbum, text) bot.handleNewAlbumCommandReply(update.Message) return } @@ -138,66 +131,66 @@ func (bot *PhotoBot) ProcessUpdate(update tgbotapi.Update) { if update.Message.IsCommand() { log.Printf("[%s] command: %s", username, text) switch update.Message.Command() { - case "start", bot.Telegram.Commands.Help: + case "start", bot.Commands.Help: bot.handleHelpCommand(update.Message) - case bot.Telegram.Commands.Share: + case bot.Commands.Share: bot.handleShareCommand(update.Message) - case bot.Telegram.Commands.Browse: + case bot.Commands.Browse: bot.handleBrowseCommand(update.Message) - case bot.Telegram.Commands.NewAlbum: + case bot.Commands.NewAlbum: bot.handleNewAlbumCommand(update.Message) - case bot.Telegram.Commands.Info: + case bot.Commands.Info: bot.handleInfoCommand(update.Message) default: - bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.DoNotUnderstand) + bot.replyToCommandWithMessage(update.Message, bot.Messages.DoNotUnderstand) } } else { - bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.DoNotUnderstand) + bot.replyToCommandWithMessage(update.Message, bot.Messages.DoNotUnderstand) } } else if update.Message.Photo != nil { err := bot.handlePhoto(update.Message) if err != nil { log.Printf("[%s] cannot add photo to current album: %s", username, err) - bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.ServerError) + bot.replyToCommandWithMessage(update.Message, bot.Messages.ServerError) return } bot.dispatchMessage(update.Message) - bot.Telegram.replyWithMessage(update.Message, bot.Telegram.Messages.ThankYouMedia) + bot.replyWithMessage(update.Message, bot.Messages.ThankYouMedia) } else if update.Message.Video != nil { err := bot.handleVideo(update.Message) if err != nil { log.Printf("[%s] cannot add video to current album: %s", username, err) - bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.ServerError) + bot.replyToCommandWithMessage(update.Message, bot.Messages.ServerError) return } bot.dispatchMessage(update.Message) - bot.Telegram.replyWithMessage(update.Message, bot.Telegram.Messages.ThankYouMedia) + bot.replyWithMessage(update.Message, bot.Messages.ThankYouMedia) } else { log.Printf("[%s] cannot handle this type of message", username) - bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.DoNotUnderstand) + bot.replyToCommandWithMessage(update.Message, bot.Messages.DoNotUnderstand) } } -func (bot *PhotoBot) dispatchMessage(message *tgbotapi.Message) { - for user, _ := range bot.Telegram.AuthorizedUsers { +func (bot *TelegramBot) dispatchMessage(message *tgbotapi.Message) { + for user, _ := range bot.AuthorizedUsers { if user != message.From.UserName { - if _, ok := bot.Telegram.ChatDB.Db[user]; !ok { + if _, ok := bot.ChatDB.Db[user]; !ok { log.Printf("[%s] The chat db does not have any mapping for %s, skipping...", message.From.UserName, user) continue } - msg := tgbotapi.NewForward(bot.Telegram.ChatDB.Db[user], message.Chat.ID, message.MessageID) + msg := tgbotapi.NewForward(bot.ChatDB.Db[user], message.Chat.ID, message.MessageID) - _, err := bot.Telegram.API.Send(msg) + _, err := bot.API.Send(msg) if err != nil { - log.Printf("[%s] Cannot dispatch message to %s (chat id = %d)", message.From.UserName, user, bot.Telegram.ChatDB.Db[user]) + log.Printf("[%s] Cannot dispatch message to %s (chat id = %d)", message.From.UserName, user, bot.ChatDB.Db[user]) } } } } -func (bot *PhotoBot) getFile(message *tgbotapi.Message, telegramFileId string, mediaStoreId string) error { - url, err := bot.Telegram.API.GetFileDirectURL(telegramFileId) +func (bot *TelegramBot) getFile(message *tgbotapi.Message, telegramFileId string, mediaStoreId string) error { + url, err := bot.API.GetFileDirectURL(telegramFileId) if err != nil { return err } @@ -250,7 +243,7 @@ func (bot *PhotoBot) getFile(message *tgbotapi.Message, telegramFileId string, m return nil } -func (bot *PhotoBot) handlePhoto(message *tgbotapi.Message) error { +func (bot *TelegramBot) handlePhoto(message *tgbotapi.Message) error { // Find the best resolution among all available sizes fileId := "" maxWidth := 0 @@ -274,7 +267,7 @@ func (bot *PhotoBot) handlePhoto(message *tgbotapi.Message) error { t := time.Unix(int64(message.Date), 0) return bot.MediaStore.CommitPhoto(mediaStoreId, t, message.Caption) } -func (bot *PhotoBot) handleVideo(message *tgbotapi.Message) error { +func (bot *TelegramBot) handleVideo(message *tgbotapi.Message) error { // Get a unique id mediaStoreId := bot.MediaStore.GetUniqueID() @@ -295,20 +288,20 @@ func (bot *PhotoBot) handleVideo(message *tgbotapi.Message) error { return bot.MediaStore.CommitVideo(mediaStoreId, t, message.Caption) } -func (bot *PhotoBot) handleHelpCommand(message *tgbotapi.Message) { - bot.Telegram.replyWithMessage(message, bot.Telegram.Messages.Help) +func (bot *TelegramBot) handleHelpCommand(message *tgbotapi.Message) { + bot.replyWithMessage(message, bot.Messages.Help) } -func (bot *PhotoBot) handleShareCommand(message *tgbotapi.Message) { +func (bot *TelegramBot) handleShareCommand(message *tgbotapi.Message) { albumList, err := bot.MediaStore.ListAlbums() if err != nil { log.Printf("[%s] cannot get album list: %s", message.From.UserName, err) - bot.Telegram.replyToCommandWithMessage(message, bot.Telegram.Messages.ServerError) + bot.replyToCommandWithMessage(message, bot.Messages.ServerError) return } var text strings.Builder - text.WriteString(fmt.Sprintf(bot.Telegram.Messages.SharedAlbum, bot.Telegram.PerAlbumTokenValidity)) + text.WriteString(fmt.Sprintf(bot.Messages.SharedAlbum, bot.PerAlbumTokenValidity)) text.WriteString("\n") sort.Sort(sort.Reverse(albumList)) var tokenData TokenData = TokenData{ @@ -323,80 +316,80 @@ func (bot *PhotoBot) handleShareCommand(message *tgbotapi.Message) { title = title + " 🔥" } tokenData.Entitlement = id - token := bot.Telegram.TokenGenerator.NewToken(tokenData) - url := fmt.Sprintf("%s/s/%s/%s/album/%s/", bot.Telegram.WebPublicURL, url.PathEscape(message.From.UserName), url.PathEscape(token), url.PathEscape(id)) + token := bot.TokenGenerator.NewToken(tokenData) + url := fmt.Sprintf("%s/s/%s/%s/album/%s/", bot.WebPublicURL, url.PathEscape(message.From.UserName), url.PathEscape(token), url.PathEscape(id)) text.WriteString(fmt.Sprintf("- [%s %s](%s)\n", album.Date.Format("2006-01"), title, url)) } - bot.Telegram.replyWithMarkdownMessage(message, text.String()) + bot.replyWithMarkdownMessage(message, text.String()) } -func (bot *PhotoBot) handleBrowseCommand(message *tgbotapi.Message) { +func (bot *TelegramBot) handleBrowseCommand(message *tgbotapi.Message) { var tokenData TokenData = TokenData{ Timestamp: time.Now(), Username: message.From.UserName, } // Global share - token := bot.Telegram.TokenGenerator.NewToken(tokenData) - url := fmt.Sprintf("%s/s/%s/%s/album/", bot.Telegram.WebPublicURL, url.PathEscape(message.From.UserName), url.PathEscape(token)) - bot.Telegram.replyWithMessage(message, fmt.Sprintf(bot.Telegram.Messages.SharedGlobal, bot.Telegram.GlobalTokenValidity)) - bot.Telegram.replyWithMessage(message, url) + token := bot.TokenGenerator.NewToken(tokenData) + url := fmt.Sprintf("%s/s/%s/%s/album/", bot.WebPublicURL, url.PathEscape(message.From.UserName), url.PathEscape(token)) + bot.replyWithMessage(message, fmt.Sprintf(bot.Messages.SharedGlobal, bot.GlobalTokenValidity)) + bot.replyWithMessage(message, url) } -func (bot *PhotoBot) handleInfoCommand(message *tgbotapi.Message) { +func (bot *TelegramBot) handleInfoCommand(message *tgbotapi.Message) { album, err := bot.MediaStore.GetCurrentAlbum() if err != nil { log.Printf("[%s] cannot get current album: %s", message.From.UserName, err) - bot.Telegram.replyToCommandWithMessage(message, bot.Telegram.Messages.ServerError) + bot.replyToCommandWithMessage(message, bot.Messages.ServerError) return } if album.Title != "" { - bot.Telegram.replyWithMessage(message, fmt.Sprintf(bot.Telegram.Messages.Info, album.Title)) + bot.replyWithMessage(message, fmt.Sprintf(bot.Messages.Info, album.Title)) } else { - bot.Telegram.replyWithMessage(message, bot.Telegram.Messages.InfoNoAlbum) + bot.replyWithMessage(message, bot.Messages.InfoNoAlbum) } } -func (bot *PhotoBot) handleNewAlbumCommand(message *tgbotapi.Message) { - bot.Telegram.replyWithForcedReply(message, bot.Telegram.Messages.MissingAlbumName) +func (bot *TelegramBot) handleNewAlbumCommand(message *tgbotapi.Message) { + bot.replyWithForcedReply(message, bot.Messages.MissingAlbumName) } -func (bot *PhotoBot) handleNewAlbumCommandReply(message *tgbotapi.Message) { +func (bot *TelegramBot) handleNewAlbumCommandReply(message *tgbotapi.Message) { albumName := message.Text err := bot.MediaStore.NewAlbum(albumName) if err != nil { log.Printf("[%s] cannot create album '%s': %s", message.From.UserName, albumName, err) - bot.Telegram.replyToCommandWithMessage(message, bot.Telegram.Messages.ServerError) + bot.replyToCommandWithMessage(message, bot.Messages.ServerError) return } - bot.Telegram.replyWithMessage(message, bot.Telegram.Messages.AlbumCreated) + bot.replyWithMessage(message, bot.Messages.AlbumCreated) } -func (telegram *TelegramBackend) replyToCommandWithMessage(message *tgbotapi.Message, text string) error { +func (telegram *TelegramBot) replyToCommandWithMessage(message *tgbotapi.Message, text string) error { msg := tgbotapi.NewMessage(message.Chat.ID, text) msg.ReplyToMessageID = message.MessageID _, err := telegram.API.Send(msg) return err } -func (telegram *TelegramBackend) replyWithMessage(message *tgbotapi.Message, text string) error { +func (telegram *TelegramBot) replyWithMessage(message *tgbotapi.Message, text string) error { msg := tgbotapi.NewMessage(message.Chat.ID, text) _, err := telegram.API.Send(msg) return err } -func (telegram *TelegramBackend) replyWithMarkdownMessage(message *tgbotapi.Message, text string) error { +func (telegram *TelegramBot) replyWithMarkdownMessage(message *tgbotapi.Message, text string) error { msg := tgbotapi.NewMessage(message.Chat.ID, text) msg.ParseMode = tgbotapi.ModeMarkdown _, err := telegram.API.Send(msg) return err } -func (telegram *TelegramBackend) replyWithForcedReply(message *tgbotapi.Message, text string) error { +func (telegram *TelegramBot) replyWithForcedReply(message *tgbotapi.Message, text string) error { msg := tgbotapi.NewMessage(message.Chat.ID, text) msg.ReplyMarkup = tgbotapi.ForceReply{ ForceReply: true, diff --git a/http.go b/http.go index 07e0a40..6438306 100644 --- a/http.go +++ b/http.go @@ -5,8 +5,6 @@ import ( "net/http" "path" "strings" - - "github.com/rakyll/statik/fs" ) // ShiftPath splits off the first component of p, which will be cleaned of @@ -25,34 +23,11 @@ func ShiftPath(p string) (head, tail string) { return p[1:i], p[i:] } -func (bot *PhotoBot) ServeWebInterface(listenAddr string, frontend *SecurityFrontend) { - statikFS, err := fs.New() - if err != nil { - log.Fatal(err) - } - - bot.WebInterface.AlbumTemplate, err = getTemplate(statikFS, "/album.html.template", "album") - if err != nil { - log.Fatal(err) - } - - bot.WebInterface.MediaTemplate, err = getTemplate(statikFS, "/media.html.template", "media") - if err != nil { - log.Fatal(err) - } - - bot.WebInterface.IndexTemplate, err = getTemplate(statikFS, "/index.html.template", "index") - if err != nil { - log.Fatal(err) - } - +func ServeWebInterface(listenAddr string, webInterface http.Handler, staticFiles http.FileSystem) { router := http.NewServeMux() - router.Handle("/js/", http.FileServer(statikFS)) - router.Handle("/css/", http.FileServer(statikFS)) - - // Put the Web Interface behind the security frontend - frontend.Protected = bot - router.Handle("/", frontend) + router.Handle("/js/", http.FileServer(staticFiles)) + router.Handle("/css/", http.FileServer(staticFiles)) + router.Handle("/", webInterface) server := &http.Server{ Addr: listenAddr, diff --git a/main.go b/main.go index 26f450e..5511146 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,8 @@ import ( "path/filepath" "time" + _ "github.com/nmasse-itix/Telegram-Photo-Album-Bot/statik" + "github.com/rakyll/statik/fs" "github.com/spf13/viper" ) @@ -122,10 +124,6 @@ func validateConfig() { log.Fatal("No OpenID Connect Client Secret provided!") } - if viper.GetString("WebInterface.OIDC.RedirectURL") == "" { - log.Fatal("No OpenID Connect Redirect URL provided!") - } - if viper.GetString("WebInterface.OIDC.ClientSecret") == "" { log.Fatal("No OpenID Connect Client Secret provided!") } @@ -173,24 +171,24 @@ func getMessagesFromConfig() TelegramMessages { } } +func getSecretKey(configKey string, minLength int) []byte { + key, err := base64.StdEncoding.DecodeString(viper.GetString(configKey)) + if err != nil { + panic(fmt.Sprintf("%s: %s", configKey, err)) + } + + if len(key) < 32 { + panic(fmt.Sprintf("%s: The given token generator authentication key is too short (got %d bytes, expected at least %d)!", configKey, len(key), minLength)) + } + + return key +} + func main() { initConfig() validateConfig() - // Create the Bot - photoBot := InitBot(viper.GetString("TargetDir")) - photoBot.Telegram.RetryDelay = time.Duration(viper.GetInt("Telegram.RetryDelay")) * time.Second - photoBot.Telegram.NewUpdateTimeout = viper.GetInt("Telegram.NewUpdateTimeout") - photoBot.Telegram.Commands = getCommandsFromConfig() - photoBot.Telegram.Messages = getMessagesFromConfig() - photoBot.WebInterface.SiteName = viper.GetString("WebInterface.SiteName") - photoBot.Telegram.WebPublicURL = viper.GetString("WebInterface.PublicURL") - - // Fill the authorized users - for _, item := range viper.GetStringSlice("Telegram.AuthorizedUsers") { - photoBot.Telegram.AuthorizedUsers[item] = true - } - + // Make sure the needed folder structure exists in the target folder targetDir := viper.GetString("TargetDir") for _, dir := range []string{"data", "db"} { fullPath := filepath.Join(targetDir, dir) @@ -200,63 +198,69 @@ func main() { } } - // Create the ChatDB and inject it - chatDB, err := InitChatDB(filepath.Join(targetDir, "db", "chatdb.yaml")) + // Create the MediaStore + mediaStore, err := InitMediaStore(filepath.Join(targetDir, "data")) if err != nil { panic(err) } - photoBot.Telegram.ChatDB = chatDB - // Create the MediaStore and inject it - mediaStore, err := InitMediaStore(filepath.Join(targetDir, "data")) + // Create the Token Generator + tokenAuthenticationKey := getSecretKey("Telegram.TokenGenerator.AuthenticationKey", 32) + tokenGenerator, err := NewTokenGenerator(tokenAuthenticationKey, crypto.SHA256) if err != nil { panic(err) } + + // Create the ChatDB + chatDB, err := InitChatDB(filepath.Join(targetDir, "db", "chatdb.yaml")) + if err != nil { + panic(err) + } + + // Create the Bot + photoBot := NewTelegramBot() + photoBot.RetryDelay = time.Duration(viper.GetInt("Telegram.RetryDelay")) * time.Second + photoBot.NewUpdateTimeout = viper.GetInt("Telegram.NewUpdateTimeout") + photoBot.Commands = getCommandsFromConfig() + photoBot.Messages = getMessagesFromConfig() + photoBot.WebPublicURL = viper.GetString("WebInterface.PublicURL") photoBot.MediaStore = mediaStore + photoBot.ChatDB = chatDB + photoBot.TokenGenerator = tokenGenerator + photoBot.GlobalTokenValidity = viper.GetInt("Telegram.TokenGenerator.GlobalValidity") + photoBot.PerAlbumTokenValidity = viper.GetInt("Telegram.TokenGenerator.PerAlbumValidity") + + // Fill the authorized users + for _, item := range viper.GetStringSlice("Telegram.AuthorizedUsers") { + photoBot.AuthorizedUsers[item] = true + } // Start the bot - photoBot.StartBot(viper.GetString("Telegram.Token")) - photoBot.Telegram.API.Debug = viper.GetBool("Telegram.Debug") + photoBot.StartBot(viper.GetString("Telegram.Token"), viper.GetBool("Telegram.Debug")) - // Token Generator - tokenAuthenticationKey, err := base64.StdEncoding.DecodeString(viper.GetString("Telegram.TokenGenerator.AuthenticationKey")) + // Setup the web interface + statikFS, err := fs.New() if err != nil { panic(err) } - if len(tokenAuthenticationKey) < 32 { - panic("The given token generator authentication key is too short!") - } - tokenGenerator, err := NewTokenGenerator(tokenAuthenticationKey, crypto.SHA256) + web, err := NewWebInterface(statikFS) if err != nil { panic(err) } - photoBot.Telegram.TokenGenerator = tokenGenerator - photoBot.Telegram.GlobalTokenValidity = viper.GetInt("Telegram.TokenGenerator.GlobalValidity") - photoBot.Telegram.PerAlbumTokenValidity = viper.GetInt("Telegram.TokenGenerator.PerAlbumValidity") + web.MediaStore = mediaStore + web.SiteName = viper.GetString("WebInterface.SiteName") - // Setup the web interface + // Setup the security frontend var oidc OpenIdSettings = OpenIdSettings{ ClientID: viper.GetString("WebInterface.OIDC.ClientID"), ClientSecret: viper.GetString("WebInterface.OIDC.ClientSecret"), DiscoveryUrl: viper.GetString("WebInterface.OIDC.DiscoveryUrl"), - RedirectURL: viper.GetString("WebInterface.OIDC.RedirectURL"), + RedirectURL: GetOAuthCallbackURL(viper.GetString("WebInterface.PublicURL")), GSuiteDomain: viper.GetString("WebInterface.OIDC.GSuiteDomain"), Scopes: viper.GetStringSlice("WebInterface.OIDC.Scopes"), } - authenticationKey, err := base64.StdEncoding.DecodeString(viper.GetString("WebInterface.Sessions.AuthenticationKey")) - if err != nil { - panic(err) - } - if len(authenticationKey) < 32 { - panic("The given session authentication key is too short!") - } - encryptionKey, err := base64.StdEncoding.DecodeString(viper.GetString("WebInterface.Sessions.EncryptionKey")) - if err != nil { - panic(err) - } - if len(encryptionKey) < 32 { - panic("The given session encryption key is too short!") - } + authenticationKey := getSecretKey("WebInterface.Sessions.AuthenticationKey", 32) + encryptionKey := getSecretKey("WebInterface.Sessions.EncryptionKey", 32) var sessions SessionSettings = SessionSettings{ AuthenticationKey: authenticationKey, EncryptionKey: encryptionKey, @@ -270,7 +274,10 @@ func main() { securityFrontend.GlobalTokenValidity = viper.GetInt("Telegram.TokenGenerator.GlobalValidity") securityFrontend.PerAlbumTokenValidity = viper.GetInt("Telegram.TokenGenerator.PerAlbumValidity") + // Put the Web Interface behind the security frontend + securityFrontend.Protected = web + initLogFile() go photoBot.Process() - photoBot.ServeWebInterface(viper.GetString("WebInterface.Listen"), securityFrontend) + ServeWebInterface(viper.GetString("WebInterface.Listen"), securityFrontend, statikFS) } diff --git a/security.go b/security.go index 3406b2c..017000f 100644 --- a/security.go +++ b/security.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "net/http" + "net/url" "strings" "time" @@ -46,6 +47,20 @@ func init() { gob.Register(&WebUser{}) } +func GetOAuthCallbackURL(publicUrl string) string { + u, err := url.Parse(publicUrl) + if err != nil { + // If the URL cannot be parsed, use it as-is + return publicUrl + } + + u.Path = "/oauth/callback" + u.Fragment = "" + u.RawQuery = "" + + return u.String() +} + func NewSecurityFrontend(openidSettings OpenIdSettings, sessionSettings SessionSettings, tokenGenerator *TokenGenerator) (*SecurityFrontend, error) { var securityFrontend SecurityFrontend provider, err := oidc.NewProvider(context.TODO(), openidSettings.DiscoveryUrl) @@ -289,6 +304,7 @@ func (securityFrontend *SecurityFrontend) handleTelegramTokenAuthentication(w ht Timestamp: time.Now(), Entitlement: album, } + // try to validate the token with an album entitlement ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.PerAlbumTokenValidity) if err != nil { http.Error(w, "Invalid Token", http.StatusBadRequest) @@ -296,6 +312,7 @@ func (securityFrontend *SecurityFrontend) handleTelegramTokenAuthentication(w ht } if !ok { + // if it fails, it may be a global token data.Entitlement = "" ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.GlobalTokenValidity) if !ok || err != nil { diff --git a/web.go b/web.go index 0b4d06d..6baa669 100644 --- a/web.go +++ b/web.go @@ -8,17 +8,38 @@ import ( "sort" "strings" "time" - - _ "github.com/nmasse-itix/Telegram-Photo-Album-Bot/statik" ) type WebInterface struct { + MediaStore *MediaStore AlbumTemplate *template.Template MediaTemplate *template.Template IndexTemplate *template.Template SiteName string } +func NewWebInterface(statikFS http.FileSystem) (*WebInterface, error) { + var err error + + web := WebInterface{} + web.AlbumTemplate, err = getTemplate(statikFS, "/album.html.template", "album") + if err != nil { + return nil, err + } + + web.MediaTemplate, err = getTemplate(statikFS, "/media.html.template", "media") + if err != nil { + return nil, err + } + + web.IndexTemplate, err = getTemplate(statikFS, "/index.html.template", "index") + if err != nil { + return nil, err + } + + return &web, nil +} + func slurpFile(statikFS http.FileSystem, filename string) (string, error) { fd, err := statikFS.Open(filename) if err != nil { @@ -66,99 +87,99 @@ func getTemplate(statikFS http.FileSystem, filename string, name string) (*templ return tmpl.Funcs(customFunctions).Parse(content) } -func (bot *PhotoBot) HandleFileNotFound(w http.ResponseWriter, r *http.Request) { +func (web *WebInterface) handleFileNotFound(w http.ResponseWriter, r *http.Request) { http.Error(w, "File not found", http.StatusNotFound) } -func (bot *PhotoBot) HandleError(w http.ResponseWriter, r *http.Request) { +func (web *WebInterface) handleError(w http.ResponseWriter, r *http.Request) { http.Error(w, "Internal server error", http.StatusInternalServerError) } -func (bot *PhotoBot) HandleDisplayAlbum(w http.ResponseWriter, r *http.Request, albumName string) { +func (web *WebInterface) handleDisplayAlbum(w http.ResponseWriter, r *http.Request, albumName string) { if albumName == "latest" { albumName = "" } - album, err := bot.MediaStore.GetAlbum(albumName, false) + album, err := web.MediaStore.GetAlbum(albumName, false) if err != nil { log.Printf("MediaStore.GetAlbum: %s", err) - bot.HandleError(w, r) + web.handleError(w, r) return } - err = bot.WebInterface.AlbumTemplate.Execute(w, album) + err = web.AlbumTemplate.Execute(w, album) if err != nil { log.Printf("Template.Execute: %s", err) - bot.HandleError(w, r) + web.handleError(w, r) return } } -func (bot *PhotoBot) HandleDisplayIndex(w http.ResponseWriter, r *http.Request) { - albums, err := bot.MediaStore.ListAlbums() +func (web *WebInterface) handleDisplayIndex(w http.ResponseWriter, r *http.Request) { + albums, err := web.MediaStore.ListAlbums() if err != nil { log.Printf("MediaStore.ListAlbums: %s", err) - bot.HandleError(w, r) + web.handleError(w, r) return } sort.Sort(sort.Reverse(albums)) - err = bot.WebInterface.IndexTemplate.Execute(w, struct { + err = web.IndexTemplate.Execute(w, struct { Title string Albums []Album }{ - bot.WebInterface.SiteName, + web.SiteName, albums, }) if err != nil { log.Printf("Template.Execute: %s", err) - bot.HandleError(w, r) + web.handleError(w, r) return } } -func (bot *PhotoBot) HandleDisplayMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaId string) { +func (web *WebInterface) handleDisplayMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaId string) { if albumName == "latest" { albumName = "" } - media, err := bot.MediaStore.GetMedia(albumName, mediaId) + media, err := web.MediaStore.GetMedia(albumName, mediaId) if err != nil { log.Printf("MediaStore.GetMedia: %s", err) - bot.HandleError(w, r) + web.handleError(w, r) return } if media == nil { - bot.HandleFileNotFound(w, r) + web.handleFileNotFound(w, r) return } - err = bot.WebInterface.MediaTemplate.Execute(w, media) + err = web.MediaTemplate.Execute(w, media) if err != nil { log.Printf("Template.Execute: %s", err) - bot.HandleError(w, r) + web.handleError(w, r) return } } -func (bot *PhotoBot) HandleGetMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaFilename string) { +func (web *WebInterface) handleGetMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaFilename string) { if albumName == "latest" { albumName = "" } - fd, modtime, err := bot.MediaStore.OpenFile(albumName, mediaFilename) + fd, modtime, err := web.MediaStore.OpenFile(albumName, mediaFilename) if err != nil { log.Printf("MediaStore.OpenFile: %s", err) - bot.HandleError(w, r) + web.handleError(w, r) return } defer fd.Close() http.ServeContent(w, r, mediaFilename, modtime, fd) } -func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) { +func (web *WebInterface) ServeHTTP(w http.ResponseWriter, r *http.Request) { originalPath := r.URL.Path var resource string resource, r.URL.Path = ShiftPath(r.URL.Path) @@ -179,13 +200,13 @@ func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, originalPath+"/", http.StatusMovedPermanently) return } - bot.HandleDisplayAlbum(w, r, albumName) + web.handleDisplayAlbum(w, r, albumName) return } else if kind == "raw" && media != "" { - bot.HandleGetMedia(w, r, albumName, media) + web.handleGetMedia(w, r, albumName, media) return } else if kind == "media" && media != "" { - bot.HandleDisplayMedia(w, r, albumName, media) + web.handleDisplayMedia(w, r, albumName, media) return } } else { @@ -193,7 +214,7 @@ func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, originalPath+"/", http.StatusMovedPermanently) return } - bot.HandleDisplayIndex(w, r) + web.handleDisplayIndex(w, r) return } } else if resource == "" { @@ -201,5 +222,5 @@ func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - bot.HandleFileNotFound(w, r) + web.handleFileNotFound(w, r) }