34 Commits

Author SHA1 Message Date
Gianni Carafa
5392992350 WIP: write version on build to form HTML 2023-11-15 18:02:58 +01:00
mms-gianni
7be62e2735 Update README.md 2023-11-15 14:55:21 +01:00
mms-gianni
5e76ff0879 Update README.md
Fix typo
2023-11-15 13:36:56 +01:00
Github action
ee641bf8f6 Generated stylesheet 2023-11-15 09:20:45 +00:00
Gianni Carafa
2fb089ea28 initial style.css 2023-11-15 10:20:05 +01:00
Gianni Carafa
9f857eca8b add permission to push to repository 2023-11-15 10:16:33 +01:00
Gianni Carafa
0673255fc8 remove styles.css from gitignore to allow github action to detect changes 2023-11-15 10:06:49 +01:00
Gianni Carafa
4dbc103cf7 generate css 2023-11-15 09:53:42 +01:00
mms-gianni
514facd2c0 Merge pull request #36 from joncrangle/tailwind-cli-build
improvement: Use Tailwind CLI to build stylesheet instead of using Play CDN
2023-11-15 09:32:28 +01:00
Gianni Carafa
531b7da811 remove useless route 2023-11-14 22:27:34 +01:00
joncrangle
9a53f28b3f Update dev instructions 2023-11-14 11:52:19 -05:00
joncrangle
6cbccbfadb Update with development instructions 2023-11-14 10:46:13 -05:00
Gianni Carafa
b07d49f230 update README 2023-11-14 16:29:43 +01:00
mms-gianni
af10efb7f2 Merge pull request #31 from deoxykev/main
Add feature to modify URLs in ruleset | Fix Relative URLs
2023-11-14 16:09:07 +01:00
joncrangle
3f0f4207a1 Undo errant removal of meta viewport tag 2023-11-14 09:01:58 -05:00
joncrangle
2236c4fff9 Add embed declaration 2023-11-13 21:46:20 -05:00
joncrangle
78454f8713 improvement: tailwind cli to build stylesheet 2023-11-13 21:44:11 -05:00
mms-gianni
6bff28e18d Merge pull request #29 from joncrangle/add-clear-button
Add x button to clear search
2023-11-13 22:57:08 +01:00
Kevin Pham
cdd429e4be Merge branch 'main' into main 2023-11-13 07:50:54 -06:00
mms-gianni
11ee581fd4 Merge pull request #33 from joncrangle/mobile-improvement-and-spelling
Mobile improvement and spelling fixes
2023-11-13 09:13:48 +01:00
joncrangle
4e44a24261 Remove materialize function 2023-11-12 21:53:24 -05:00
joncrangle
0ddb029aae Fix spelling 2023-11-12 21:44:50 -05:00
joncrangle
a262afe035 Improvements for mobile 2023-11-12 21:42:53 -05:00
Kevin Pham
fdca9d39d9 Merge remote-tracking branch 'refs/remotes/origin/main' 2023-11-12 17:03:19 -06:00
Kevin Pham
30a6ab501d handle URL encoded URLs in proxy for other app integrations 2023-11-12 17:02:32 -06:00
Kevin Pham
7a51243ff4 Merge branch 'main' into main 2023-11-12 11:58:46 -06:00
Kevin Pham
c8b94dc702 remove debug logging messages 2023-11-12 11:52:43 -06:00
Kevin Pham
fbc9567820 Handle relative URLs when using proxy 2023-11-12 11:48:47 -06:00
Gianni C
4d5c25c148 Merge pull request #28 from joncrangle/ft.com
Add Ft.com
2023-11-12 18:11:47 +01:00
Kevin Pham
082868af2d Add feature to modify URLs in ruleset 2023-11-12 10:30:06 -06:00
joncrangle
a4abce78fb Add button to clear search 2023-11-12 00:21:46 -05:00
joncrangle
190de6d9c5 Remove article signup iframe 2023-11-11 20:19:07 -05:00
joncrangle
bdd19dcbb6 Remove console log 2023-11-11 20:02:00 -05:00
joncrangle
02e6b1c090 Add ft.com 2023-11-11 20:01:29 -05:00
14 changed files with 281 additions and 29 deletions

