Skip to content

Commit 3f3a5a8

Browse files
committed
initial commit
0 parents  commit 3f3a5a8

File tree

10 files changed

+396
-0
lines changed

10 files changed

+396
-0
lines changed

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.*
2+
node_modules/
3+
Dockerfile
4+
README.md
5+
docker-compose.yml

.github/workflows/build.yml

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
name: Docker build
2+
3+
on:
4+
push:
5+
branches:
6+
- master
7+
tags:
8+
- 'v*'
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- uses: docker/setup-qemu-action@v3
18+
19+
- uses: docker/setup-buildx-action@v3
20+
21+
- name: Login to DockerHub
22+
uses: docker/login-action@v3
23+
with:
24+
username: ${{ vars.DOCKERHUB_USERNAME }}
25+
password: ${{ secrets.DOCKERHUB_TOKEN }}
26+
27+
- name: Log in to GitHub Container registry
28+
uses: docker/login-action@v3
29+
with:
30+
registry: ghcr.io
31+
username: ${{ github.actor }}
32+
password: ${{ secrets.GITHUB_TOKEN }}
33+
34+
- uses: docker/metadata-action@v5
35+
id: meta
36+
with:
37+
images: |
38+
${{ vars.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}
39+
ghcr.io/${{ github.repository }}
40+
tags: |
41+
type=ref,event=branch
42+
type=semver,pattern={{version}}
43+
type=semver,pattern={{major}}.{{minor}}
44+
type=semver,pattern={{major}}
45+
46+
- uses: docker/build-push-action@v5
47+
with:
48+
context: .
49+
push: true
50+
tags: ${{ steps.meta.outputs.tags }}
51+
labels: ${{ steps.meta.outputs.labels }}
52+
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64/v8
53+
54+
- name: Docker Hub Description
55+
uses: peter-evans/dockerhub-description@v4
56+
with:
57+
username: ${{ vars.DOCKERHUB_USERNAME }}
58+
password: ${{ secrets.DOCKERHUB_TOKEN }}
59+
repository: ${{ vars.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}
60+
short-description: ${{ github.event.repository.description }}
61+
enable-url-completion: true

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.env
2+
node_modules/

Dockerfile

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM node:lts-alpine
2+
3+
WORKDIR /app
4+
5+
COPY ./ ./
6+
7+
RUN npm ci
8+
9+
USER node
10+
11+
CMD ["node", "index.js"]

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 nezu.cc
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# niji-proxy
2+
3+
Proxy splitter server. Save data by only proxying the the domains that matter.
4+
5+
## Use case
6+
7+
You have a high quality proxy with limited bandwidth.
8+
The program using the proxy will try to proxy everything through it
9+
(images, scripts, cdn content, etc) but these requests don't actually
10+
need to be proxied since almost no CDN is doing any ip filtering.
11+
By using this project you can save bandwidth by only proxying the domains that matter
12+
(APIs, auth, etc).
13+
14+
## Limitations
15+
16+
- Only supports HTTP/HTTPS proxies
17+
- Only filtering by domain (no path filtering, most trafic is encrypted anyway)
18+
19+
## Usage
20+
21+
### Configuration
22+
23+
Configuration is done through environment variables. (.env file is supported)
24+
25+
| Variable | Description | Default | Required |
26+
|----------|-------------|---------|----------|
27+
| LISTEN_HOST | Host to listen on | `0.0.0.0` | NO |
28+
| LISTEN_PORT | Port to listen on | `8080` | NO |
29+
| GOOD_HOST_REGEX | Regex to match good hosts | - | YES |
30+
| GOOD_PROXY | Proxy to use for good hosts (matched by regex) | - | YES |
31+
| BAD_PROXY | Proxy to use for bad hosts (not matched by regex) | - | NO |
32+
| DEBUG | Enable debug logging | `false` | NO |
33+
34+
#### NOTES
35+
36+
> [!CAUTION]
37+
> This proxy has not authentication of its own,
38+
> do not expose it to the internet if you don't want to become an open proxy!
39+
> It's designed to be used locally (localhost, docker network, etc)
40+
41+
- if `GOOD_PROXY` has credentials, it will always use them and ignore the credentials from the client
42+
- if `GOOD_PROXY` has no credentials, it will passthrough the credentials from the client
43+
- if `BAD_PROXY` has no credentials, no authentication will be used (passthrough not supported to avoid leaking credentials)
44+
- if `BAD_PROXY` is missing, "bad" connections will be made directly to the target host (no proxy)
45+
- `DEBUG` will expose credentials in the logs, use with caution
46+
47+
#### Example
48+
49+
```env
50+
LISTEN_HOST=127.0.0.1
51+
LISTEN_PORT=8080
52+
GOOD_HOST_REGEX=^api\.
53+
GOOD_PROXY=http://user:pass@good-proxy-server:8080
54+
BAD_PROXY=http://bad-proxy-server:8080
55+
```
56+
57+
### Running
58+
59+
```bash
60+
git clone https://github.com/dumbasPL/niji-proxy.git
61+
cd niji-proxy
62+
# Create .env file with the configuration
63+
64+
# Using docker

docker-compose.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
services:
2+
niji-proxy:
3+
# image: dumbaspl/niji-proxy
4+
build:
5+
context: .
6+
dockerfile: Dockerfile
7+
ports:
8+
- "127.0.0.1:8080:8080"
9+
# use .env file
10+
env_file:
11+
- .env
12+
# or use environment
13+
# environment:
14+
# - GOOD_HOST_REGEX=
15+
# - GOOD_PROXY=
16+
# - BAD_PROXY=
17+
# # - DEBUG=1

index.js

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import 'dotenv/config';
2+
import { Server } from "proxy-chain";
3+
4+
console.log("Starting... (hint: use DEBUG=1 to enable debug messages)");
5+
6+
const {
7+
LISTEN_HOST = "0.0.0.0",
8+
LISTEN_PORT = "8080",
9+
GOOD_HOST_REGEX,
10+
GOOD_PROXY,
11+
BAD_PROXY,
12+
DEBUG: ENV_DEBUG,
13+
} = process.env;
14+
15+
/** @type {<T>(fn: () => T, message: string) => T} */
16+
const TRY = (fn, message) => {
17+
try {
18+
return fn();
19+
} catch (error) {
20+
console.error(message);
21+
process.exit(1);
22+
}
23+
}
24+
25+
/** @type {(condition: any, message: string, warn?: boolean) => void} */
26+
const CHECK = (condition, message, warn) => {
27+
if (!condition) {
28+
if (warn === true) {
29+
console.warn(message);
30+
return;
31+
}
32+
console.error(message);
33+
process.exit(1);
34+
}
35+
}
36+
37+
CHECK(!!GOOD_HOST_REGEX, "GOOD_HOST_REGEX is required");
38+
39+
const listenHost = LISTEN_HOST;
40+
const listenPort = parseInt(LISTEN_PORT);
41+
const goodHostRegex = TRY(() => new RegExp(GOOD_HOST_REGEX), "GOOD_HOST_REGEX must be a valid regular expression");
42+
const goodProxy = TRY(() => new URL(GOOD_PROXY), "GOOD_PROXY must be a valid URL");
43+
const badProxy = TRY(() => BAD_PROXY ? new URL(BAD_PROXY) : null, "BAD_PROXY must be a valid URL");
44+
45+
CHECK(listenPort >= 0 && listenPort <= 65535, "LISTEN_PORT must be a valid port number");
46+
CHECK(!/(?<!\\)\./.test(goodHostRegex), "GOOD_HOST_REGEX contains unescaped dot (.) character(s) (aka. wildcards). Make sure this is intentional! If not, escape them with a backslash (\\)", true);
47+
CHECK(/https?/.test(goodProxy.protocol), "GOOD_PROXY must be an HTTP or HTTPS proxy URL");
48+
49+
CHECK(badProxy != null, "BAD_PROXY is missing. Using direct connection instead", true);
50+
if (badProxy) {
51+
CHECK(/https?/.test(badProxy.protocol), "BAD_PROXY must be an HTTP or HTTPS proxy URL");
52+
CHECK(badProxy.username || badProxy.password, "no authentication provided for BAD_PROXY. BAD_PROXY will be used without authentication", true);
53+
}
54+
55+
const goodProxyHasAuth = !!(goodProxy.username || goodProxy.password);
56+
if (goodProxyHasAuth) {
57+
console.warn("GOOD_PROXY will use the provided authentication and ignore any authentication provided by the client");
58+
} else {
59+
console.warn("no authentication provided for GOOD_PROXY. GOOD_PROXY will use authentication from provided by the client");
60+
}
61+
62+
const DEBUG = (...message) => {
63+
if (ENV_DEBUG && ENV_DEBUG !== "0" && ENV_DEBUG !== "false") {
64+
console.debug(...message);
65+
}
66+
}
67+
68+
DEBUG('config', {
69+
listenHost,
70+
listenPort,
71+
goodHostRegex,
72+
goodProxy: goodProxy.href,
73+
badProxy: badProxy?.href ?? null,
74+
goodProxyHasAuth,
75+
})
76+
77+
const server = new Server({
78+
host: listenHost,
79+
port: listenPort,
80+
prepareRequestFunction: ({ hostname, username, password, connectionId }) => {
81+
if (!goodHostRegex.test(hostname)) {
82+
DEBUG(`${connectionId}: ${hostname} used ${badProxy ? "bad proxy" : "direct connection"}`);
83+
return {
84+
requestAuthentication: false,
85+
upstreamProxyUrl: badProxy?.href ?? undefined,
86+
};
87+
}
88+
if (goodProxyHasAuth) {
89+
DEBUG(`${connectionId}: ${hostname} used good proxy with predefined authentication`);
90+
return {
91+
requestAuthentication: false,
92+
upstreamProxyUrl: goodProxy.href,
93+
};
94+
}
95+
if (!username && !password) {
96+
DEBUG(`${connectionId}: ${hostname} missing authentication`);
97+
return {
98+
requestAuthentication: true,
99+
failMsg: "No authentication provided",
100+
};
101+
}
102+
const proxyUrl = new URL(goodProxy.href);
103+
proxyUrl.username = username;
104+
proxyUrl.password = password;
105+
DEBUG(`${connectionId}: ${hostname} used good proxy with provided authentication ${username}:${password}`);
106+
return {
107+
requestAuthentication: false,
108+
upstreamProxyUrl: proxyUrl.href,
109+
};
110+
},
111+
});
112+
113+
server.listen().then(() => {
114+
console.log(`Listening on ${server.host ?? '0.0.0.0'}:${server.port}`);
115+
});
116+
117+
// ctrl+c handler
118+
let closing = false;
119+
async function close() {
120+
if (closing) {
121+
console.log('Forcing exit...');
122+
await server.close(true);
123+
process.exit(1);
124+
}
125+
126+
console.log('Closing server...');
127+
closing = true;
128+
await server.close();
129+
process.exit(0);
130+
}
131+
process.on('SIGINT', close);
132+
process.on('SIGTERM', close);
133+
134+
// unhandled rejection handler
135+
process.on('unhandledRejection', (reason, promise) => {
136+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
137+
process.exit(1);
138+
});
139+
140+
// uncaught exception handler
141+
process.on('uncaughtException', (error) => {
142+
console.error('Uncaught Exception thrown', error);
143+
process.exit(1);
144+
});

package-lock.json

Lines changed: 47 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)