diff --git a/config/config.go b/config/config.go index 7788a381..05b5d3b5 100644 --- a/config/config.go +++ b/config/config.go @@ -5,6 +5,7 @@ package config import ( + "net/url" "os" "strconv" ) @@ -26,7 +27,10 @@ const ( // Config manages configuration parameters. type Config struct { - IsHTTPS bool + IsHTTPS bool + baseURL string + rootURL string + basePath string } func (c *Config) get(key, fallback string) string { @@ -53,13 +57,34 @@ func (c *Config) HasDebugMode() bool { return c.get("DEBUG", "") != "" } -// BaseURL returns the application base URL. +// BaseURL returns the application base URL with path. func (c *Config) BaseURL() string { - baseURL := c.get("BASE_URL", defaultBaseURL) - if baseURL[len(baseURL)-1:] == "/" { - baseURL = baseURL[:len(baseURL)-1] + if c.baseURL == "" { + c.baseURL = c.get("BASE_URL", defaultBaseURL) + if c.baseURL[len(c.baseURL)-1:] == "/" { + c.baseURL = c.baseURL[:len(c.baseURL)-1] + } } - return baseURL + return c.baseURL +} + +// RootURL returns the base URL without path. +func (c *Config) RootURL() string { + if c.rootURL == "" { + u, _ := url.Parse(c.BaseURL()) + u.Path = "" + c.rootURL = u.String() + } + return c.rootURL +} + +// BasePath returns the application base path according to the base URL. +func (c *Config) BasePath() string { + if c.basePath == "" { + u, _ := url.Parse(c.BaseURL()) + c.basePath = u.Path + } + return c.basePath } // DatabaseURL returns the database URL. diff --git a/config/config_test.go b/config/config_test.go index 4c01bc5e..fbc7175e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -9,7 +9,26 @@ import ( "testing" ) -func TestGetCustomBaseURL(t *testing.T) { +func TestDebugModeOn(t *testing.T) { + os.Clearenv() + os.Setenv("DEBUG", "1") + cfg := NewConfig() + + if !cfg.HasDebugMode() { + t.Fatalf(`Unexpected debug mode value, got "%v"`, cfg.HasDebugMode()) + } +} + +func TestDebugModeOff(t *testing.T) { + os.Clearenv() + cfg := NewConfig() + + if cfg.HasDebugMode() { + t.Fatalf(`Unexpected debug mode value, got "%v"`, cfg.HasDebugMode()) + } +} + +func TestCustomBaseURL(t *testing.T) { os.Clearenv() os.Setenv("BASE_URL", "http://example.org") cfg := NewConfig() @@ -17,9 +36,17 @@ func TestGetCustomBaseURL(t *testing.T) { if cfg.BaseURL() != "http://example.org" { t.Fatalf(`Unexpected base URL, got "%s"`, cfg.BaseURL()) } + + if cfg.RootURL() != "http://example.org" { + t.Fatalf(`Unexpected root URL, got "%s"`, cfg.RootURL()) + } + + if cfg.BasePath() != "" { + t.Fatalf(`Unexpected base path, got "%s"`, cfg.BasePath()) + } } -func TestGetCustomBaseURLWithTrailingSlash(t *testing.T) { +func TestCustomBaseURLWithTrailingSlash(t *testing.T) { os.Clearenv() os.Setenv("BASE_URL", "http://example.org/folder/") cfg := NewConfig() @@ -27,13 +54,29 @@ func TestGetCustomBaseURLWithTrailingSlash(t *testing.T) { if cfg.BaseURL() != "http://example.org/folder" { t.Fatalf(`Unexpected base URL, got "%s"`, cfg.BaseURL()) } + + if cfg.RootURL() != "http://example.org" { + t.Fatalf(`Unexpected root URL, got "%s"`, cfg.BaseURL()) + } + + if cfg.BasePath() != "/folder" { + t.Fatalf(`Unexpected base path, got "%s"`, cfg.BasePath()) + } } -func TestGetDefaultBaseURL(t *testing.T) { +func TestDefaultBaseURL(t *testing.T) { os.Clearenv() cfg := NewConfig() if cfg.BaseURL() != "http://localhost" { t.Fatalf(`Unexpected base URL, got "%s"`, cfg.BaseURL()) } + + if cfg.RootURL() != "http://localhost" { + t.Fatalf(`Unexpected root URL, got "%s"`, cfg.RootURL()) + } + + if cfg.BasePath() != "" { + t.Fatalf(`Unexpected base path, got "%s"`, cfg.BasePath()) + } } diff --git a/daemon/routes.go b/daemon/routes.go index 6b44fb4b..35cec9eb 100644 --- a/daemon/routes.go +++ b/daemon/routes.go @@ -45,6 +45,10 @@ func routes(cfg *config.Config, store *storage.Storage, feedHandler *feed.Handle middleware.NewSessionMiddleware(cfg, store).Handler, )) + if cfg.BasePath() != "" { + router = router.PathPrefix(cfg.BasePath()).Subrouter() + } + router.Handle("/fever/", feverHandler.Use(feverController.Handler)).Name("feverEndpoint") router.Handle("/v1/users", apiHandler.Use(apiController.CreateUser)).Methods("POST") diff --git a/http/cookie/cookie.go b/http/cookie/cookie.go index d1f3e723..4407daab 100644 --- a/http/cookie/cookie.go +++ b/http/cookie/cookie.go @@ -19,11 +19,11 @@ const ( ) // New creates a new cookie. -func New(name, value string, isHTTPS bool) *http.Cookie { +func New(name, value string, isHTTPS bool, path string) *http.Cookie { return &http.Cookie{ Name: name, Value: value, - Path: "/", + Path: basePath(path), Secure: isHTTPS, HttpOnly: true, Expires: time.Now().Add(cookieDuration * 24 * time.Hour), @@ -31,14 +31,21 @@ func New(name, value string, isHTTPS bool) *http.Cookie { } // Expired returns an expired cookie. -func Expired(name string, isHTTPS bool) *http.Cookie { +func Expired(name string, isHTTPS bool, path string) *http.Cookie { return &http.Cookie{ Name: name, Value: "", - Path: "/", + Path: basePath(path), Secure: isHTTPS, HttpOnly: true, MaxAge: -1, Expires: time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC), } } + +func basePath(path string) string { + if path == "" { + return "/" + } + return path +} diff --git a/http/middleware/session.go b/http/middleware/session.go index c3876f66..9b7dd865 100644 --- a/http/middleware/session.go +++ b/http/middleware/session.go @@ -36,7 +36,7 @@ func (s *SessionMiddleware) Handler(next http.Handler) http.Handler { return } - http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, s.cfg.IsHTTPS)) + http.SetCookie(w, cookie.New(cookie.CookieSessionID, session.ID, s.cfg.IsHTTPS, s.cfg.BasePath())) } else { logger.Debug("[Middleware:Session] %s", session) } diff --git a/template/html/integrations.html b/template/html/integrations.html index 8c597eef..91f5b7ac 100644 --- a/template/html/integrations.html +++ b/template/html/integrations.html @@ -40,7 +40,7 @@ -
{{ t "Fever API endpoint:" }} {{ baseURL }}{{ route "feverEndpoint" }}
+{{ t "Fever API endpoint:" }} {{ rootURL }}{{ route "feverEndpoint" }}
{{ t "This special link allows you to subscribe to a website directly by using a bookmark in your web browser." }}
{{ t "Drag and drop this link to your bookmarks." }}
diff --git a/template/template.go b/template/template.go index e392dad1..b3374ff5 100644 --- a/template/template.go +++ b/template/template.go @@ -38,6 +38,9 @@ func (e *Engine) parseAll() { "baseURL": func() string { return e.cfg.BaseURL() }, + "rootURL": func() string { + return e.cfg.RootURL() + }, "hasOAuth2Provider": func(provider string) bool { return e.cfg.OAuth2Provider() == provider }, diff --git a/template/views.go b/template/views.go index acf96d80..e189e5d2 100644 --- a/template/views.go +++ b/template/views.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// 2018-01-31 22:01:40.010173412 -0800 PST m=+0.035694895 +// 2018-02-03 15:28:45.540437885 -0800 PST m=+0.032377624 package template @@ -815,7 +815,7 @@ var templateViewsMap = map[string]string{ -{{ t "Fever API endpoint:" }} {{ baseURL }}{{ route "feverEndpoint" }}
+{{ t "Fever API endpoint:" }} {{ rootURL }}{{ route "feverEndpoint" }}
{{ t "This special link allows you to subscribe to a website directly by using a bookmark in your web browser." }}
{{ t "Drag and drop this link to your bookmarks." }}
@@ -1225,7 +1225,7 @@ var templateViewsMapChecksums = map[string]string{ "feeds": "65b0a47c4438810b9d51c60f3f3b2519690e56ff74029e6296c68626b83a470b", "history": "d2476fd727e4f53428b5ed1f3f9423063583337ec8cfe1dd9c931fcb03852a20", "import": "73b5112e20bfd232bf73334544186ea419505936bc237d481517a8622901878f", - "integrations": "a677434e9a8be1f80cfbc1d04828dacc7abcec2a22b45c80594d49cc2ba7c0e5", + "integrations": "958b73d632a3e2a79368bb1582efb8aabc438cef4fa6e8dc1aa4932494916aca", "login": "7d83c3067c02f1f6aafdd8816c7f97a4eb5a5a4bdaaaa4cc1e2fbb9c17ea65e8", "sessions": "d8ef5900d8ea8395804b320002e5f45ed0ab8b790e43f674f61f8b9787041cbd", "settings": "ea2505b9d0a6d6bb594dba87a92079de19baa6d494f0651693a7685489fb7de9", diff --git a/ui/login.go b/ui/login.go index f44e1813..fc10bc35 100644 --- a/ui/login.go +++ b/ui/login.go @@ -59,7 +59,7 @@ func (c *Controller) CheckLogin(ctx *handler.Context, request *handler.Request, logger.Info("[Controller:CheckLogin] username=%s just logged in", authForm.Username) - response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS)) + response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS, c.cfg.BasePath())) response.Redirect(ctx.Route("unread")) } @@ -75,6 +75,6 @@ func (c *Controller) Logout(ctx *handler.Context, request *handler.Request, resp logger.Error("[Controller:Logout] %v", err) } - response.SetCookie(cookie.Expired(cookie.CookieUserSessionID, c.cfg.IsHTTPS)) + response.SetCookie(cookie.Expired(cookie.CookieUserSessionID, c.cfg.IsHTTPS, c.cfg.BasePath())) response.Redirect(ctx.Route("login")) } diff --git a/ui/oauth2.go b/ui/oauth2.go index 4afc7d8e..c5bf9311 100644 --- a/ui/oauth2.go +++ b/ui/oauth2.go @@ -132,7 +132,7 @@ func (c *Controller) OAuth2Callback(ctx *handler.Context, request *handler.Reque logger.Info("[Controller:OAuth2Callback] username=%s just logged in", user.Username) - response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS)) + response.SetCookie(cookie.New(cookie.CookieUserSessionID, sessionToken, c.cfg.IsHTTPS, c.cfg.BasePath())) response.Redirect(ctx.Route("unread")) }