mirror of
https://github.com/drakkan/sftpgo.git
synced 2024-11-21 23:20:24 +00:00
web UI: allow to load custom css
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
4c710d731f
commit
93b9c1617e
24 changed files with 825 additions and 677 deletions
|
@ -108,6 +108,7 @@ var (
|
|||
CrossOriginOpenerPolicy: "",
|
||||
ExpectCTHeader: "",
|
||||
},
|
||||
ExtraCSS: []httpd.CustomCSS{},
|
||||
}
|
||||
defaultRateLimiter = common.RateLimiterConfig{
|
||||
Average: 0,
|
||||
|
@ -1268,6 +1269,25 @@ func getHTTPDOIDCFromEnv(idx int) (httpd.OIDC, bool) {
|
|||
return result, isSet
|
||||
}
|
||||
|
||||
func getHTTPDExtraCSSFromEnv(idx int) []httpd.CustomCSS {
|
||||
var css []httpd.CustomCSS
|
||||
|
||||
for subIdx := 0; subIdx < 10; subIdx++ {
|
||||
var customCSS httpd.CustomCSS
|
||||
|
||||
path, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__EXTRA_CSS__%v__PATH", idx, subIdx))
|
||||
if ok {
|
||||
customCSS.Path = path
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
css = append(css, customCSS)
|
||||
}
|
||||
}
|
||||
|
||||
return css
|
||||
}
|
||||
|
||||
func getHTTPDWebClientIntegrationsFromEnv(idx int) []httpd.WebClientIntegration {
|
||||
var integrations []httpd.WebClientIntegration
|
||||
|
||||
|
@ -1306,6 +1326,36 @@ func getDefaultHTTPBinding(idx int) httpd.Binding {
|
|||
return binding
|
||||
}
|
||||
|
||||
func getHTTPDNestedObjectsFromEnv(idx int, binding *httpd.Binding) bool {
|
||||
isSet := false
|
||||
|
||||
webClientIntegrations := getHTTPDWebClientIntegrationsFromEnv(idx)
|
||||
if len(webClientIntegrations) > 0 {
|
||||
binding.WebClientIntegrations = webClientIntegrations
|
||||
isSet = true
|
||||
}
|
||||
|
||||
oidc, ok := getHTTPDOIDCFromEnv(idx)
|
||||
if ok {
|
||||
binding.OIDC = oidc
|
||||
isSet = true
|
||||
}
|
||||
|
||||
securityConf, ok := getHTTPDSecurityConfFromEnv(idx)
|
||||
if ok {
|
||||
binding.Security = securityConf
|
||||
isSet = true
|
||||
}
|
||||
|
||||
extraCSS := getHTTPDExtraCSSFromEnv(idx)
|
||||
if len(extraCSS) > 0 {
|
||||
binding.ExtraCSS = extraCSS
|
||||
isSet = true
|
||||
}
|
||||
|
||||
return isSet
|
||||
}
|
||||
|
||||
func getHTTPDBindingFromEnv(idx int) {
|
||||
binding := getDefaultHTTPBinding(idx)
|
||||
isSet := false
|
||||
|
@ -1340,12 +1390,6 @@ func getHTTPDBindingFromEnv(idx int) {
|
|||
isSet = true
|
||||
}
|
||||
|
||||
webClientIntegrations := getHTTPDWebClientIntegrationsFromEnv(idx)
|
||||
if len(webClientIntegrations) > 0 {
|
||||
binding.WebClientIntegrations = webClientIntegrations
|
||||
isSet = true
|
||||
}
|
||||
|
||||
enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS", idx))
|
||||
if ok {
|
||||
binding.EnableHTTPS = enableHTTPS
|
||||
|
@ -1382,15 +1426,7 @@ func getHTTPDBindingFromEnv(idx int) {
|
|||
isSet = true
|
||||
}
|
||||
|
||||
oidc, ok := getHTTPDOIDCFromEnv(idx)
|
||||
if ok {
|
||||
binding.OIDC = oidc
|
||||
isSet = true
|
||||
}
|
||||
|
||||
securityConf, ok := getHTTPDSecurityConfFromEnv(idx)
|
||||
if ok {
|
||||
binding.Security = securityConf
|
||||
if getHTTPDNestedObjectsFromEnv(idx, &binding) {
|
||||
isSet = true
|
||||
}
|
||||
|
||||
|
|
|
@ -828,6 +828,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_HTTPD__BINDINGS__1__PORT", "8000")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS", "0")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__1__HIDE_LOGIN_URL", " 1")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__1__EXTRA_CSS__0__PATH", "")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS", "127.0.1.1")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__PORT", "9000")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_WEB_ADMIN", "0")
|
||||
|
@ -863,6 +864,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY", "fullscreen=(), geolocation=()")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY", "same-origin")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER", `max-age=86400, enforce, report-uri="https://foo.example/report"`)
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH", "path1")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH", "path2")
|
||||
t.Cleanup(func() {
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__ADDRESS")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__0__PORT")
|
||||
|
@ -871,6 +874,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__PORT")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__ENABLE_HTTPS")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__HIDE_LOGIN_URL")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__1__EXTRA_CSS__0__PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ADDRESS")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__PORT")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS")
|
||||
|
@ -906,6 +910,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__PERMISSIONS_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__CROSS_ORIGIN_OPENER_POLICY")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__SECURITY__EXPECT_CT_HEADER")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__0__PATH")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__EXTRA_CSS__1__PATH")
|
||||
})
|
||||
|
||||
configDir := ".."
|
||||
|
@ -929,6 +935,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, "127.0.0.1", bindings[1].Address)
|
||||
require.False(t, bindings[1].EnableHTTPS)
|
||||
require.Equal(t, 12, bindings[0].MinTLSVersion)
|
||||
require.Len(t, bindings[0].ExtraCSS, 0)
|
||||
require.True(t, bindings[1].EnableWebAdmin)
|
||||
require.True(t, bindings[1].EnableWebClient)
|
||||
require.True(t, bindings[1].RenderOpenAPI)
|
||||
|
@ -936,6 +943,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, 1, bindings[1].HideLoginURL)
|
||||
require.Empty(t, bindings[1].OIDC.ClientID)
|
||||
require.False(t, bindings[1].Security.Enabled)
|
||||
require.Len(t, bindings[1].ExtraCSS, 0)
|
||||
require.Equal(t, 9000, bindings[2].Port)
|
||||
require.Equal(t, "127.0.1.1", bindings[2].Address)
|
||||
require.True(t, bindings[2].EnableHTTPS)
|
||||
|
@ -978,6 +986,9 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, "fullscreen=(), geolocation=()", bindings[2].Security.PermissionsPolicy)
|
||||
require.Equal(t, "same-origin", bindings[2].Security.CrossOriginOpenerPolicy)
|
||||
require.Equal(t, `max-age=86400, enforce, report-uri="https://foo.example/report"`, bindings[2].Security.ExpectCTHeader)
|
||||
require.Len(t, bindings[2].ExtraCSS, 2)
|
||||
require.Equal(t, "path1", bindings[2].ExtraCSS[0].Path)
|
||||
require.Equal(t, "path2", bindings[2].ExtraCSS[1].Path)
|
||||
}
|
||||
|
||||
func TestHTTPClientCertificatesFromEnv(t *testing.T) {
|
||||
|
|
|
@ -2619,7 +2619,7 @@ func sendKeyboardAuthHTTPReq(url string, request *plugin.KeyboardAuthRequest) (*
|
|||
}
|
||||
|
||||
func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (int, error) {
|
||||
answers, err := client(user.Username, "", []string{"Password: "}, []bool{false})
|
||||
answers, err := client("", "", []string{"Password: "}, []bool{false})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -2639,7 +2639,7 @@ func doBuiltinKeyboardInteractiveAuth(user *User, client ssh.KeyboardInteractive
|
|||
user.Username, protocol, err)
|
||||
return 0, err
|
||||
}
|
||||
answers, err = client(user.Username, "", []string{"Authentication code: "}, []bool{false})
|
||||
answers, err = client("", "", []string{"Authentication code: "}, []bool{false})
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -2742,7 +2742,7 @@ func getKeyboardInteractiveAnswers(client ssh.KeyboardInteractiveChallenge, resp
|
|||
user *User, ip, protocol string,
|
||||
) ([]string, error) {
|
||||
questions := response.Questions
|
||||
answers, err := client(user.Username, response.Instruction, questions, response.Echos)
|
||||
answers, err := client("", response.Instruction, questions, response.Echos)
|
||||
if err != nil {
|
||||
providerLog(logger.LevelInfo, "error getting interactive auth client response: %v", err)
|
||||
return answers, err
|
||||
|
|
|
@ -263,6 +263,8 @@ The configuration file contains the following sections:
|
|||
- `permissions_policy`, string. Allows to set the `Permissions-Policy` header value. Default: blank.
|
||||
- `cross_origin_opener_policy`, string. Allows to set the `Cross-Origin-Opener-Policy` header value. Default: blank.
|
||||
- `expect_ct_header`, string. Allows to set the `Expect-CT` header value. Default: blank.
|
||||
- `extra_css`, list of structs. Defines additional CSS files. Each struct has the following fields:
|
||||
- `path`, string. Path to the CSS file relative to `static_files_path`. For example, if you create a directory named `extra_css` inside the static dir and put the `my.css` file in it, you must set `/extra_css/my.css` as path.
|
||||
- `templates_path`, string. Path to the HTML web templates. This can be an absolute path or a path relative to the config dir
|
||||
- `static_files_path`, string. Path to the static files for the web interface. This can be an absolute path or a path relative to the config dir. If both `templates_path` and `static_files_path` are empty the built-in web interface will be disabled
|
||||
- `openapi_path`, string. Path to the directory that contains the OpenAPI schema and the default renderer. This can be an absolute path or a path relative to the config dir. If empty the OpenAPI schema and the renderer will not be served regardless of the `render_openapi` directive
|
||||
|
|
2
go.mod
2
go.mod
|
@ -64,7 +64,7 @@ require (
|
|||
golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8
|
||||
golang.org/x/time v0.0.0-20220224211638-0e9765cccd65
|
||||
google.golang.org/api v0.73.0
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -907,8 +907,8 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||
golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4=
|
||||
golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8 h1:OH54vjqzRWmbJ62fjuhxy7AxFFgoHN0/DPc/UrL8cAs=
|
||||
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
|
|
|
@ -151,9 +151,9 @@ func deleteShare(w http.ResponseWriter, r *http.Request) {
|
|||
sendAPIResponse(w, r, err, "Share deleted", http.StatusOK)
|
||||
}
|
||||
|
||||
func readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -178,9 +178,9 @@ func readBrowsableShareContents(w http.ResponseWriter, r *http.Request) {
|
|||
renderAPIDirContents(w, r, contents, true)
|
||||
}
|
||||
|
||||
func downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -224,9 +224,9 @@ func downloadBrowsableSharedFile(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func downloadFromShare(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) downloadFromShare(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -273,12 +273,12 @@ func downloadFromShare(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func uploadFileToShare(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) uploadFileToShare(w http.ResponseWriter, r *http.Request) {
|
||||
if maxUploadFileSize > 0 {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
}
|
||||
name := getURLParam(r, "name")
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -296,11 +296,11 @@ func uploadFileToShare(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
|
||||
if maxUploadFileSize > 0 {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxUploadFileSize)
|
||||
}
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
@ -346,12 +346,12 @@ func uploadFilesToShare(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
func checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope dataprovider.ShareScope,
|
||||
func (s *httpdServer) checkPublicShare(w http.ResponseWriter, r *http.Request, shareShope dataprovider.ShareScope,
|
||||
isWebClient bool,
|
||||
) (dataprovider.Share, *Connection, error) {
|
||||
renderError := func(err error, message string, statusCode int) {
|
||||
if isWebClient {
|
||||
renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "")
|
||||
s.renderClientMessagePage(w, r, "Unable to access the share", message, statusCode, err, "")
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, message, statusCode)
|
||||
}
|
||||
|
|
|
@ -298,6 +298,14 @@ func (s *SecurityConf) getHTTPSProxyHeaders() map[string]string {
|
|||
return headers
|
||||
}
|
||||
|
||||
// CustomCSS defines the configuration for custom CSS
|
||||
type CustomCSS struct {
|
||||
// Path to the CSS file relative to "static_files_path".
|
||||
// For example, if you create a directory named "extra_css" inside the static dir
|
||||
// and put the "my.css" file in it, you must set "/extra_css/my.css" as path.
|
||||
Path string `json:"path" mapstructure:"path"`
|
||||
}
|
||||
|
||||
// WebClientIntegration defines the configuration for an external Web Client integration
|
||||
type WebClientIntegration struct {
|
||||
// Files with these extensions can be sent to the configured URL
|
||||
|
@ -354,7 +362,9 @@ type Binding struct {
|
|||
// Defining an OIDC configuration the web admin and web client UI will use OpenID to authenticate users.
|
||||
OIDC OIDC `json:"oidc" mapstructure:"oidc"`
|
||||
// Security defines security headers to add to HTTP responses and allows to restrict allowed hosts
|
||||
Security SecurityConf `json:"security" mapstructure:"security"`
|
||||
Security SecurityConf `json:"security" mapstructure:"security"`
|
||||
// Additional CSS
|
||||
ExtraCSS []CustomCSS `json:"extra_css" mapstructure:"extra_css"`
|
||||
allowHeadersFrom []func(net.IP) bool
|
||||
}
|
||||
|
||||
|
@ -368,6 +378,16 @@ func (b *Binding) checkWebClientIntegrations() {
|
|||
b.WebClientIntegrations = integrations
|
||||
}
|
||||
|
||||
func (b *Binding) checkExtraCSS() {
|
||||
var extraCSS []CustomCSS
|
||||
for _, css := range b.ExtraCSS {
|
||||
extraCSS = append(extraCSS, CustomCSS{
|
||||
Path: path.Join("/", css.Path),
|
||||
})
|
||||
}
|
||||
b.ExtraCSS = extraCSS
|
||||
}
|
||||
|
||||
func (b *Binding) parseAllowedProxy() error {
|
||||
allowedFuncs, err := util.ParseAllowedIPAndRanges(b.ProxyAllowed)
|
||||
if err != nil {
|
||||
|
@ -606,6 +626,7 @@ func (c *Conf) Initialize(configDir string) error {
|
|||
return err
|
||||
}
|
||||
binding.checkWebClientIntegrations()
|
||||
binding.checkExtraCSS()
|
||||
binding.Security.updateProxyHeaders()
|
||||
|
||||
go func(b Binding) {
|
||||
|
|
|
@ -301,6 +301,23 @@ func TestShouldBind(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestExtraCSSValidation(t *testing.T) {
|
||||
b := Binding{
|
||||
ExtraCSS: []CustomCSS{
|
||||
{
|
||||
Path: "path1",
|
||||
},
|
||||
{
|
||||
Path: "../path2",
|
||||
},
|
||||
},
|
||||
}
|
||||
b.checkExtraCSS()
|
||||
require.Len(t, b.ExtraCSS, 2)
|
||||
assert.Equal(t, "/path1", b.ExtraCSS[0].Path)
|
||||
assert.Equal(t, "/path2", b.ExtraCSS[1].Path)
|
||||
}
|
||||
|
||||
func TestRedactedConf(t *testing.T) {
|
||||
c := Conf{
|
||||
SigningPassphrase: "passphrase",
|
||||
|
@ -356,6 +373,8 @@ func TestGCSWebInvalidFormFile(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestInvalidToken(t *testing.T) {
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
admin := dataprovider.Admin{
|
||||
Username: "admin",
|
||||
}
|
||||
|
@ -510,27 +529,27 @@ func TestInvalidToken(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebRestore(rr, req)
|
||||
server.handleWebRestore(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebAddUserPost(rr, req)
|
||||
server.handleWebAddUserPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebUpdateUserPost(rr, req)
|
||||
server.handleWebUpdateUserPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebTemplateFolderPost(rr, req)
|
||||
server.handleWebTemplateFolderPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebTemplateUserPost(rr, req)
|
||||
server.handleWebTemplateUserPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
|
@ -545,7 +564,7 @@ func TestInvalidToken(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebUpdateFolderPost(rr, req)
|
||||
server.handleWebUpdateFolderPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
|
@ -575,12 +594,10 @@ func TestInvalidToken(t *testing.T) {
|
|||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebAddAdminPost(rr, req)
|
||||
server.handleWebAddAdminPost(rr, req)
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "invalid token claims")
|
||||
|
||||
server := httpdServer{}
|
||||
server.initializeRouter()
|
||||
rr = httptest.NewRecorder()
|
||||
server.handleWebClientTwoFactorRecoveryPost(rr, req)
|
||||
assert.Equal(t, http.StatusNotFound, rr.Code)
|
||||
|
@ -624,7 +641,7 @@ func TestUpdateWebAdminInvalidClaims(t *testing.T) {
|
|||
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleWebUpdateAdminPost(rr, req)
|
||||
server.handleWebUpdateAdminPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
}
|
||||
|
@ -796,13 +813,13 @@ func TestCreateTokenError(t *testing.T) {
|
|||
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebClientProfilePost(rr, req)
|
||||
server.handleWebClientProfilePost(rr, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
|
||||
|
||||
req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath+"?a=a%C3%AO%GB", bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebAdminProfilePost(rr, req)
|
||||
server.handleWebAdminProfilePost(rr, req)
|
||||
assert.Equal(t, http.StatusInternalServerError, rr.Code, rr.Body.String())
|
||||
|
||||
req, _ = http.NewRequest(http.MethodPost, webAdminTwoFactorPath+"?a=a%C3%AO%GC", bytes.NewBuffer([]byte(form.Encode())))
|
||||
|
@ -836,14 +853,14 @@ func TestCreateTokenError(t *testing.T) {
|
|||
req, _ = http.NewRequest(http.MethodPost, webAdminForgotPwdPath+"?a=a%C3%A1%GD", bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebAdminForgotPwdPost(rr, req)
|
||||
server.handleWebAdminForgotPwdPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
assert.Contains(t, rr.Body.String(), "invalid URL escape")
|
||||
|
||||
req, _ = http.NewRequest(http.MethodPost, webClientForgotPwdPath+"?a=a%C2%A1%GD", bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
rr = httptest.NewRecorder()
|
||||
handleWebClientForgotPwdPost(rr, req)
|
||||
server.handleWebClientForgotPwdPost(rr, req)
|
||||
assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String())
|
||||
assert.Contains(t, rr.Body.String(), "invalid URL escape")
|
||||
|
||||
|
@ -939,13 +956,17 @@ func TestJWTTokenValidation(t *testing.T) {
|
|||
token, _, err := tokenAuth.Encode(claims)
|
||||
assert.NoError(t, err)
|
||||
|
||||
r := GetHTTPRouter(Binding{
|
||||
Address: "",
|
||||
Port: 8080,
|
||||
EnableWebAdmin: true,
|
||||
EnableWebClient: true,
|
||||
RenderOpenAPI: true,
|
||||
})
|
||||
server := httpdServer{
|
||||
binding: Binding{
|
||||
Address: "",
|
||||
Port: 8080,
|
||||
EnableWebAdmin: true,
|
||||
EnableWebClient: true,
|
||||
RenderOpenAPI: true,
|
||||
},
|
||||
}
|
||||
server.initializeRouter()
|
||||
r := server.router
|
||||
fn := jwtAuthenticatorAPI(r)
|
||||
rr := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodGet, userPath, nil)
|
||||
|
@ -970,7 +991,7 @@ func TestJWTTokenValidation(t *testing.T) {
|
|||
assert.Equal(t, webClientLoginPath, rr.Header().Get("Location"))
|
||||
|
||||
errTest := errors.New("test error")
|
||||
permFn := checkPerm(dataprovider.PermAdminAny)
|
||||
permFn := server.checkPerm(dataprovider.PermAdminAny)
|
||||
fn = permFn(r)
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, userPath, nil)
|
||||
|
@ -978,7 +999,7 @@ func TestJWTTokenValidation(t *testing.T) {
|
|||
fn.ServeHTTP(rr, req.WithContext(ctx))
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
|
||||
permFn = checkPerm(dataprovider.PermAdminAny)
|
||||
permFn = server.checkPerm(dataprovider.PermAdminAny)
|
||||
fn = permFn(r)
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, webUserPath, nil)
|
||||
|
@ -987,7 +1008,7 @@ func TestJWTTokenValidation(t *testing.T) {
|
|||
fn.ServeHTTP(rr, req.WithContext(ctx))
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
|
||||
permClientFn := checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)
|
||||
permClientFn := server.checkHTTPUserPerm(sdk.WebClientPubKeyChangeDisabled)
|
||||
fn = permClientFn(r)
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)
|
||||
|
@ -1003,7 +1024,7 @@ func TestJWTTokenValidation(t *testing.T) {
|
|||
fn.ServeHTTP(rr, req.WithContext(ctx))
|
||||
assert.Equal(t, http.StatusBadRequest, rr.Code)
|
||||
|
||||
fn = checkSecondFactorRequirement(r)
|
||||
fn = server.checkSecondFactorRequirement(r)
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodPost, webClientProfilePath, nil)
|
||||
req.RequestURI = webClientProfilePath
|
||||
|
@ -1989,42 +2010,42 @@ func TestWebUserInvalidClaims(t *testing.T) {
|
|||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, webClientDownloadZipPath, nil)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleWebClientDownloadZip(rr, req)
|
||||
server.handleWebClientDownloadZip(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, webClientEditFilePath, nil)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleClientEditFile(rr, req)
|
||||
server.handleClientEditFile(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, webClientSharePath, nil)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleClientUpdateShareGet(rr, req)
|
||||
server.handleClientUpdateShareGet(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodPost, webClientSharePath, nil)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleClientAddSharePost(rr, req)
|
||||
server.handleClientAddSharePost(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodPost, webClientSharePath+"/id", nil)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleClientUpdateSharePost(rr, req)
|
||||
server.handleClientUpdateSharePost(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
|
||||
rr = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest(http.MethodGet, webClientSharesPath, nil)
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleClientGetShares(rr, req)
|
||||
server.handleClientGetShares(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
assert.Contains(t, rr.Body.String(), "Invalid token claims")
|
||||
}
|
||||
|
@ -2053,7 +2074,7 @@ func TestInvalidClaims(t *testing.T) {
|
|||
req, _ := http.NewRequest(http.MethodPost, webClientProfilePath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleWebClientProfilePost(rr, req)
|
||||
server.handleWebClientProfilePost(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
|
||||
admin := dataprovider.Admin{
|
||||
|
@ -2073,7 +2094,7 @@ func TestInvalidClaims(t *testing.T) {
|
|||
req, _ = http.NewRequest(http.MethodPost, webAdminProfilePath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
req.Header.Set("Cookie", fmt.Sprintf("jwt=%v", token["access_token"]))
|
||||
handleWebAdminProfilePost(rr, req)
|
||||
server.handleWebAdminProfilePost(rr, req)
|
||||
assert.Equal(t, http.StatusForbidden, rr.Code)
|
||||
}
|
||||
|
||||
|
|
|
@ -87,13 +87,13 @@ func validateJWTToken(w http.ResponseWriter, r *http.Request, audience tokenAudi
|
|||
return nil
|
||||
}
|
||||
|
||||
func validateJWTPartialToken(w http.ResponseWriter, r *http.Request, audience tokenAudience) error {
|
||||
func (s *httpdServer) validateJWTPartialToken(w http.ResponseWriter, r *http.Request, audience tokenAudience) error {
|
||||
token, _, err := jwtauth.FromContext(r.Context())
|
||||
var notFoundFunc func(w http.ResponseWriter, r *http.Request, err error)
|
||||
if audience == tokenAudienceWebAdminPartial {
|
||||
notFoundFunc = renderNotFoundPage
|
||||
notFoundFunc = s.renderNotFoundPage
|
||||
} else {
|
||||
notFoundFunc = renderClientNotFoundPage
|
||||
notFoundFunc = s.renderClientNotFoundPage
|
||||
}
|
||||
if err != nil || token == nil || jwt.Validate(token) != nil {
|
||||
notFoundFunc(w, r, nil)
|
||||
|
@ -112,10 +112,10 @@ func validateJWTPartialToken(w http.ResponseWriter, r *http.Request, audience to
|
|||
return nil
|
||||
}
|
||||
|
||||
func jwtAuthenticatorPartial(audience tokenAudience) func(next http.Handler) http.Handler {
|
||||
func (s *httpdServer) jwtAuthenticatorPartial(audience tokenAudience) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := validateJWTPartialToken(w, r, audience); err != nil {
|
||||
if err := s.validateJWTPartialToken(w, r, audience); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -169,13 +169,13 @@ func jwtAuthenticatorWebClient(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
|
||||
func (s *httpdServer) checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, claims, err := jwtauth.FromContext(r.Context())
|
||||
if err != nil {
|
||||
if isWebRequest(r) {
|
||||
renderClientBadRequestPage(w, r, err)
|
||||
s.renderClientBadRequestPage(w, r, err)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
}
|
||||
|
@ -186,7 +186,7 @@ func checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
|
|||
// for web client perms are negated and not granted
|
||||
if tokenClaims.hasPerm(perm) {
|
||||
if isWebRequest(r) {
|
||||
renderClientForbiddenPage(w, r, "You don't have permission for this action")
|
||||
s.renderClientForbiddenPage(w, r, "You don't have permission for this action")
|
||||
} else {
|
||||
sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
}
|
||||
|
@ -198,12 +198,12 @@ func checkHTTPUserPerm(perm string) func(next http.Handler) http.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
func checkSecondFactorRequirement(next http.Handler) http.Handler {
|
||||
func (s *httpdServer) checkSecondFactorRequirement(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, claims, err := jwtauth.FromContext(r.Context())
|
||||
if err != nil {
|
||||
if isWebRequest(r) {
|
||||
renderClientBadRequestPage(w, r, err)
|
||||
s.renderClientBadRequestPage(w, r, err)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
}
|
||||
|
@ -215,7 +215,7 @@ func checkSecondFactorRequirement(next http.Handler) http.Handler {
|
|||
message := fmt.Sprintf("Two-factor authentication requirements not met, please configure two-factor authentication for the following protocols: %v",
|
||||
strings.Join(tokenClaims.RequiredTwoFactorProtocols, ", "))
|
||||
if isWebRequest(r) {
|
||||
renderClientForbiddenPage(w, r, message)
|
||||
s.renderClientForbiddenPage(w, r, message)
|
||||
} else {
|
||||
sendAPIResponse(w, r, nil, message, http.StatusForbidden)
|
||||
}
|
||||
|
@ -226,13 +226,13 @@ func checkSecondFactorRequirement(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func requireBuiltinLogin(next http.Handler) http.Handler {
|
||||
func (s *httpdServer) requireBuiltinLogin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if isLoggedInWithOIDC(r) {
|
||||
if isWebClientRequest(r) {
|
||||
renderClientForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID")
|
||||
s.renderClientForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID")
|
||||
} else {
|
||||
renderForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID")
|
||||
s.renderForbiddenPage(w, r, "This feature is not available if you are logged in with OpenID")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
@ -240,13 +240,13 @@ func requireBuiltinLogin(next http.Handler) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func checkPerm(perm string) func(next http.Handler) http.Handler {
|
||||
func (s *httpdServer) checkPerm(perm string) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
_, claims, err := jwtauth.FromContext(r.Context())
|
||||
if err != nil {
|
||||
if isWebRequest(r) {
|
||||
renderBadRequestPage(w, r, err)
|
||||
s.renderBadRequestPage(w, r, err)
|
||||
} else {
|
||||
sendAPIResponse(w, r, err, http.StatusText(http.StatusBadRequest), http.StatusBadRequest)
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ func checkPerm(perm string) func(next http.Handler) http.Handler {
|
|||
|
||||
if !tokenClaims.hasPerm(perm) {
|
||||
if isWebRequest(r) {
|
||||
renderForbiddenPage(w, r, "You don't have permission for this action")
|
||||
s.renderForbiddenPage(w, r, "You don't have permission for this action")
|
||||
} else {
|
||||
sendAPIResponse(w, r, nil, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
}
|
||||
|
|
|
@ -534,7 +534,7 @@ func (s *httpdServer) handleOIDCRedirect(w http.ResponseWriter, r *http.Request)
|
|||
authReq, err := oidcMgr.getPendingAuth(state)
|
||||
if err != nil {
|
||||
logger.Debug(logSender, "", "oidc authentication state did not match")
|
||||
renderClientMessagePage(w, r, "Invalid authentication request", "Authentication state did not match",
|
||||
s.renderClientMessagePage(w, r, "Invalid authentication request", "Authentication state did not match",
|
||||
http.StatusBadRequest, nil, "")
|
||||
return
|
||||
}
|
||||
|
|
569
httpd/server.go
569
httpd/server.go
File diff suppressed because it is too large
Load diff
|
@ -32,6 +32,7 @@ type loginPage struct {
|
|||
AltLoginURL string
|
||||
ForgotPwdURL string
|
||||
OpenIDLoginURL string
|
||||
ExtraCSS []CustomCSS
|
||||
}
|
||||
|
||||
type twoFactorPage struct {
|
||||
|
@ -41,6 +42,7 @@ type twoFactorPage struct {
|
|||
CSRFToken string
|
||||
StaticURL string
|
||||
RecoveryURL string
|
||||
ExtraCSS []CustomCSS
|
||||
}
|
||||
|
||||
type forgotPwdPage struct {
|
||||
|
@ -49,6 +51,7 @@ type forgotPwdPage struct {
|
|||
CSRFToken string
|
||||
StaticURL string
|
||||
Title string
|
||||
ExtraCSS []CustomCSS
|
||||
}
|
||||
|
||||
type resetPwdPage struct {
|
||||
|
@ -57,6 +60,7 @@ type resetPwdPage struct {
|
|||
CSRFToken string
|
||||
StaticURL string
|
||||
Title string
|
||||
ExtraCSS []CustomCSS
|
||||
}
|
||||
|
||||
func getSliceFromDelimitedValues(values, delimiter string) []string {
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -98,6 +98,7 @@ type baseClientPage struct {
|
|||
CSRFToken string
|
||||
HasExternalLogin bool
|
||||
LoggedUser *dataprovider.User
|
||||
ExtraCSS []CustomCSS
|
||||
}
|
||||
|
||||
type dirMapping struct {
|
||||
|
@ -109,6 +110,7 @@ type viewPDFPage struct {
|
|||
Title string
|
||||
URL string
|
||||
StaticURL string
|
||||
ExtraCSS []CustomCSS
|
||||
}
|
||||
|
||||
type editFilePage struct {
|
||||
|
@ -309,7 +311,7 @@ func loadClientTemplates(templatesPath string) {
|
|||
clientTemplates[templateUploadToShare] = shareUploadTmpl
|
||||
}
|
||||
|
||||
func getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
|
||||
func (s *httpdServer) getBaseClientPageData(title, currentURL string, r *http.Request) baseClientPage {
|
||||
var csrfToken string
|
||||
if currentURL != "" {
|
||||
csrfToken = createCSRFToken()
|
||||
|
@ -335,27 +337,30 @@ func getBaseClientPageData(title, currentURL string, r *http.Request) baseClient
|
|||
CSRFToken: csrfToken,
|
||||
HasExternalLogin: isLoggedInWithOIDC(r),
|
||||
LoggedUser: getUserFromToken(r),
|
||||
ExtraCSS: s.binding.ExtraCSS,
|
||||
}
|
||||
}
|
||||
|
||||
func renderClientForgotPwdPage(w http.ResponseWriter, error string) {
|
||||
func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, error string) {
|
||||
data := forgotPwdPage{
|
||||
CurrentURL: webClientForgotPwdPath,
|
||||
Error: error,
|
||||
CSRFToken: createCSRFToken(),
|
||||
StaticURL: webStaticFilesPath,
|
||||
Title: pageClientForgotPwdTitle,
|
||||
ExtraCSS: s.binding.ExtraCSS,
|
||||
}
|
||||
renderClientTemplate(w, templateForgotPassword, data)
|
||||
}
|
||||
|
||||
func renderClientResetPwdPage(w http.ResponseWriter, error string) {
|
||||
func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, error string) {
|
||||
data := resetPwdPage{
|
||||
CurrentURL: webClientResetPwdPath,
|
||||
Error: error,
|
||||
CSRFToken: createCSRFToken(),
|
||||
StaticURL: webStaticFilesPath,
|
||||
Title: pageClientResetPwdTitle,
|
||||
ExtraCSS: s.binding.ExtraCSS,
|
||||
}
|
||||
renderClientTemplate(w, templateResetPassword, data)
|
||||
}
|
||||
|
@ -367,7 +372,7 @@ func renderClientTemplate(w http.ResponseWriter, tmplName string, data interface
|
|||
}
|
||||
}
|
||||
|
||||
func renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) {
|
||||
func (s *httpdServer) renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body string, statusCode int, err error, message string) {
|
||||
var errorString string
|
||||
if body != "" {
|
||||
errorString = body + " "
|
||||
|
@ -376,7 +381,7 @@ func renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body
|
|||
errorString += err.Error()
|
||||
}
|
||||
data := clientMessagePage{
|
||||
baseClientPage: getBaseClientPageData(title, "", r),
|
||||
baseClientPage: s.getBaseClientPageData(title, "", r),
|
||||
Error: errorString,
|
||||
Success: message,
|
||||
}
|
||||
|
@ -384,23 +389,23 @@ func renderClientMessagePage(w http.ResponseWriter, r *http.Request, title, body
|
|||
renderClientTemplate(w, templateClientMessage, data)
|
||||
}
|
||||
|
||||
func renderClientInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
renderClientMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
|
||||
func (s *httpdServer) renderClientInternalServerErrorPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
s.renderClientMessagePage(w, r, page500Title, page500Body, http.StatusInternalServerError, err, "")
|
||||
}
|
||||
|
||||
func renderClientBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
renderClientMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
|
||||
func (s *httpdServer) renderClientBadRequestPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
s.renderClientMessagePage(w, r, page400Title, "", http.StatusBadRequest, err, "")
|
||||
}
|
||||
|
||||
func renderClientForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
|
||||
renderClientMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body)
|
||||
func (s *httpdServer) renderClientForbiddenPage(w http.ResponseWriter, r *http.Request, body string) {
|
||||
s.renderClientMessagePage(w, r, page403Title, "", http.StatusForbidden, nil, body)
|
||||
}
|
||||
|
||||
func renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
renderClientMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
|
||||
func (s *httpdServer) renderClientNotFoundPage(w http.ResponseWriter, r *http.Request, err error) {
|
||||
s.renderClientMessagePage(w, r, page404Title, page404Body, http.StatusNotFound, err, "")
|
||||
}
|
||||
|
||||
func renderClientTwoFactorPage(w http.ResponseWriter, error string) {
|
||||
func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, error string) {
|
||||
data := twoFactorPage{
|
||||
CurrentURL: webClientTwoFactorPath,
|
||||
Version: version.Get().Version,
|
||||
|
@ -408,24 +413,26 @@ func renderClientTwoFactorPage(w http.ResponseWriter, error string) {
|
|||
CSRFToken: createCSRFToken(),
|
||||
StaticURL: webStaticFilesPath,
|
||||
RecoveryURL: webClientTwoFactorRecoveryPath,
|
||||
ExtraCSS: s.binding.ExtraCSS,
|
||||
}
|
||||
renderClientTemplate(w, templateTwoFactor, data)
|
||||
}
|
||||
|
||||
func renderClientTwoFactorRecoveryPage(w http.ResponseWriter, error string) {
|
||||
func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, error string) {
|
||||
data := twoFactorPage{
|
||||
CurrentURL: webClientTwoFactorRecoveryPath,
|
||||
Version: version.Get().Version,
|
||||
Error: error,
|
||||
CSRFToken: createCSRFToken(),
|
||||
StaticURL: webStaticFilesPath,
|
||||
ExtraCSS: s.binding.ExtraCSS,
|
||||
}
|
||||
renderClientTemplate(w, templateTwoFactorRecovery, data)
|
||||
}
|
||||
|
||||
func renderClientMFAPage(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) renderClientMFAPage(w http.ResponseWriter, r *http.Request) {
|
||||
data := clientMFAPage{
|
||||
baseClientPage: getBaseClientPageData(pageMFATitle, webClientMFAPath, r),
|
||||
baseClientPage: s.getBaseClientPageData(pageMFATitle, webClientMFAPath, r),
|
||||
TOTPConfigs: mfa.GetAvailableTOTPConfigNames(),
|
||||
GenerateTOTPURL: webClientTOTPGeneratePath,
|
||||
ValidateTOTPURL: webClientTOTPValidatePath,
|
||||
|
@ -435,16 +442,16 @@ func renderClientMFAPage(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
user, err := dataprovider.UserExists(data.LoggedUser.Username)
|
||||
if err != nil {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
data.TOTPConfig = user.Filters.TOTPConfig
|
||||
renderClientTemplate(w, templateClientMFA, data)
|
||||
}
|
||||
|
||||
func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) {
|
||||
func (s *httpdServer) renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileData string, readOnly bool) {
|
||||
data := editFilePage{
|
||||
baseClientPage: getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r),
|
||||
baseClientPage: s.getBaseClientPageData(pageClientEditFileTitle, webClientEditFilePath, r),
|
||||
Path: fileName,
|
||||
Name: path.Base(fileName),
|
||||
CurrentDir: path.Dir(fileName),
|
||||
|
@ -456,7 +463,7 @@ func renderEditFilePage(w http.ResponseWriter, r *http.Request, fileName, fileDa
|
|||
renderClientTemplate(w, templateClientEditFile, data)
|
||||
}
|
||||
|
||||
func renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share,
|
||||
func (s *httpdServer) renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dataprovider.Share,
|
||||
error string, isAdd bool) {
|
||||
currentURL := webClientSharePath
|
||||
title := "Add a new share"
|
||||
|
@ -465,7 +472,7 @@ func renderAddUpdateSharePage(w http.ResponseWriter, r *http.Request, share *dat
|
|||
title = "Update share"
|
||||
}
|
||||
data := clientSharePage{
|
||||
baseClientPage: getBaseClientPageData(title, currentURL, r),
|
||||
baseClientPage: s.getBaseClientPageData(title, currentURL, r),
|
||||
Share: share,
|
||||
Error: error,
|
||||
IsAdd: isAdd,
|
||||
|
@ -495,10 +502,12 @@ func getDirMapping(dirName, baseWebPath string) []dirMapping {
|
|||
return paths
|
||||
}
|
||||
|
||||
func renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, share dataprovider.Share) {
|
||||
func (s *httpdServer) renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string,
|
||||
share dataprovider.Share,
|
||||
) {
|
||||
currentURL := path.Join(webClientPubSharesPath, share.ShareID, "browse")
|
||||
data := shareFilesPage{
|
||||
baseClientPage: getBaseClientPageData(pageExtShareTitle, currentURL, r),
|
||||
baseClientPage: s.getBaseClientPageData(pageExtShareTitle, currentURL, r),
|
||||
CurrentDir: url.QueryEscape(dirName),
|
||||
DirsURL: path.Join(webClientPubSharesPath, share.ShareID, "dirs"),
|
||||
FilesURL: currentURL,
|
||||
|
@ -509,21 +518,21 @@ func renderSharedFilesPage(w http.ResponseWriter, r *http.Request, dirName, erro
|
|||
renderClientTemplate(w, templateShareFiles, data)
|
||||
}
|
||||
|
||||
func renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share dataprovider.Share) {
|
||||
func (s *httpdServer) renderUploadToSharePage(w http.ResponseWriter, r *http.Request, share dataprovider.Share) {
|
||||
currentURL := path.Join(webClientPubSharesPath, share.ShareID, "upload")
|
||||
data := shareUploadPage{
|
||||
baseClientPage: getBaseClientPageData(pageUploadToShareTitle, currentURL, r),
|
||||
baseClientPage: s.getBaseClientPageData(pageUploadToShareTitle, currentURL, r),
|
||||
Share: &share,
|
||||
UploadBasePath: path.Join(webClientPubSharesPath, share.ShareID),
|
||||
}
|
||||
renderClientTemplate(w, templateUploadToShare, data)
|
||||
}
|
||||
|
||||
func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User,
|
||||
func (s *httpdServer) renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error string, user dataprovider.User,
|
||||
hasIntegrations bool,
|
||||
) {
|
||||
data := filesPage{
|
||||
baseClientPage: getBaseClientPageData(pageClientFilesTitle, webClientFilesPath, r),
|
||||
baseClientPage: s.getBaseClientPageData(pageClientFilesTitle, webClientFilesPath, r),
|
||||
Error: error,
|
||||
CurrentDir: url.QueryEscape(dirName),
|
||||
DownloadURL: webClientDownloadZipPath,
|
||||
|
@ -542,14 +551,14 @@ func renderFilesPage(w http.ResponseWriter, r *http.Request, dirName, error stri
|
|||
renderClientTemplate(w, templateClientFiles, data)
|
||||
}
|
||||
|
||||
func renderClientProfilePage(w http.ResponseWriter, r *http.Request, error string) {
|
||||
func (s *httpdServer) renderClientProfilePage(w http.ResponseWriter, r *http.Request, error string) {
|
||||
data := clientProfilePage{
|
||||
baseClientPage: getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r),
|
||||
baseClientPage: s.getBaseClientPageData(pageClientProfileTitle, webClientProfilePath, r),
|
||||
Error: error,
|
||||
}
|
||||
user, err := dataprovider.UserExists(data.LoggedUser.Username)
|
||||
if err != nil {
|
||||
renderClientInternalServerErrorPage(w, r, err)
|
||||
s.renderClientInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
data.PublicKeys = user.PublicKeys
|
||||
|
@ -560,26 +569,26 @@ func renderClientProfilePage(w http.ResponseWriter, r *http.Request, error strin
|
|||
renderClientTemplate(w, templateClientProfile, data)
|
||||
}
|
||||
|
||||
func renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) {
|
||||
func (s *httpdServer) renderClientChangePasswordPage(w http.ResponseWriter, r *http.Request, error string) {
|
||||
data := changeClientPasswordPage{
|
||||
baseClientPage: getBaseClientPageData(pageClientChangePwdTitle, webChangeClientPwdPath, r),
|
||||
baseClientPage: s.getBaseClientPageData(pageClientChangePwdTitle, webChangeClientPwdPath, r),
|
||||
Error: error,
|
||||
}
|
||||
|
||||
renderClientTemplate(w, templateClientChangePwd, data)
|
||||
}
|
||||
|
||||
func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
renderClientMessagePage(w, r, "Invalid token claims", "", http.StatusForbidden, nil, "")
|
||||
s.renderClientMessagePage(w, r, "Invalid token claims", "", http.StatusForbidden, nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -587,7 +596,7 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
|||
protocol := getProtocolFromRequest(r)
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, connID)
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
s.renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
connection := &Connection{
|
||||
|
@ -603,7 +612,7 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
|||
var filesList []string
|
||||
err = json.Unmarshal([]byte(files), &filesList)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, "Unable to get files list", "", http.StatusInternalServerError, err, "")
|
||||
s.renderClientMessagePage(w, r, "Unable to get files list", "", http.StatusInternalServerError, err, "")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -611,19 +620,19 @@ func handleWebClientDownloadZip(w http.ResponseWriter, r *http.Request) {
|
|||
renderCompressedFiles(w, connection, name, filesList, nil)
|
||||
}
|
||||
|
||||
func handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := validateBrowsableShare(share, connection); err != nil {
|
||||
renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "")
|
||||
s.renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "")
|
||||
return
|
||||
}
|
||||
name, err := getBrowsableSharedPath(share, r)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "")
|
||||
s.renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "")
|
||||
return
|
||||
}
|
||||
common.Connections.Add(connection)
|
||||
|
@ -657,28 +666,28 @@ func handleShareGetDirContents(w http.ResponseWriter, r *http.Request) {
|
|||
render.JSON(w, r, results)
|
||||
}
|
||||
|
||||
func handleClientUploadToShare(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientUploadToShare(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, _, err := checkPublicShare(w, r, dataprovider.ShareScopeWrite, true)
|
||||
share, _, err := s.checkPublicShare(w, r, dataprovider.ShareScopeWrite, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
renderUploadToSharePage(w, r, share)
|
||||
s.renderUploadToSharePage(w, r, share)
|
||||
}
|
||||
|
||||
func handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share, connection, err := checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
|
||||
share, connection, err := s.checkPublicShare(w, r, dataprovider.ShareScopeRead, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err := validateBrowsableShare(share, connection); err != nil {
|
||||
renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "")
|
||||
s.renderClientMessagePage(w, r, "Unable to validate share", "", getRespStatus(err), err, "")
|
||||
return
|
||||
}
|
||||
name, err := getBrowsableSharedPath(share, r)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "")
|
||||
s.renderClientMessagePage(w, r, "Invalid share path", "", getRespStatus(err), err, "")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -692,11 +701,11 @@ func handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
|
|||
info, err = connection.Stat(name, 1)
|
||||
}
|
||||
if err != nil {
|
||||
renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share)
|
||||
s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share)
|
||||
return
|
||||
}
|
||||
if info.IsDir() {
|
||||
renderSharedFilesPage(w, r, share.GetRelativePath(name), "", share)
|
||||
s.renderSharedFilesPage(w, r, share.GetRelativePath(name), "", share)
|
||||
return
|
||||
}
|
||||
inline := r.URL.Query().Get("inline") != ""
|
||||
|
@ -704,7 +713,7 @@ func handleShareGetFiles(w http.ResponseWriter, r *http.Request) {
|
|||
if status, err := downloadFile(w, r, connection, name, info, inline, &share); err != nil {
|
||||
dataprovider.UpdateShareLastUse(&share, -1) //nolint:errcheck
|
||||
if status > 0 {
|
||||
renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share)
|
||||
s.renderSharedFilesPage(w, r, path.Dir(share.GetRelativePath(name)), err.Error(), share)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -787,13 +796,13 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
|
|||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -801,7 +810,7 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
|
|||
protocol := getProtocolFromRequest(r)
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, connID)
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
s.renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
connection := &Connection{
|
||||
|
@ -820,37 +829,37 @@ func (s *httpdServer) handleClientGetFiles(w http.ResponseWriter, r *http.Reques
|
|||
info, err = connection.Stat(name, 0)
|
||||
}
|
||||
if err != nil {
|
||||
renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %#v: %v", name, err),
|
||||
s.renderFilesPage(w, r, path.Dir(name), fmt.Sprintf("unable to stat file %#v: %v", name, err),
|
||||
user, len(s.binding.WebClientIntegrations) > 0)
|
||||
return
|
||||
}
|
||||
if info.IsDir() {
|
||||
renderFilesPage(w, r, name, "", user, len(s.binding.WebClientIntegrations) > 0)
|
||||
s.renderFilesPage(w, r, name, "", user, len(s.binding.WebClientIntegrations) > 0)
|
||||
return
|
||||
}
|
||||
inline := r.URL.Query().Get("inline") != ""
|
||||
if status, err := downloadFile(w, r, connection, name, info, inline, nil); err != nil && status != 0 {
|
||||
if status > 0 {
|
||||
if status == http.StatusRequestedRangeNotSatisfiable {
|
||||
renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
|
||||
s.renderClientMessagePage(w, r, http.StatusText(status), "", status, err, "")
|
||||
return
|
||||
}
|
||||
renderFilesPage(w, r, path.Dir(name), err.Error(), user, len(s.binding.WebClientIntegrations) > 0)
|
||||
s.renderFilesPage(w, r, path.Dir(name), err.Error(), user, len(s.binding.WebClientIntegrations) > 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientEditFile(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||
s.renderClientMessagePage(w, r, "Unable to retrieve your user", "", getRespStatus(err), nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -858,7 +867,7 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
|
|||
protocol := getProtocolFromRequest(r)
|
||||
connectionID := fmt.Sprintf("%v_%v", protocol, connID)
|
||||
if err := checkHTTPClientUser(&user, r, connectionID); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
s.renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
connection := &Connection{
|
||||
|
@ -872,24 +881,24 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
|
|||
name := connection.User.GetCleanedPath(r.URL.Query().Get("path"))
|
||||
info, err := connection.Stat(name, 0)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, fmt.Sprintf("Unable to stat file %#v", name), "",
|
||||
s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to stat file %#v", name), "",
|
||||
getRespStatus(err), nil, "")
|
||||
return
|
||||
}
|
||||
if info.IsDir() {
|
||||
renderClientMessagePage(w, r, fmt.Sprintf("The path %#v does not point to a file", name), "",
|
||||
s.renderClientMessagePage(w, r, fmt.Sprintf("The path %#v does not point to a file", name), "",
|
||||
http.StatusBadRequest, nil, "")
|
||||
return
|
||||
}
|
||||
if info.Size() > httpdMaxEditFileSize {
|
||||
renderClientMessagePage(w, r, fmt.Sprintf("The file size %v for %#v exceeds the maximum allowed size",
|
||||
s.renderClientMessagePage(w, r, fmt.Sprintf("The file size %v for %#v exceeds the maximum allowed size",
|
||||
util.ByteCountIEC(info.Size()), name), "", http.StatusBadRequest, nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
reader, err := connection.getFileReader(name, 0, r.Method)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, fmt.Sprintf("Unable to get a reader for the file %#v", name), "",
|
||||
s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to get a reader for the file %#v", name), "",
|
||||
getRespStatus(err), nil, "")
|
||||
return
|
||||
}
|
||||
|
@ -898,15 +907,15 @@ func handleClientEditFile(w http.ResponseWriter, r *http.Request) {
|
|||
var b bytes.Buffer
|
||||
_, err = io.Copy(&b, reader)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, fmt.Sprintf("Unable to read the file %#v", name), "", http.StatusInternalServerError,
|
||||
s.renderClientMessagePage(w, r, fmt.Sprintf("Unable to read the file %#v", name), "", http.StatusInternalServerError,
|
||||
nil, "")
|
||||
return
|
||||
}
|
||||
|
||||
renderEditFilePage(w, r, name, b.String(), util.IsStringInSlice(sdk.WebClientWriteDisabled, user.Filters.WebClient))
|
||||
s.renderEditFilePage(w, r, name, b.String(), util.IsStringInSlice(sdk.WebClientWriteDisabled, user.Filters.WebClient))
|
||||
}
|
||||
|
||||
func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
share := &dataprovider.Share{Scope: dataprovider.ShareScopeRead}
|
||||
dirName := "/"
|
||||
|
@ -919,7 +928,7 @@ func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
|
|||
var filesList []string
|
||||
err := json.Unmarshal([]byte(files), &filesList)
|
||||
if err != nil {
|
||||
renderClientMessagePage(w, r, "Invalid share list", "", http.StatusBadRequest, err, "")
|
||||
s.renderClientMessagePage(w, r, "Invalid share list", "", http.StatusBadRequest, err, "")
|
||||
return
|
||||
}
|
||||
for _, f := range filesList {
|
||||
|
@ -929,42 +938,42 @@ func handleClientAddShareGet(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
}
|
||||
|
||||
renderAddUpdateSharePage(w, r, share, "", true)
|
||||
s.renderAddUpdateSharePage(w, r, share, "", true)
|
||||
}
|
||||
|
||||
func handleClientUpdateShareGet(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientUpdateShareGet(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
return
|
||||
}
|
||||
shareID := getURLParam(r, "id")
|
||||
share, err := dataprovider.ShareExists(shareID, claims.Username)
|
||||
if err == nil {
|
||||
share.HideConfidentialData()
|
||||
renderAddUpdateSharePage(w, r, &share, "", false)
|
||||
s.renderAddUpdateSharePage(w, r, &share, "", false)
|
||||
} else if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||
renderClientNotFoundPage(w, r, err)
|
||||
s.renderClientNotFoundPage(w, r, err)
|
||||
} else {
|
||||
renderClientInternalServerErrorPage(w, r, err)
|
||||
s.renderClientInternalServerErrorPage(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleClientAddSharePost(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientAddSharePost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
return
|
||||
}
|
||||
share, err := getShareFromPostFields(r)
|
||||
if err != nil {
|
||||
renderAddUpdateSharePage(w, r, share, err.Error(), true)
|
||||
s.renderAddUpdateSharePage(w, r, share, err.Error(), true)
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
s.renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
share.ID = 0
|
||||
|
@ -973,7 +982,7 @@ func handleClientAddSharePost(w http.ResponseWriter, r *http.Request) {
|
|||
share.Username = claims.Username
|
||||
if share.Password == "" {
|
||||
if util.IsStringInSlice(sdk.WebClientShareNoPasswordDisabled, claims.Permissions) {
|
||||
renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password")
|
||||
s.renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -981,33 +990,33 @@ func handleClientAddSharePost(w http.ResponseWriter, r *http.Request) {
|
|||
if err == nil {
|
||||
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
|
||||
} else {
|
||||
renderAddUpdateSharePage(w, r, share, err.Error(), true)
|
||||
s.renderAddUpdateSharePage(w, r, share, err.Error(), true)
|
||||
}
|
||||
}
|
||||
|
||||
func handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
return
|
||||
}
|
||||
shareID := getURLParam(r, "id")
|
||||
share, err := dataprovider.ShareExists(shareID, claims.Username)
|
||||
if _, ok := err.(*util.RecordNotFoundError); ok {
|
||||
renderClientNotFoundPage(w, r, err)
|
||||
s.renderClientNotFoundPage(w, r, err)
|
||||
return
|
||||
} else if err != nil {
|
||||
renderClientInternalServerErrorPage(w, r, err)
|
||||
s.renderClientInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
updatedShare, err := getShareFromPostFields(r)
|
||||
if err != nil {
|
||||
renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
|
||||
s.renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
s.renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
updatedShare.ShareID = shareID
|
||||
|
@ -1017,7 +1026,7 @@ func handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
if updatedShare.Password == "" {
|
||||
if util.IsStringInSlice(sdk.WebClientShareNoPasswordDisabled, claims.Permissions) {
|
||||
renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password")
|
||||
s.renderClientForbiddenPage(w, r, "You are not authorized to share files/folders without a password")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -1025,15 +1034,15 @@ func handleClientUpdateSharePost(w http.ResponseWriter, r *http.Request) {
|
|||
if err == nil {
|
||||
http.Redirect(w, r, webClientSharesPath, http.StatusSeeOther)
|
||||
} else {
|
||||
renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
|
||||
s.renderAddUpdateSharePage(w, r, updatedShare, err.Error(), false)
|
||||
}
|
||||
}
|
||||
|
||||
func handleClientGetShares(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
return
|
||||
}
|
||||
limit := defaultQueryLimit
|
||||
|
@ -1046,57 +1055,57 @@ func handleClientGetShares(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
shares := make([]dataprovider.Share, 0, limit)
|
||||
for {
|
||||
s, err := dataprovider.GetShares(limit, len(shares), dataprovider.OrderASC, claims.Username)
|
||||
sh, err := dataprovider.GetShares(limit, len(shares), dataprovider.OrderASC, claims.Username)
|
||||
if err != nil {
|
||||
renderInternalServerErrorPage(w, r, err)
|
||||
s.renderInternalServerErrorPage(w, r, err)
|
||||
return
|
||||
}
|
||||
shares = append(shares, s...)
|
||||
if len(s) < limit {
|
||||
shares = append(shares, sh...)
|
||||
if len(sh) < limit {
|
||||
break
|
||||
}
|
||||
}
|
||||
data := clientSharesPage{
|
||||
baseClientPage: getBaseClientPageData(pageClientSharesTitle, webClientSharesPath, r),
|
||||
baseClientPage: s.getBaseClientPageData(pageClientSharesTitle, webClientSharesPath, r),
|
||||
Shares: shares,
|
||||
BasePublicSharesURL: webClientPubSharesPath,
|
||||
}
|
||||
renderClientTemplate(w, templateClientShares, data)
|
||||
}
|
||||
|
||||
func handleClientGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientGetProfile(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderClientProfilePage(w, r, "")
|
||||
s.renderClientProfilePage(w, r, "")
|
||||
}
|
||||
|
||||
func handleWebClientChangePwd(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleWebClientChangePwd(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderClientChangePasswordPage(w, r, "")
|
||||
s.renderClientChangePasswordPage(w, r, "")
|
||||
}
|
||||
|
||||
func handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
renderClientProfilePage(w, r, err.Error())
|
||||
s.renderClientProfilePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
s.renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
claims, err := getTokenClaims(r)
|
||||
if err != nil || claims.Username == "" {
|
||||
renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
s.renderClientForbiddenPage(w, r, "Invalid token claims")
|
||||
return
|
||||
}
|
||||
user, err := dataprovider.UserExists(claims.Username)
|
||||
if err != nil {
|
||||
renderClientProfilePage(w, r, err.Error())
|
||||
s.renderClientProfilePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
if !user.CanManagePublicKeys() && !user.CanChangeAPIKeyAuth() && !user.CanChangeInfo() {
|
||||
renderClientForbiddenPage(w, r, "You are not allowed to change anything")
|
||||
s.renderClientForbiddenPage(w, r, "You are not allowed to change anything")
|
||||
return
|
||||
}
|
||||
if user.CanManagePublicKeys() {
|
||||
|
@ -1111,26 +1120,26 @@ func handleWebClientProfilePost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
err = dataprovider.UpdateUser(&user, dataprovider.ActionExecutorSelf, util.GetIPFromRemoteAddress(r.RemoteAddr))
|
||||
if err != nil {
|
||||
renderClientProfilePage(w, r, err.Error())
|
||||
s.renderClientProfilePage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
renderClientMessagePage(w, r, "Profile updated", "", http.StatusOK, nil,
|
||||
s.renderClientMessagePage(w, r, "Profile updated", "", http.StatusOK, nil,
|
||||
"Your profile has been successfully updated")
|
||||
}
|
||||
|
||||
func handleWebClientMFA(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleWebClientMFA(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderClientMFAPage(w, r)
|
||||
s.renderClientMFAPage(w, r)
|
||||
}
|
||||
|
||||
func handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleWebClientTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderClientTwoFactorPage(w, "")
|
||||
s.renderClientTwoFactorPage(w, "")
|
||||
}
|
||||
|
||||
func handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleWebClientTwoFactorRecovery(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
renderClientTwoFactorRecoveryPage(w, "")
|
||||
s.renderClientTwoFactorRecoveryPage(w, "")
|
||||
}
|
||||
|
||||
func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
|
||||
|
@ -1166,53 +1175,53 @@ func getShareFromPostFields(r *http.Request) (*dataprovider.Share, error) {
|
|||
return share, nil
|
||||
}
|
||||
|
||||
func handleWebClientForgotPwd(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleWebClientForgotPwd(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
if !smtp.IsEnabled() {
|
||||
renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
|
||||
s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
|
||||
return
|
||||
}
|
||||
renderClientForgotPwdPage(w, "")
|
||||
s.renderClientForgotPwdPage(w, "")
|
||||
}
|
||||
|
||||
func handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleWebClientForgotPwdPost(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxRequestSize)
|
||||
err := r.ParseForm()
|
||||
if err != nil {
|
||||
renderClientForgotPwdPage(w, err.Error())
|
||||
s.renderClientForgotPwdPage(w, err.Error())
|
||||
return
|
||||
}
|
||||
if err := verifyCSRFToken(r.Form.Get(csrfFormToken)); err != nil {
|
||||
renderClientForbiddenPage(w, r, err.Error())
|
||||
s.renderClientForbiddenPage(w, r, err.Error())
|
||||
return
|
||||
}
|
||||
username := r.Form.Get("username")
|
||||
err = handleForgotPassword(r, username, false)
|
||||
if err != nil {
|
||||
if e, ok := err.(*util.ValidationError); ok {
|
||||
renderClientForgotPwdPage(w, e.GetErrorString())
|
||||
s.renderClientForgotPwdPage(w, e.GetErrorString())
|
||||
return
|
||||
}
|
||||
renderClientForgotPwdPage(w, err.Error())
|
||||
s.renderClientForgotPwdPage(w, err.Error())
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, webClientResetPwdPath, http.StatusFound)
|
||||
}
|
||||
|
||||
func handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleWebClientPasswordReset(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
if !smtp.IsEnabled() {
|
||||
renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
|
||||
s.renderClientNotFoundPage(w, r, errors.New("this page does not exist"))
|
||||
return
|
||||
}
|
||||
renderClientResetPwdPage(w, "")
|
||||
s.renderClientResetPwdPage(w, "")
|
||||
}
|
||||
|
||||
func handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
|
||||
r.Body = http.MaxBytesReader(w, r.Body, maxLoginBodySize)
|
||||
name := r.URL.Query().Get("path")
|
||||
if name == "" {
|
||||
renderClientBadRequestPage(w, r, errors.New("no file specified"))
|
||||
s.renderClientBadRequestPage(w, r, errors.New("no file specified"))
|
||||
return
|
||||
}
|
||||
name = util.CleanPath(name)
|
||||
|
@ -1220,6 +1229,7 @@ func handleClientViewPDF(w http.ResponseWriter, r *http.Request) {
|
|||
Title: path.Base(name),
|
||||
URL: fmt.Sprintf("%v?path=%v&inline=1", webClientFilesPath, url.QueryEscape(name)),
|
||||
StaticURL: webStaticFilesPath,
|
||||
ExtraCSS: s.binding.ExtraCSS,
|
||||
}
|
||||
renderClientTemplate(w, templateClientViewPDF, data)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#!/bin/bash
|
||||
|
||||
NFPM_VERSION=2.14.0
|
||||
NFPM_VERSION=2.15.0
|
||||
NFPM_ARCH=${NFPM_ARCH:-amd64}
|
||||
if [ -z ${SFTPGO_VERSION} ]
|
||||
then
|
||||
|
|
|
@ -250,7 +250,8 @@
|
|||
"permissions_policy": "",
|
||||
"cross_origin_opener_policy": "",
|
||||
"expect_ct_header": ""
|
||||
}
|
||||
},
|
||||
"extra_css": []
|
||||
}
|
||||
],
|
||||
"templates_path": "templates",
|
||||
|
|
|
@ -70,6 +70,10 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
{{range .ExtraCSS}}
|
||||
<link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
|
||||
{{end}}
|
||||
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-primary">
|
||||
|
|
|
@ -70,6 +70,10 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
{{range .ExtraCSS}}
|
||||
<link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
|
||||
{{end}}
|
||||
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-primary">
|
||||
|
|
|
@ -53,6 +53,10 @@
|
|||
</style>
|
||||
{{block "extra_css" .}}{{end}}
|
||||
|
||||
{{range .ExtraCSS}}
|
||||
<link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
|
||||
{{end}}
|
||||
|
||||
</head>
|
||||
|
||||
<body id="page-top">
|
||||
|
|
|
@ -71,6 +71,10 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
{{range .ExtraCSS}}
|
||||
<link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
|
||||
{{end}}
|
||||
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-primary">
|
||||
|
|
|
@ -53,6 +53,10 @@
|
|||
</style>
|
||||
{{block "extra_css" .}}{{end}}
|
||||
|
||||
{{range .ExtraCSS}}
|
||||
<link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
|
||||
{{end}}
|
||||
|
||||
</head>
|
||||
|
||||
<body id="page-top">
|
||||
|
|
|
@ -71,6 +71,10 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
{{range .ExtraCSS}}
|
||||
<link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
|
||||
{{end}}
|
||||
|
||||
</head>
|
||||
|
||||
<body class="bg-gradient-primary">
|
||||
|
|
|
@ -4,6 +4,10 @@
|
|||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>{{.Title}}</title>
|
||||
|
||||
{{range .ExtraCSS}}
|
||||
<link href="{{$.StaticURL}}{{.Path}}" rel="stylesheet" type="text/css">
|
||||
{{end}}
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
|
Loading…
Reference in a new issue