Merge pull request from GHSA-4gj5-xj97-j8fp

* ci: add build check to pull requests

* Build(deps-dev): Bump @types/node from 20.10.6 to 20.11.24 (#91)

Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 20.10.6 to 20.11.24.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* feat: image compression (#101)

* feat: image compression

* fix: not compress svg

Maybe we should add a function to disable this fix. Since I noticed that if you compress svg in webp it becomes magically smaller.

* Configuration page (#104)

* feat: image compression

* feat: configuration page

* refactor: json stringify

and change engine buttons to ordered list

* fix: engine distributor matching

* fix: formatting

* build: update txtdot version to 1.7.0

* fix: configuration page title

* doc: features (#102)

* fix: ssrf

GHSA-4gj5-xj97-j8fp

Doesn't fully correct the error! You need to configure the server to block requests.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
Artemy Egorov 2024-03-07 14:49:54 +03:00 committed by GitHub
parent f241a46e05
commit 7c72d985f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 932 additions and 86 deletions

View File

@ -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 PORT=8080
TIMEOUT=0 # 0 means no timeout
REVERSE_PROXY=false # only with reverse proxy; see docs
TIMEOUT=0 # Connection timout 0 (no timout) # Features
REVERSE_PROXY=true # only for reverse proxy; see docs
## Proxy
PROXY_RES=true 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 > ## Documentation
SEARX_URL="" # Base url of searxng instance without /search, etc 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

19
.github/workflows/build.yml vendored Normal file
View File

@ -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

View File

@ -1,7 +1,7 @@
name: format-check name: format-check
on: pull_request on: pull_request
jobs: jobs:
check-formatting: format-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code

View File

@ -11,28 +11,32 @@
HTTP proxy that parses only text, links and pictures from pages HTTP proxy that parses only text, links and pictures from pages
reducing internet traffic, removing ads and heavy scripts. 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), ## Features
[🔗 linkedom](https://github.com/WebReflection/linkedom),
[Fastify web framework](https://github.com/fastify/fastify).
## Installation - Server-side page simplification
- Media proxy
```bash - Image compression with Sharp
npm install - Search with SearXNG
``` - Custom parsers for StackOverflow and SearXNG
- Handy API endpoints
- No client JavaScript
- Some kind of Material Design 3
## Running ## Running
### Dev ### Development
```bash ```bash
npm install
npm run dev npm run dev
``` ```
### Prod ### Production
```bash ```bash
npm install
npm run build npm run build
npm run start npm run start
``` ```
@ -40,6 +44,63 @@ npm run start
### Docker ### Docker
```bash ```bash
docker compose build
docker compose up -d docker compose up -d
``` ```
## Screenshots
<div align="center">
<img src="https://raw.githubusercontent.com/TxtDot/.github/main/imgs/ui_url_input.png" alt="Main page with URL input field">
<img src="https://raw.githubusercontent.com/TxtDot/.github/main/imgs/ui_search_page.png" alt="SearXNG search results page">
</div>
## 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.
<details>
<summary>Expand</summary>
| | 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
</details>
## 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)

542
package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "txtdot", "name": "txtdot",
"version": "1.6.1", "version": "1.7.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "txtdot", "name": "txtdot",
"version": "1.6.1", "version": "1.7.0",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@fastify/static": "^6.12.0", "@fastify/static": "^6.12.0",
@ -24,14 +24,15 @@
"json-schema-to-ts": "^3.0.0", "json-schema-to-ts": "^3.0.0",
"linkedom": "^0.16.8", "linkedom": "^0.16.8",
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"route-parser": "^0.0.5" "route-parser": "^0.0.5",
"sharp": "^0.33.2"
}, },
"devDependencies": { "devDependencies": {
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/ejs": "^3.1.5", "@types/ejs": "^3.1.5",
"@types/jsdom": "^21.1.6", "@types/jsdom": "^21.1.6",
"@types/micromatch": "^4.0.6", "@types/micromatch": "^4.0.6",
"@types/node": "^20.11.20", "@types/node": "^20.11.24",
"@types/route-parser": "^0.1.7", "@types/route-parser": "^0.1.7",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.1.0",
@ -63,6 +64,15 @@
"node": ">=6.9.0" "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": { "node_modules/@eslint-community/eslint-utils": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
@ -313,6 +323,437 @@
"integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
"dev": true "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": { "node_modules/@lukeed/ms": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz", "resolved": "https://registry.npmjs.org/@lukeed/ms/-/ms-2.0.2.tgz",
@ -411,9 +852,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.11.22", "version": "20.11.24",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.22.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.24.tgz",
"integrity": "sha512-/G+IxWxma6V3E+pqK1tSl2Fo1kl41pK1yeCyDsgkF9WlVAme4j5ISYM2zR11bgLFJGLN5sVK40T4RJNuiZbEjA==", "integrity": "sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
@ -1034,6 +1475,18 @@
"wrap-ansi": "^7.0.0" "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": { "node_modules/color-convert": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "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": { "node_modules/combined-stream": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
@ -1244,6 +1706,14 @@
"node": ">= 0.8" "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": { "node_modules/dir-glob": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
@ -2141,6 +2611,11 @@
"node": ">= 0.10" "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": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "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", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" "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": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -3104,6 +3618,14 @@
"node": ">=8" "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": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -3362,6 +3884,12 @@
"typescript": "*" "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": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "txtdot", "name": "txtdot",
"version": "1.6.1", "version": "1.7.0",
"private": true, "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", "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", "main": "dist/app.js",
@ -20,14 +20,15 @@
"json-schema-to-ts": "^3.0.0", "json-schema-to-ts": "^3.0.0",
"linkedom": "^0.16.8", "linkedom": "^0.16.8",
"micromatch": "^4.0.5", "micromatch": "^4.0.5",
"route-parser": "^0.0.5" "route-parser": "^0.0.5",
"sharp": "^0.33.2"
}, },
"devDependencies": { "devDependencies": {
"@types/dompurify": "^3.0.5", "@types/dompurify": "^3.0.5",
"@types/ejs": "^3.1.5", "@types/ejs": "^3.1.5",
"@types/jsdom": "^21.1.6", "@types/jsdom": "^21.1.6",
"@types/micromatch": "^4.0.6", "@types/micromatch": "^4.0.6",
"@types/node": "^20.11.20", "@types/node": "^20.11.24",
"@types/route-parser": "^0.1.7", "@types/route-parser": "^0.1.7",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.1.0",

View File

@ -18,6 +18,9 @@ import errorHandler from './errors/handler';
import getConfig from './config/main'; import getConfig from './config/main';
import redirectRoute from './routes/browser/redirect'; import redirectRoute from './routes/browser/redirect';
import dynConfig from './config/dynamic.config';
import configurationRoute from './routes/browser/configuration';
class App { class App {
async init() { async init() {
const config = getConfig(); const config = getConfig();
@ -42,6 +45,7 @@ class App {
}); });
if (config.swagger) { if (config.swagger) {
dynConfig.addRoute('/doc');
await fastify.register(fastifySwagger, { await fastify.register(fastifySwagger, {
swagger: { swagger: {
info: { info: {
@ -54,14 +58,16 @@ class App {
await fastify.register(fastifySwaggerUi, { routePrefix: '/doc' }); await fastify.register(fastifySwaggerUi, { routePrefix: '/doc' });
} }
fastify.addHook('onRoute', (route) => {
dynConfig.addRoute(route.url);
});
fastify.register(indexRoute); fastify.register(indexRoute);
fastify.register(getRoute); fastify.register(getRoute);
fastify.register(configurationRoute);
if (config.search.enabled) { config.search.enabled && fastify.register(redirectRoute);
fastify.register(redirectRoute); config.proxy.enabled && fastify.register(proxyRoute);
}
if (config.proxy_res) fastify.register(proxyRoute);
fastify.register(parseRoute); fastify.register(parseRoute);
fastify.register(rawHtml); fastify.register(rawHtml);

View File

@ -5,7 +5,7 @@ export class ConfigService {
public readonly port: number; public readonly port: number;
public readonly timeout: number; public readonly timeout: number;
public readonly reverse_proxy: boolean; public readonly reverse_proxy: boolean;
public readonly proxy_res: boolean; public readonly proxy: ProxyConfig;
public readonly swagger: boolean; public readonly swagger: boolean;
public readonly search: SearchConfig; public readonly search: SearchConfig;
@ -19,7 +19,11 @@ export class ConfigService {
this.reverse_proxy = this.parseBool(process.env.REVERSE_PROXY, false); 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.swagger = this.parseBool(process.env.SWAGGER, false);
this.search = { this.search = {
@ -34,6 +38,11 @@ export class ConfigService {
} }
} }
interface ProxyConfig {
enabled: boolean;
img_compress: boolean;
}
interface SearchConfig { interface SearchConfig {
enabled: boolean; enabled: boolean;
searx_url?: string; searx_url?: string;

View File

@ -0,0 +1,10 @@
class DynConfigService {
public routes: Set<string> = new Set();
constructor() {}
addRoute(route: string) {
this.routes.add(route);
}
}
const config = new DynConfigService();
export default config;

View File

@ -55,7 +55,8 @@ function htmlErrorHandler(error: Error, reply: FastifyReply, url: string) {
url, url,
code: error.code, code: error.code,
description: error.description, description: error.description,
proxyBtn: error instanceof NotHtmlMimetypeError && getConfig().proxy_res, proxyBtn:
error instanceof NotHtmlMimetypeError && getConfig().proxy.enabled,
}); });
} }

View File

@ -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 { export class NotHtmlMimetypeError extends TxtDotError {
constructor() { constructor() {
super( super(
421, 421,
'NotHtmlMimetypeError', 'NotHtmlMimetypeError',
'Received non-HTML content, ' + 'Received non-HTML content, ' +
(getConfig().proxy_res (getConfig().proxy.enabled
? 'use proxy instead of parser.' ? 'use proxy instead of parser.'
: 'proxying is disabled by the instance admin.') : 'proxying is disabled by the instance admin.')
); );

View File

@ -7,9 +7,7 @@ import DOMPurify from 'dompurify';
import { Readable } from 'stream'; import { Readable } from 'stream';
import isLocalResource from '../utils/islocal'; import { NotHtmlMimetypeError } from '../errors/main';
import { LocalResourceError, NotHtmlMimetypeError } from '../errors/main';
import { HandlerInput } from './handler-input'; import { HandlerInput } from './handler-input';
import { decodeStream, parseEncodingName } from '../utils/http'; import { decodeStream, parseEncodingName } from '../utils/http';
import replaceHref from '../utils/replace-href'; import replaceHref from '../utils/replace-href';
@ -40,10 +38,6 @@ export class Distributor {
): Promise<IHandlerOutput> { ): Promise<IHandlerOutput> {
const urlObj = new URL(remoteUrl); const urlObj = new URL(remoteUrl);
if (await isLocalResource(urlObj)) {
throw new LocalResourceError();
}
const response = await axios.get(remoteUrl); const response = await axios.get(remoteUrl);
const data: Readable = response.data; const data: Readable = response.data;
const mime: string | undefined = const mime: string | undefined =
@ -76,6 +70,7 @@ export class Distributor {
if (specified) { if (specified) {
return this.fallback[this.engines_id[specified]]; return this.fallback[this.engines_id[specified]];
} }
for (const engine of this.fallback) { for (const engine of this.fallback) {
if (micromatch.isMatch(host, engine.domains)) { if (micromatch.isMatch(host, engine.domains)) {
return engine; return engine;

View File

@ -11,12 +11,14 @@ interface IRoute<TParams extends RouteValues> {
export class Engine { export class Engine {
name: string; name: string;
description: string;
domains: string[]; domains: string[];
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
routes: IRoute<any>[] = []; routes: IRoute<any>[] = [];
constructor(name: string, domains: string[] = []) { constructor(name: string, description: string, domains: string[] = []) {
this.domains = domains; this.domains = domains;
this.name = name; this.name = name;
this.description = description;
} }
route<TParams extends RouteValues>( route<TParams extends RouteValues>(

View File

@ -3,7 +3,11 @@ import { EngineParseError } from '../../errors/main';
import { Engine } from '../engine'; 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) => { ReadabilityEngine.route('*path', async (input, ro) => {
const reader = new Readability(input.parseDom().window.document); const reader = new Readability(input.parseDom().window.document);

View File

@ -2,7 +2,9 @@ import { Route } from '../../types/handlers';
import { Engine } from '../engine'; import { Engine } from '../engine';
import { HandlerInput } from '../handler-input'; 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( async function search(
input: HandlerInput, input: HandlerInput,

View File

@ -1,7 +1,10 @@
import { Engine } from '../../engine'; import { Engine } from '../../engine';
import questions from './questions'; import questions from './questions';
import users from './users'; import users from './users';
const soEngine = new Engine('StackOverflow', [ const soEngine = new Engine(
'StackOverflow',
"Engine for 'StackOverflow'. Available routes: '/questions/' and '/users/'",
[
'stackoverflow.com', 'stackoverflow.com',
'*.stackoverflow.com', '*.stackoverflow.com',
'*.stackexchange.com', '*.stackexchange.com',
@ -10,7 +13,8 @@ const soEngine = new Engine('StackOverflow', [
'mathoverflow.net', 'mathoverflow.net',
'superuser.com', 'superuser.com',
'serverfault.com', 'serverfault.com',
]); ]
);
soEngine.route('/questions/:id/*slug', questions); soEngine.route('/questions/:id/*slug', questions);
soEngine.route('/users/:id/*slug', users); soEngine.route('/users/:id/*slug', users);

View File

@ -5,9 +5,9 @@ import StackOverflow from './engines/stackoverflow/main';
const distributor = new Distributor(); const distributor = new Distributor();
distributor.engine(Readability);
distributor.engine(SearX);
distributor.engine(StackOverflow); distributor.engine(StackOverflow);
distributor.engine(SearX);
distributor.engine(Readability);
export const engineList = distributor.list; export const engineList = distributor.list;
export default distributor; export default distributor;

View File

@ -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(),
});
});
}

View File

@ -1,6 +1,9 @@
import { FastifyInstance } from 'fastify'; import { FastifyInstance } from 'fastify';
import { IProxySchema, ProxySchema } from '../../types/requests/browser'; import { IProxySchema, ProxySchema } from '../../types/requests/browser';
import axios from '../../types/axios'; 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 isLocalResource from '../../utils/islocal';
import { LocalResourceError } from '../../errors/main'; import { LocalResourceError } from '../../errors/main';
@ -24,4 +27,49 @@ export default async function proxyRoute(fastify: FastifyInstance) {
return reply.send(response.data); return reply.send(response.data);
} }
); );
if (getConfig().proxy.img_compress)
fastify.get<IProxySchema>(
'/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);
}
);
} }

View File

@ -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: { headers: {
'User-Agent': 'User-Agent':
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0', 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/116.0',
}, },
responseType: 'stream', 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;

View File

@ -27,12 +27,13 @@ export function generateParserUrl(
export function generateProxyUrl( export function generateProxyUrl(
requestUrl: URL, requestUrl: URL,
remoteUrl: URL, remoteUrl: URL,
href: string href: string,
subProxy?: string
): string { ): string {
const realHref = getRealURL(href, remoteUrl); const realHref = getRealURL(href, remoteUrl);
const urlParam = `?url=${encodeURIComponent(realHref.href)}`; const urlParam = `?url=${encodeURIComponent(realHref.href)}`;
return `${requestUrl.origin}/proxy${urlParam}`; return `${requestUrl.origin}/proxy${subProxy ? `/${subProxy}` : ''}${urlParam}`;
} }
function getRealURL(href: string, remoteUrl: URL) { function getRealURL(href: string, remoteUrl: URL) {

View File

@ -1,5 +1,5 @@
import dns from 'dns';
import ipRangeCheck from 'ip-range-check'; import ipRangeCheck from 'ip-range-check';
import dns from 'dns';
const subnets = [ const subnets = [
'0.0.0.0/8', '0.0.0.0/8',
@ -35,7 +35,11 @@ const subnets = [
'ff00::/8', 'ff00::/8',
]; ];
export default async function isLocalResource(url: URL): Promise<boolean> { export function isLocalResource(addr: string): boolean {
return ipRangeCheck(addr, subnets);
}
export async function isLocalResourceURL(url: URL): Promise<boolean> {
// Resolve domain name // Resolve domain name
const addr = (await dns.promises.lookup(url.hostname)).address; const addr = (await dns.promises.lookup(url.hostname)).address;

View File

@ -15,16 +15,27 @@ export default function replaceHref(
const proxyUrl = (href: string) => const proxyUrl = (href: string) =>
generateProxyUrl(requestUrl, remoteUrl, href); generateProxyUrl(requestUrl, remoteUrl, href);
const imgProxyUrl = (href: string) =>
generateProxyUrl(requestUrl, remoteUrl, href, 'img');
modifyLinks(doc.querySelectorAll('a[href]'), 'href', parserUrl); modifyLinks(doc.querySelectorAll('a[href]'), 'href', parserUrl);
modifyLinks(doc.querySelectorAll('frame,iframe'), 'src', parserUrl); modifyLinks(doc.querySelectorAll('frame,iframe'), 'src', parserUrl);
if (getConfig().proxy_res) { const config = getConfig();
if (config.proxy.enabled) {
modifyLinks( modifyLinks(
doc.querySelectorAll('img,image,video,audio,embed,track,source'), doc.querySelectorAll('video,audio,embed,track,source'),
'src', 'src',
proxyUrl proxyUrl
); );
modifyLinks(
doc.querySelectorAll('img,image'),
'src',
config.proxy.img_compress ? imgProxyUrl : proxyUrl
);
modifyLinks(doc.getElementsByTagName('object'), 'data', proxyUrl); modifyLinks(doc.getElementsByTagName('object'), 'data', proxyUrl);
const sources = doc.querySelectorAll('source,img'); const sources = doc.querySelectorAll('source,img');
for (const source of sources) { for (const source of sources) {

26
static/configuration.css Normal file
View File

@ -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;
}

View File

@ -1,7 +1,9 @@
<% search = config.search.enabled %> <% search = config.search.enabled %>
<% if (search) { %> <%
if (search) {
%>
<input type="checkbox" id="switch-search" checked> <input type="checkbox" id="switch-search" checked>
<label for="switch-search" class="switch-label"> <label for="switch-search" class="switch-label">
@ -19,8 +21,10 @@
</div> </div>
<input type="hidden" name="url" value="<%= config.search.searx_url %>/search"/> <input type="hidden" name="url" value="<%= config.search.searx_url %>/search"/>
</form> </form>
<%
}
<% } %> %>
<form action="/get" method="get" class="input-grid <%= search ? "main-form-url" : "" %>"> <form action="/get" method="get" class="input-grid <%= search ? "main-form-url" : "" %>">
<div class="input"> <div class="input">

View File

@ -0,0 +1,54 @@
<%
// hide private properties from config
const to_hide = ["host", "port"];
function replacer(key,value) {
if (to_hide.includes(key)) return undefined;
else return value;
}
function to_pretty(obj) {
return JSON.stringify(obj, replacer, 2).replace(/[\[\]{}"]/g, "").replace(/,/g, "");
}
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="description" content="<%= packageJSON.description %>">
<title>txt. configuration</title>
<link rel="stylesheet" href="/static/common.css">
<link rel="stylesheet" href="/static/configuration.css">
</head>
<body>
<main>
<header>
<h1>txt<span class="dot">.</span></h1>
<div class="menu">
<a class="button secondary" href="/">Home</a>
</div>
<p><%= packageJSON.description %></p>
</header>
<div class="configuration">
<h2>Configuration</h2>
<pre> version: <%= packageJSON.version %><%= to_pretty(config) %></pre>
<h2>Available engines</h2>
<ol>
<%
for (const engine of engines) {
%><li><%= engine.name %>: <%= engine.description %></li><%
}
%>
</ol>
<h2>Available routes</h2>
<%
for (const route of dynConfig.routes) {
%><a class="button secondary" href="<%= route %>"><%= route %></a><%
}
%>
</div>
</main>
</body>
</html>