From 893caf8ec402e45bc85913f1ebc633a008619a5e Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Tue, 14 Dec 2021 17:38:36 +0100 Subject: [PATCH] Rework sessions, config and some tests --- authentication_test.go | 33 +++------ captcha_test.go | 23 +----- config.go | 160 +++++++++++++++++++++++++++-------------- config_test.go | 18 +++++ dbmigrations/00026.sql | 3 + example-config.yml | 2 - go.mod | 8 +-- go.sum | 12 ++-- httpClient_test.go | 2 +- main.go | 6 +- sessions.go | 96 +++++++++++++------------ telegram_test.go | 104 ++++++++------------------- 12 files changed, 237 insertions(+), 230 deletions(-) create mode 100644 config_test.go create mode 100644 dbmigrations/00026.sql diff --git a/authentication_test.go b/authentication_test.go index a932e2e..2e26f11 100644 --- a/authentication_test.go +++ b/authentication_test.go @@ -5,7 +5,6 @@ import ( "net/http" "net/http/httptest" "net/url" - "path/filepath" "strings" "testing" @@ -17,32 +16,20 @@ import ( func Test_authMiddleware(t *testing.T) { app := &goBlog{ - cfg: &config{ - Db: &configDb{ - File: filepath.Join(t.TempDir(), "test.db"), - }, - Server: &configServer{ - PublicAddress: "https://example.com", - }, - Blogs: map[string]*configBlog{ - "en": { - Lang: "en", - }, - }, - DefaultBlog: "en", - User: &configUser{ - Nick: "test", - Password: "pass", - AppPasswords: []*configAppPassword{ - { - Username: "app1", - Password: "pass1", - }, - }, + cfg: createDefaultTestConfig(t), + } + app.cfg.User = &configUser{ + Nick: "test", + Password: "pass", + AppPasswords: []*configAppPassword{ + { + Username: "app1", + Password: "pass1", }, }, } + _ = app.initConfig() _ = app.initDatabase(false) app.initComponents(false) diff --git a/captcha_test.go b/captcha_test.go index 5cd855f..e911276 100644 --- a/captcha_test.go +++ b/captcha_test.go @@ -1,11 +1,9 @@ package main import ( - "context" "io" "net/http" "net/http/httptest" - "path/filepath" "testing" "github.com/stretchr/testify/assert" @@ -14,23 +12,10 @@ import ( func Test_captchaMiddleware(t *testing.T) { app := &goBlog{ - cfg: &config{ - Db: &configDb{ - File: filepath.Join(t.TempDir(), "test.db"), - }, - Server: &configServer{ - PublicAddress: "https://example.com", - }, - Blogs: map[string]*configBlog{ - "en": { - Lang: "en", - }, - }, - DefaultBlog: "en", - User: &configUser{}, - }, + cfg: createDefaultTestConfig(t), } + _ = app.initConfig() _ = app.initDatabase(false) app.initComponents(false) @@ -39,11 +24,9 @@ func Test_captchaMiddleware(t *testing.T) { })) t.Run("Default", func(t *testing.T) { - req := httptest.NewRequest(http.MethodPost, "/abc", nil) - rec := httptest.NewRecorder() - h.ServeHTTP(rec, req.WithContext(context.WithValue(req.Context(), blogKey, "en"))) + h.ServeHTTP(rec, reqWithDefaultBlog(httptest.NewRequest(http.MethodPost, "/abc", nil))) res := rec.Result() resBody, _ := io.ReadAll(res.Body) diff --git a/config.go b/config.go index e1b820a..ae4c767 100644 --- a/config.go +++ b/config.go @@ -27,6 +27,7 @@ type config struct { EasterEgg *configEasterEgg `mapstructure:"easterEgg"` Debug bool `mapstructure:"debug"` MapTiles *configMapTiles `mapstructure:"mapTiles"` + initialized bool } type configServer struct { @@ -42,7 +43,6 @@ type configServer struct { Tor bool `mapstructure:"tor"` SecurityHeaders bool `mapstructure:"securityHeaders"` CSPDomains []string `mapstructure:"cspDomains"` - JWTSecret string `mapstructure:"jwtSecret"` publicHostname string shortPublicHostname string mediaHostname string @@ -287,86 +287,83 @@ type configMapTiles struct { MaxZoom int `mapstructure:"maxZoom"` } -func (a *goBlog) initConfig(file string) error { - log.Println("Initialize configuration...") +func (a *goBlog) loadConfigFile(file string) error { + // Use viper to load the config file + v := viper.New() if file != "" { // Use config file from the flag - viper.SetConfigFile(file) + v.SetConfigFile(file) } else { - viper.SetConfigName("config") - viper.AddConfigPath("./config/") + // Search in default locations + v.SetConfigName("config") + v.AddConfigPath("./config/") } - err := viper.ReadInConfig() - if err != nil { + // Read config + if err := v.ReadInConfig(); err != nil { return err } - // Defaults - viper.SetDefault("server.logging", false) - viper.SetDefault("server.logFile", "data/access.log") - viper.SetDefault("server.port", 8080) - viper.SetDefault("server.publicAddress", "http://localhost:8080") - viper.SetDefault("server.publicHttps", false) - viper.SetDefault("database.file", "data/db.sqlite") - viper.SetDefault("cache.enable", true) - viper.SetDefault("cache.expiration", 600) - viper.SetDefault("user.nick", "admin") - viper.SetDefault("user.password", "secret") - viper.SetDefault("hooks.shell", "/bin/bash") - viper.SetDefault("micropub.categoryParam", "tags") - viper.SetDefault("micropub.replyParam", "replylink") - viper.SetDefault("micropub.replyTitleParam", "replytitle") - viper.SetDefault("micropub.likeParam", "likelink") - viper.SetDefault("micropub.likeTitleParam", "liketitle") - viper.SetDefault("micropub.bookmarkParam", "link") - viper.SetDefault("micropub.audioParam", "audio") - viper.SetDefault("micropub.photoParam", "images") - viper.SetDefault("micropub.photoDescriptionParam", "imagealts") - viper.SetDefault("micropub.locationParam", "location") - viper.SetDefault("activityPub.tagsTaxonomies", []string{"tags"}) // Unmarshal config - a.cfg = &config{} - err = viper.Unmarshal(a.cfg) - if err != nil { - return err + a.cfg = createDefaultConfig() + return v.Unmarshal(a.cfg) +} + +func (a *goBlog) initConfig() error { + if a.cfg == nil { + a.cfg = createDefaultConfig() + } + if a.cfg.initialized { + return nil } // Check config + // Parse addresses and hostnames + if a.cfg.Server.PublicAddress == "" { + return errors.New("no public address configured") + } publicURL, err := url.Parse(a.cfg.Server.PublicAddress) if err != nil { - return err + return errors.New("Invalid public address: " + err.Error()) } a.cfg.Server.publicHostname = publicURL.Hostname() if sa := a.cfg.Server.ShortPublicAddress; sa != "" { shortPublicURL, err := url.Parse(sa) if err != nil { - return err + return errors.New("Invalid short public address: " + err.Error()) } a.cfg.Server.shortPublicHostname = shortPublicURL.Hostname() } if ma := a.cfg.Server.MediaAddress; ma != "" { mediaUrl, err := url.Parse(ma) if err != nil { - return err + return errors.New("Invalid media address: " + err.Error()) } a.cfg.Server.mediaHostname = mediaUrl.Hostname() } - if a.cfg.Server.JWTSecret == "" { - return errors.New("no JWT secret configured") - } - if len(a.cfg.Blogs) == 0 { - return errors.New("no blog configured") - } - if len(a.cfg.DefaultBlog) == 0 || a.cfg.Blogs[a.cfg.DefaultBlog] == nil { - return errors.New("no default blog or default blog not present") - } - if a.cfg.Micropub.MediaStorage != nil { - if a.cfg.Micropub.MediaStorage.MediaURL == "" || - a.cfg.Micropub.MediaStorage.BunnyStorageKey == "" || - a.cfg.Micropub.MediaStorage.BunnyStorageName == "" { - a.cfg.Micropub.MediaStorage.BunnyStorageKey = "" - a.cfg.Micropub.MediaStorage.BunnyStorageName = "" + // Check if any blog is configured + if a.cfg.Blogs == nil || len(a.cfg.Blogs) == 0 { + a.cfg.Blogs = map[string]*configBlog{ + "default": createDefaultBlog(), } - a.cfg.Micropub.MediaStorage.MediaURL = strings.TrimSuffix(a.cfg.Micropub.MediaStorage.MediaURL, "/") } + // Check if default blog is set + if a.cfg.DefaultBlog == "" { + if len(a.cfg.Blogs) == 1 { + // Set default blog to the only blog that is configured + for k := range a.cfg.Blogs { + a.cfg.DefaultBlog = k + } + } else { + return errors.New("no default blog configured") + } + } + // Check if default blog exists + if a.cfg.Blogs[a.cfg.DefaultBlog] == nil { + return errors.New("default blog does not exist") + } + // Check media storage config + if ms := a.cfg.Micropub.MediaStorage; ms != nil && ms.MediaURL != "" { + ms.MediaURL = strings.TrimSuffix(ms.MediaURL, "/") + } + // Check if webmention receiving is disabled if wm := a.cfg.Webmention; wm != nil && wm.DisableReceiving { // Disable comments for all blogs for _, b := range a.cfg.Blogs { @@ -380,10 +377,65 @@ func (a *goBlog) initConfig(file string) error { br.Enabled = false } } + // Log success + a.cfg.initialized = true log.Println("Initialized configuration") return nil } +func createDefaultConfig() *config { + return &config{ + Server: &configServer{ + Port: 8080, + PublicAddress: "http://localhost:8080", + }, + Db: &configDb{ + File: "data/db.sqlite", + }, + Cache: &configCache{ + Enable: true, + Expiration: 600, + }, + User: &configUser{ + Nick: "admin", + Password: "secret", + }, + Hooks: &configHooks{ + Shell: "/bin/bash", + }, + Micropub: &configMicropub{ + CategoryParam: "tags", + ReplyParam: "replylink", + ReplyTitleParam: "replytitle", + LikeParam: "likelink", + LikeTitleParam: "liketitle", + BookmarkParam: "link", + AudioParam: "audio", + PhotoParam: "images", + PhotoDescriptionParam: "imagealts", + LocationParam: "location", + }, + ActivityPub: &configActivityPub{ + TagsTaxonomies: []string{"tags"}, + }, + } +} + +func createDefaultBlog() *configBlog { + return &configBlog{ + Path: "/", + Lang: "en", + Title: "My Blog", + Description: "Welcome to my blog.", + Sections: map[string]*configSection{ + "posts": { + Title: "Posts", + }, + }, + DefaultSection: "posts", + } +} + func (a *goBlog) httpsConfigured(checkAddress bool) bool { return a.cfg.Server.PublicHTTPS || a.cfg.Server.TailscaleHTTPS || diff --git a/config_test.go b/config_test.go new file mode 100644 index 0000000..310206f --- /dev/null +++ b/config_test.go @@ -0,0 +1,18 @@ +package main + +import ( + "context" + "net/http" + "path/filepath" + "testing" +) + +func createDefaultTestConfig(t *testing.T) *config { + c := createDefaultConfig() + c.Db.File = filepath.Join(t.TempDir(), "blog.db") + return c +} + +func reqWithDefaultBlog(req *http.Request) *http.Request { + return req.WithContext(context.WithValue(req.Context(), blogKey, "default")) +} diff --git a/dbmigrations/00026.sql b/dbmigrations/00026.sql new file mode 100644 index 0000000..652f4e9 --- /dev/null +++ b/dbmigrations/00026.sql @@ -0,0 +1,3 @@ +drop table sessions; +create table sessions (id text primary key, data blob, created text default '', modified text default '', expires text default ''); +create index sessions_exp on sessions (expires); \ No newline at end of file diff --git a/example-config.yml b/example-config.yml index a2be07c..395c1c9 100644 --- a/example-config.yml +++ b/example-config.yml @@ -23,8 +23,6 @@ server: securityHeaders: true # Set security HTTP headers (to always use HTTPS etc.) cspDomains: # Specify additional domains to allow embedded content with enabled securityHeaders - media.example.com - # Cookies - jwtSecret: changeThisWeakSecret # secret to use for cookies (login and captcha) # Tor tor: true # Publish onion service, requires Tor to be installed and available in path # Tailscale (see https://tailscale.com) diff --git a/go.mod b/go.mod index f73286d..27bff41 100644 --- a/go.mod +++ b/go.mod @@ -20,10 +20,9 @@ require ( github.com/emersion/go-smtp v0.15.0 github.com/go-chi/chi/v5 v5.0.7 github.com/go-fed/httpsig v1.1.0 - github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.0 + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/google/uuid v1.3.0 github.com/gorilla/handlers v1.5.1 - github.com/gorilla/securecookie v1.1.1 github.com/gorilla/sessions v1.2.1 github.com/gorilla/websocket v1.4.2 github.com/hacdias/indieauth v1.7.1 @@ -44,7 +43,7 @@ require ( github.com/spf13/cast v1.4.1 github.com/spf13/viper v1.10.0 github.com/stretchr/testify v1.7.0 - github.com/tdewolff/minify/v2 v2.9.22 + github.com/tdewolff/minify/v2 v2.9.23 github.com/thoas/go-funk v0.9.1 github.com/tkrajina/gpxgo v1.1.2 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 @@ -87,6 +86,7 @@ require ( github.com/google/btree v1.0.1 // indirect github.com/google/go-cmp v0.5.6 // indirect github.com/gorilla/css v1.0.0 // indirect + github.com/gorilla/securecookie v1.1.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/insomniacslk/dhcp v0.0.0-20210621130208-1cac67f12b1e // indirect github.com/jonboulle/clockwork v0.2.2 // indirect @@ -112,7 +112,7 @@ require ( github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect - github.com/tdewolff/parse/v2 v2.5.21 // indirect + github.com/tdewolff/parse/v2 v2.5.24 // indirect github.com/u-root/uio v0.0.0-20210528114334-82958018845c // indirect github.com/vishvananda/netlink v1.1.1-0.20211101163509-b10eb8fe5cf6 // indirect github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae // indirect diff --git a/go.sum b/go.sum index a3a6793..3fc561f 100644 --- a/go.sum +++ b/go.sum @@ -139,8 +139,8 @@ github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1 github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.0 h1:BtndtqqCQfPsL2uMkYmduOip1+dPcSmh40l82mBUPKk= -github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.0/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -387,10 +387,10 @@ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQ github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= -github.com/tdewolff/minify/v2 v2.9.22 h1:PlmaAakaJHdMMdTTwjjsuSwIxKqWPTlvjTj6a/g/ILU= -github.com/tdewolff/minify/v2 v2.9.22/go.mod h1:dNlaFdXaIxgSXh3UFASqjTY0/xjpDkkCsYHA1NCGnmQ= -github.com/tdewolff/parse/v2 v2.5.21 h1:s/OLsVxxmQUlbFtPODDVHA836qchgmoxjEsk/cUZl48= -github.com/tdewolff/parse/v2 v2.5.21/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= +github.com/tdewolff/minify/v2 v2.9.23 h1:UrLltJpnJPm7/fYFP3Ue/GD5tHufx2z7ERQihACLkmg= +github.com/tdewolff/minify/v2 v2.9.23/go.mod h1:4o1Mw4T3RLV0CHUny7OEnntezuwoj/FNst4QzrNxIts= +github.com/tdewolff/parse/v2 v2.5.24 h1:sJPG5Viy2lq9NBbnK4KpWEA+17RNZz8EOXVqErHKHgs= +github.com/tdewolff/parse/v2 v2.5.24/go.mod h1:WzaJpRSbwq++EIQHYIRTpbYKNA3gn9it1Ik++q4zyho= github.com/tdewolff/test v1.0.6 h1:76mzYJQ83Op284kMT+63iCNCI7NEERsIN8dLM+RiKr4= github.com/tdewolff/test v1.0.6/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M= diff --git a/httpClient_test.go b/httpClient_test.go index 9373363..ef667b5 100644 --- a/httpClient_test.go +++ b/httpClient_test.go @@ -29,7 +29,7 @@ func newFakeHttpClient() *fakeHttpClient { } // Copy result status code and body rw.WriteHeader(fc.res.StatusCode) - io.Copy(rw, rec.Body) + _, _ = io.Copy(rw, rec.Body) } }), }, diff --git a/main.go b/main.go index ccefbde..b1e42b2 100644 --- a/main.go +++ b/main.go @@ -57,7 +57,11 @@ func main() { } // Initialize config - if err = app.initConfig(*configfile); err != nil { + if err = app.loadConfigFile(*configfile); err != nil { + app.logErrAndQuit("Failed to load config file:", err.Error()) + return + } + if err = app.initConfig(); err != nil { app.logErrAndQuit("Failed to init config:", err.Error()) return } diff --git a/sessions.go b/sessions.go index cdebfc9..f00c137 100644 --- a/sessions.go +++ b/sessions.go @@ -1,14 +1,16 @@ package main import ( + "bytes" "database/sql" - "fmt" + "encoding/gob" "log" "net/http" + "strings" "time" "github.com/araddon/dateparse" - "github.com/gorilla/securecookie" + "github.com/google/uuid" "github.com/gorilla/sessions" ) @@ -30,7 +32,6 @@ func (a *goBlog) initSessions() { deleteExpiredSessions() a.hourlyHooks = append(a.hourlyHooks, deleteExpiredSessions) a.loginSessions = &dbSessionStore{ - codecs: securecookie.CodecsFromPairs([]byte(a.cfg.Server.JWTSecret)), options: &sessions.Options{ Secure: a.httpsConfigured(true), HttpOnly: true, @@ -41,7 +42,6 @@ func (a *goBlog) initSessions() { db: a.db, } a.captchaSessions = &dbSessionStore{ - codecs: securecookie.CodecsFromPairs([]byte(a.cfg.Server.JWTSecret)), options: &sessions.Options{ Secure: a.httpsConfigured(true), HttpOnly: true, @@ -55,7 +55,6 @@ func (a *goBlog) initSessions() { type dbSessionStore struct { options *sessions.Options - codecs []securecookie.Codec db *database } @@ -67,29 +66,33 @@ func (s *dbSessionStore) New(r *http.Request, name string) (session *sessions.Se session = sessions.NewSession(s, name) opts := *s.options session.Options = &opts - session.IsNew = true - if cook, errCookie := r.Cookie(name); errCookie == nil { - if err = securecookie.DecodeMulti(name, cook.Value, &session.ID, s.codecs...); err == nil { - session.IsNew = s.load(session) == nil + if c, cErr := r.Cookie(name); cErr == nil && strings.HasPrefix(c.Value, session.Name()+"-") { + // Has cookie, load from database + session.ID = c.Value + if s.load(session) != nil { + // Failed to load session from database, delete the ID (= new session) + session.ID = "" } } + // If no ID, the session is new + session.IsNew = session.ID == "" return session, err } func (s *dbSessionStore) Save(r *http.Request, w http.ResponseWriter, ss *sessions.Session) (err error) { if ss.ID == "" { + // Is new session, save it to database if err = s.insert(ss); err != nil { return err } - } else if err = s.save(ss); err != nil { - return err - } - if encoded, err := securecookie.EncodeMulti(ss.Name(), ss.ID, s.codecs...); err != nil { - return err } else { - http.SetCookie(w, sessions.NewCookie(ss.Name(), encoded, ss.Options)) - return nil + // Update existing session + if err = s.save(ss); err != nil { + return err + } } + http.SetCookie(w, sessions.NewCookie(ss.Name(), ss.ID, ss.Options)) + return nil } func (s *dbSessionStore) Delete(r *http.Request, w http.ResponseWriter, session *sessions.Session) error { @@ -106,15 +109,20 @@ func (s *dbSessionStore) Delete(r *http.Request, w http.ResponseWriter, session } func (s *dbSessionStore) load(session *sessions.Session) (err error) { - row, err := s.db.queryRow("select data, created, modified, expires from sessions where id = @id and expires > @now", sql.Named("id", session.ID), sql.Named("now", utcNowString())) + row, err := s.db.queryRow( + "select data, created, modified, expires from sessions where id = @id and expires > @now", + sql.Named("id", session.ID), + sql.Named("now", utcNowString()), + ) if err != nil { return err } - var data, createdStr, modifiedStr, expiresStr string + var createdStr, modifiedStr, expiresStr string + var data []byte if err = row.Scan(&data, &createdStr, &modifiedStr, &expiresStr); err != nil { return err } - if err = securecookie.DecodeMulti(session.Name(), data, &session.Values, s.codecs...); err != nil { + if err = gob.NewDecoder(bytes.NewReader(data)).Decode(&session.Values); err != nil { return err } session.Values[sessionCreatedOn] = timeNoErr(dateparse.ParseLocal(createdStr)) @@ -124,44 +132,44 @@ func (s *dbSessionStore) load(session *sessions.Session) (err error) { } func (s *dbSessionStore) insert(session *sessions.Session) (err error) { - created := time.Now().UTC() - modified := time.Now().UTC() - expires := time.Now().UTC().Add(time.Second * time.Duration(session.Options.MaxAge)) - delete(session.Values, sessionCreatedOn) - delete(session.Values, sessionExpiresOn) - delete(session.Values, sessionModifiedOn) - encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, s.codecs...) - if err != nil { + deleteSessionValuesNotNeededForDb(session) + var encoded bytes.Buffer + if err := gob.NewEncoder(&encoded).Encode(session.Values); err != nil { return err } - res, err := s.db.exec("insert or replace into sessions(data, created, modified, expires) values(@data, @created, @modified, @expires)", - sql.Named("data", encoded), sql.Named("created", created.Format(time.RFC3339)), sql.Named("modified", modified.Format(time.RFC3339)), sql.Named("expires", expires.Format(time.RFC3339))) - if err != nil { - return err - } - lastInserted, err := res.LastInsertId() - if err != nil { - return err - } - session.ID = fmt.Sprintf("%d", lastInserted) - return nil + session.ID = session.Name() + "-" + uuid.NewString() + created, modified := utcNowString(), utcNowString() + expires := time.Now().UTC().Add(time.Second * time.Duration(session.Options.MaxAge)).Format(time.RFC3339) + _, err = s.db.exec( + "insert or replace into sessions(id, data, created, modified, expires) values(@id, @data, @created, @modified, @expires)", + sql.Named("id", session.ID), + sql.Named("data", encoded.Bytes()), + sql.Named("created", created), + sql.Named("modified", modified), + sql.Named("expires", expires), + ) + return err } func (s *dbSessionStore) save(session *sessions.Session) (err error) { if session.IsNew { return s.insert(session) } - delete(session.Values, sessionCreatedOn) - delete(session.Values, sessionExpiresOn) - delete(session.Values, sessionModifiedOn) - encoded, err := securecookie.EncodeMulti(session.Name(), session.Values, s.codecs...) - if err != nil { + deleteSessionValuesNotNeededForDb(session) + var encoded bytes.Buffer + if err = gob.NewEncoder(&encoded).Encode(session.Values); err != nil { return err } _, err = s.db.exec("update sessions set data = @data, modified = @modified where id = @id", - sql.Named("data", encoded), sql.Named("modified", utcNowString()), sql.Named("id", session.ID)) + sql.Named("data", encoded.Bytes()), sql.Named("modified", utcNowString()), sql.Named("id", session.ID)) if err != nil { return err } return nil } + +func deleteSessionValuesNotNeededForDb(session *sessions.Session) { + delete(session.Values, sessionCreatedOn) + delete(session.Values, sessionExpiresOn) + delete(session.Values, sessionModifiedOn) +} diff --git a/telegram_test.go b/telegram_test.go index 6bbb67a..d0a8514 100644 --- a/telegram_test.go +++ b/telegram_test.go @@ -2,7 +2,6 @@ package main import ( "net/http" - "path/filepath" "testing" "time" @@ -11,42 +10,15 @@ import ( ) func Test_configTelegram_enabled(t *testing.T) { - if (&configTelegram{}).enabled() == true { - t.Error("Telegram shouldn't be enabled") - } - + assert.False(t, (&configTelegram{}).enabled()) var tg *configTelegram - if tg.enabled() == true { - t.Error("Telegram shouldn't be enabled") - } + assert.False(t, tg.enabled()) - if (&configTelegram{ - Enabled: true, - }).enabled() == true { - t.Error("Telegram shouldn't be enabled") - } + assert.False(t, (&configTelegram{Enabled: true}).enabled()) + assert.False(t, (&configTelegram{Enabled: true, ChatID: "abc"}).enabled()) + assert.False(t, (&configTelegram{Enabled: true, BotToken: "abc"}).enabled()) - if (&configTelegram{ - Enabled: true, - ChatID: "abc", - }).enabled() == true { - t.Error("Telegram shouldn't be enabled") - } - - if (&configTelegram{ - Enabled: true, - BotToken: "abc", - }).enabled() == true { - t.Error("Telegram shouldn't be enabled") - } - - if (&configTelegram{ - Enabled: true, - BotToken: "abc", - ChatID: "abc", - }).enabled() != true { - t.Error("Telegram should be enabled") - } + assert.True(t, (&configTelegram{Enabled: true, ChatID: "abc", BotToken: "abc"}).enabled()) } func Test_configTelegram_generateHTML(t *testing.T) { @@ -78,11 +50,11 @@ func Test_configTelegram_send(t *testing.T) { fakeClient.setHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if r.URL.String() == "https://api.telegram.org/botbottoken/getMe" { rw.WriteHeader(http.StatusOK) - rw.Write([]byte(`{"ok":true,"result":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"}}`)) + _, _ = rw.Write([]byte(`{"ok":true,"result":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"}}`)) return } rw.WriteHeader(http.StatusOK) - rw.Write([]byte(`{"ok":true,"result":{"message_id":123,"from":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"},"chat":{"id":789,"first_name":"Test","username":"testbot"},"date":1564181818,"text":"Message"}}`)) + _, _ = rw.Write([]byte(`{"ok":true,"result":{"message_id":123,"from":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"},"chat":{"id":789,"first_name":"Test","username":"testbot"},"date":1564181818,"text":"Message"}}`)) })) tg := &configTelegram{ @@ -126,38 +98,31 @@ func Test_goBlog_initTelegram(t *testing.T) { func Test_telegram(t *testing.T) { t.Run("Send post to Telegram", func(t *testing.T) { fakeClient := newFakeHttpClient() - fakeClient.setHandler(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if r.URL.String() == "https://api.telegram.org/botbottoken/getMe" { rw.WriteHeader(http.StatusOK) - rw.Write([]byte(`{"ok":true,"result":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"}}`)) + _, _ = rw.Write([]byte(`{"ok":true,"result":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"}}`)) return } rw.WriteHeader(http.StatusOK) - rw.Write([]byte(`{"ok":true,"result":{"message_id":123,"from":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"},"chat":{"id":123456789,"first_name":"Test","username":"testbot"},"date":1564181818,"text":"Message"}}`)) + _, _ = rw.Write([]byte(`{"ok":true,"result":{"message_id":123,"from":{"id":123456789,"is_bot":true,"first_name":"Test","username":"testbot"},"chat":{"id":123456789,"first_name":"Test","username":"testbot"},"date":1564181818,"text":"Message"}}`)) })) + cfg := createDefaultTestConfig(t) + cfg.Blogs = map[string]*configBlog{ + "en": createDefaultBlog(), + } + cfg.Blogs["en"].Telegram = &configTelegram{ + Enabled: true, + ChatID: "chatid", + BotToken: "bottoken", + } + app := &goBlog{ - pPostHooks: []postHookFunc{}, - cfg: &config{ - Db: &configDb{ - File: filepath.Join(t.TempDir(), "test.db"), - }, - Server: &configServer{ - PublicAddress: "https://example.com", - }, - Blogs: map[string]*configBlog{ - "en": { - Telegram: &configTelegram{ - Enabled: true, - ChatID: "chatid", - BotToken: "bottoken", - }, - }, - }, - }, + cfg: cfg, httpClient: fakeClient.Client, } + _ = app.initConfig() _ = app.initDatabase(false) app.initMarkdown() @@ -179,43 +144,32 @@ func Test_telegram(t *testing.T) { req := fakeClient.req assert.Equal(t, "chatid", req.FormValue("chat_id")) assert.Equal(t, "HTML", req.FormValue("parse_mode")) - assert.Equal(t, "Title\n\nhttps://example.com/s/1", req.FormValue("text")) + assert.Equal(t, "Title\n\nhttp://localhost:8080/s/1", req.FormValue("text")) }) t.Run("Telegram disabled", func(t *testing.T) { fakeClient := newFakeHttpClient() app := &goBlog{ - pPostHooks: []postHookFunc{}, - cfg: &config{ - Db: &configDb{ - File: filepath.Join(t.TempDir(), "test.db"), - }, - Server: &configServer{ - PublicAddress: "https://example.com", - }, - Blogs: map[string]*configBlog{ - "en": {}, - }, - }, + cfg: createDefaultTestConfig(t), httpClient: fakeClient.Client, } + + _ = app.initConfig() _ = app.initDatabase(false) app.initTelegram() - p := &post{ + app.postPostHooks(&post{ Path: "/test", Parameters: map[string][]string{ "title": {"Title"}, }, Published: time.Now().String(), Section: "test", - Blog: "en", + Blog: "default", Status: statusPublished, - } - - app.pPostHooks[0](p) + }) assert.Nil(t, fakeClient.req) })