diff --git a/.env.example b/.env.example
index 1f2fe5f..b836b09 100644
--- a/.env.example
+++ b/.env.example
@@ -1,14 +1,20 @@
-HOST=127.0.0.1 # 0.0.0.0 if txtdot is not behind reverse proxy
+# Server settings
+
+HOST=0.0.0.0
PORT=8080
+TIMEOUT=0 # 0 means no timeout
+REVERSE_PROXY=false # only with reverse proxy; see docs
-TIMEOUT=0 # Connection timout 0 (no timout)
-
-REVERSE_PROXY=true # only for reverse proxy; see docs
+# Features
+## Proxy
PROXY_RES=true
-SWAGGER=false # whether to add API docs route or not
-# Search
+IMG_COMPRESS=true # enable image compressing; proxy_res is required
-SEARCH_ENABLED=false # If enabled then you need >
-SEARX_URL="" # Base url of searxng instance without /search, etc
+## Documentation
+SWAGGER=false # whether to add API docs route
+
+## Search
+SEARCH_ENABLED=false # searx_url is required when enabled
+SEARX_URL="" # SearXNG base URL, e.g. https://searx.dc09.ru
\ No newline at end of file
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
new file mode 100644
index 0000000..1812c56
--- /dev/null
+++ b/.github/workflows/build.yml
@@ -0,0 +1,19 @@
+name: build-check
+on: pull_request
+jobs:
+ build-check:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v2
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v2
+ with:
+ node-version: '14'
+
+ - name: Install dependencies
+ run: npm install
+
+ - name: Start build
+ run: npm run build
diff --git a/.github/workflows/format-check.yml b/.github/workflows/format-check.yml
index 6ea15f5..1653278 100644
--- a/.github/workflows/format-check.yml
+++ b/.github/workflows/format-check.yml
@@ -1,7 +1,7 @@
name: format-check
on: pull_request
jobs:
- check-formatting:
+ format-check:
runs-on: ubuntu-latest
steps:
- name: Checkout code
diff --git a/README.md b/README.md
index b2fd2a5..d78f3f2 100644
--- a/README.md
+++ b/README.md
@@ -11,28 +11,32 @@
HTTP proxy that parses only text, links and pictures from pages
reducing internet traffic, removing ads and heavy scripts.
+Mozilla's Readability library is used under the hood.
-Uses [Mozilla's readability.js](https://github.com/mozilla/readability),
-[🔗 linkedom](https://github.com/WebReflection/linkedom),
-[Fastify web framework](https://github.com/fastify/fastify).
+## Features
-## Installation
-
-```bash
-npm install
-```
+- Server-side page simplification
+- Media proxy
+- Image compression with Sharp
+- Search with SearXNG
+- Custom parsers for StackOverflow and SearXNG
+- Handy API endpoints
+- No client JavaScript
+- Some kind of Material Design 3
## Running
-### Dev
+### Development
```bash
+npm install
npm run dev
```
-### Prod
+### Production
```bash
+npm install
npm run build
npm run start
```
@@ -40,6 +44,63 @@ npm run start
### Docker
```bash
-docker compose build
docker compose up -d
```
+
+## Screenshots
+
+
+

+

+
+
+## Performance tests
+
+txtdot is a great tool in case of slow internet connection or weak signal.
+Here is the comparision of performance metrics from pagespeed.web.dev
+between original page and proxied one.
+
+"Mobile" test includes "Slow 4G" artificial network throttling.
+
+
+Expand
+
+| | Original page | Proxied through txtdot |
+| :------------------------------- | :-------------------: | :--------------------: |
+| [Habr][habr-link] Desktop | ![56%][habr-do-img] | ![99%][habr-dt-img] |
+| [Habr][habr-link] Mobile | ![21%][habr-mo-img] | ![100%][habr-mt-img] |
+| [Medium][medium-link] Desktop | ![44%][medium-do-img] | ![100%][medium-dt-img] |
+| [Medium][medium-link] Mobile | ![36%][medium-mo-img] | ![100%][medium-mt-img] |
+| [Nginx Blog][nginx-link] Desktop | ![53%][nginx-do-img] | ![100%][nginx-dt-img] |
+| [Nginx Blog][nginx-link] Mobile | ![26%][nginx-mo-img] | ![100%][nginx-mt-img] |
+
+[habr-link]: https://habr.com/ru/articles/780692/
+[habr-do-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/habr/desktop_orig.png
+[habr-dt-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/habr/desktop_txtdot.png
+[habr-mo-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/habr/mobile_orig.png
+[habr-mt-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/habr/mobile_txtdot.png
+[medium-link]: https://levelup.gitconnected.com/proxy-servers-how-proxies-work-0ec083fc1030
+[medium-do-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/medium/desktop_orig.png
+[medium-dt-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/medium/desktop_txtdot.png
+[medium-mo-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/medium/mobile_orig.png
+[medium-mt-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/medium/mobile_txtdot.png
+[nginx-link]: https://www.nginx.com/blog/rate-limiting-nginx/
+[nginx-do-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/nginx-blog/desktop_orig.png
+[nginx-dt-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/nginx-blog/desktop_txtdot.png
+[nginx-mo-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/nginx-blog/mobile_orig.png
+[nginx-mt-img]: https://raw.githubusercontent.com/TxtDot/.github/main/tests/nginx-blog/mobile_txtdot.png
+
+
+
+## Credits
+
+- [Readability.js](https://github.com/mozilla/readability)
+- [🔗 LinkeDOM](https://github.com/WebReflection/linkedom)
+- [Fastify web framework](https://github.com/fastify/fastify)
+- [EJS](https://github.com/mde/ejs)
+- [Axios](https://github.com/axios/axios)
+- [DOMPurify](https://github.com/cure53/DOMPurify)
+- [Sharp](https://github.com/lovell/sharp)
+- [MicroMatch](https://github.com/micromatch/micromatch)
+- [RouteParser](https://github.com/rcs/route-parser)
+- [IconvLite](https://github.com/ashtuchkin/iconv-lite)
diff --git a/package-lock.json b/package-lock.json
index b32cc33..040e322 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "txtdot",
- "version": "1.6.1",
+ "version": "1.7.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "txtdot",
- "version": "1.6.1",
+ "version": "1.7.0",
"license": "MIT",
"dependencies": {
"@fastify/static": "^6.12.0",
@@ -24,14 +24,15 @@
"json-schema-to-ts": "^3.0.0",
"linkedom": "^0.16.8",
"micromatch": "^4.0.5",
- "route-parser": "^0.0.5"
+ "route-parser": "^0.0.5",
+ "sharp": "^0.33.2"
},
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/ejs": "^3.1.5",
"@types/jsdom": "^21.1.6",
"@types/micromatch": "^4.0.6",
- "@types/node": "^20.11.20",
+ "@types/node": "^20.11.24",
"@types/route-parser": "^0.1.7",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
@@ -63,6 +64,15 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@emnapi/runtime": {
+ "version": "0.45.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-0.45.0.tgz",
+ "integrity": "sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@@ -313,6 +323,437 @@
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true
},
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.2.tgz",
+ "integrity": "sha512-itHBs1rPmsmGF9p4qRe++CzCgd+kFYktnsoR1sbIAfsRMrJZau0Tt1AH9KVnufc2/tU02Gf6Ibujx+15qRE03w==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.2.tgz",
+ "integrity": "sha512-/rK/69Rrp9x5kaWBjVN07KixZanRr+W1OiyKdXcbjQD6KbW+obaTeBBtLUAtbBsnlTTmWthw99xqoOS7SsySDg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.1.tgz",
+ "integrity": "sha512-kQyrSNd6lmBV7O0BUiyu/OEw9yeNGFbQhbxswS1i6rMDwBBSX+e+rPzu3S+MwAiGU3HdLze3PanQ4Xkfemgzcw==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "macos": ">=11",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.1.tgz",
+ "integrity": "sha512-eVU/JYLPVjhhrd8Tk6gosl5pVlvsqiFlt50wotCvdkFGf+mDNBJxMh+bvav+Wt3EBnNZWq8Sp2I7XfSjm8siog==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "macos": ">=10.13",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.1.tgz",
+ "integrity": "sha512-FtdMvR4R99FTsD53IA3LxYGghQ82t3yt0ZQ93WMZ2xV3dqrb0E8zq4VHaTOuLEAuA83oDawHV3fd+BsAPadHIQ==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.1.tgz",
+ "integrity": "sha512-bnGG+MJjdX70mAQcSLxgeJco11G+MxTz+ebxlz8Y3dxyeb3Nkl7LgLI0mXupoO+u1wRNx/iRj5yHtzA4sde1yA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.1.tgz",
+ "integrity": "sha512-3+rzfAR1YpMOeA2zZNp+aYEzGNWK4zF3+sdMxuCS3ey9HhDbJ66w6hDSHDMoap32DueFwhhs3vwooAB2MaK4XQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.1.tgz",
+ "integrity": "sha512-3NR1mxFsaSgMMzz1bAnnKbSAI+lHXVTqAHgc1bgzjHuXjo4hlscpUxc0vFSAPKI3yuzdzcZOkq7nDPrP2F8Jgw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.1.tgz",
+ "integrity": "sha512-5aBRcjHDG/T6jwC3Edl3lP8nl9U2Yo8+oTl5drd1dh9Z1EBfzUKAJFUDTDisDjUwc7N4AjnPGfCA3jl3hY8uDg==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.1.tgz",
+ "integrity": "sha512-dcT7inI9DBFK6ovfeWRe3hG30h51cBAP5JXlZfx6pzc/Mnf9HFCQDLtYf4MCBjxaaTfjCCjkBxcy3XzOAo5txw==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.2.tgz",
+ "integrity": "sha512-Fndk/4Zq3vAc4G/qyfXASbS3HBZbKrlnKZLEJzPLrXoJuipFNNwTes71+Ki1hwYW5lch26niRYoZFAtZVf3EGA==",
+ "cpu": [
+ "arm"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.2.tgz",
+ "integrity": "sha512-pz0NNo882vVfqJ0yNInuG9YH71smP4gRSdeL09ukC2YLE6ZyZePAlWKEHgAzJGTiOh8Qkaov6mMIMlEhmLdKew==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.2.tgz",
+ "integrity": "sha512-MBoInDXDppMfhSzbMmOQtGfloVAflS2rP1qPcUIiITMi36Mm5YR7r0ASND99razjQUpHTzjrU1flO76hKvP5RA==",
+ "cpu": [
+ "s390x"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.28",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.2.tgz",
+ "integrity": "sha512-xUT82H5IbXewKkeF5aiooajoO1tQV4PnKfS/OZtb5DDdxS/FCI/uXTVZ35GQ97RZXsycojz/AJ0asoz6p2/H/A==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "glibc": ">=2.26",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.2.tgz",
+ "integrity": "sha512-F+0z8JCu/UnMzg8IYW1TMeiViIWBVg7IWP6nE0p5S5EPQxlLd76c8jYemG21X99UzFwgkRo5yz2DS+zbrnxZeA==",
+ "cpu": [
+ "arm64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.2.tgz",
+ "integrity": "sha512-+ZLE3SQmSL+Fn1gmSaM8uFusW5Y3J9VOf+wMGNnTtJUMUxFhv+P4UPaYEYT8tqnyYVaOVGgMN/zsOxn9pSsO2A==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "musl": ">=1.2.2",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.1"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.2.tgz",
+ "integrity": "sha512-fLbTaESVKuQcpm8ffgBD7jLb/CQLcATju/jxtTXR1XCLwbOQt+OL5zPHSDMmp2JZIeq82e18yE0Vv7zh6+6BfQ==",
+ "cpu": [
+ "wasm32"
+ ],
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^0.45.0"
+ },
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.2.tgz",
+ "integrity": "sha512-okBpql96hIGuZ4lN3+nsAjGeggxKm7hIRu9zyec0lnfB8E7Z6p95BuRZzDDXZOl2e8UmR4RhYt631i7mfmKU8g==",
+ "cpu": [
+ "ia32"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.2.tgz",
+ "integrity": "sha512-E4magOks77DK47FwHUIGH0RYWSgRBfGdK56kIHSVeB9uIS4pPFr4N2kIVsXdQQo4LzOsENKV5KAhRlRL7eMAdg==",
+ "cpu": [
+ "x64"
+ ],
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0",
+ "npm": ">=9.6.5",
+ "pnpm": ">=7.1.0",
+ "yarn": ">=3.2.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@lukeed/ms": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
@@ -411,9 +852,9 @@
}
},
"node_modules/@types/node": {
- "version": "20.11.22",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.22.tgz",
- "integrity": "sha512-/G+IxWxma6V3E+pqK1tSl2Fo1kl41pK1yeCyDsgkF9WlVAme4j5ISYM2zR11bgLFJGLN5sVK40T4RJNuiZbEjA==",
+ "version": "20.11.24",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
+ "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
@@ -1034,6 +1475,18 @@
"wrap-ansi": "^7.0.0"
}
},
+ "node_modules/color": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
+ "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
+ "dependencies": {
+ "color-convert": "^2.0.1",
+ "color-string": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=12.5.0"
+ }
+ },
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@@ -1050,6 +1503,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
+ "node_modules/color-string": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
+ "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
+ "dependencies": {
+ "color-name": "^1.0.0",
+ "simple-swizzle": "^0.2.2"
+ }
+ },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@@ -1244,6 +1706,14 @@
"node": ">= 0.8"
}
},
+ "node_modules/detect-libc": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz",
+ "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/dir-glob": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@@ -2141,6 +2611,11 @@
"node": ">= 0.10"
}
},
+ "node_modules/is-arrayish": {
+ "version": "0.3.2",
+ "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
+ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
+ },
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -3083,6 +3558,45 @@
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="
},
+ "node_modules/sharp": {
+ "version": "0.33.2",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.2.tgz",
+ "integrity": "sha512-WlYOPyyPDiiM07j/UO+E720ju6gtNtHjEGg5vovUk1Lgxyjm2LFO+37Nt/UI3MMh2l6hxTWQWi7qk3cXJTutcQ==",
+ "hasInstallScript": true,
+ "dependencies": {
+ "color": "^4.2.3",
+ "detect-libc": "^2.0.2",
+ "semver": "^7.5.4"
+ },
+ "engines": {
+ "libvips": ">=8.15.1",
+ "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.33.2",
+ "@img/sharp-darwin-x64": "0.33.2",
+ "@img/sharp-libvips-darwin-arm64": "1.0.1",
+ "@img/sharp-libvips-darwin-x64": "1.0.1",
+ "@img/sharp-libvips-linux-arm": "1.0.1",
+ "@img/sharp-libvips-linux-arm64": "1.0.1",
+ "@img/sharp-libvips-linux-s390x": "1.0.1",
+ "@img/sharp-libvips-linux-x64": "1.0.1",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.0.1",
+ "@img/sharp-libvips-linuxmusl-x64": "1.0.1",
+ "@img/sharp-linux-arm": "0.33.2",
+ "@img/sharp-linux-arm64": "0.33.2",
+ "@img/sharp-linux-s390x": "0.33.2",
+ "@img/sharp-linux-x64": "0.33.2",
+ "@img/sharp-linuxmusl-arm64": "0.33.2",
+ "@img/sharp-linuxmusl-x64": "0.33.2",
+ "@img/sharp-wasm32": "0.33.2",
+ "@img/sharp-win32-ia32": "0.33.2",
+ "@img/sharp-win32-x64": "0.33.2"
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3104,6 +3618,14 @@
"node": ">=8"
}
},
+ "node_modules/simple-swizzle": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
+ "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
+ "dependencies": {
+ "is-arrayish": "^0.3.1"
+ }
+ },
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@@ -3362,6 +3884,12 @@
"typescript": "*"
}
},
+ "node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
+ "optional": true
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
diff --git a/package.json b/package.json
index 9b84838..77f3726 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "txtdot",
- "version": "1.6.1",
+ "version": "1.7.0",
"private": true,
"description": "txtdot is an HTTP proxy that parses only text, links and pictures from pages reducing internet bandwidth usage, removing ads and heavy scripts",
"main": "dist/app.js",
@@ -20,14 +20,15 @@
"json-schema-to-ts": "^3.0.0",
"linkedom": "^0.16.8",
"micromatch": "^4.0.5",
- "route-parser": "^0.0.5"
+ "route-parser": "^0.0.5",
+ "sharp": "^0.33.2"
},
"devDependencies": {
"@types/dompurify": "^3.0.5",
"@types/ejs": "^3.1.5",
"@types/jsdom": "^21.1.6",
"@types/micromatch": "^4.0.6",
- "@types/node": "^20.11.20",
+ "@types/node": "^20.11.24",
"@types/route-parser": "^0.1.7",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
diff --git a/src/app.ts b/src/app.ts
index b35d4e2..b483c9f 100644
--- a/src/app.ts
+++ b/src/app.ts
@@ -18,6 +18,9 @@ import errorHandler from './errors/handler';
import getConfig from './config/main';
import redirectRoute from './routes/browser/redirect';
+import dynConfig from './config/dynamic.config';
+import configurationRoute from './routes/browser/configuration';
+
class App {
async init() {
const config = getConfig();
@@ -42,6 +45,7 @@ class App {
});
if (config.swagger) {
+ dynConfig.addRoute('/doc');
await fastify.register(fastifySwagger, {
swagger: {
info: {
@@ -54,14 +58,16 @@ class App {
await fastify.register(fastifySwaggerUi, { routePrefix: '/doc' });
}
+ fastify.addHook('onRoute', (route) => {
+ dynConfig.addRoute(route.url);
+ });
+
fastify.register(indexRoute);
fastify.register(getRoute);
+ fastify.register(configurationRoute);
- if (config.search.enabled) {
- fastify.register(redirectRoute);
- }
-
- if (config.proxy_res) fastify.register(proxyRoute);
+ config.search.enabled && fastify.register(redirectRoute);
+ config.proxy.enabled && fastify.register(proxyRoute);
fastify.register(parseRoute);
fastify.register(rawHtml);
diff --git a/src/config/config.service.ts b/src/config/config.service.ts
index 144962e..c585a42 100644
--- a/src/config/config.service.ts
+++ b/src/config/config.service.ts
@@ -5,7 +5,7 @@ export class ConfigService {
public readonly port: number;
public readonly timeout: number;
public readonly reverse_proxy: boolean;
- public readonly proxy_res: boolean;
+ public readonly proxy: ProxyConfig;
public readonly swagger: boolean;
public readonly search: SearchConfig;
@@ -19,7 +19,11 @@ export class ConfigService {
this.reverse_proxy = this.parseBool(process.env.REVERSE_PROXY, false);
- this.proxy_res = this.parseBool(process.env.PROXY_RES, true);
+ this.proxy = {
+ enabled: this.parseBool(process.env.PROXY_RES, true),
+ img_compress: this.parseBool(process.env.IMG_COMPRESS, true),
+ };
+
this.swagger = this.parseBool(process.env.SWAGGER, false);
this.search = {
@@ -34,6 +38,11 @@ export class ConfigService {
}
}
+interface ProxyConfig {
+ enabled: boolean;
+ img_compress: boolean;
+}
+
interface SearchConfig {
enabled: boolean;
searx_url?: string;
diff --git a/src/config/dynamic.config.ts b/src/config/dynamic.config.ts
new file mode 100644
index 0000000..ccddc1d
--- /dev/null
+++ b/src/config/dynamic.config.ts
@@ -0,0 +1,10 @@
+class DynConfigService {
+ public routes: Set = new Set();
+ constructor() {}
+ addRoute(route: string) {
+ this.routes.add(route);
+ }
+}
+
+const config = new DynConfigService();
+export default config;
diff --git a/src/errors/handler.ts b/src/errors/handler.ts
index f5cf4c3..a7d2c15 100644
--- a/src/errors/handler.ts
+++ b/src/errors/handler.ts
@@ -55,7 +55,8 @@ function htmlErrorHandler(error: Error, reply: FastifyReply, url: string) {
url,
code: error.code,
description: error.description,
- proxyBtn: error instanceof NotHtmlMimetypeError && getConfig().proxy_res,
+ proxyBtn:
+ error instanceof NotHtmlMimetypeError && getConfig().proxy.enabled,
});
}
diff --git a/src/errors/main.ts b/src/errors/main.ts
index 62c54bf..420346a 100644
--- a/src/errors/main.ts
+++ b/src/errors/main.ts
@@ -31,13 +31,23 @@ export class LocalResourceError extends TxtDotError {
}
}
+export class UnsupportedMimetypeError extends TxtDotError {
+ constructor(expected: string, got?: string) {
+ super(
+ 415,
+ 'UnsupportedMimetypeError',
+ `Unsupported mimetype, expected ${expected}, got ${got}`
+ );
+ }
+}
+
export class NotHtmlMimetypeError extends TxtDotError {
constructor() {
super(
421,
'NotHtmlMimetypeError',
'Received non-HTML content, ' +
- (getConfig().proxy_res
+ (getConfig().proxy.enabled
? 'use proxy instead of parser.'
: 'proxying is disabled by the instance admin.')
);
diff --git a/src/handlers/distributor.ts b/src/handlers/distributor.ts
index 5dc7009..040ecc4 100644
--- a/src/handlers/distributor.ts
+++ b/src/handlers/distributor.ts
@@ -7,9 +7,7 @@ import DOMPurify from 'dompurify';
import { Readable } from 'stream';
-import isLocalResource from '../utils/islocal';
-
-import { LocalResourceError, NotHtmlMimetypeError } from '../errors/main';
+import { NotHtmlMimetypeError } from '../errors/main';
import { HandlerInput } from './handler-input';
import { decodeStream, parseEncodingName } from '../utils/http';
import replaceHref from '../utils/replace-href';
@@ -40,10 +38,6 @@ export class Distributor {
): Promise {
const urlObj = new URL(remoteUrl);
- if (await isLocalResource(urlObj)) {
- throw new LocalResourceError();
- }
-
const response = await axios.get(remoteUrl);
const data: Readable = response.data;
const mime: string | undefined =
@@ -76,6 +70,7 @@ export class Distributor {
if (specified) {
return this.fallback[this.engines_id[specified]];
}
+
for (const engine of this.fallback) {
if (micromatch.isMatch(host, engine.domains)) {
return engine;
diff --git a/src/handlers/engine.ts b/src/handlers/engine.ts
index 22aea15..197ac0f 100644
--- a/src/handlers/engine.ts
+++ b/src/handlers/engine.ts
@@ -11,12 +11,14 @@ interface IRoute {
export class Engine {
name: string;
+ description: string;
domains: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
routes: IRoute[] = [];
- constructor(name: string, domains: string[] = []) {
+ constructor(name: string, description: string, domains: string[] = []) {
this.domains = domains;
this.name = name;
+ this.description = description;
}
route(
diff --git a/src/handlers/engines/readability.ts b/src/handlers/engines/readability.ts
index c52b10b..3ebec2f 100644
--- a/src/handlers/engines/readability.ts
+++ b/src/handlers/engines/readability.ts
@@ -3,7 +3,11 @@ import { EngineParseError } from '../../errors/main';
import { Engine } from '../engine';
-const ReadabilityEngine = new Engine('Readability');
+const ReadabilityEngine = new Engine(
+ 'Readability',
+ 'Engine for parsing content with Readability',
+ ['*']
+);
ReadabilityEngine.route('*path', async (input, ro) => {
const reader = new Readability(input.parseDom().window.document);
diff --git a/src/handlers/engines/searx.ts b/src/handlers/engines/searx.ts
index cf2b7e1..43f7637 100644
--- a/src/handlers/engines/searx.ts
+++ b/src/handlers/engines/searx.ts
@@ -2,7 +2,9 @@ import { Route } from '../../types/handlers';
import { Engine } from '../engine';
import { HandlerInput } from '../handler-input';
-const SearXEngine = new Engine('SearX', ['searx.*']);
+const SearXEngine = new Engine('SearX', "Engine for searching with 'SearXNG'", [
+ 'searx.*',
+]);
async function search(
input: HandlerInput,
diff --git a/src/handlers/engines/stackoverflow/main.ts b/src/handlers/engines/stackoverflow/main.ts
index 43608bf..a72f4da 100644
--- a/src/handlers/engines/stackoverflow/main.ts
+++ b/src/handlers/engines/stackoverflow/main.ts
@@ -1,16 +1,20 @@
import { Engine } from '../../engine';
import questions from './questions';
import users from './users';
-const soEngine = new Engine('StackOverflow', [
- 'stackoverflow.com',
- '*.stackoverflow.com',
- '*.stackexchange.com',
- 'askubuntu.com',
- 'stackapps.com',
- 'mathoverflow.net',
- 'superuser.com',
- 'serverfault.com',
-]);
+const soEngine = new Engine(
+ 'StackOverflow',
+ "Engine for 'StackOverflow'. Available routes: '/questions/' and '/users/'",
+ [
+ 'stackoverflow.com',
+ '*.stackoverflow.com',
+ '*.stackexchange.com',
+ 'askubuntu.com',
+ 'stackapps.com',
+ 'mathoverflow.net',
+ 'superuser.com',
+ 'serverfault.com',
+ ]
+);
soEngine.route('/questions/:id/*slug', questions);
soEngine.route('/users/:id/*slug', users);
diff --git a/src/handlers/main.ts b/src/handlers/main.ts
index 377d613..800738a 100644
--- a/src/handlers/main.ts
+++ b/src/handlers/main.ts
@@ -5,9 +5,9 @@ import StackOverflow from './engines/stackoverflow/main';
const distributor = new Distributor();
-distributor.engine(Readability);
-distributor.engine(SearX);
distributor.engine(StackOverflow);
+distributor.engine(SearX);
+distributor.engine(Readability);
export const engineList = distributor.list;
export default distributor;
diff --git a/src/routes/browser/configuration.ts b/src/routes/browser/configuration.ts
new file mode 100644
index 0000000..d5ce938
--- /dev/null
+++ b/src/routes/browser/configuration.ts
@@ -0,0 +1,19 @@
+import { FastifyInstance } from 'fastify';
+
+import packageJSON from '../../package';
+import distributor from '../../handlers/main';
+import { indexSchema } from '../../types/requests/browser';
+
+import getConfig from '../../config/main';
+import dynConfig from '../../config/dynamic.config';
+
+export default async function configurationRoute(fastify: FastifyInstance) {
+ fastify.get('/configuration', { schema: indexSchema }, async (_, reply) => {
+ return reply.view('/templates/configuration.ejs', {
+ packageJSON,
+ engines: distributor.fallback,
+ dynConfig,
+ config: getConfig(),
+ });
+ });
+}
diff --git a/src/routes/browser/proxy.ts b/src/routes/browser/proxy.ts
index c49f8ed..100a89c 100644
--- a/src/routes/browser/proxy.ts
+++ b/src/routes/browser/proxy.ts
@@ -1,6 +1,9 @@
import { FastifyInstance } from 'fastify';
import { IProxySchema, ProxySchema } from '../../types/requests/browser';
import axios from '../../types/axios';
+import sharp from 'sharp';
+import getConfig from '../../config/main';
+import { UnsupportedMimetypeError } from '../../errors/main';
import isLocalResource from '../../utils/islocal';
import { LocalResourceError } from '../../errors/main';
@@ -24,4 +27,49 @@ export default async function proxyRoute(fastify: FastifyInstance) {
return reply.send(response.data);
}
);
+
+ if (getConfig().proxy.img_compress)
+ fastify.get(
+ '/proxy/img',
+ { schema: ProxySchema },
+ async (request, reply) => {
+ const response = await axios.get(request.query.url, {
+ responseType: 'arraybuffer',
+ });
+
+ const mime: string | undefined =
+ response.headers['content-type']?.toString();
+
+ if (!(mime && mime.startsWith('image/'))) {
+ throw new UnsupportedMimetypeError('image/*', mime);
+ }
+
+ const clen: number | undefined = parseInt(
+ response.headers['content-length']?.toString() || '0'
+ );
+
+ if (mime.startsWith('image/svg')) {
+ reply.header('Content-Type', mime);
+ reply.header('Content-Length', clen);
+ return reply.send(response.data);
+ }
+
+ const buffer = await sharp(response.data)
+ // .grayscale(true)
+ .toFormat('webp', {
+ quality: 25,
+ progressive: true,
+ optimizeScans: true,
+ })
+ .toBuffer();
+
+ reply.header('Content-Type', 'image/webp');
+ reply.header('Content-Length', buffer.length);
+
+ reply.header('x-original-size', clen);
+ reply.header('x-bytes-saved', clen - buffer.length);
+
+ return reply.send(buffer);
+ }
+ );
}
diff --git a/src/types/axios.ts b/src/types/axios.ts
index 6e4628b..bf30a3e 100644
--- a/src/types/axios.ts
+++ b/src/types/axios.ts
@@ -1,9 +1,30 @@
-import axios from 'axios';
+import origAxios from 'axios';
+import { isLocalResource, isLocalResourceURL } from '../utils/islocal';
+import { LocalResourceError } from '../errors/main';
-export default axios.create({
+const axios = origAxios.create({
headers: {
'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0',
},
responseType: 'stream',
});
+
+axios.interceptors.response.use(
+ (response) => {
+ if (isLocalResource(response.request.socket.remoteAddress)) {
+ throw new LocalResourceError();
+ }
+
+ return response;
+ },
+ async (error) => {
+ if (await isLocalResourceURL(new URL(error.config?.url))) {
+ throw new LocalResourceError();
+ }
+
+ throw error;
+ }
+);
+
+export default axios;
diff --git a/src/utils/generate.ts b/src/utils/generate.ts
index f5a3295..3fa58a4 100644
--- a/src/utils/generate.ts
+++ b/src/utils/generate.ts
@@ -27,12 +27,13 @@ export function generateParserUrl(
export function generateProxyUrl(
requestUrl: URL,
remoteUrl: URL,
- href: string
+ href: string,
+ subProxy?: string
): string {
const realHref = getRealURL(href, remoteUrl);
const urlParam = `?url=${encodeURIComponent(realHref.href)}`;
- return `${requestUrl.origin}/proxy${urlParam}`;
+ return `${requestUrl.origin}/proxy${subProxy ? `/${subProxy}` : ''}${urlParam}`;
}
function getRealURL(href: string, remoteUrl: URL) {
diff --git a/src/utils/islocal.ts b/src/utils/islocal.ts
index 752cb7f..1d98165 100644
--- a/src/utils/islocal.ts
+++ b/src/utils/islocal.ts
@@ -1,5 +1,5 @@
-import dns from 'dns';
import ipRangeCheck from 'ip-range-check';
+import dns from 'dns';
const subnets = [
'0.0.0.0/8',
@@ -35,7 +35,11 @@ const subnets = [
'ff00::/8',
];
-export default async function isLocalResource(url: URL): Promise {
+export function isLocalResource(addr: string): boolean {
+ return ipRangeCheck(addr, subnets);
+}
+
+export async function isLocalResourceURL(url: URL): Promise {
// Resolve domain name
const addr = (await dns.promises.lookup(url.hostname)).address;
diff --git a/src/utils/replace-href.ts b/src/utils/replace-href.ts
index 522159f..f32ce17 100644
--- a/src/utils/replace-href.ts
+++ b/src/utils/replace-href.ts
@@ -15,16 +15,27 @@ export default function replaceHref(
const proxyUrl = (href: string) =>
generateProxyUrl(requestUrl, remoteUrl, href);
+ const imgProxyUrl = (href: string) =>
+ generateProxyUrl(requestUrl, remoteUrl, href, 'img');
+
modifyLinks(doc.querySelectorAll('a[href]'), 'href', parserUrl);
modifyLinks(doc.querySelectorAll('frame,iframe'), 'src', parserUrl);
- if (getConfig().proxy_res) {
+ const config = getConfig();
+
+ if (config.proxy.enabled) {
modifyLinks(
- doc.querySelectorAll('img,image,video,audio,embed,track,source'),
+ doc.querySelectorAll('video,audio,embed,track,source'),
'src',
proxyUrl
);
+ modifyLinks(
+ doc.querySelectorAll('img,image'),
+ 'src',
+ config.proxy.img_compress ? imgProxyUrl : proxyUrl
+ );
+
modifyLinks(doc.getElementsByTagName('object'), 'data', proxyUrl);
const sources = doc.querySelectorAll('source,img');
for (const source of sources) {
diff --git a/static/configuration.css b/static/configuration.css
new file mode 100644
index 0000000..6982be1
--- /dev/null
+++ b/static/configuration.css
@@ -0,0 +1,26 @@
+main {
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ gap: 0.375rem;
+}
+
+h1 {
+ width: fit-content;
+ margin: auto;
+}
+
+h1 > .dot {
+ color: var(--accent);
+}
+
+.menu {
+ justify-content: center;
+}
+
+.configuration {
+ display: flex;
+ flex-direction: column;
+ align-items: start;
+ gap: 0.375rem;
+}
diff --git a/templates/components/form-main.ejs b/templates/components/form-main.ejs
index 2d225ec..04d9569 100644
--- a/templates/components/form-main.ejs
+++ b/templates/components/form-main.ejs
@@ -1,26 +1,30 @@
<% search = config.search.enabled %>
-<% if (search) { %>
+<%
-
+if (search) {
+ %>
+
-
+
-
+
+ <%
+}
-<% } %>
+%>