From 64cbd26602bb3bf68bbe6217bffe8ac9a46b79ec Mon Sep 17 00:00:00 2001 From: poyo Date: Mon, 24 Aug 2026 23:58:29 +0200 Subject: [PATCH] remove useless PRODUCTION flag, replace with PUBLIC_URL which should be easier to comprehend. automatically set and disable cookie "secure" attribute based off inferred scheme (HTTP or HTTPS?) --- README.md | 7 +++--- app/app.go | 2 +- app/repo-view.go | 11 ++++++++++ config/config.go | 42 ++++++++++++++++++++---------------- example.env | 4 ++-- lib/oauth2/hackatime.go | 9 +------- lib/oauth2/hackatime_test.go | 15 +------------ main.go | 1 + 8 files changed, 45 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index c3ad431..c3132ee 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # Farga Farga is a FOSS lightweight and easily self-hostable Git forge meant for personal use. - -It's so tiny you could run it on a PS3. +It's so tiny you could theoretically run it on a PS3. ## Features - It's small and uses less than 50MB of RAM. Host it on a PS3! @@ -37,7 +36,7 @@ Note: this "quick setup" will only work on machines running Arch Linux. ### Setup 1. Clone this repository, and `cd` into it. 2. Copy `example.env` and rename it to `.env`. -3. Modify any environment variables you need. CSRF_KEY will be automatically set by Farga. +3. Modify any environment variables you need. CSRF_KEY will be automatically set by Farga. Set PUBLIC_URL to the URL at which you would access Farga's index, including http(s) (e.g: `https://localhost:3333`). 4. (Optional) Setup Hackatime OAuth: If you want to enable the hackatime integration, go to Hackatime's [OAuth2 Applications](https://hackatime.hackclub.com/oauth/applications) page, create a new application, and put the generated App ID and Secret to the the `.env`. 5. `go run .` @@ -128,6 +127,8 @@ Items with a `?` prefix are possible future features. ## Contributing Contributions are welcome and greatly appreciated. Read [DEVELOPMENT.md](DEVELOPMENT.md) for reference. +Main development is happening at [https://tangled.org/poy.ooo/farga](https://tangled.org/poy.ooo/farga) + Don't hesitate to contact me if you have any questions or want to talk about the project itself. ## Security diff --git a/app/app.go b/app/app.go index 06ee03f..74bd5d9 100644 --- a/app/app.go +++ b/app/app.go @@ -33,7 +33,7 @@ func New(store Store, config config.Config) *App { csrfMiddleware := csrf.Protect( []byte(config.CSRFKey), - csrf.Secure(false), // todo: production flag + csrf.Secure(strings.HasPrefix(config.PublicUrl, "https")), csrf.SameSite(csrf.SameSiteLaxMode), // send cookies on top-level safe navigations, block on cross-site subrequests csrf.Path("/"), // cookie path defaults to the request directory (e.g. /auth) otherwise csrf.TrustedOrigins([]string{"localhost:3333", fmt.Sprintf("%s:%v", config.Domain, config.Port), config.Domain}), diff --git a/app/repo-view.go b/app/repo-view.go index 445dbaf..9ff274e 100644 --- a/app/repo-view.go +++ b/app/repo-view.go @@ -271,6 +271,17 @@ func (a *App) RepoHandler(w http.ResponseWriter, r *http.Request) { return } + head, err := git.ReadBlobPrefix(repoPath, entry.Hash, 512) + if err != nil { + fmt.Println(err) + http.Error(w, err.Error(), 500) + return + } + + if mime.IsText(mime.Type(head)) { + w.Header().Set("Content-Type", "text/plain") // prevents stored-XSS + } + w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Length", strconv.FormatInt(size, 10)) diff --git a/config/config.go b/config/config.go index bc73bc4..889fdf8 100644 --- a/config/config.go +++ b/config/config.go @@ -3,8 +3,10 @@ package config import ( "farga/lib" "log" + "net/url" "os" "strconv" + "strings" "time" "github.com/joho/godotenv" @@ -14,11 +16,12 @@ type Config struct { SQLitePath string // path relative to the executable where the database is at. CSRFKey string // random string at least 32 bytes long. - Domain string // domain used both for the frontend and SSH - Port uint // HTTP/S port - SSHPort uint // SSH port - AllowSignups bool // boolean. Invites allow to signup users. - Production bool // boolean. Omit the port and use https in OAuth callback URLs. + Domain string // domain used both for the frontend and SSH + Port uint // HTTP/S port + SSHPort uint // SSH port + PublicUrl string // Full url of the frontend, including HTTP and no trailing slash. http://localhost:3333 is an example. + + AllowSignups bool // boolean. Invites allow to signup users. InviteTokenLength uint // length of token. 8 -> "abc12345". If set to 0 it will error out. InviteTokenDuration time.Duration // how much an invite should last: 7d or 4h or 2h45m @@ -41,13 +44,13 @@ func New() Config { CSRF_KEY := os.Getenv("CSRF_KEY") SQLITE_PATH := os.Getenv("SQLITE_PATH") ALLOW_SIGNUPS := os.Getenv("ALLOW_SIGNUPS") - PRODUCTION := os.Getenv("PRODUCTION") + PUBLIC_URL := os.Getenv("PUBLIC_URL") HACKATIME_CLIENT_ID := os.Getenv("HACKATIME_CLIENT_ID") HACKATIME_CLIENT_SECRET := os.Getenv("HACKATIME_CLIENT_SECRET") INVITE_TOKEN_LENGTH := os.Getenv("INVITE_TOKEN_LENGTH") - if DOMAIN == "" || PORT == "" || SQLITE_PATH == "" { - log.Fatal("You're missing one (or more) required env vars: 'SQLITE_PATH, DOMAIN, PORT, CSRF_KEY'.") + if DOMAIN == "" || PUBLIC_URL == "" || PORT == "" || SQLITE_PATH == "" { + log.Fatal("You're missing one (or more) of the following env vars: 'SQLITE_PATH, DOMAIN, PORT, PUBLIC_URL'.") } Port, err := strconv.ParseUint(PORT, 10, 32) @@ -60,6 +63,16 @@ func New() Config { log.Fatal("Invalid SSH_PORT env var.") } + // assume HTTP scheme if none provided. + if !strings.HasPrefix(PUBLIC_URL, "http://") && !strings.HasPrefix(PUBLIC_URL, "https://") { + PUBLIC_URL = "http://" + PUBLIC_URL + } + + ParsedPublicUrl, err := url.ParseRequestURI(PUBLIC_URL) + if err != nil { + log.Fatal("Invalid PUBLIC_URL env var.") + } + var InviteTokenLength uint64 = 16 // sensible default with 80 bits of entropy. if INVITE_TOKEN_LENGTH != "" { @@ -75,6 +88,8 @@ func New() Config { log.Panic(err) } + // generate a new, safe CSRF key and write to .env. + // it magically formats the .env too. CSRF_KEY = lib.Base64Token(64) env["CSRF_KEY"] = CSRF_KEY godotenv.Write(env, ".env") @@ -89,15 +104,6 @@ func New() Config { } } - var Production bool - - if PRODUCTION != "" { - Production, err = strconv.ParseBool(PRODUCTION) - if err != nil { - log.Fatal("Invalid PRODUCTION value. Set it to either 'true' or 'false'") - } - } - // 24 hours * 2 days = 48 hours InviteTokenDuration, err := time.ParseDuration("48h") // todo: check if not negative if err != nil { @@ -107,11 +113,11 @@ func New() Config { C = Config{ SQLitePath: SQLITE_PATH, AllowSignups: AllowSignups, - Production: Production, Port: uint(Port), SSHPort: uint(SSHPort), CSRFKey: CSRF_KEY, Domain: DOMAIN, + PublicUrl: strings.TrimSuffix(ParsedPublicUrl.String(), "/"), // omit trailing slash HackatimeClientID: HACKATIME_CLIENT_ID, HackatimeClientSecret: HACKATIME_CLIENT_SECRET, InviteTokenLength: uint(InviteTokenLength), diff --git a/example.env b/example.env index 7b6645a..e2a8ac6 100644 --- a/example.env +++ b/example.env @@ -2,6 +2,7 @@ SQLITE_PATH="data/farga.sqlite" CSRF_KEY=# will be auto-generated by Farga. DOMAIN="localhost" +PUBLIC_URL="http://localhost:3333"# set to the base URL it'll be available from. PORT=3333 SSH_PORT=6969 @@ -9,5 +10,4 @@ HACKATIME_CLIENT_ID=# https://hackatime.hackclub.com/docs/oauth/oauth-apps HACKATIME_CLIENT_SECRET=# https://hackatime.hackclub.com/docs/oauth/oauth-apps ALLOW_SIGNUPS="false" -PRODUCTION=false -INVITE_TOKEN_LENGTH=8 \ No newline at end of file +INVITE_TOKEN_LENGTH=16 \ No newline at end of file diff --git a/lib/oauth2/hackatime.go b/lib/oauth2/hackatime.go index a4b1de9..a98aa3d 100644 --- a/lib/oauth2/hackatime.go +++ b/lib/oauth2/hackatime.go @@ -8,17 +8,10 @@ import ( ) func HackatimeConfig(config config.Config) *oauth2.Config { - scheme := "http" - host := fmt.Sprintf("%s:%d", config.Domain, config.Port) - if config.Production { - scheme = "https" - host = config.Domain - } - return &oauth2.Config{ ClientID: config.HackatimeClientID, ClientSecret: config.HackatimeClientSecret, - RedirectURL: fmt.Sprintf("%s://%s/auth/hackatime/callback", scheme, host), + RedirectURL: fmt.Sprintf("%s/auth/hackatime/callback", config.PublicUrl), Scopes: []string{ "profile", // slack ID, username diff --git a/lib/oauth2/hackatime_test.go b/lib/oauth2/hackatime_test.go index 1b3fcfe..b654056 100644 --- a/lib/oauth2/hackatime_test.go +++ b/lib/oauth2/hackatime_test.go @@ -9,6 +9,7 @@ func TestHackatimeConfig(t *testing.T) { c := HackatimeConfig(config.Config{ Domain: "example.com", Port: 8080, + PublicUrl: "http://example.com:8080", // no trailing slash. HackatimeClientID: "client-id", HackatimeClientSecret: "client-secret", }) @@ -32,17 +33,3 @@ func TestHackatimeConfig(t *testing.T) { t.Errorf("TokenURL = %q, want https://hackatime.hackclub.com/oauth/token", c.Endpoint.TokenURL) } } - -func TestHackatimeConfigProduction(t *testing.T) { - c := HackatimeConfig(config.Config{ - Domain: "example.com", - Port: 8080, - Production: true, - HackatimeClientID: "client-id", - HackatimeClientSecret: "client-secret", - }) - - if c.RedirectURL != "https://example.com/auth/hackatime/callback" { - t.Errorf("RedirectURL = %q, want https://example.com/auth/hackatime/callback", c.RedirectURL) - } -} diff --git a/main.go b/main.go index 5246ff3..a7667df 100644 --- a/main.go +++ b/main.go @@ -40,6 +40,7 @@ func main() { os.Exit(0) } + fmt.Println(config.PublicUrl) app := app.New(d, config) -- 2.54.0