Merge pull request #22 from TxtDot/fix-localhost

Bugfix: disallow proxying of local resources
This commit is contained in:
Artemy Egorov 2023-08-17 17:09:32 +03:00 committed by GitHub
commit fb96f9863d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 87 additions and 21 deletions

View File

@ -1,15 +1,18 @@
<h1 align="center"> <h1 align="center">
<a href="https://github.com/TxtDot/txtdot"><img src="https://github.com/TxtDot/.github/blob/main/imgs/TXTDot%20gh.png?raw=true" alt="txt." width="200"></a> <br> <a href="https://github.com/TxtDot/txtdot"><img src="https://github.com/TxtDot/.github/blob/main/imgs/TXTDot%20gh.png?raw=true" alt="txt." width="200"></a>
<img alt="GitHub" src="https://img.shields.io/github/license/txtdot/txtdot"> <br>
<img alt="GitHub release (with filter)" src="https://img.shields.io/github/v/release/TxtDot/txtdot?display_name=release"> <a href="https://github.com/TxtDot/txtdot/blob/main/LICENSE"><img alt="MIT license" src="https://img.shields.io/github/license/txtdot/txtdot?color=blue"></a>
<a href="https://matrix.to/#/#txtdot:matrix.org"><img alt="Static Badge" src="https://img.shields.io/badge/matrix_chat-green"> <a href="https://github.com/TxtDot/txtdot/releases/latest"><img alt="Latest release" src="https://img.shields.io/github/v/release/TxtDot/txtdot?display_name=release"></a>
</a> <a href="https://matrix.to/#/#txtdot:matrix.org"><img alt="Matrix chat" src="https://img.shields.io/badge/chat-matrix-blue"></a>
</h1> </h1>
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.
Uses [Mozilla's readability.js](https://github.com/mozilla/readability),
[JSDOM](https://github.com/jsdom/jsdom),
[Fastify web framework](https://github.com/fastify/fastify).
## Installation ## Installation
```bash ```bash
@ -30,7 +33,3 @@ npm run dev
npm run build npm run build
npm run start npm run start
``` ```
Uses [Mozilla's readability.js](https://github.com/mozilla/readability),
[JSDOM](https://github.com/jsdom/jsdom),
[Fastify web framework](https://github.com/fastify/fastify).

9
package-lock.json generated
View File

@ -18,6 +18,7 @@
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"ejs": "^3.1.9", "ejs": "^3.1.9",
"fastify": "^4.21.0", "fastify": "^4.21.0",
"ip-range-check": "^0.2.0",
"jsdom": "^22.1.0" "jsdom": "^22.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -1968,6 +1969,14 @@
"version": "2.0.4", "version": "2.0.4",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ip-range-check": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/ip-range-check/-/ip-range-check-0.2.0.tgz",
"integrity": "sha512-oaM3l/3gHbLlt/tCWLvt0mj1qUaI+STuRFnUvARGCujK9vvU61+2JsDpmkMzR4VsJhuFXWWgeKKVnwwoFfzCqw==",
"dependencies": {
"ipaddr.js": "^1.0.1"
}
},
"node_modules/ipaddr.js": { "node_modules/ipaddr.js": {
"version": "1.9.1", "version": "1.9.1",
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",

View File

@ -14,6 +14,7 @@
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"ejs": "^3.1.9", "ejs": "^3.1.9",
"fastify": "^4.21.0", "fastify": "^4.21.0",
"ip-range-check": "^0.2.0",
"jsdom": "^22.1.0" "jsdom": "^22.1.0"
}, },
"devDependencies": { "devDependencies": {

View File

@ -1,5 +1,6 @@
export class EngineParseError extends Error {} export class EngineParseError extends Error {}
export class InvalidParameterError extends Error {} export class InvalidParameterError extends Error {}
export class LocalResourceError extends Error {}
export class NotHtmlMimetypeError extends Error { export class NotHtmlMimetypeError extends Error {
url: string; url: string;
constructor(params: { url: string }) { constructor(params: { url: string }) {

View File

@ -6,15 +6,27 @@ import { DOMWindow } from "jsdom";
import readability from "./readability"; import readability from "./readability";
import google from "./google"; import google from "./google";
import { generateProxyUrl } from "../utils";
import { InvalidParameterError, NotHtmlMimetypeError } from "../errors/main"; import { generateProxyUrl } from "../utils/generate";
import isLocalResource from "../utils/islocal";
import {
InvalidParameterError,
LocalResourceError,
NotHtmlMimetypeError,
} from "../errors/main";
export default async function handlePage( export default async function handlePage(
url: string, url: string, // remote URL
requestUrl: URL, requestUrl: URL, // proxy URL
engine?: string engine?: string
): Promise<IHandlerOutput> { ): Promise<IHandlerOutput> {
const urlObj = new URL(url);
if (await isLocalResource(urlObj)) {
throw new LocalResourceError();
}
if (engine && engineList.indexOf(engine) === -1) { if (engine && engineList.indexOf(engine) === -1) {
throw new InvalidParameterError("Invalid engine"); throw new InvalidParameterError("Invalid engine");
} }
@ -26,7 +38,7 @@ export default async function handlePage(
throw new NotHtmlMimetypeError({ url }); throw new NotHtmlMimetypeError({ url });
} }
const window = new JSDOM(response.data, { url: url }).window; const window = new JSDOM(response.data, { url }).window;
[...window.document.getElementsByTagName("a")].forEach((link) => { [...window.document.getElementsByTagName("a")].forEach((link) => {
link.href = generateProxyUrl(requestUrl, link.href, engine); link.href = generateProxyUrl(requestUrl, link.href, engine);
@ -36,9 +48,7 @@ export default async function handlePage(
return engines[engine](window); return engines[engine](window);
} }
const host = new URL(url).hostname; return fallback[urlObj.host]?.(window) || fallback["*"](window);
return fallback[host]?.(window) || fallback["*"](window);
} }
interface Engines { interface Engines {

View File

@ -2,7 +2,7 @@ import { FastifyInstance } from "fastify";
import { GetSchema, IGetSchema } from "../types/requests"; import { GetSchema, IGetSchema } from "../types/requests";
import handlePage from "../handlers/main"; import handlePage from "../handlers/main";
import { generateRequestUrl } from "../utils"; import { generateRequestUrl } from "../utils/generate";
export default async function getRoute(fastify: FastifyInstance) { export default async function getRoute(fastify: FastifyInstance) {
fastify.get<IGetSchema>( fastify.get<IGetSchema>(

View File

@ -1,7 +1,7 @@
import { EngineRequest, IParseSchema, parseSchema } from "../types/requests"; import { EngineRequest, IParseSchema, parseSchema } from "../types/requests";
import { FastifyInstance } from "fastify"; import { FastifyInstance } from "fastify";
import handlePage from "../handlers/main"; import handlePage from "../handlers/main";
import { generateRequestUrl } from "../utils"; import { generateRequestUrl } from "../utils/generate";
export default async function parseRoute(fastify: FastifyInstance) { export default async function parseRoute(fastify: FastifyInstance) {
fastify.get<IParseSchema>( fastify.get<IParseSchema>(

View File

@ -2,7 +2,7 @@ import { FastifyInstance } from "fastify";
import { GetRequest, IParseSchema, rawHtmlSchema } from "../types/requests"; import { GetRequest, IParseSchema, rawHtmlSchema } from "../types/requests";
import handlePage from "../handlers/main"; import handlePage from "../handlers/main";
import { generateRequestUrl } from "../utils"; import { generateRequestUrl } from "../utils/generate";
export default async function rawHtml(fastify: FastifyInstance) { export default async function rawHtml(fastify: FastifyInstance) {
fastify.get<IParseSchema>( fastify.get<IParseSchema>(

46
src/utils/islocal.ts Normal file
View File

@ -0,0 +1,46 @@
import dns from "dns";
import ipRangeCheck from "ip-range-check";
const subnets = [
"0.0.0.0/8",
"127.0.0.0/8",
"10.0.0.0/8",
"100.64.0.0/10",
"169.254.0.0/16",
"172.16.0.0/12",
"192.0.0.0/24",
"192.0.2.0/24",
"192.88.99.0/24",
"192.168.0.0/16",
"198.18.0.0/15",
"198.51.100.0/24",
"203.0.113.0/24",
"224.0.0.0/4",
"233.252.0.0/24",
"240.0.0.0/4",
"255.255.255.255/32",
"::/128",
"::1/128",
"::ffff:0:0/96",
"::ffff:0:0:0/96",
"64:ff9b::/96",
"64:ff9b:1::/48",
"100::/64",
"2001:0000::/32",
"2001:20::/28",
"2001:db8::/32",
"2002::/16",
"fc00::/7",
"fe80::/64",
"ff00::/8",
];
export default async function isLocalResource(url: URL): Promise<boolean> {
// Resolve domain name
const addr = (
await dns.promises.lookup(url.hostname)
).address;
// Check if IP is in local network
return ipRangeCheck(addr, subnets);
}