Link to source
This commit is contained in:
@@ -19,6 +19,11 @@ type Config struct {
|
|||||||
BasePath string // normalised: always "/" or "/prefix/"
|
BasePath string // normalised: always "/" or "/prefix/"
|
||||||
PublicURL string // absolute origin+path for generated links; "" => relative
|
PublicURL string // absolute origin+path for generated links; "" => relative
|
||||||
|
|
||||||
|
// SourceURL is linked in the footer. It defaults to where this program is
|
||||||
|
// developed, and a fork should point it at its own; setting it empty drops
|
||||||
|
// the footer entirely, for an instance that would rather not advertise.
|
||||||
|
SourceURL string
|
||||||
|
|
||||||
MaxSize int64 // per-upload cap; Unlimited means no cap
|
MaxSize int64 // per-upload cap; Unlimited means no cap
|
||||||
MaxExpiry time.Duration // longest lifetime a caller may request
|
MaxExpiry time.Duration // longest lifetime a caller may request
|
||||||
DefaultExpiry time.Duration // lifetime when the caller does not ask
|
DefaultExpiry time.Duration // lifetime when the caller does not ask
|
||||||
@@ -37,6 +42,11 @@ type Config struct {
|
|||||||
|
|
||||||
const EnvPrefix = "UNCENSORED_SEND_"
|
const EnvPrefix = "UNCENSORED_SEND_"
|
||||||
|
|
||||||
|
// defaultSourceURL is where this program is developed. Anyone running a
|
||||||
|
// modified copy should say so with --source-url, and anyone who would rather
|
||||||
|
// not mention it at all can pass an empty one.
|
||||||
|
const defaultSourceURL = "https://git.uncensored.hu/thayol/uncensored-send"
|
||||||
|
|
||||||
// Register wires every option onto s. Short forms exist only for the options
|
// Register wires every option onto s. Short forms exist only for the options
|
||||||
// reached often; everything else is long-only, by design.
|
// reached often; everything else is long-only, by design.
|
||||||
func (c *Config) Register(s *Set) {
|
func (c *Config) Register(s *Set) {
|
||||||
@@ -50,6 +60,8 @@ func (c *Config) Register(s *Set) {
|
|||||||
"path prefix this service is mounted under")
|
"path prefix this service is mounted under")
|
||||||
s.String(&c.PublicURL, "public-url", "u", "", "URL",
|
s.String(&c.PublicURL, "public-url", "u", "", "URL",
|
||||||
"absolute base URL used in generated links; relative links when empty")
|
"absolute base URL used in generated links; relative links when empty")
|
||||||
|
s.String(&c.SourceURL, "source-url", "", defaultSourceURL, "URL",
|
||||||
|
"where the footer's source link points; empty hides the footer")
|
||||||
|
|
||||||
s.Size(&c.MaxSize, "max-size", "s", "2GiB",
|
s.Size(&c.MaxSize, "max-size", "s", "2GiB",
|
||||||
"largest upload accepted from an anonymous caller")
|
"largest upload accepted from an anonymous caller")
|
||||||
@@ -110,6 +122,17 @@ func (c *Config) Normalise() error {
|
|||||||
c.PublicURL = strings.TrimSuffix(u.String(), "/")
|
c.PublicURL = strings.TrimSuffix(u.String(), "/")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if c.SourceURL != "" {
|
||||||
|
u, err := url.Parse(c.SourceURL)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("--source-url: %w", err)
|
||||||
|
}
|
||||||
|
if !u.IsAbs() {
|
||||||
|
return fmt.Errorf("--source-url: %q is not absolute", c.SourceURL)
|
||||||
|
}
|
||||||
|
c.SourceURL = u.String()
|
||||||
|
}
|
||||||
|
|
||||||
if c.TokensPath == "" {
|
if c.TokensPath == "" {
|
||||||
c.TokensPath = c.DataDir + "/tokens.json"
|
c.TokensPath = c.DataDir + "/tokens.json"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -156,6 +156,43 @@ func TestParseRefusesStrayArguments(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The source link is a URL someone will set by hand on a fork, so a value that
|
||||||
|
// would render a broken link has to be refused at startup rather than shipped
|
||||||
|
// to every page.
|
||||||
|
func TestSourceURLMustBeAbsoluteOrEmpty(t *testing.T) {
|
||||||
|
base := func() Config {
|
||||||
|
c := Config{}
|
||||||
|
fs := NewSet("test", "UNCENSORED_SEND_TEST_")
|
||||||
|
c.Register(fs)
|
||||||
|
if err := fs.Parse(nil); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
c := base()
|
||||||
|
if err := c.Normalise(); err != nil {
|
||||||
|
t.Fatalf("the default source URL was refused: %v", err)
|
||||||
|
}
|
||||||
|
if c.SourceURL == "" {
|
||||||
|
t.Error("the default build links no source at all")
|
||||||
|
}
|
||||||
|
|
||||||
|
c = base()
|
||||||
|
c.SourceURL = ""
|
||||||
|
if err := c.Normalise(); err != nil {
|
||||||
|
t.Errorf("switching the source link off was refused: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, bad := range []string{"not a url", "/relative/path", "example.org/repo"} {
|
||||||
|
c = base()
|
||||||
|
c.SourceURL = bad
|
||||||
|
if err := c.Normalise(); err == nil {
|
||||||
|
t.Errorf("--source-url %q was accepted", bad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The convention is: one hyphen for a letter, two for a word. It is enforced
|
// The convention is: one hyphen for a letter, two for a word. It is enforced
|
||||||
// here because the stdlib flag package treats both forms as the same.
|
// here because the stdlib flag package treats both forms as the same.
|
||||||
func TestHyphenConvention(t *testing.T) {
|
func TestHyphenConvention(t *testing.T) {
|
||||||
|
|||||||
@@ -241,6 +241,9 @@ type page struct {
|
|||||||
// case the markup carries no link rather than one that 404s.
|
// case the markup carries no link rather than one that 404s.
|
||||||
Favicon string
|
Favicon string
|
||||||
|
|
||||||
|
// Source is where the footer links; empty means no footer.
|
||||||
|
Source string
|
||||||
|
|
||||||
// Wide widens the page for content that is a table rather than a form.
|
// Wide widens the page for content that is a table rather than a form.
|
||||||
// The reading measure that suits the upload page is far too narrow for a
|
// The reading measure that suits the upload page is far too narrow for a
|
||||||
// listing, which otherwise ends up behind a horizontal scrollbar.
|
// listing, which otherwise ends up behind a horizontal scrollbar.
|
||||||
@@ -250,7 +253,7 @@ type page struct {
|
|||||||
// page builds the common fields, resolving the session so the header can show
|
// page builds the common fields, resolving the session so the header can show
|
||||||
// who is logged in and offer only the links they can use.
|
// who is logged in and offer only the links they can use.
|
||||||
func (s *Server) page(r *http.Request, title string, script bool) page {
|
func (s *Server) page(r *http.Request, title string, script bool) page {
|
||||||
p := page{Base: s.cfg.BasePath, Title: title, Script: script}
|
p := page{Base: s.cfg.BasePath, Title: title, Script: script, Source: s.cfg.SourceURL}
|
||||||
if s.favicon != "" {
|
if s.favicon != "" {
|
||||||
p.Favicon = s.cfg.BasePath + "static/" + s.favicon
|
p.Favicon = s.cfg.BasePath + "static/" + s.favicon
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1146,6 +1146,49 @@ func TestNoFaviconMeansNoLink(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- the footer ------------------------------------------------------------
|
||||||
|
|
||||||
|
// The footer names where the source lives, and an instance that would rather
|
||||||
|
// not say so can drop it. A fork gets to point at its own.
|
||||||
|
func TestSourceLinkIsOptional(t *testing.T) {
|
||||||
|
shown := newHarness(t, nil)
|
||||||
|
resp := shown.get(t, "/", "")
|
||||||
|
page, _ := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if !strings.Contains(string(page), `<footer><a href="https://git.uncensored.hu/thayol/uncensored-send">Source</a>`) {
|
||||||
|
t.Error("the default build does not link its source")
|
||||||
|
}
|
||||||
|
|
||||||
|
forked := newHarness(t, func(c *config.Config) { c.SourceURL = "https://example.org/me/fork" })
|
||||||
|
resp = forked.get(t, "/login", "")
|
||||||
|
page, _ = io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if !strings.Contains(string(page), `href="https://example.org/me/fork"`) {
|
||||||
|
t.Error("a fork's own source URL is not used")
|
||||||
|
}
|
||||||
|
|
||||||
|
quiet := newHarness(t, func(c *config.Config) { c.SourceURL = "" })
|
||||||
|
for _, path := range []string{"/", "/login", "/files"} {
|
||||||
|
resp := quiet.get(t, path, "")
|
||||||
|
page, _ := io.ReadAll(resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
if strings.Contains(string(page), "<footer>") {
|
||||||
|
t.Errorf("GET %s still carries a footer with the source link switched off", path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The footer is the one link that leaves this origin, so the header that keeps
|
||||||
|
// it from naming this instance to the far end has to stay put.
|
||||||
|
func TestOutboundRequestsCarryNoReferrer(t *testing.T) {
|
||||||
|
h := newHarness(t, nil)
|
||||||
|
resp := h.get(t, "/", "")
|
||||||
|
resp.Body.Close()
|
||||||
|
if got := resp.Header.Get("Referrer-Policy"); got != "no-referrer" {
|
||||||
|
t.Errorf("Referrer-Policy = %q, want no-referrer", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- content security policy ---------------------------------------------
|
// --- content security policy ---------------------------------------------
|
||||||
|
|
||||||
// The page's own behaviour and its CSP have to agree, and nothing in a Go test
|
// The page's own behaviour and its CSP have to agree, and nothing in a Go test
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
<main>
|
<main>
|
||||||
{{template "content" .}}
|
{{template "content" .}}
|
||||||
</main>
|
</main>
|
||||||
|
{{if .Source}}<footer><a href="{{.Source}}">Source</a></footer>{{end}}
|
||||||
{{if .Script}}<script src="{{.Base}}static/app.js" defer></script>{{end}}
|
{{if .Script}}<script src="{{.Base}}static/app.js" defer></script>{{end}}
|
||||||
</body>
|
</body>
|
||||||
</html>{{end}}
|
</html>{{end}}
|
||||||
|
|||||||
Reference in New Issue
Block a user