diff --git a/.air.toml b/.air.toml
new file mode 100644
index 0000000..fa8d9aa
--- /dev/null
+++ b/.air.toml
@@ -0,0 +1,46 @@
+root = "./"
+testdata_dir = "testdata"
+tmp_dir = "tmp"
+
+[build]
+ args_bin = []
+ bin = "./tmp/main"
+ cmd = "go build -o ./tmp/main ./cmd"
+ delay = 1000
+ exclude_dir = ["assets", "tmp", "vendor", "testdata"]
+ exclude_file = []
+ exclude_regex = ["_test.go"]
+ exclude_unchanged = false
+ follow_symlink = false
+ full_bin = "RULESET=./ruleset.yaml ./tmp/main"
+ include_dir = []
+ include_ext = ["go", "tpl", "tmpl", "yaml", "html"]
+ include_file = []
+ kill_delay = "0s"
+ log = "build-errors.log"
+ poll = false
+ poll_interval = 0
+ post_cmd = []
+ pre_cmd = ["echo 'dev' > handlers/VERSION"]
+ rerun = false
+ rerun_delay = 500
+ send_interrupt = false
+ stop_on_error = false
+
+[color]
+ app = ""
+ build = "yellow"
+ main = "magenta"
+ runner = "green"
+ watcher = "cyan"
+
+[log]
+ main_only = false
+ time = false
+
+[misc]
+ clean_on_exit = true
+
+[screen]
+ clear_on_rebuild = true
+ keep_scroll = true
diff --git a/Makefile b/Makefile
index fdce449..98f3097 100644
--- a/Makefile
+++ b/Makefile
@@ -1,6 +1,6 @@
lint:
gofumpt -l -w .
- golangci-lint run -c .golangci-lint.yaml
+ golangci-lint run -c .golangci-lint.yaml --fix
go mod tidy
go clean
diff --git a/README.md b/README.md
index e1e0129..be1b96f 100644
--- a/README.md
+++ b/README.md
@@ -14,6 +14,18 @@ Freedom of information is an essential pillar of democracy and informed decision
> **Disclaimer:** This project is intended for educational purposes only. The author does not endorse or encourage any unethical or illegal activity. Use this tool at your own risk.
+### How it works
+
+```mermaid
+sequenceDiagram
+ client->>+ladder: GET
+ ladder-->>ladder: apply RequestModifications
+ ladder->>+website: GET
+ website->>-ladder: 200 OK
+ ladder-->>ladder: apply ResultModifications
+ ladder->>-client: 200 OK
+```
+
### Features
- [x] Bypass Paywalls
- [x] Remove CORS headers from responses, assets, and images ...
@@ -181,4 +193,14 @@ echo "dev" > handlers/VERSION
RULESET="./ruleset.yaml" go run cmd/main.go
```
+### Optional: Live reloading development server with [cosmtrek/air](https://github.com/cosmtrek/air)
+
+Install air according to the [installation instructions](https://github.com/cosmtrek/air#installation).
+
+Run a development server at http://localhost:8080:
+
+```bash
+air # or the path to air if you haven't added a path alias to your .bashrc or .zshrc
+```
+
This project uses [pnpm](https://pnpm.io/) to build a stylesheet with the [Tailwind CSS](https://tailwindcss.com/) classes. For local development, if you modify styles in `form.html`, run `pnpm build` to generate a new stylesheet.
diff --git a/cmd/main.go b/cmd/main.go
index 6822c7d..2106713 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -29,6 +29,7 @@ func main() {
if os.Getenv("PORT") == "" {
portEnv = "8080"
}
+
port := parser.String("p", "port", &argparse.Options{
Required: false,
Default: portEnv,
@@ -56,10 +57,12 @@ func main() {
Required: false,
Help: "Compiles a directory of yaml files into a single ruleset.yaml. Requires --ruleset arg.",
})
+
mergeRulesetsGzip := parser.Flag("", "merge-rulesets-gzip", &argparse.Options{
Required: false,
Help: "Compiles a directory of yaml files into a single ruleset.gz Requires --ruleset arg.",
})
+
mergeRulesetsOutput := parser.String("", "merge-rulesets-output", &argparse.Options{
Required: false,
Help: "Specify output file for --merge-rulesets and --merge-rulesets-gzip. Requires --ruleset and --merge-rulesets args.",
@@ -72,7 +75,18 @@ func main() {
// utility cli flag to compile ruleset directory into single ruleset.yaml
if *mergeRulesets || *mergeRulesetsGzip {
- err = cli.HandleRulesetMerge(ruleset, mergeRulesets, mergeRulesetsGzip, mergeRulesetsOutput)
+ output := os.Stdout
+
+ if *mergeRulesetsOutput != "" {
+ output, err = os.Create(*mergeRulesetsOutput)
+
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ }
+
+ err = cli.HandleRulesetMerge(*ruleset, *mergeRulesets, *mergeRulesetsGzip, output)
if err != nil {
fmt.Println(err)
os.Exit(1)
@@ -96,6 +110,7 @@ func main() {
userpass := os.Getenv("USERPASS")
if userpass != "" {
userpass := strings.Split(userpass, ":")
+
app.Use(basicauth.New(basicauth.Config{
Users: map[string]string{
userpass[0]: userpass[1],
@@ -112,24 +127,25 @@ func main() {
if os.Getenv("NOLOGS") != "true" {
app.Use(func(c *fiber.Ctx) error {
log.Println(c.Method(), c.Path())
+
return c.Next()
})
}
app.Get("/", handlers.Form)
- // TODO: move this logic to handers/styles.go
app.Get("/styles.css", func(c *fiber.Ctx) error {
cssData, err := cssData.ReadFile("styles.css")
if err != nil {
return c.Status(fiber.StatusInternalServerError).SendString("Internal Server Error")
}
+
c.Set("Content-Type", "text/css")
+
return c.Send(cssData)
})
app.Get("ruleset", handlers.Ruleset)
-
app.Get("raw/*", handlers.Raw)
app.Get("api/*", handlers.Api)
@@ -140,5 +156,6 @@ func main() {
app.Get("/*", handlers.NewProxySiteHandler(proxyOpts))
app.Post("/*", handlers.NewProxySiteHandler(proxyOpts))
+
log.Fatal(app.Listen(":" + *port))
}
diff --git a/handlers/proxy.go b/handlers/proxy.go
index 3d13f96..196353a 100644
--- a/handlers/proxy.go
+++ b/handlers/proxy.go
@@ -1,38 +1,13 @@
package handlers
import (
- "fmt"
- "io"
- "log"
- "net/http"
- "net/url"
- "os"
- "regexp"
- "strings"
-
- "ladder/pkg/ruleset"
"ladder/proxychain"
rx "ladder/proxychain/requestmodifers"
tx "ladder/proxychain/responsemodifers"
- "github.com/PuerkitoBio/goquery"
"github.com/gofiber/fiber/v2"
)
-var (
- UserAgent = getenv("USER_AGENT", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")
- ForwardedFor = getenv("X_FORWARDED_FOR", "66.249.66.1")
- rulesSet = ruleset.NewRulesetFromEnv()
- allowedDomains = []string{}
-)
-
-func init() {
- allowedDomains = strings.Split(os.Getenv("ALLOWED_DOMAINS"), ",")
- if os.Getenv("ALLOWED_DOMAINS_RULESET") == "true" {
- allowedDomains = append(allowedDomains, rulesSet.Domains()...)
- }
-}
-
type ProxyOptions struct {
RulesetPath string
Verbose bool
@@ -71,220 +46,4 @@ func NewProxySiteHandler(opts *ProxyOptions) fiber.Handler {
return proxychain
}
-
-}
-
-func modifyURL(uri string, rule ruleset.Rule) (string, error) {
- newUrl, err := url.Parse(uri)
- if err != nil {
- return "", err
- }
-
- for _, urlMod := range rule.UrlMods.Domain {
- re := regexp.MustCompile(urlMod.Match)
- newUrl.Host = re.ReplaceAllString(newUrl.Host, urlMod.Replace)
- }
-
- for _, urlMod := range rule.UrlMods.Path {
- re := regexp.MustCompile(urlMod.Match)
- newUrl.Path = re.ReplaceAllString(newUrl.Path, urlMod.Replace)
- }
-
- v := newUrl.Query()
- for _, query := range rule.UrlMods.Query {
- if query.Value == "" {
- v.Del(query.Key)
- continue
- }
- v.Set(query.Key, query.Value)
- }
- newUrl.RawQuery = v.Encode()
-
- if rule.GoogleCache {
- newUrl, err = url.Parse("https://webcache.googleusercontent.com/search?q=cache:" + newUrl.String())
- if err != nil {
- return "", err
- }
- }
-
- return newUrl.String(), nil
-}
-
-func fetchSite(urlpath string, queries map[string]string) (string, *http.Request, *http.Response, error) {
- urlQuery := "?"
- if len(queries) > 0 {
- for k, v := range queries {
- urlQuery += k + "=" + v + "&"
- }
- }
- urlQuery = strings.TrimSuffix(urlQuery, "&")
- urlQuery = strings.TrimSuffix(urlQuery, "?")
-
- u, err := url.Parse(urlpath)
- if err != nil {
- return "", nil, nil, err
- }
-
- if len(allowedDomains) > 0 && !StringInSlice(u.Host, allowedDomains) {
- return "", nil, nil, fmt.Errorf("domain not allowed. %s not in %s", u.Host, allowedDomains)
- }
-
- if os.Getenv("LOG_URLS") == "true" {
- log.Println(u.String() + urlQuery)
- }
-
- // Modify the URI according to ruleset
- rule := fetchRule(u.Host, u.Path)
- url, err := modifyURL(u.String()+urlQuery, rule)
- if err != nil {
- return "", nil, nil, err
- }
-
- // Fetch the site
- client := &http.Client{}
- req, _ := http.NewRequest("GET", url, nil)
-
- if rule.Headers.UserAgent != "" {
- req.Header.Set("User-Agent", rule.Headers.UserAgent)
- } else {
- req.Header.Set("User-Agent", UserAgent)
- }
-
- if rule.Headers.XForwardedFor != "" {
- if rule.Headers.XForwardedFor != "none" {
- req.Header.Set("X-Forwarded-For", rule.Headers.XForwardedFor)
- }
- } else {
- req.Header.Set("X-Forwarded-For", ForwardedFor)
- }
-
- if rule.Headers.Referer != "" {
- if rule.Headers.Referer != "none" {
- req.Header.Set("Referer", rule.Headers.Referer)
- }
- } else {
- req.Header.Set("Referer", u.String())
- }
-
- if rule.Headers.Cookie != "" {
- req.Header.Set("Cookie", rule.Headers.Cookie)
- }
-
- resp, err := client.Do(req)
- if err != nil {
- return "", nil, nil, err
- }
- defer resp.Body.Close()
-
- bodyB, err := io.ReadAll(resp.Body)
- if err != nil {
- return "", nil, nil, err
- }
-
- if rule.Headers.CSP != "" {
- //log.Println(rule.Headers.CSP)
- resp.Header.Set("Content-Security-Policy", rule.Headers.CSP)
- }
-
- //log.Print("rule", rule) TODO: Add a debug mode to print the rule
- body := rewriteHtml(bodyB, u, rule)
- return body, req, resp, nil
-}
-
-func rewriteHtml(bodyB []byte, u *url.URL, rule ruleset.Rule) string {
- // Rewrite the HTML
- body := string(bodyB)
-
- // images
- imagePattern := `]*\s+)?src="(/)([^"]*)"`
- re := regexp.MustCompile(imagePattern)
- body = re.ReplaceAllString(body, fmt.Sprintf(`
]*\s+)?src="(/)([^"]*)"`
- reScript := regexp.MustCompile(scriptPattern)
- body = reScript.ReplaceAllString(body, fmt.Sprintf(`
- About Us
-