42
.github/workflows/build-css.yaml vendored Normal file
View File

@@ -0,0 +1,42 @@
name: Build Tailwind CSS
on:
push:
paths:
- "handlers/form.html"
workflow_dispatch:
jobs:
tailwindbuilder:
permissions:
# Give the default GITHUB_TOKEN write permission to commit and push the
# added or changed files to the repository.
contents: write
runs-on: ubuntu-latest
steps:
-
name: Checkout
uses: actions/checkout@v3
-
name: Install pnpm
uses: pnpm/action-setup@v2
with:
version: 8
-
name: Build Tailwind CSS
run: pnpm build
-
name: Commit generated stylesheet
run: |
if git diff --quiet cmd/styles.css; then
echo "No changes to commit."
exit 0
else
echo "Changes detected, committing..."
git config --global user.name "Github action"
git config --global user.email "username@users.noreply.github.com"
git add cmd
git commit -m "Generated stylesheet"
git push
fi

View File

@@ -22,7 +22,11 @@ jobs:
- -
name: Set version name: Set version
run: | run: |
echo -n $(git describe --tags --abbrev=0) > handlers/VERSION VERSION=$(git describe --tags --abbrev=0)
echo -n $VERSION > handlers/VERSION
sed -i 's\VERSION\${VERSION}\g' handlers/form.html
echo handlers/form.html >> .gitignore
echo .gitignore >> .gitignore
- -
name: Set up Go name: Set up Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3

View File

