Compare commits
36 Commits
feature/ad
...
v0.0.14
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
81aa00c2ea | ||
|
|
6c1f58e2e7 | ||
|
|
d3c995df34 | ||
|
|
6f4a2daeca | ||
|
|
f728b2c1de | ||
|
|
bc346a3954 | ||
|
|
5442da81b9 | ||
|
|
73b13914fe | ||
|
|
b127f81a9b | ||
|
|
79438a0b59 | ||
|
|
8058ebf0ca | ||
|
|
afca5eda80 | ||
|
|
1aa917e0c1 | ||
|
|
84617b32e3 | ||
|
|
501dfb106a | ||
|
|
719373bb7d | ||
|
|
a9f22ef428 | ||
|
|
54926a6644 | ||
|
|
63933991fd | ||
|
|
46ca742f92 | ||
|
|
a1e63c9ecb | ||
|
|
9e9c50181c | ||
|
|
43e90cf7f2 | ||
|
|
46a32ec548 | ||
|
|
bad7eebd36 | ||
|
|
51476759da | ||
|
|
1a708959f7 | ||
|
|
1f89661ed9 | ||
|
|
a7299049c3 | ||
|
|
3a1d2bc187 | ||
|
|
46c91a05d0 | ||
|
|
5df9a937c5 | ||
|
|
945f499e88 | ||
|
|
cdcbfd4ee9 | ||
|
|
a2f909501c | ||
|
|
cc56f03607 |
0
.github/pull_request_template.md
vendored
0
.github/pull_request_template.md
vendored
30
README.md
30
README.md
@@ -3,6 +3,8 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h1 align="center">Ladder</h1>
|
<h1 align="center">Ladder</h1>
|
||||||
|
<div><img alt="License" src="https://img.shields.io/github/license/kubero-dev/ladder"> <img alt="go.mod Go version " src="https://img.shields.io/github/go-mod/go-version/kubero-dev/ladder"> <img alt="GitHub tag (with filter)" src="https://img.shields.io/github/v/tag/kubero-dev/ladder"> <img alt="GitHub (Pre-)Release Date" src="https://img.shields.io/github/release-date-pre/kubero-dev/ladder"> <img alt="GitHub Downloads all releases" src="https://img.shields.io/github/downloads/kubero-dev/ladder/total"> <img alt="GitHub Build Status (with event)" src="https://img.shields.io/github/actions/workflow/status/kubero-dev/ladder/release-binaries.yaml"></div>
|
||||||
|
|
||||||
|
|
||||||
*Ladder is a web proxy to help bypass paywalls.* This is a selfhosted version of [1ft.io](https://1ft.io) and [12ft.io](https://12ft.io). It is inspired by [13ft](https://github.com/wasi-master/13ft).
|
*Ladder is a web proxy to help bypass paywalls.* This is a selfhosted version of [1ft.io](https://1ft.io) and [12ft.io](https://12ft.io). It is inspired by [13ft](https://github.com/wasi-master/13ft).
|
||||||
|
|
||||||
@@ -25,7 +27,7 @@ Freedom of information is an essential pillar of democracy and informed decision
|
|||||||
- [x] Linux binary
|
- [x] Linux binary
|
||||||
- [x] Mac OS binary
|
- [x] Mac OS binary
|
||||||
- [x] Windows binary (untested)
|
- [x] Windows binary (untested)
|
||||||
- [x] Removes most of the ads (unexpected side effect ¯\_(ツ)_/¯ )
|
- [x] Removes most of the ads (unexpected side effect ¯\\\_(ツ)_/¯ )
|
||||||
- [x] Basic Auth
|
- [x] Basic Auth
|
||||||
- [x] Disable logs
|
- [x] Disable logs
|
||||||
- [x] No Tracking
|
- [x] No Tracking
|
||||||
@@ -36,7 +38,7 @@ Freedom of information is an essential pillar of democracy and informed decision
|
|||||||
- [ ] Fetch from Google Cache if not available
|
- [ ] Fetch from Google Cache if not available
|
||||||
|
|
||||||
### Limitations
|
### Limitations
|
||||||
Certain sites may display missing images or encounter formatting issues. This can be attributed to the site's reliance on JavaScript or CSS for image and resource loading, which presents a limitation when accessed through this proxy. If you prefer a full experience, please concider buying a subscription for the site.
|
Certain sites may display missing images or encounter formatting issues. This can be attributed to the site's reliance on JavaScript or CSS for image and resource loading, which presents a limitation when accessed through this proxy. If you prefer a full experience, please consider buying a subscription for the site.
|
||||||
|
|
||||||
Some sites do not expose their content to search engines, which means that the proxy cannot access the content. A future version will try to fetch the content from Google Cache.
|
Some sites do not expose their content to search engines, which means that the proxy cannot access the content. A future version will try to fetch the content from Google Cache.
|
||||||
|
|
||||||
@@ -60,6 +62,9 @@ curl https://raw.githubusercontent.com/kubero-dev/ladder/main/docker-compose.yam
|
|||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Helm
|
||||||
|
See [README.md](/helm-chart/README.md) in helm-chart sub-directory for more information.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Browser
|
### Browser
|
||||||
@@ -70,6 +75,11 @@ docker-compose up -d
|
|||||||
Or direct by appending the URL to the end of the proxy URL:
|
Or direct by appending the URL to the end of the proxy URL:
|
||||||
http://localhost:8080/https://www.example.com
|
http://localhost:8080/https://www.example.com
|
||||||
|
|
||||||
|
Or create a bookmark with the following URL:
|
||||||
|
```javascript
|
||||||
|
javascript:window.location.href="http://localhost:8080/"+location.href
|
||||||
|
```
|
||||||
|
|
||||||
### API
|
### API
|
||||||
```bash
|
```bash
|
||||||
curl -X GET "http://localhost:8080/api/https://www.example.com"
|
curl -X GET "http://localhost:8080/api/https://www.example.com"
|
||||||
@@ -96,7 +106,7 @@ http://localhost:8080/ruleset
|
|||||||
| `LOG_URLS` | Log fetched URL's | `true` |
|
| `LOG_URLS` | Log fetched URL's | `true` |
|
||||||
| `DISABLE_FORM` | Disables URL Form Frontpage | `false` |
|
| `DISABLE_FORM` | Disables URL Form Frontpage | `false` |
|
||||||
| `FORM_PATH` | Path to custom Form HTML | `` |
|
| `FORM_PATH` | Path to custom Form HTML | `` |
|
||||||
| `RULESET` | URL to a ruleset file | `https://raw.githubusercontent.com/kubero-dev/ladder/main/ruleset.yaml` or `/path/to/my/rules.yaml` or `default` |
|
| `RULESET` | URL to a ruleset file | `https://raw.githubusercontent.com/kubero-dev/ladder/main/ruleset.yaml` or `/path/to/my/rules.yaml` |
|
||||||
| `EXPOSE_RULESET` | Make your Ruleset available to other ladders | `true` |
|
| `EXPOSE_RULESET` | Make your Ruleset available to other ladders | `true` |
|
||||||
| `ALLOWED_DOMAINS` | Comma separated list of allowed domains. Empty = no limitations | `` |
|
| `ALLOWED_DOMAINS` | Comma separated list of allowed domains. Empty = no limitations | `` |
|
||||||
| `ALLOWED_DOMAINS_RULESET` | Allow Domains from Ruleset. false = no limitations | `false` |
|
| `ALLOWED_DOMAINS_RULESET` | Allow Domains from Ruleset. false = no limitations | `false` |
|
||||||
@@ -110,13 +120,21 @@ It is possible to apply custom rules to modify the response. This can be used to
|
|||||||
See in [ruleset.yaml](ruleset.yaml) for an example.
|
See in [ruleset.yaml](ruleset.yaml) for an example.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
- domain: www.example.com
|
- domain: example.com # Inbcludes all subdomains
|
||||||
|
domains: # Additional domains to apply the rule
|
||||||
|
- www.example.de
|
||||||
|
- www.beispiel.de
|
||||||
|
headers:
|
||||||
|
x-forwarded-for: none # override X-Forwarded-For header or delete with none
|
||||||
|
referer: none # override Referer header or delete with none
|
||||||
|
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
|
||||||
|
cookie: privacy=1
|
||||||
regexRules:
|
regexRules:
|
||||||
- match: <script\s+([^>]*\s+)?src="(/)([^"]*)"
|
- match: <script\s+([^>]*\s+)?src="(/)([^"]*)"
|
||||||
replace: <script $1 script="/https://www.example.com/$3"
|
replace: <script $1 script="/https://www.example.com/$3"
|
||||||
injections:
|
injections:
|
||||||
- position: head # Position where to inject the code
|
- position: head # Position where to inject the code
|
||||||
append: |
|
append: | # possible keys: append, prepend, replace
|
||||||
<script>
|
<script>
|
||||||
window.localStorage.clear();
|
window.localStorage.clear();
|
||||||
console.log("test");
|
console.log("test");
|
||||||
@@ -125,7 +143,7 @@ See in [ruleset.yaml](ruleset.yaml) for an example.
|
|||||||
- domain: www.anotherdomain.com # Domain where the rule applies
|
- domain: www.anotherdomain.com # Domain where the rule applies
|
||||||
paths: # Paths where the rule applies
|
paths: # Paths where the rule applies
|
||||||
- /article
|
- /article
|
||||||
googleCache: false # Search also in Google Cache
|
googleCache: false # Use Google Cache to fetch the content
|
||||||
regexRules: # Regex rules to apply
|
regexRules: # Regex rules to apply
|
||||||
- match: <script\s+([^>]*\s+)?src="(/)([^"]*)"
|
- match: <script\s+([^>]*\s+)?src="(/)([^"]*)"
|
||||||
replace: <script $1 script="/https://www.example.com/$3"
|
replace: <script $1 script="/https://www.example.com/$3"
|
||||||
|
|||||||
22
cmd/main.go
22
cmd/main.go
@@ -7,7 +7,6 @@ import (
|
|||||||
"ladder/handlers"
|
"ladder/handlers"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/akamensky/argparse"
|
"github.com/akamensky/argparse"
|
||||||
@@ -23,37 +22,32 @@ func main() {
|
|||||||
|
|
||||||
parser := argparse.NewParser("ladder", "Every Wall needs a Ladder")
|
parser := argparse.NewParser("ladder", "Every Wall needs a Ladder")
|
||||||
|
|
||||||
p := os.Getenv("PORT")
|
portEnv := os.Getenv("PORT")
|
||||||
if os.Getenv("PORT") == "" {
|
if os.Getenv("PORT") == "" {
|
||||||
p = "8080"
|
portEnv = "8080"
|
||||||
}
|
}
|
||||||
port := parser.String("p", "port", &argparse.Options{
|
port := parser.String("p", "port", &argparse.Options{
|
||||||
Required: false,
|
Required: false,
|
||||||
Default: p,
|
Default: portEnv,
|
||||||
Help: "Port the webserver will listen on"})
|
Help: "Port the webserver will listen on"})
|
||||||
|
|
||||||
pf, _ := strconv.ParseBool(os.Getenv("PREFORK"))
|
|
||||||
prefork := parser.Flag("P", "prefork", &argparse.Options{
|
prefork := parser.Flag("P", "prefork", &argparse.Options{
|
||||||
Required: false,
|
Required: false,
|
||||||
Default: pf,
|
|
||||||
Help: "This will spawn multiple processes listening"})
|
Help: "This will spawn multiple processes listening"})
|
||||||
|
|
||||||
r := os.Getenv("RULESET")
|
|
||||||
ruleset := parser.String("r", "ruleset", &argparse.Options{
|
|
||||||
Required: false,
|
|
||||||
Default: r,
|
|
||||||
Help: "Path or URL to your ruleset"})
|
|
||||||
|
|
||||||
handlers.LoadRules(*ruleset)
|
|
||||||
|
|
||||||
err := parser.Parse(os.Args)
|
err := parser.Parse(os.Args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Print(parser.Usage(err))
|
fmt.Print(parser.Usage(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if os.Getenv("PREFORK") == "true" {
|
||||||
|
*prefork = true
|
||||||
|
}
|
||||||
|
|
||||||
app := fiber.New(
|
app := fiber.New(
|
||||||
fiber.Config{
|
fiber.Config{
|
||||||
Prefork: *prefork,
|
Prefork: *prefork,
|
||||||
|
GETOnly: true,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -3,12 +3,17 @@ services:
|
|||||||
ladder:
|
ladder:
|
||||||
image: ghcr.io/kubero-dev/ladder:latest
|
image: ghcr.io/kubero-dev/ladder:latest
|
||||||
container_name: ladder
|
container_name: ladder
|
||||||
build: .
|
#build: .
|
||||||
#restart: always
|
#restart: always
|
||||||
#command: sh -c ./ladder
|
#command: sh -c ./ladder
|
||||||
environment:
|
environment:
|
||||||
- PORT=8080
|
- PORT=8080
|
||||||
#- PREFORK=true
|
- RULESET=/app/ruleset.yaml
|
||||||
|
#- ALLOWED_DOMAINS_RULESET=false
|
||||||
|
#- EXPOSE_RULESET=true
|
||||||
|
#- PREFORK=false
|
||||||
|
#- DISABLE_FORM=fase
|
||||||
|
#- FORM_PATH=/app/form.html
|
||||||
#- X_FORWARDED_FOR=66.249.66.1
|
#- X_FORWARDED_FOR=66.249.66.1
|
||||||
#- USER_AGENT=Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
|
#- USER_AGENT=Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
|
||||||
#- USERPASS=foo:bar
|
#- USERPASS=foo:bar
|
||||||
@@ -16,11 +21,6 @@ services:
|
|||||||
#- GODEBUG=netdns=go
|
#- GODEBUG=netdns=go
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
deploy:
|
volumes:
|
||||||
resources:
|
- ./ruleset.yaml:/app/ruleset.yaml
|
||||||
limits:
|
- ./handlers/form.html:/app/form.html
|
||||||
cpus: "0.50"
|
|
||||||
memory: 512M
|
|
||||||
reservations:
|
|
||||||
cpus: "0.25"
|
|
||||||
memory: 128M
|
|
||||||
@@ -27,7 +27,8 @@ func Api(c *fiber.Ctx) error {
|
|||||||
Version: version,
|
Version: version,
|
||||||
Body: body,
|
Body: body,
|
||||||
}
|
}
|
||||||
response.Request.Headers = make([]interface{}, 0)
|
|
||||||
|
response.Request.Headers = make([]any, 0, len(req.Header))
|
||||||
for k, v := range req.Header {
|
for k, v := range req.Header {
|
||||||
response.Request.Headers = append(response.Request.Headers, map[string]string{
|
response.Request.Headers = append(response.Request.Headers, map[string]string{
|
||||||
"key": k,
|
"key": k,
|
||||||
@@ -35,7 +36,7 @@ func Api(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
response.Response.Headers = make([]interface{}, 0)
|
response.Response.Headers = make([]any, 0, len(resp.Header))
|
||||||
for k, v := range resp.Header {
|
for k, v := range resp.Header {
|
||||||
response.Response.Headers = append(response.Response.Headers, map[string]string{
|
response.Response.Headers = append(response.Response.Headers, map[string]string{
|
||||||
"key": k,
|
"key": k,
|
||||||
|
|||||||
@@ -17,11 +17,8 @@ import (
|
|||||||
|
|
||||||
var UserAgent = getenv("USER_AGENT", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")
|
var UserAgent = getenv("USER_AGENT", "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)")
|
||||||
var ForwardedFor = getenv("X_FORWARDED_FOR", "66.249.66.1")
|
var ForwardedFor = getenv("X_FORWARDED_FOR", "66.249.66.1")
|
||||||
var rulesSet RuleSet
|
var rulesSet = loadRules()
|
||||||
|
|
||||||
// var rulesSet = loadRules()
|
|
||||||
var allowedDomains = strings.Split(os.Getenv("ALLOWED_DOMAINS"), ",")
|
var allowedDomains = strings.Split(os.Getenv("ALLOWED_DOMAINS"), ",")
|
||||||
var Aaaa = "aaaa"
|
|
||||||
|
|
||||||
func ProxySite(c *fiber.Ctx) error {
|
func ProxySite(c *fiber.Ctx) error {
|
||||||
// Get the url from the URL
|
// Get the url from the URL
|
||||||
@@ -59,17 +56,49 @@ func fetchSite(urlpath string, queries map[string]string) (string, *http.Request
|
|||||||
return "", nil, nil, fmt.Errorf("domain not allowed. %s not in %s", u.Host, allowedDomains)
|
return "", nil, nil, fmt.Errorf("domain not allowed. %s not in %s", u.Host, allowedDomains)
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.Getenv("DEBUG ") == "true" {
|
if os.Getenv("LOG_URLS") == "true" {
|
||||||
log.Println(u.String() + urlQuery)
|
log.Println(u.String() + urlQuery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rule := fetchRule(u.Host, u.Path)
|
||||||
|
|
||||||
|
if rule.GoogleCache {
|
||||||
|
u, err = url.Parse("https://webcache.googleusercontent.com/search?q=cache:" + u.String())
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the site
|
// Fetch the site
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
req, _ := http.NewRequest("GET", u.String()+urlQuery, nil)
|
req, _ := http.NewRequest("GET", u.String()+urlQuery, nil)
|
||||||
req.Header.Set("User-Agent", UserAgent)
|
|
||||||
req.Header.Set("X-Forwarded-For", ForwardedFor)
|
if rule.Headers.UserAgent != "" {
|
||||||
req.Header.Set("Referer", u.String())
|
req.Header.Set("User-Agent", rule.Headers.UserAgent)
|
||||||
req.Header.Set("Host", u.Host)
|
} 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)
|
resp, err := client.Do(req)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -82,11 +111,12 @@ func fetchSite(urlpath string, queries map[string]string) (string, *http.Request
|
|||||||
return "", nil, nil, err
|
return "", nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
body := rewriteHtml(bodyB, u)
|
log.Print("rule", rule)
|
||||||
|
body := rewriteHtml(bodyB, u, rule)
|
||||||
return body, req, resp, nil
|
return body, req, resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func rewriteHtml(bodyB []byte, u *url.URL) string {
|
func rewriteHtml(bodyB []byte, u *url.URL, rule Rule) string {
|
||||||
// Rewrite the HTML
|
// Rewrite the HTML
|
||||||
body := string(bodyB)
|
body := string(bodyB)
|
||||||
|
|
||||||
@@ -107,7 +137,7 @@ func rewriteHtml(bodyB []byte, u *url.URL) string {
|
|||||||
body = strings.ReplaceAll(body, "href=\"https://"+u.Host, "href=\"/https://"+u.Host+"/")
|
body = strings.ReplaceAll(body, "href=\"https://"+u.Host, "href=\"/https://"+u.Host+"/")
|
||||||
|
|
||||||
if os.Getenv("RULESET") != "" {
|
if os.Getenv("RULESET") != "" {
|
||||||
body = applyRules(u.Host, u.Path, body)
|
body = applyRules(body, rule)
|
||||||
}
|
}
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
@@ -120,18 +150,13 @@ func getenv(key, fallback string) string {
|
|||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadRules(rulesUrl string) RuleSet {
|
func loadRules() RuleSet {
|
||||||
//rulesUrl := os.Getenv("RULESET")
|
rulesUrl := os.Getenv("RULESET")
|
||||||
if rulesUrl == "" {
|
if rulesUrl == "" {
|
||||||
RulesList := RuleSet{}
|
RulesList := RuleSet{}
|
||||||
return RulesList
|
return RulesList
|
||||||
}
|
}
|
||||||
|
log.Println("Loading rules")
|
||||||
if rulesUrl == "default" {
|
|
||||||
rulesUrl = "https://raw.githubusercontent.com/kubero-dev/ladder/main/ruleset.yaml"
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("Loading rules: " + rulesUrl)
|
|
||||||
|
|
||||||
var ruleSet RuleSet
|
var ruleSet RuleSet
|
||||||
if strings.HasPrefix(rulesUrl, "http") {
|
if strings.HasPrefix(rulesUrl, "http") {
|
||||||
@@ -163,75 +188,75 @@ func LoadRules(rulesUrl string) RuleSet {
|
|||||||
yaml.Unmarshal(yamlFile, &ruleSet)
|
yaml.Unmarshal(yamlFile, &ruleSet)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
domains := []string{}
|
||||||
for _, rule := range ruleSet {
|
for _, rule := range ruleSet {
|
||||||
//log.Println("Loaded rules for", rule.Domain)
|
|
||||||
|
domains = append(domains, rule.Domain)
|
||||||
|
domains = append(domains, rule.Domains...)
|
||||||
if os.Getenv("ALLOWED_DOMAINS_RULESET") == "true" {
|
if os.Getenv("ALLOWED_DOMAINS_RULESET") == "true" {
|
||||||
allowedDomains = append(allowedDomains, rule.Domain)
|
allowedDomains = append(allowedDomains, domains...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Println("Loaded rules for", len(ruleSet), "Domains")
|
log.Println("Loaded ", len(ruleSet), " rules for", len(domains), "Domains")
|
||||||
return ruleSet
|
return ruleSet
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyRules(domain string, path string, body string) string {
|
func fetchRule(domain string, path string) Rule {
|
||||||
|
if len(rulesSet) == 0 {
|
||||||
|
return Rule{}
|
||||||
|
}
|
||||||
|
rule := Rule{}
|
||||||
|
for _, rule := range rulesSet {
|
||||||
|
domains := rule.Domains
|
||||||
|
if rule.Domain != "" {
|
||||||
|
domains = append(domains, rule.Domain)
|
||||||
|
}
|
||||||
|
for _, ruleDomain := range domains {
|
||||||
|
if ruleDomain == domain || strings.HasSuffix(domain, ruleDomain) {
|
||||||
|
if len(rule.Paths) > 0 && !StringInSlice(path, rule.Paths) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// return first match
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rule
|
||||||
|
}
|
||||||
|
|
||||||
|
func applyRules(body string, rule Rule) string {
|
||||||
if len(rulesSet) == 0 {
|
if len(rulesSet) == 0 {
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, rule := range rulesSet {
|
for _, regexRule := range rule.RegexRules {
|
||||||
if rule.Domain != domain {
|
re := regexp.MustCompile(regexRule.Match)
|
||||||
continue
|
body = re.ReplaceAllString(body, regexRule.Replace)
|
||||||
|
}
|
||||||
|
for _, injection := range rule.Injections {
|
||||||
|
doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(rule.Paths) > 0 && !StringInSlice(path, rule.Paths) {
|
if injection.Replace != "" {
|
||||||
continue
|
doc.Find(injection.Position).ReplaceWithHtml(injection.Replace)
|
||||||
}
|
}
|
||||||
for _, regexRule := range rule.RegexRules {
|
if injection.Append != "" {
|
||||||
re := regexp.MustCompile(regexRule.Match)
|
doc.Find(injection.Position).AppendHtml(injection.Append)
|
||||||
body = re.ReplaceAllString(body, regexRule.Replace)
|
|
||||||
}
|
}
|
||||||
for _, injection := range rule.Injections {
|
if injection.Prepend != "" {
|
||||||
doc, err := goquery.NewDocumentFromReader(strings.NewReader(body))
|
doc.Find(injection.Position).PrependHtml(injection.Prepend)
|
||||||
if err != nil {
|
}
|
||||||
log.Fatal(err)
|
body, err = doc.Html()
|
||||||
}
|
if err != nil {
|
||||||
if injection.Replace != "" {
|
log.Fatal(err)
|
||||||
doc.Find(injection.Position).ReplaceWithHtml(injection.Replace)
|
|
||||||
}
|
|
||||||
if injection.Append != "" {
|
|
||||||
doc.Find(injection.Position).AppendHtml(injection.Append)
|
|
||||||
}
|
|
||||||
if injection.Prepend != "" {
|
|
||||||
doc.Find(injection.Position).PrependHtml(injection.Prepend)
|
|
||||||
}
|
|
||||||
body, err = doc.Html()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return body
|
return body
|
||||||
}
|
}
|
||||||
|
|
||||||
type Rule struct {
|
|
||||||
Match string `yaml:"match"`
|
|
||||||
Replace string `yaml:"replace"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RuleSet []struct {
|
|
||||||
Domain string `yaml:"domain"`
|
|
||||||
Paths []string `yaml:"paths,omitempty"`
|
|
||||||
GoogleCache bool `yaml:"googleCache,omitempty"`
|
|
||||||
RegexRules []Rule `yaml:"regexRules"`
|
|
||||||
Injections []struct {
|
|
||||||
Position string `yaml:"position"`
|
|
||||||
Append string `yaml:"append"`
|
|
||||||
Prepend string `yaml:"prepend"`
|
|
||||||
Replace string `yaml:"replace"`
|
|
||||||
} `yaml:"injections"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func StringInSlice(s string, list []string) bool {
|
func StringInSlice(s string, list []string) bool {
|
||||||
for _, x := range list {
|
for _, x := range list {
|
||||||
if strings.HasPrefix(s, x) {
|
if strings.HasPrefix(s, x) {
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func TestRewriteHtml(t *testing.T) {
|
|||||||
</html>
|
</html>
|
||||||
`
|
`
|
||||||
|
|
||||||
actual := rewriteHtml(bodyB, u)
|
actual := rewriteHtml(bodyB, u, Rule{})
|
||||||
assert.Equal(t, expected, actual)
|
assert.Equal(t, expected, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
28
handlers/types.go
Normal file
28
handlers/types.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
type Regex struct {
|
||||||
|
Match string `yaml:"match"`
|
||||||
|
Replace string `yaml:"replace"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleSet []Rule
|
||||||
|
|
||||||
|
type Rule struct {
|
||||||
|
Domain string `yaml:"domain,omitempty"`
|
||||||
|
Domains []string `yaml:"domains,omitempty"`
|
||||||
|
Paths []string `yaml:"paths,omitempty"`
|
||||||
|
Headers struct {
|
||||||
|
UserAgent string `yaml:"user-agent,omitempty"`
|
||||||
|
XForwardedFor string `yaml:"x-forwarded-for,omitempty"`
|
||||||
|
Referer string `yaml:"referer,omitempty"`
|
||||||
|
Cookie string `yaml:"cookie,omitempty"`
|
||||||
|
} `yaml:"headers,omitempty"`
|
||||||
|
GoogleCache bool `yaml:"googleCache,omitempty"`
|
||||||
|
RegexRules []Regex `yaml:"regexRules"`
|
||||||
|
Injections []struct {
|
||||||
|
Position string `yaml:"position"`
|
||||||
|
Append string `yaml:"append"`
|
||||||
|
Prepend string `yaml:"prepend"`
|
||||||
|
Replace string `yaml:"replace"`
|
||||||
|
} `yaml:"injections"`
|
||||||
|
}
|
||||||
6
helm-chart/Chart.yaml
Normal file
6
helm-chart/Chart.yaml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: ladder
|
||||||
|
description: A helm chart to deploy kubero-dev/ladder
|
||||||
|
type: application
|
||||||
|
version: "1.0"
|
||||||
|
appVersion: "v0.0.11"
|
||||||
27
helm-chart/README.md
Normal file
27
helm-chart/README.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Helm Chart for deployment of Ladder
|
||||||
|
This folder contains a basic helm chart deployment for the ladder app.
|
||||||
|
|
||||||
|
# Deployment pre-reqs
|
||||||
|
## Values
|
||||||
|
Edit the values to your own preferences, with the only minimum requirement being `ingress.HOST` (line 19) being updated to your intended domain name.
|
||||||
|
|
||||||
|
Other variables in `values.yaml` can be updated as to your preferences, with details on each variable being listed in the main [README.md](/README.md) in the root of this repo.
|
||||||
|
|
||||||
|
## Defaults in K8s
|
||||||
|
No ingress default has been specified.
|
||||||
|
You can set this manually by adding an annotation to the ingress.yaml - if needed.
|
||||||
|
For example, to use Traefik -
|
||||||
|
```yaml
|
||||||
|
metadata:
|
||||||
|
name: ladder-ingress
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: traefik
|
||||||
|
```
|
||||||
|
|
||||||
|
## Helm Install
|
||||||
|
`helm install <name> <location> -n <namespace-name> --create-namespace`
|
||||||
|
`helm install ladder .\ladder\ -n ladder --create-namespace`
|
||||||
|
|
||||||
|
## Helm Upgrade
|
||||||
|
`helm upgrade <name> <location> -n <namespace-name>`
|
||||||
|
`helm upgrade ladder .\ladder\ -n ladder`
|
||||||
55
helm-chart/templates/deployment.yaml
Normal file
55
helm-chart/templates/deployment.yaml
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ladder
|
||||||
|
name: ladder
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: ladder
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: ladder
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- image: "{{ .Values.image.RELEASE }}"
|
||||||
|
imagePullPolicy: Always
|
||||||
|
name: ladder
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
requests:
|
||||||
|
cpu: 250m
|
||||||
|
memory: 128Mi
|
||||||
|
env:
|
||||||
|
- name: PORT
|
||||||
|
value: "{{ .Values.env.PORT }}"
|
||||||
|
- name: PREFORK
|
||||||
|
value: "{{ .Values.env.PREFORK }}"
|
||||||
|
- name: USER_AGENT
|
||||||
|
value: "{{ .Values.env.USER_AGENT }}"
|
||||||
|
- name: X_FORWARDED_FOR
|
||||||
|
value: "{{ .Values.env.X_FORWARDED_FOR }}"
|
||||||
|
- name: USERPASS
|
||||||
|
value: "{{ .Values.env.USERPASS }}"
|
||||||
|
- name: LOG_URLS
|
||||||
|
value: "{{ .Values.env.LOG_URLS }}"
|
||||||
|
- name: DISABLE_FORM
|
||||||
|
value: "{{ .Values.env.DISABLE_FORM }}"
|
||||||
|
- name: FORM_PATH
|
||||||
|
value: "{{ .Values.env.FORM_PATH }}"
|
||||||
|
- name: RULESET
|
||||||
|
value: "{{ .Values.env.RULESET }}"
|
||||||
|
- name: EXPOSE_RULESET
|
||||||
|
value: "{{ .Values.env.EXPOSE_RULESET }}"
|
||||||
|
- name: ALLOWED_DOMAINS
|
||||||
|
value: "{{ .Values.env.ALLOWED_DOMAINS }}"
|
||||||
|
- name: ALLOWED_DOMAINS_RULESET
|
||||||
|
value: "{{ .Values.env.ALLOWED_DOMAINS_RULESET }}"
|
||||||
|
restartPolicy: Always
|
||||||
|
terminationGracePeriodSeconds: 30
|
||||||
17
helm-chart/templates/ingress.yaml
Normal file
17
helm-chart/templates/ingress.yaml
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: ladder-ingress
|
||||||
|
spec:
|
||||||
|
rules:
|
||||||
|
- host: "{{ .Values.ingress.HOST }}"
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: ladder-service
|
||||||
|
port:
|
||||||
|
number: {{ .Values.ingress.PORT }}
|
||||||
14
helm-chart/templates/service.yaml
Normal file
14
helm-chart/templates/service.yaml
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
kind: Service
|
||||||
|
apiVersion: v1
|
||||||
|
metadata:
|
||||||
|
name: ladder-service
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
selector:
|
||||||
|
app: ladder
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
port: {{ .Values.ingress.PORT }}
|
||||||
|
protocol: TCP
|
||||||
|
targetPort: {{ .Values.env.PORT }}
|
||||||
20
helm-chart/values.yaml
Normal file
20
helm-chart/values.yaml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
image:
|
||||||
|
RELEASE: ghcr.io/kubero-dev/ladder:v0.0.11
|
||||||
|
|
||||||
|
env:
|
||||||
|
PORT: 8080
|
||||||
|
PREFORK: "false"
|
||||||
|
USER_AGENT: "Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)"
|
||||||
|
X_FORWARDED_FOR:
|
||||||
|
USERPASS: ""
|
||||||
|
LOG_URLS: "true"
|
||||||
|
DISABLE_FORM: "false"
|
||||||
|
FORM_PATH: ""
|
||||||
|
RULESET: "https://raw.githubusercontent.com/kubero-dev/ladder/main/ruleset.yaml"
|
||||||
|
EXPOSE_RULESET: "true"
|
||||||
|
ALLOWED_DOMAINS: ""
|
||||||
|
ALLOWED_DOMAINS_RULESET: "false"
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
HOST: "ladder.domain.com"
|
||||||
|
PORT: 80
|
||||||
111
ruleset.yaml
111
ruleset.yaml
@@ -1,4 +1,12 @@
|
|||||||
- domain: www.example.com
|
- domain: example.com
|
||||||
|
domains:
|
||||||
|
- www.beispiel.de
|
||||||
|
googleCache: true
|
||||||
|
headers:
|
||||||
|
x-forwarded-for: none
|
||||||
|
referer: none
|
||||||
|
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
|
||||||
|
cookie: privacy=1
|
||||||
regexRules:
|
regexRules:
|
||||||
- match: <script\s+([^>]*\s+)?src="(/)([^"]*)"
|
- match: <script\s+([^>]*\s+)?src="(/)([^"]*)"
|
||||||
replace: <script $1 script="/https://www.example.com/$3"
|
replace: <script $1 script="/https://www.example.com/$3"
|
||||||
@@ -53,3 +61,104 @@
|
|||||||
removeDOMElement(paywall)
|
removeDOMElement(paywall)
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
- domains:
|
||||||
|
- www.architecturaldigest.com
|
||||||
|
- www.bonappetit.com
|
||||||
|
- www.cntraveler.com
|
||||||
|
- www.epicurious.com
|
||||||
|
- www.gq.com
|
||||||
|
- www.newyorker.com
|
||||||
|
- www.vanityfair.com
|
||||||
|
- www.vogue.com
|
||||||
|
- www.wired.com
|
||||||
|
injections:
|
||||||
|
- position: head
|
||||||
|
append: |
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const banners = document.querySelectorAll('.paywall-bar, div[class^="MessageBannerWrapper-"');
|
||||||
|
banners.forEach(el => { el.remove(); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
- domains:
|
||||||
|
- www.nytimes.com
|
||||||
|
- www.time.com
|
||||||
|
headers:
|
||||||
|
ueser-agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)
|
||||||
|
cookie: nyt-a=; nyt-gdpr=0; nyt-geo=DE; nyt-privacy=1
|
||||||
|
referer: https://www.google.com/
|
||||||
|
injections:
|
||||||
|
- position: head
|
||||||
|
append: |
|
||||||
|
<script>
|
||||||
|
window.localStorage.clear();
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const banners = document.querySelectorAll('div[data-testid="inline-message"], div[id^="ad-"], div[id^="leaderboard-"], div.expanded-dock, div.pz-ad-box, div[id="top-wrapper"], div[id="bottom-wrapper"]');
|
||||||
|
banners.forEach(el => { el.remove(); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
- domains:
|
||||||
|
- www.thestar.com
|
||||||
|
- www.niagarafallsreview.ca
|
||||||
|
- www.stcatharinesstandard.ca
|
||||||
|
- www.thepeterboroughexaminer.com
|
||||||
|
- www.therecord.com
|
||||||
|
- www.thespec.com
|
||||||
|
- www.wellandtribune.ca
|
||||||
|
injections:
|
||||||
|
- position: head
|
||||||
|
append: |
|
||||||
|
<script>
|
||||||
|
window.localStorage.clear();
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const paywall = document.querySelectorAll('div.subscriber-offers');
|
||||||
|
paywall.forEach(el => { el.remove(); });
|
||||||
|
const subscriber_only = document.querySelectorAll('div.subscriber-only');
|
||||||
|
for (const elem of subscriber_only) {
|
||||||
|
if (elem.classList.contains('encrypted-content') && dompurify_loaded) {
|
||||||
|
const parser = new DOMParser();
|
||||||
|
const doc = parser.parseFromString('<div>' + DOMPurify.sanitize(unscramble(elem.innerText)) + '</div>', 'text/html');
|
||||||
|
const content_new = doc.querySelector('div');
|
||||||
|
elem.parentNode.replaceChild(content_new, elem);
|
||||||
|
}
|
||||||
|
elem.removeAttribute('style');
|
||||||
|
elem.removeAttribute('class');
|
||||||
|
}
|
||||||
|
const banners = document.querySelectorAll('div.subscription-required, div.redacted-overlay, div.subscriber-hide, div.tnt-ads-container');
|
||||||
|
banners.forEach(el => { el.remove(); });
|
||||||
|
const ads = document.querySelectorAll('div.tnt-ads-container, div[class*="adLabelWrapper"]');
|
||||||
|
ads.forEach(el => { el.remove(); });
|
||||||
|
const recommendations = document.querySelectorAll('div[id^="tncms-region-article"]');
|
||||||
|
recommendations.forEach(el => { el.remove(); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
- domain: www.usatoday.com
|
||||||
|
injections:
|
||||||
|
- position: head
|
||||||
|
append: |
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
const banners = document.querySelectorAll('div.roadblock-container, .gnt_nb, [aria-label="advertisement"], div[id="main-frame-error"]');
|
||||||
|
banners.forEach(el => { el.remove(); });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
- domain: www.washingtonpost.com
|
||||||
|
injections:
|
||||||
|
- position: head
|
||||||
|
append: |
|
||||||
|
<script>
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
let paywall = document.querySelectorAll('div[data-qa$="-ad"], div[id="leaderboard-wrapper"], div[data-qa="subscribe-promo"]');
|
||||||
|
paywall.forEach(el => { el.remove(); });
|
||||||
|
const images = document.querySelectorAll('img');
|
||||||
|
images.forEach(image => { image.parentElement.style.filter = ''; });
|
||||||
|
const headimage = document.querySelectorAll('div .aspect-custom');
|
||||||
|
headimage.forEach(image => { image.style.filter = ''; });
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
- domain: medium.com
|
||||||
|
headers:
|
||||||
|
referer: https://t.co/x?amp=1
|
||||||
|
x-forwarded-for: none
|
||||||
|
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36
|
||||||
|
cookie:
|
||||||
Reference in New Issue
Block a user