diff --git a/README.md b/README.md index cc4c72e..d4218ce 100644 --- a/README.md +++ b/README.md @@ -129,6 +129,7 @@ http://localhost:8080/ruleset | `ALLOWED_DOMAINS` | Comma separated list of allowed domains. Empty = no limitations | `` | | `ALLOWED_DOMAINS_RULESET` | Allow Domains from Ruleset. false = no limitations | `false` | | `FLARESOLVERR_HOST` | URL for the FlareSolverr service for Cloudflare bypass (optional) | `http://localhost:8191` | +| `DEFAULT_SCHEME` | Scheme prepended when a requested URL has no scheme (`example.com/page` → `://example.com/page`). Must be `http` or `https`. Also configurable via the `--default-scheme` / `-s` CLI flag. | `https` | `ALLOWED_DOMAINS` and `ALLOWED_DOMAINS_RULESET` are joined together. If both are empty, no limitations are applied. | `BASE_PATH` | Base path for the proxy, useful if you want to run the proxy on a subpath (e.g. http://localhost:8080/proxy/) | `` | diff --git a/cmd/main.go b/cmd/main.go index 54cab3a..e242bc9 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -61,6 +61,17 @@ func main() { Help: "Specify output file for --merge-rulesets and --merge-rulesets-gzip. Requires --ruleset and --merge-rulesets args.", }) + defaultSchemeEnv := os.Getenv("DEFAULT_SCHEME") + if defaultSchemeEnv == "" { + defaultSchemeEnv = "https" + } + + defaultScheme := parser.String("s", "default-scheme", &argparse.Options{ + Required: false, + Default: defaultSchemeEnv, + Help: "Scheme to prepend when a proxied URL has no scheme. Must be \"http\" or \"https\".", + }) + err := parser.Parse(os.Args) if err != nil { fmt.Print(parser.Usage(err)) @@ -91,6 +102,11 @@ func main() { *prefork = true } + if err := handlers.SetDefaultScheme(*defaultScheme); err != nil { + fmt.Println(err) + os.Exit(1) + } + // BASE_PATH allows running ladder under a sub-path, e.g. /mypath basePath := os.Getenv("BASE_PATH") if basePath != "" { diff --git a/handlers/api.go b/handlers/api.go index f88e79e..8e984bc 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -33,6 +33,7 @@ func Api(c *fiber.Ctx) error { url = c.Params("*") } + url = ensureScheme(url) body, req, resp, err := fetchSite(url, queries) if err != nil { log.Println("ERROR:", err) diff --git a/handlers/apply_rules_test.go b/handlers/apply_rules_test.go new file mode 100644 index 0000000..ea4407c --- /dev/null +++ b/handlers/apply_rules_test.go @@ -0,0 +1,127 @@ +package handlers + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "net/url" + "strings" + "testing" + + "ladder/pkg/ruleset" + + "github.com/stretchr/testify/assert" +) + +// TestApplyRulesUnit exercises applyRules directly: a regex strip and a head +// injection should both land on the input body. +func TestApplyRulesUnit(t *testing.T) { + // Seed rulesSet so applyRules doesn't early-return when len(rulesSet)==0. + prev := rulesSet + rulesSet = ruleset.RuleSet{{Domain: "example.test"}} + t.Cleanup(func() { rulesSet = prev }) + + input := `

hello

` + + rule := ruleset.Rule{ + Domain: "example.test", + RegexRules: []ruleset.Regex{ + {Match: `]*>`, Replace: ""}, + }, + } + rule.Injections = append(rule.Injections, struct { + Position string `yaml:"position,omitempty"` + Append string `yaml:"append,omitempty"` + Prepend string `yaml:"prepend,omitempty"` + Replace string `yaml:"replace,omitempty"` + }{Position: "head", Append: ``}) + + out := applyRules(input, rule) + + assert.NotContains(t, out, `

body content

` + + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "text/html") + fmt.Fprint(w, upstreamHTML) + })) + t.Cleanup(server.Close) + + u, err := url.Parse(server.URL) + assert.NoError(t, err) + + rule := ruleset.Rule{ + Domain: u.Host, + RegexRules: []ruleset.Regex{ + // Strict pattern requiring src= — only matches if applyRules runs + // BEFORE rewriteHtml's src→script rename. Confirms ordering too. + {Match: `]*src="https://example\.test/paywall\.js"[^>]*>`, Replace: ""}, + }, + } + rule.Injections = append(rule.Injections, struct { + Position string `yaml:"position,omitempty"` + Append string `yaml:"append,omitempty"` + Prepend string `yaml:"prepend,omitempty"` + Replace string `yaml:"replace,omitempty"` + }{Position: "head", Append: ``}) + + prev := rulesSet + rulesSet = ruleset.RuleSet{rule} + t.Cleanup(func() { rulesSet = prev }) + + body, _, _, err := fetchSite(server.URL, map[string]string{}) + assert.NoError(t, err) + + assert.NotContains(t, body, `paywall.js`, "regexRules in fetchSite should strip the upstream script") + assert.Contains(t, body, `