@@ -42,7 +42,11 @@ jobs:
- name: Set version - name: Set version
id: version id: version
run: | run: |
echo ${GITHUB_REF#refs/tags/v} > handlers/VERSION VERSION=$(git describe --tags --abbrev=0)
echo -n $VERSION > handlers/VERSION
sed -i 's\VERSION\${VERSION}\g' handlers/form.html
echo handlers/form.html >> .gitignore
echo .gitignore >> .gitignore
# Install the cosign tool except on PR # Install the cosign tool except on PR
# https://github.com/sigstore/cosign-installer # https://github.com/sigstore/cosign-installer

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@
ladder ladder
VERSION VERSION
output.css

View File

@@ -17,7 +17,7 @@ Freedom of information is an essential pillar of democracy and informed decision
### Features ### Features
- [x] Bypass Paywalls - [x] Bypass Paywalls
- [x] Remove CORS headers from responses, assets, and images ... - [x] Remove CORS headers from responses, assets, and images ...
- [x] Apply domain based ruleset/code to modify response - [x] Apply domain based ruleset/code to modify response / requested URL
- [x] Keep site browsable - [x] Keep site browsable
- [x] API - [x] API
- [x] Fetch RAW HTML - [x] Fetch RAW HTML
@@ -38,10 +38,10 @@ Freedom of information is an essential pillar of democracy and informed decision
- [ ] A key to share only one URL - [ ] A key to share only one URL
### 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 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.
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.
## Installation ## Installation
> **Warning:** If your instance will be publicly accessible, make sure to enable Basic Auth. This will prevent unauthorized users from using your proxy. If you do not enable Basic Auth, anyone can use your proxy to browse nasty/illegal stuff. And you will be responsible for it. > **Warning:** If your instance will be publicly accessible, make sure to enable Basic Auth. This will prevent unauthorized users from using your proxy. If you do not enable Basic Auth, anyone can use your proxy to browse nasty/illegal stuff. And you will be responsible for it.
@@ -115,12 +115,12 @@ http://localhost:8080/ruleset
### Ruleset ### Ruleset
It is possible to apply custom rules to modify the response. This can be used to remove unwanted or modify elements from the page. The ruleset is a YAML file that contains a list of rules for each domain and is loaded on startup It is possible to apply custom rules to modify the response or the requested URL. This can be used to remove unwanted or modify elements from the page. The ruleset is a YAML file that contains a list of rules for each domain and is loaded on startup
See in [ruleset.yaml](ruleset.yaml) for an example. See in [ruleset.yaml](ruleset.yaml) for an example.
```yaml ```yaml
- domain: example.com # Inbcludes all subdomains - domain: example.com # Includes all subdomains
domains: # Additional domains to apply the rule domains: # Additional domains to apply the rule
- www.example.de - www.example.de
- www.beispiel.de - www.beispiel.de
@@ -154,5 +154,30 @@ See in [ruleset.yaml](ruleset.yaml) for an example.
<h1>My Custom Title</h1> <h1>My Custom Title</h1>
- position: .left-content article # Position where to inject the code into DOM - position: .left-content article # Position where to inject the code into DOM
prepend: | prepend: |
<h2>Suptitle</h2> <h2>Subtitle</h2>
- domain: demo.com
headers:
content-security-policy: script-src 'self';
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
urlMods: # Modify the URL
query:
- key: amp # (this will append ?amp=1 to the URL)
value: 1
domain:
- match: www # regex to match part of domain
replace: amp # (this would modify the domain from www.demo.de to amp.demo.de)
path:
- match: ^ # regex to match part of path
replace: /amp/ # (modify the url from https://www.demo.com/article/ to https://www.demo.de/amp/article/)
``` ```
## Development
To run a development server at http://localhost:8080:
```bash
echo "DEV" > handler/VERSION
RULESET="./ruleset.yaml" go run cmd/main.go
```
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.

View File

@@ -1,7 +1,7 @@
package main package main
import ( import (
_ "embed" "embed"
"fmt" "fmt"
"log" "log"
"os" "os"
@@ -17,6 +17,8 @@ import (
//go:embed favicon.ico //go:embed favicon.ico
var faviconData string var faviconData string
//go:embed styles.css
var cssData embed.FS
func main() { func main() {
parser := argparse.NewParser("ladder", "Every Wall needs a Ladder") parser := argparse.NewParser("ladder", "Every Wall needs a Ladder")
@@ -75,11 +77,18 @@ func main() {
} }
app.Get("/", handlers.Form) app.Get("/", handlers.Form)
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("ruleset", handlers.Ruleset)
app.Get("raw/*", handlers.Raw) app.Get("raw/*", handlers.Raw)
app.Get("api/*", handlers.Api) app.Get("api/*", handlers.Api)
app.Get("ruleset", handlers.Raw)
app.Get("/*", handlers.ProxySite) app.Get("/*", handlers.ProxySite)
log.Fatal(app.Listen(":" + *port)) log.Fatal(app.Listen(":" + *port))

1
cmd/styles.css Normal file

File diff suppressed because one or more lines are too long

View File

@@ -3,8 +3,9 @@
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ladder</title> <title>ladder</title>
<script src="https://cdn.tailwindcss.com"></script> <link rel="stylesheet" href="/styles.css">
</head> </head>
<body class="antialiased text-slate-500 dark:text-slate-400 bg-white dark:bg-slate-900"> <body class="antialiased text-slate-500 dark:text-slate-400 bg-white dark:bg-slate-900">
@@ -16,24 +17,24 @@
<header> <header>
<h1 class="text-center text-3xl sm:text-4xl font-extrabold text-slate-900 tracking-tight dark:text-slate-200">ladddddddder</h1> <h1 class="text-center text-3xl sm:text-4xl font-extrabold text-slate-900 tracking-tight dark:text-slate-200">ladddddddder</h1>
</header> </header>
<form id="inputForm" method="get" class="mx-4"> <form id="inputForm" method="get" class="mx-4 relative">
<div> <div>
<input type="text" id="inputField" placeholder="Proxy Search" name="inputField" class="w-full text-sm leading-6 text-slate-400 rounded-md ring-1 ring-slate-900/10 shadow-sm py-1.5 pl-2 pr-3 hover:ring-slate-300 dark:bg-slate-800 dark:highlight-white/5 dark:hover:bg-slate-700" required autofocus> <input type="text" id="inputField" placeholder="Proxy Search" name="inputField" class="w-full text-sm leading-6 text-slate-400 rounded-md ring-1 ring-slate-900/10 shadow-sm py-1.5 pl-2 pr-3 hover:ring-slate-300 dark:bg-slate-800 dark:highlight-white/5 dark:hover:bg-slate-700" required autofocus>
<button id="clearButton" type="button" aria-label="Clear Search" title="Clear Search" class="hidden absolute inset-y-0 right-0 items-center pr-2 hover:text-slate-400 hover:dark:text-slate-300" tabindex="-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round""><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
</button>
</div> </div>
</form> </form>
<footer class="mt-10 text-center text-slate-600 dark:text-slate-400"> <footer class="mt-10 mx-4 text-center text-slate-600 dark:text-slate-400">
<p> <p>
Code Licensed Under GPL v3.0 | Code Licensed Under GPL v3.0 |
<a href="https://github.com/everywall/ladder" class="hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300">View Source</a> | <a href="https://github.com/everywall/ladder" class="hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300">Source</a> |
<a href="https://github.com/everywall" class="hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300">Everywall</a> <a href="https://github.com/everywall/ladder/releases" class="hover:text-blue-500 hover:underline underline-offset-2 transition-colors duration-300">VERSION</a>
</p> </p>
</footer> </footer>
</div> </div>
<script> <script>
document.addEventListener('DOMContentLoaded', function () {
M.AutoInit();
});
document.getElementById('inputForm').addEventListener('submit', function (e) { document.getElementById('inputForm').addEventListener('submit', function (e) {
e.preventDefault(); e.preventDefault();
let url = document.getElementById('inputField').value; let url = document.getElementById('inputField').value;
@@ -43,6 +44,19 @@
window.location.href = '/' + url; window.location.href = '/' + url;
return false; return false;
}); });
document.getElementById('inputField').addEventListener('input', function() {
const clearButton = document.getElementById('clearButton');
if (this.value.trim().length > 0) {
clearButton.style.display = 'block';
} else {
clearButton.style.display = 'none';
}
});
document.getElementById('clearButton').addEventListener('click', function() {
document.getElementById('inputField').value = '';
this.style.display = 'none';
document.getElementById('inputField').focus();
});
</script> </script>
<style> <style>

View File

@@ -22,9 +22,65 @@ var (
allowedDomains = strings.Split(os.Getenv("ALLOWED_DOMAINS"), ",") allowedDomains = strings.Split(os.Getenv("ALLOWED_DOMAINS"), ",")
) )
// extracts a URL from the request ctx. If the URL in the request
// is a relative path, it reconstructs the full URL using the referer header.
func extractUrl(c *fiber.Ctx) (string, error) {
// try to extract url-encoded
reqUrl, err := url.QueryUnescape(c.Params("*"))
if err != nil {
// fallback
reqUrl = c.Params("*")
}
// Extract the actual path from req ctx
urlQuery, err := url.Parse(reqUrl)
if err != nil {
return "", fmt.Errorf("error parsing request URL '%s': %v", reqUrl, err)
}
isRelativePath := urlQuery.Scheme == ""
// eg: https://localhost:8080/images/foobar.jpg -> https://realsite.com/images/foobar.jpg
if isRelativePath {
// Parse the referer URL from the request header.
refererUrl, err := url.Parse(c.Get("referer"))
if err != nil {
return "", fmt.Errorf("error parsing referer URL from req: '%s': %v", reqUrl, err)
}
// Extract the real url from referer path
realUrl, err := url.Parse(strings.TrimPrefix(refererUrl.Path, "/"))
if err != nil {
return "", fmt.Errorf("error parsing real URL from referer '%s': %v", refererUrl.Path, err)
}
// reconstruct the full URL using the referer's scheme, host, and the relative path / queries
fullUrl := &url.URL{
Scheme: realUrl.Scheme,
Host: realUrl.Host,
Path: urlQuery.Path,
RawQuery: urlQuery.RawQuery,
}
if os.Getenv("LOG_URLS") == "true" {
log.Printf("modified relative URL: '%s' -> '%s'", reqUrl, fullUrl.String())
}
return fullUrl.String(), nil
}
// default behavior:
// eg: https://localhost:8080/https://realsite.com/images/foobar.jpg -> https://realsite.com/images/foobar.jpg
return urlQuery.String(), nil
}
func ProxySite(c *fiber.Ctx) error { func ProxySite(c *fiber.Ctx) error {
// Get the url from the URL // Get the url from the URL
url := c.Params("*") url, err := extractUrl(c)
if err != nil {
log.Println("ERROR In URL extraction:", err)
}
queries := c.Queries() queries := c.Queries()
body, _, resp, err := fetchSite(url, queries) body, _, resp, err := fetchSite(url, queries)
@@ -40,6 +96,42 @@ func ProxySite(c *fiber.Ctx) error {
return c.SendString(body) return c.SendString(body)
} }
func modifyURL(uri string, rule 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) { func fetchSite(urlpath string, queries map[string]string) (string, *http.Request, *http.Response, error) {
urlQuery := "?" urlQuery := "?"
if len(queries) > 0 { if len(queries) > 0 {
@@ -63,18 +155,16 @@ func fetchSite(urlpath string, queries map[string]string) (string, *http.Request
log.Println(u.String() + urlQuery) log.Println(u.String() + urlQuery)
} }
// Modify the URI according to ruleset
rule := fetchRule(u.Host, u.Path) rule := fetchRule(u.Host, u.Path)
url, err := modifyURL(u.String()+urlQuery, rule)
if rule.GoogleCache {
u, err = url.Parse("https://webcache.googleusercontent.com/search?q=cache:" + u.String())
if err != nil { if err != nil {
return "", nil, nil, err 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", url, nil)
if rule.Headers.UserAgent != "" { if rule.Headers.UserAgent != "" {
req.Header.Set("User-Agent", rule.Headers.UserAgent) req.Header.Set("User-Agent", rule.Headers.UserAgent)
@@ -114,6 +204,7 @@ func fetchSite(urlpath string, queries map[string]string) (string, *http.Request
} }
if rule.Headers.CSP != "" { if rule.Headers.CSP != "" {
log.Println(rule.Headers.CSP)
resp.Header.Set("Content-Security-Policy", rule.Headers.CSP) resp.Header.Set("Content-Security-Policy", rule.Headers.CSP)
} }

View File

@@ -4,6 +4,10 @@ type Regex struct {
Match string `yaml:"match"` Match string `yaml:"match"`
Replace string `yaml:"replace"` Replace string `yaml:"replace"`
} }
type KV struct {
Key string `yaml:"key"`
Value string `yaml:"value"`
}
type RuleSet []Rule type RuleSet []Rule
@@ -20,6 +24,13 @@ type Rule struct {
} `yaml:"headers,omitempty"` } `yaml:"headers,omitempty"`
GoogleCache bool `yaml:"googleCache,omitempty"` GoogleCache bool `yaml:"googleCache,omitempty"`
RegexRules []Regex `yaml:"regexRules"` RegexRules []Regex `yaml:"regexRules"`
UrlMods struct {
Domain []Regex `yaml:"domain"`
Path []Regex `yaml:"path"`
Query []KV `yaml:"query"`
} `yaml:"urlMods"`
Injections []struct { Injections []struct {
Position string `yaml:"position"` Position string `yaml:"position"`
Append string `yaml:"append"` Append string `yaml:"append"`

9
package.json Normal file
View File

@@ -0,0 +1,9 @@
{
"scripts": {
"build": "pnpx tailwindcss -i ./styles/input.css -o ./styles/output.css --build && pnpx minify ./styles/output.css > ./cmd/styles.css"
},
"devDependencies": {
"minify": "^10.5.2",
"tailwindcss": "^3.3.5"
}
}

View File

@@ -163,3 +163,32 @@
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 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
content-security-policy: script-src 'self'; content-security-policy: script-src 'self';
cookie: cookie:
- domain: tagesspiegel.de
headers:
content-security-policy: script-src 'self';
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
urlMods:
query:
- key: amp
value: 1
- domain: www.ft.com
headers:
referer: https://t.co/x?amp=1
injections:
- position: head
append: |
<script>
document.addEventListener("DOMContentLoaded", () => {
const styleTags = document.querySelectorAll('link[rel="stylesheet"]');
styleTags.forEach(el => {
const href = el.getAttribute('href').substring(1);
const updatedHref = href.replace(/(https?:\/\/.+?)\/{2,}/, '$1/');
el.setAttribute('href', updatedHref);
});
setTimeout(() => {
const cookie = document.querySelectorAll('.o-cookie-message, .js-article-ribbon, .o-ads, .o-banner, .o-message, .article__content-sign-up');
cookie.forEach(el => { el.remove(); });
}, 1000);
})
</script>

3
styles/input.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

9
tailwind.config.js Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./handlers/**/*.html"],
theme: {
extend: {},
},
plugins: [],
}