mirror of
https://github.com/drakkan/sftpgo.git
synced 2026-03-14 14:25:52 +01:00
httpd: add base URL configuration
Allow overriding the browser URL when generating share links. Fixes #1858 Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
ed0c1a01ab
commit
e44ff487e5
9 changed files with 142 additions and 8 deletions
|
|
@ -120,6 +120,7 @@ var (
|
|||
ClientIPHeaderDepth: 0,
|
||||
HideLoginURL: 0,
|
||||
RenderOpenAPI: true,
|
||||
BaseURL: "",
|
||||
Languages: []string{"en"},
|
||||
OIDC: httpd.OIDC{
|
||||
ClientID: "",
|
||||
|
|
@ -1886,6 +1887,12 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo
|
|||
isSet = true
|
||||
}
|
||||
|
||||
baseURL, ok := os.LookupEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%d__BASE_URL", idx))
|
||||
if ok {
|
||||
binding.BaseURL = baseURL
|
||||
isSet = true
|
||||
}
|
||||
|
||||
languages, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%d__LANGUAGES", idx))
|
||||
if ok {
|
||||
binding.Languages = languages
|
||||
|
|
|
|||
|
|
@ -1200,6 +1200,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS", "3")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS", "12")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI", "0")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__BASE_URL", "https://example.com")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES", "en,es")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS", "1 ")
|
||||
os.Setenv("SFTPGO_HTTPD__BINDINGS__2__MIN_TLS_VERSION", "13")
|
||||
|
|
@ -1273,6 +1274,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__DISABLED_LOGIN_METHODS")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__BASE_URL")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE")
|
||||
os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES")
|
||||
|
|
@ -1339,6 +1341,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, 0, bindings[0].EnabledLoginMethods)
|
||||
require.Equal(t, 0, bindings[0].DisabledLoginMethods)
|
||||
require.True(t, bindings[0].RenderOpenAPI)
|
||||
require.Empty(t, bindings[0].BaseURL)
|
||||
require.Len(t, bindings[0].Languages, 1)
|
||||
assert.Contains(t, bindings[0].Languages, "en")
|
||||
require.Len(t, bindings[0].TLSCipherSuites, 1)
|
||||
|
|
@ -1362,6 +1365,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, 0, bindings[1].EnabledLoginMethods)
|
||||
require.Equal(t, 0, bindings[1].DisabledLoginMethods)
|
||||
require.True(t, bindings[1].RenderOpenAPI)
|
||||
require.Empty(t, bindings[1].BaseURL)
|
||||
require.Len(t, bindings[1].Languages, 1)
|
||||
assert.Contains(t, bindings[1].Languages, "en")
|
||||
require.Nil(t, bindings[1].TLSCipherSuites)
|
||||
|
|
@ -1385,6 +1389,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) {
|
|||
require.Equal(t, 3, bindings[2].EnabledLoginMethods)
|
||||
require.Equal(t, 12, bindings[2].DisabledLoginMethods)
|
||||
require.False(t, bindings[2].RenderOpenAPI)
|
||||
require.Equal(t, "https://example.com", bindings[2].BaseURL)
|
||||
require.Len(t, bindings[2].Languages, 2)
|
||||
assert.Contains(t, bindings[2].Languages, "en")
|
||||
assert.Contains(t, bindings[2].Languages, "es")
|
||||
|
|
|
|||
|
|
@ -179,7 +179,7 @@ func isTLS(r *http.Request) bool {
|
|||
return true
|
||||
}
|
||||
if proto, ok := r.Context().Value(forwardedProtoKey).(string); ok {
|
||||
return proto == "https"
|
||||
return proto == "https" //nolint:goconst
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
|
@ -627,6 +628,10 @@ type Binding struct {
|
|||
HideLoginURL int `json:"hide_login_url" mapstructure:"hide_login_url"`
|
||||
// Enable the built-in OpenAPI renderer
|
||||
RenderOpenAPI bool `json:"render_openapi" mapstructure:"render_openapi"`
|
||||
// BaseURL defines the external base URL for generating public links
|
||||
// (currently share access link), bypassing the default browser-based
|
||||
// detection.
|
||||
BaseURL string `json:"base_url" mapstructure:"base_url"`
|
||||
// Languages defines the list of enabled translations for the WebAdmin and WebClient UI.
|
||||
Languages []string `json:"languages" mapstructure:"languages"`
|
||||
// Defining an OIDC configuration the web admin and web client UI will use OpenID to authenticate users.
|
||||
|
|
@ -667,6 +672,24 @@ func (b *Binding) languages() []string {
|
|||
return b.Languages
|
||||
}
|
||||
|
||||
func (b *Binding) validateBaseURL() error {
|
||||
if b.BaseURL == "" {
|
||||
return nil
|
||||
}
|
||||
u, err := url.ParseRequestURI(b.BaseURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if u.Scheme != "http" && u.Scheme != "https" {
|
||||
return fmt.Errorf("invalid base URL schema %s", b.BaseURL)
|
||||
}
|
||||
if u.Host == "" {
|
||||
return fmt.Errorf("invalid base URL host %s", b.BaseURL)
|
||||
}
|
||||
b.BaseURL = strings.TrimRight(u.String(), "/")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Binding) parseAllowedProxy() error {
|
||||
if filepath.IsAbs(b.Address) && len(b.ProxyAllowed) > 0 {
|
||||
// unix domain socket
|
||||
|
|
@ -700,6 +723,18 @@ func (b *Binding) IsValid() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (b *Binding) check() error {
|
||||
if err := b.parseAllowedProxy(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := b.validateBaseURL(); err != nil {
|
||||
return err
|
||||
}
|
||||
b.checkBranding()
|
||||
b.Security.updateProxyHeaders()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Binding) isWebAdminOIDCLoginDisabled() bool {
|
||||
if b.EnableWebAdmin {
|
||||
return b.DisabledLoginMethods&1 != 0
|
||||
|
|
@ -1135,11 +1170,9 @@ func (c *Conf) Initialize(configDir string, isShared int) error {
|
|||
if !binding.IsValid() {
|
||||
continue
|
||||
}
|
||||
if err := binding.parseAllowedProxy(); err != nil {
|
||||
if err := binding.check(); err != nil {
|
||||
return err
|
||||
}
|
||||
binding.checkBranding()
|
||||
binding.Security.updateProxyHeaders()
|
||||
|
||||
go func(b Binding) {
|
||||
if err := b.OIDC.initialize(); err != nil {
|
||||
|
|
|
|||
|
|
@ -580,6 +580,12 @@ func TestInitialization(t *testing.T) {
|
|||
assert.Contains(t, err.Error(), "oidc")
|
||||
}
|
||||
httpdConf.Bindings[0].OIDC = httpd.OIDC{}
|
||||
httpdConf.Bindings[0].BaseURL = "ftp://127.0.0.1"
|
||||
err = httpdConf.Initialize(configDir, isShared)
|
||||
if assert.Error(t, err) {
|
||||
assert.Contains(t, err.Error(), "URL schema")
|
||||
}
|
||||
httpdConf.Bindings[0].BaseURL = ""
|
||||
httpdConf.Bindings[0].EnableWebClient = true
|
||||
httpdConf.Bindings[0].EnableWebAdmin = true
|
||||
httpdConf.Bindings[0].EnableRESTAPI = true
|
||||
|
|
|
|||
|
|
@ -4247,6 +4247,80 @@ func TestConvertEnabledLoginMethods(t *testing.T) {
|
|||
assert.Equal(t, 0, b.DisabledLoginMethods)
|
||||
}
|
||||
|
||||
func TestValidateBaseURL(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
inputURL string
|
||||
expectedURL string
|
||||
expectErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid HTTPS URL",
|
||||
inputURL: "https://sftp.example.com",
|
||||
expectedURL: "https://sftp.example.com",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Remove trailing slash",
|
||||
inputURL: "https://sftp.example.com/",
|
||||
expectedURL: "https://sftp.example.com",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Remove multiple trailing slashes",
|
||||
inputURL: "http://192.168.1.100:8080///",
|
||||
expectedURL: "http://192.168.1.100:8080",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Empty BaseURL (optional case)",
|
||||
inputURL: "",
|
||||
expectedURL: "",
|
||||
expectErr: false,
|
||||
},
|
||||
{
|
||||
name: "Unsupported scheme (FTP)",
|
||||
inputURL: "ftp://files.example.com",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Malformed URL string",
|
||||
inputURL: "not-a-url",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Missing Host",
|
||||
inputURL: "https://",
|
||||
expectErr: true,
|
||||
},
|
||||
{
|
||||
name: "Preserve path without trailing slash",
|
||||
inputURL: "https://example.com/sftp/",
|
||||
expectedURL: "https://example.com/sftp",
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
b := &Binding{
|
||||
BaseURL: tt.inputURL,
|
||||
}
|
||||
|
||||
err := b.validateBaseURL()
|
||||
|
||||
if (err != nil) != tt.expectErr {
|
||||
t.Errorf("validateBaseURL() error = %v, expectErr %v", err, tt.expectErr)
|
||||
return
|
||||
}
|
||||
|
||||
if !tt.expectErr && b.BaseURL != tt.expectedURL {
|
||||
t.Errorf("validateBaseURL() got = %v, want %v", b.BaseURL, tt.expectedURL)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getCSRFTokenFromBody(body io.Reader) (string, error) {
|
||||
doc, err := html.Parse(body)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -212,6 +212,7 @@ type clientMFAPage struct {
|
|||
type clientSharesPage struct {
|
||||
baseClientPage
|
||||
BasePublicSharesURL string
|
||||
BaseURL string
|
||||
}
|
||||
|
||||
type clientSharePage struct {
|
||||
|
|
@ -1609,6 +1610,7 @@ func (s *httpdServer) handleClientGetShares(w http.ResponseWriter, r *http.Reque
|
|||
data := clientSharesPage{
|
||||
baseClientPage: s.getBaseClientPageData(util.I18nSharesTitle, webClientSharesPath, w, r),
|
||||
BasePublicSharesURL: webClientPubSharesPath,
|
||||
BaseURL: s.binding.BaseURL,
|
||||
}
|
||||
renderClientTemplate(w, templateClientShares, data)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -283,6 +283,7 @@
|
|||
"client_ip_header_depth": 0,
|
||||
"hide_login_url": 0,
|
||||
"render_openapi": true,
|
||||
"base_url": "",
|
||||
"languages": [
|
||||
"en"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -229,6 +229,12 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
$('#writeShare').hide();
|
||||
$('#readShare').hide();
|
||||
} else {
|
||||
let baseURL;
|
||||
//{{- if .BaseURL}}
|
||||
baseURL = '{{.BaseURL}}';
|
||||
//{{- else}}
|
||||
baseURL = getCurrentURI();
|
||||
//{{- end}}
|
||||
let shareURL = '{{.BasePublicSharesURL}}' + "/" + encodeURIComponent(shareID);
|
||||
if (shareScope == '1') {
|
||||
$('#expiredShare').hide();
|
||||
|
|
@ -236,20 +242,20 @@ explicit grant from the SFTPGo Team (support@sftpgo.com).
|
|||
$('#readShare').show();
|
||||
$('#readLink').attr("href", shareURL + "/download");
|
||||
$('#readLink').attr("title", shareURL + "/download");
|
||||
$('#readLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/download");
|
||||
$('#readLinkCopy').attr("data-clipboard-text",baseURL+shareURL + "/download");
|
||||
$('#readUncompressedLink').attr("href", shareURL + "/download?compress=false");
|
||||
$('#readUncompressedLink').attr("title", shareURL + "/download?compress=false");
|
||||
$('#readUncompressedLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/download?compress=false");
|
||||
$('#readUncompressedLinkCopy').attr("data-clipboard-text",baseURL+shareURL + "/download?compress=false");
|
||||
$('#readBrowseLink').attr("href", shareURL + "/browse");
|
||||
$('#readBrowseLink').attr("title", shareURL + "/browse");
|
||||
$('#readBrowseLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/browse");
|
||||
$('#readBrowseLinkCopy').attr("data-clipboard-text",baseURL+shareURL + "/browse");
|
||||
} else {
|
||||
$('#expiredShare').hide();
|
||||
$('#writeShare').show();
|
||||
$('#readShare').hide();
|
||||
$('#writePageLink').attr("href", shareURL + "/upload");
|
||||
$('#writePageLink').attr("title", shareURL + "/upload");
|
||||
$('#writePageLinkCopy').attr("data-clipboard-text",getCurrentURI()+shareURL + "/upload");
|
||||
$('#writePageLinkCopy').attr("data-clipboard-text",baseURL+shareURL + "/upload");
|
||||
}
|
||||
}
|
||||
$('#link_modal').modal('show');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue