Compare commits
253 Commits
Author | SHA1 | Date | |
---|---|---|---|
06101cd1b1 | |||
438d65107d | |||
233b26c308 | |||
ba787729e8 | |||
4854d7c38d | |||
e841bda003 | |||
477b930a37 | |||
935bd95723 | |||
0e33ea4eb5 | |||
6181065963 | |||
1a586dcbd7 | |||
ee03224561 | |||
483cbb3634 | |||
c77b31b72c | |||
8cb8fa1a52 | |||
8e5bb12edb | |||
9be9a426ad | |||
32d875aed9 | |||
4747462cff | |||
70f69ef1ea | |||
2be1c57dd7 | |||
58bd6b4a85 | |||
63e1cd48e8 | |||
5150ddc18e | |||
4bee483954 | |||
4328d4365f | |||
21e9d0fd0d | |||
6c0c65bb1a | |||
23f61eb60b | |||
a4ad6c59c1 | |||
e67eff0fcc | |||
e5db2e171c | |||
7389072841 | |||
9dd56a9362 | |||
1e7c45918e | |||
49b65508a5 | |||
3e66debb01 | |||
f1bb1702c1 | |||
5abc0d8a14 | |||
9150e8c5fc | |||
8e4d3b7565 | |||
459ee7130f | |||
ceede84774 | |||
70bdf074a1 | |||
3eb4b576a7 | |||
c1f2c64e8b | |||
4ba943ee59 | |||
4c50e9a556 | |||
f2428412bb | |||
f114968298 | |||
def6644e80 | |||
c85f3da924 | |||
6cdb23ed66 | |||
0adb32e7e9 | |||
5d65e1668b | |||
632015a7bd | |||
972ee2af54 | |||
9b1ff5eed8 | |||
0739d1093a | |||
ee4f7fc48d | |||
f6e656361b | |||
e51c2a88cc | |||
7f8112930d | |||
b5c83b5c75 | |||
63ce1a44a4 | |||
759f70b84d | |||
45ce56b118 | |||
0cc7184e58 | |||
392e241208 | |||
32c6d77178 | |||
2c4316d2d3 | |||
62e6387c1d | |||
7fe22e962a | |||
3f1f718308 | |||
ce94d283c1 | |||
a1c4f3c341 | |||
8087bab197 | |||
db63e7bf79 | |||
2615a0ebd4 | |||
d5d77af98d | |||
1f1bf77807 | |||
d4269d290d | |||
e05e5ede55 | |||
b6c7f13baa | |||
055d328bd0 | |||
20b9a220fc | |||
2170fe3518 | |||
04b13e53b9 | |||
f1a4fae704 | |||
5ee5147606 | |||
748c6e14e4 | |||
f018957de4 | |||
a6583b037c | |||
3ab4144c9a | |||
0d2885ace4 | |||
1723275215 | |||
977d8b0310 | |||
5bb065f82b | |||
942b812f97 | |||
59a025b308 | |||
458e7d6b58 | |||
7b0f824d29 | |||
b5796b86d5 | |||
1f8ea59221 | |||
d717568572 | |||
28d050851f | |||
acbd109985 | |||
cc38a6d10e | |||
748b07efe2 | |||
be4fd0978a | |||
4521010b82 | |||
bd1f1a4c1c | |||
d3bdd56660 | |||
c38a7c4c32 | |||
858628196a | |||
4910679058 | |||
97db2012ca | |||
0ee13b4e06 | |||
21f5882fa3 | |||
48b43f9f0d | |||
d3d476fd53 | |||
b80b8a0a20 | |||
384943f697 | |||
e9239ed978 | |||
baf1844866 | |||
0b3d7f8a06 | |||
c38a2745e9 | |||
a0f39d1c5b | |||
c67ac868a5 | |||
90e1a0453e | |||
d7765fb5dc | |||
0fdd17b430 | |||
0562de6aa1 | |||
7b550a35aa | |||
fb66aac6e7 | |||
208790cfcf | |||
5978bbaf66 | |||
1c47eafe5f | |||
69e3a71354 | |||
21e92bf0c1 | |||
d732e6e7aa | |||
5fdfcdb407 | |||
49e2e90bda | |||
b8e53e7b42 | |||
1136841b3d | |||
42cbc51d22 | |||
2d16403ad1 | |||
afe847499a | |||
f980bb70b4 | |||
f192a8f041 | |||
64bf3aef6d | |||
a5e3cbd05b | |||
2f0fad999a | |||
5e6477720d | |||
bad8bf0688 | |||
4f1db106fb | |||
d47829a8b2 | |||
ca55d06244 | |||
7284924b26 | |||
10857aa12b | |||
c968c7f844 | |||
b07015f6c4 | |||
ca4ddade17 | |||
17eaea4124 | |||
d3d8f6ff57 | |||
906661c7f4 | |||
ea46caebb7 | |||
973e896fbf | |||
4605035b01 | |||
1e4e2c4ab6 | |||
30896b045f | |||
08f382b9fa | |||
1629dc1f5a | |||
b33acdea41 | |||
101470dcd4 | |||
ca73849541 | |||
b64523b0b2 | |||
963ad6efa4 | |||
d271029302 | |||
018fcbf71e | |||
fa04732241 | |||
da19fab8d8 | |||
8d318dca28 | |||
d03bfcc793 | |||
4ba2686977 | |||
d24c4d4b7a | |||
e1d4d6cf38 | |||
11344ac0df | |||
85fcfc3c36 | |||
e9ac7b2347 | |||
2c59540768 | |||
0f82d63f5c | |||
b5fcdadd3d | |||
6168b07414 | |||
588179335a | |||
703cbedad4 | |||
dd7e9e8416 | |||
da060fa986 | |||
df001e13f3 | |||
ef7e54be34 | |||
d800b6ed6e | |||
af42598464 | |||
93b1048cb7 | |||
29549b126e | |||
736113eb4e | |||
3b2d140836 | |||
70690f6400 | |||
ae561e3e88 | |||
8a02a0c506 | |||
58ec01526a | |||
1c3619040c | |||
0eabdcde28 | |||
3f935b3a03 | |||
c7b8b6ff66 | |||
a9815c61d2 | |||
82730877ce | |||
b68b143a3f | |||
3e7416574d | |||
7f843bef50 | |||
360c31c6b6 | |||
4ca748ec93 | |||
6dd3e473c6 | |||
3856a1d7fb | |||
fb76ecfd8a | |||
6c84406574 | |||
945065279f | |||
07a8d9bec6 | |||
2c55a6b819 | |||
8f4421fbc3 | |||
5eebc434bb | |||
ed03c3ec4b | |||
06808cb2c9 | |||
ba3d4d4240 | |||
9c49b9a9e5 | |||
f7259a6309 | |||
7c4d9cf301 | |||
5ddee94f99 | |||
209e5644c0 | |||
128c9f9751 | |||
0dfb763b17 | |||
2883d2b926 | |||
4113bf551e | |||
2896c92c04 | |||
f9e81ba7cd | |||
8fcada6d4e | |||
f43151916d | |||
d416355ea1 | |||
13c84f7146 | |||
3ddb4c4c75 | |||
b39f607e63 | |||
793e108e7e | |||
3238ea5cfd | |||
20dc9238e4 |
66
.gitea/workflows/default_nottags.yaml
Normal file
66
.gitea/workflows/default_nottags.yaml
Normal file
@ -0,0 +1,66 @@
|
||||
name: Default (not tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags-ignore:
|
||||
- '**'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Install pnpm and npmci
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
|
||||
- name: Run npm prepare
|
||||
run: npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
124
.gitea/workflows/default_tags.yaml
Normal file
124
.gitea/workflows/default_tags.yaml
Normal file
@ -0,0 +1,124 @@
|
||||
name: Default (tags)
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
env:
|
||||
IMAGE: code.foss.global/host.today/ht-docker-node:npmci
|
||||
NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@/${{gitea.repository}}.git
|
||||
NPMCI_TOKEN_NPM: ${{secrets.NPMCI_TOKEN_NPM}}
|
||||
NPMCI_TOKEN_NPM2: ${{secrets.NPMCI_TOKEN_NPM2}}
|
||||
NPMCI_GIT_GITHUBTOKEN: ${{secrets.NPMCI_GIT_GITHUBTOKEN}}
|
||||
NPMCI_URL_CLOUDLY: ${{secrets.NPMCI_URL_CLOUDLY}}
|
||||
|
||||
jobs:
|
||||
security:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: true
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Audit production dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --prod
|
||||
continue-on-error: true
|
||||
|
||||
- name: Audit development dependencies
|
||||
run: |
|
||||
npmci command npm config set registry https://registry.npmjs.org
|
||||
npmci command pnpm audit --audit-level=high --dev
|
||||
continue-on-error: true
|
||||
|
||||
test:
|
||||
if: ${{ always() }}
|
||||
needs: security
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Test stable
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm test
|
||||
|
||||
- name: Test build
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
npmci npm build
|
||||
|
||||
release:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm publish
|
||||
|
||||
metadata:
|
||||
needs: test
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: ${{ env.IMAGE }}
|
||||
continue-on-error: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Prepare
|
||||
run: |
|
||||
pnpm install -g pnpm
|
||||
pnpm install -g @ship.zone/npmci
|
||||
npmci npm prepare
|
||||
|
||||
- name: Code quality
|
||||
run: |
|
||||
npmci command npm install -g typescript
|
||||
npmci npm install
|
||||
|
||||
- name: Trigger
|
||||
run: npmci trigger
|
||||
|
||||
- name: Build docs and upload artifacts
|
||||
run: |
|
||||
npmci node install stable
|
||||
npmci npm install
|
||||
pnpm install -g @git.zone/tsdoc
|
||||
npmci command tsdoc
|
||||
continue-on-error: true
|
7
.gitignore
vendored
7
.gitignore
vendored
@ -3,7 +3,6 @@
|
||||
# artifacts
|
||||
coverage/
|
||||
public/
|
||||
pages/
|
||||
|
||||
# installs
|
||||
node_modules/
|
||||
@ -15,8 +14,6 @@ node_modules/
|
||||
|
||||
# builds
|
||||
dist/
|
||||
dist_web/
|
||||
dist_serve/
|
||||
dist_ts_web/
|
||||
dist_*/
|
||||
|
||||
# custom
|
||||
#------# custom
|
119
.gitlab-ci.yml
119
.gitlab-ci.yml
@ -1,119 +0,0 @@
|
||||
# gitzone ci_default
|
||||
image: registry.gitlab.com/hosttoday/ht-docker-node:npmci
|
||||
|
||||
cache:
|
||||
paths:
|
||||
- .npmci_cache/
|
||||
key: "$CI_BUILD_STAGE"
|
||||
|
||||
stages:
|
||||
- security
|
||||
- test
|
||||
- release
|
||||
- metadata
|
||||
|
||||
# ====================
|
||||
# security stage
|
||||
# ====================
|
||||
mirror:
|
||||
stage: security
|
||||
script:
|
||||
- npmci git mirror
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
snyk:
|
||||
stage: security
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci command npm install -g snyk
|
||||
- npmci command npm install --ignore-scripts
|
||||
- npmci command snyk test
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# test stage
|
||||
# ====================
|
||||
|
||||
testLTS:
|
||||
stage: test
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci node install lts
|
||||
- npmci npm install
|
||||
- npmci npm test
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
- priv
|
||||
|
||||
testBuild:
|
||||
stage: test
|
||||
script:
|
||||
- npmci npm prepare
|
||||
- npmci node install lts
|
||||
- npmci npm install
|
||||
- npmci command npm run build
|
||||
coverage: /\d+.?\d+?\%\s*coverage/
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
release:
|
||||
stage: release
|
||||
script:
|
||||
- npmci node install lts
|
||||
- npmci npm publish
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
# ====================
|
||||
# metadata stage
|
||||
# ====================
|
||||
codequality:
|
||||
stage: metadata
|
||||
allow_failure: true
|
||||
script:
|
||||
- npmci command npm install -g tslint typescript
|
||||
- npmci npm install
|
||||
- npmci command "tslint -c tslint.json ./ts/**/*.ts"
|
||||
tags:
|
||||
- docker
|
||||
- priv
|
||||
|
||||
trigger:
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci trigger
|
||||
only:
|
||||
- tags
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
|
||||
pages:
|
||||
image: hosttoday/ht-docker-dbase:npmci
|
||||
services:
|
||||
- docker:18-dind
|
||||
stage: metadata
|
||||
script:
|
||||
- npmci command npm install -g @gitzone/tsdoc
|
||||
- npmci npm prepare
|
||||
- npmci npm install
|
||||
- npmci command tsdoc
|
||||
tags:
|
||||
- docker
|
||||
- notpriv
|
||||
only:
|
||||
- tags
|
||||
artifacts:
|
||||
expire_in: 1 week
|
||||
paths:
|
||||
- public
|
||||
allow_failure: true
|
24
.vscode/launch.json
vendored
24
.vscode/launch.json
vendored
@ -2,28 +2,10 @@
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "current file",
|
||||
"type": "node",
|
||||
"command": "npm test",
|
||||
"name": "Run npm test",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"${relativeFile}"
|
||||
],
|
||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
},
|
||||
{
|
||||
"name": "test.ts",
|
||||
"type": "node",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"test/test.ts"
|
||||
],
|
||||
"runtimeArgs": ["-r", "@gitzone/tsrun"],
|
||||
"cwd": "${workspaceRoot}",
|
||||
"protocol": "inspector",
|
||||
"internalConsoleOptions": "openOnSessionStart"
|
||||
"type": "node-terminal"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
26
.vscode/settings.json
vendored
Normal file
26
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
{
|
||||
"json.schemas": [
|
||||
{
|
||||
"fileMatch": ["/npmextra.json"],
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"npmci": {
|
||||
"type": "object",
|
||||
"description": "settings for npmci"
|
||||
},
|
||||
"gitzone": {
|
||||
"type": "object",
|
||||
"description": "settings for gitzone",
|
||||
"properties": {
|
||||
"projectType": {
|
||||
"type": "string",
|
||||
"enum": ["website", "element", "service", "npm", "wcc"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
19
assets/certs/cert.pem
Normal file
19
assets/certs/cert.pem
Normal file
@ -0,0 +1,19 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDCzCCAfOgAwIBAgIUPU4tviz3ZvsMDjCz1NZRT16b0Y4wDQYJKoZIhvcNAQEL
|
||||
BQAwFTETMBEGA1UEAwwKcHVzaC5yb2NrczAeFw0yNTAyMDMyMzA5MzRaFw0yNjAy
|
||||
MDMyMzA5MzRaMBUxEzARBgNVBAMMCnB1c2gucm9ja3MwggEiMA0GCSqGSIb3DQEB
|
||||
AQUAA4IBDwAwggEKAoIBAQCZMkBYD/pYLBv9MiyHTLRT24kQyPeJBtZqryibi1jk
|
||||
BT1ZgNl3yo5U6kjj/nYBU/oy7M4OFC0xyaJQ4wpvLHu7xzREqwT9N9WcDcxaahUi
|
||||
P8+PsjGyznPrtXa1ASzGAYMNvXyWWp3351UWZHMEs6eY/Y7i8m4+0NwP5h8RNBCF
|
||||
KSFS41Ee9rNAMCnQSHZv1vIzKeVYPmYnCVmL7X2kQb+gS6Rvq5sEGLLKMC5QtTwI
|
||||
rdkPGpx4xZirIyf8KANbt0sShwUDpiCSuOCtpze08jMzoHLG9Nv97cJQjb/BhiES
|
||||
hLL+YjfAUFjq0rQ38zFKLJ87QB9Jym05mY6IadGQLXVXAgMBAAGjUzBRMB0GA1Ud
|
||||
DgQWBBQjpowWjrql/Eo2EVjl29xcjuCgkTAfBgNVHSMEGDAWgBQjpowWjrql/Eo2
|
||||
EVjl29xcjuCgkTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAY
|
||||
44vqbaf6ewFrZC0f3Kk4A10lC6qjWkcDFfw+JE8nzt+4+xPqp1eWgZKF2rONyAv2
|
||||
nG41Xygt19ByancXLU44KB24LX8F1GV5Oo7CGBA+xtoSPc0JulXw9fGclZDC6XiR
|
||||
P/+vhGgCHicbfP2O+N00pOifrTtf2tmOT4iPXRRo4TxmPzuCd+ZJTlBhPlKCmICq
|
||||
yGdAiEo6HsSiP+M5qVlNx8s57MhQYk5TpgmI6FU4mO7zfDfSatFonlg+aDbrnaqJ
|
||||
v/+km02M+oB460GmKwsSTnThHZgLNCLiKqD8bdziiCQjx5u0GjLI6468o+Aehb8l
|
||||
l/x9vWTTk/QKq41X5hFk
|
||||
-----END CERTIFICATE-----
|
28
assets/certs/key.pem
Normal file
28
assets/certs/key.pem
Normal file
@ -0,0 +1,28 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCZMkBYD/pYLBv9
|
||||
MiyHTLRT24kQyPeJBtZqryibi1jkBT1ZgNl3yo5U6kjj/nYBU/oy7M4OFC0xyaJQ
|
||||
4wpvLHu7xzREqwT9N9WcDcxaahUiP8+PsjGyznPrtXa1ASzGAYMNvXyWWp3351UW
|
||||
ZHMEs6eY/Y7i8m4+0NwP5h8RNBCFKSFS41Ee9rNAMCnQSHZv1vIzKeVYPmYnCVmL
|
||||
7X2kQb+gS6Rvq5sEGLLKMC5QtTwIrdkPGpx4xZirIyf8KANbt0sShwUDpiCSuOCt
|
||||
pze08jMzoHLG9Nv97cJQjb/BhiEShLL+YjfAUFjq0rQ38zFKLJ87QB9Jym05mY6I
|
||||
adGQLXVXAgMBAAECggEARGCBBq1PBHbfoUH5TQSIAlvdEEBa9+602lZG7jIioVfT
|
||||
W7Uem5Ctuan+kcDcY9hbNsqqZ+9KgsvoJmlIGXoF2jjeE/4vUmRO9AHWoc5yk2Be
|
||||
4NjcxN3QMLdEfiLBnLlFCOd4CdX1ZxZ6TG3WRpV3a1pVIeeqHGB1sKT6Xd/atcwG
|
||||
RvpiXzu0SutGxVb6WE9r6hovZ4fVERCyCRczUGrUH5ICbxf6E7L4u8xjEYR4uEKK
|
||||
/8ZkDqrWdRASDAdPPMNqnHUEAho/WnxpNeb6B4lvvv2QWxIS9H1OikF/NzWPgVNS
|
||||
oPpvtJgjyo5xdgLm3zE4lcSPNVSrh1TBXuAn9TG4WQKBgQDScPFkUNBqjC5iPMof
|
||||
bqDHlhlptrHmiv9LC0lgjEDPgIEQfjLfdCugwDk32QyAcb5B60upDYeqCFDkfV/C
|
||||
T536qxevYPjPAjahLPHqMxkWpjvtY6NOTgbbcpVtblU2Fj8R8qbyPNADG31LicU9
|
||||
GVPtQ4YcVaMWCYbg5107+9dFWQKBgQC6XK+foKK+81RFdrqaNNgebTWTsANnBcZe
|
||||
xl0bj6oL5yY0IzroxHvgcNS7UMriWCu+K2xfkUBdMmxU773VN5JQ5k15ezjgtrvc
|
||||
8oAaEsxYP4su12JSTC/zsBANUgrNbFj8++qqKYWt2aQc2O/kbZ4MNfekIVFc8AjM
|
||||
2X9PxvxKLwKBgHXL7QO3TQLnVyt8VbQEjBFMzwriznB7i+4o8jkOKVU93IEr8zQr
|
||||
5iQElcLSR3I6uUJTALYvsaoXH5jXKVwujwL69LYiNQRDe+r6qqvrUHbiNJdsd8Rk
|
||||
XuhGGqj34tD04Pcd+h+MtO+YWqmHBBZwcA9XBeIkebbjPFH2kLT8AwN5AoGAYQy9
|
||||
hMJxnkE3hIkk+gNE/OtgeE20J+Vw/ZANkrnJEzPHyGUEW41e+W2oyvdzAFZsSTdx
|
||||
037f5ujIU58Z27x53NliRT4vS4693H0Iyws5EUfeIoGVuUflvODWKymraHjhCrXh
|
||||
6cV/0R5DAabTnsCbCr7b/MRBC8YQvyUQ0KnOXo8CgYBQYGpvJnSWyvsCjtb6apTP
|
||||
drjcBhVd0aSBpLGtDdtUCV4oLl9HPy+cLzcGaqckBqCwEq5DKruhMEf7on56bUMd
|
||||
m/3ItFk1TnhysAeJHb3zLqmJ9CKBitpqLlsOE7MEXVNmbTYeXU10Uo9yOfyt1i7T
|
||||
su+nT5VtyPkmF/l4wZl5+g==
|
||||
-----END PRIVATE KEY-----
|
166
changelog.md
Normal file
166
changelog.md
Normal file
@ -0,0 +1,166 @@
|
||||
# Changelog
|
||||
|
||||
## 2025-02-21 - 3.8.1 - fix(plugins)
|
||||
Simplified plugin import structure across codebase
|
||||
|
||||
- Consolidated plugin imports under a single 'plugins.ts' file.
|
||||
- Replaced individual plugin imports in smartproxy files with the consolidated plugin imports.
|
||||
- Fixed error handling for early socket errors in PortProxy setup.
|
||||
|
||||
## 2025-02-21 - 3.8.0 - feat(PortProxy)
|
||||
Add active connection tracking and logging in PortProxy
|
||||
|
||||
- Implemented a feature to track active incoming connections in PortProxy.
|
||||
- Active connections are now logged every 10 seconds for monitoring purposes.
|
||||
- Refactored connection handling to ensure proper cleanup and logging.
|
||||
|
||||
## 2025-02-21 - 3.7.3 - fix(portproxy)
|
||||
Fix handling of connections in PortProxy to improve stability and performance.
|
||||
|
||||
- Improved IP normalization and matching
|
||||
- Better SNI extraction and handling for TLS
|
||||
- Streamlined connection handling with robust error management
|
||||
|
||||
## 2025-02-21 - 3.7.2 - fix(PortProxy)
|
||||
Improve SNICallback and connection handling in PortProxy
|
||||
|
||||
- Fixed SNICallback to create minimal TLS context for SNI.
|
||||
- Changed connection setup to use net.connect for raw passthrough.
|
||||
|
||||
## 2025-02-21 - 3.7.1 - fix(smartproxy.portproxy)
|
||||
Optimize SNI handling by simplifying context creation
|
||||
|
||||
- Removed unnecessary SecureContext creation for SNI requests in PortProxy
|
||||
- Improved handling of SNI passthrough by acknowledging requests without context creation
|
||||
|
||||
## 2025-02-21 - 3.7.0 - feat(PortProxy)
|
||||
Add optional source IP preservation support in PortProxy
|
||||
|
||||
- Added a feature to optionally preserve the client's source IP when proxying connections.
|
||||
- Enhanced test cases to include scenarios for source IP preservation.
|
||||
|
||||
## 2025-02-21 - 3.6.0 - feat(PortProxy)
|
||||
Add feature to preserve original client IP through chained proxies
|
||||
|
||||
- Added support to bind local address in PortProxy to preserve original client IP.
|
||||
- Implemented test for chained proxies to ensure client IP is preserved.
|
||||
|
||||
## 2025-02-21 - 3.5.0 - feat(PortProxy)
|
||||
Enhance PortProxy to support domain-specific target IPs
|
||||
|
||||
- Introduced support for domain-specific target IP configurations in PortProxy.
|
||||
- Updated connection handling to prioritize domain-specific target IPs if provided.
|
||||
- Added tests to verify forwarding based on domain-specific target IPs.
|
||||
|
||||
## 2025-02-21 - 3.4.4 - fix(PortProxy)
|
||||
Fixed handling of SNI domain connections and IP allowance checks
|
||||
|
||||
- Improved logic for handling SNI domain checks, ensuring IPs are correctly verified.
|
||||
- Fixed issue where default allowed IPs were not being checked correctly for non-SNI connections.
|
||||
- Revised the SNICallback behavior to handle connections more gracefully when domain configurations are unavailable.
|
||||
|
||||
## 2025-02-21 - 3.4.3 - fix(PortProxy)
|
||||
Fixed indentation issue and ensured proper cleanup of sockets in PortProxy
|
||||
|
||||
- Fixed inconsistent indentation in IP allowance check.
|
||||
- Ensured proper cleanup of sockets on connection end in PortProxy.
|
||||
|
||||
## 2025-02-21 - 3.4.2 - fix(smartproxy)
|
||||
Enhance SSL/TLS handling with SNI and error logging
|
||||
|
||||
- Improved handling for SNI-enabled and non-SNI connections
|
||||
- Added detailed logging for connection establishment and rejections
|
||||
- Introduced error logging for TLS client errors and server errors
|
||||
|
||||
## 2025-02-21 - 3.4.1 - fix(PortProxy)
|
||||
Normalize IP addresses for port proxy to handle IPv4-mapped IPv6 addresses.
|
||||
|
||||
- Improved IP normalization logic in PortProxy to support IPv4-mapped IPv6 addresses.
|
||||
- Updated isAllowed function to expand patterns for better matching accuracy.
|
||||
|
||||
## 2025-02-21 - 3.4.0 - feat(PortProxy)
|
||||
Enhanced PortProxy with custom target host and improved testing
|
||||
|
||||
- PortProxy constructor now accepts 'fromPort', 'toPort', and optional 'toHost' directly from settings
|
||||
- Refactored test cases to cover forwarding to the custom host
|
||||
- Added support to handle multiple concurrent connections
|
||||
- Refactored internal connection handling logic to utilize default configurations
|
||||
|
||||
## 2025-02-21 - 3.3.1 - fix(PortProxy)
|
||||
fixed import usage of net and tls libraries for PortProxy
|
||||
|
||||
- Corrected the use of plugins for importing 'tls' and 'net' libraries in the PortProxy module.
|
||||
- Updated the constructor of PortProxy to accept combined tls options with ProxySettings.
|
||||
|
||||
## 2025-02-21 - 3.3.0 - feat(PortProxy)
|
||||
Enhanced PortProxy with domain and IP filtering, SNI support, and minimatch integration
|
||||
|
||||
- Added new ProxySettings interface to configure domain patterns, SNI, and default allowed IPs.
|
||||
- Integrated minimatch to filter allowed IPs and domains.
|
||||
- Enabled SNI support for PortProxy connections.
|
||||
- Updated port proxy test to accommodate new settings.
|
||||
|
||||
## 2025-02-04 - 3.2.0 - feat(testing)
|
||||
Added a comprehensive test suite for the PortProxy class
|
||||
|
||||
- Set up a test environment for PortProxy using net.Server.
|
||||
- Test coverage includes starting and stopping the proxy, handling TCP connections, concurrent connections, and timeouts.
|
||||
- Ensures proper resource cleanup after tests.
|
||||
|
||||
## 2025-02-04 - 3.1.4 - fix(core)
|
||||
No uncommitted changes. Preparing for potential minor improvements or bug fixes.
|
||||
|
||||
|
||||
## 2025-02-04 - 3.1.3 - fix(networkproxy)
|
||||
Refactor and improve WebSocket handling and request processing
|
||||
|
||||
- Improved error handling in WebSocket connection and request processing.
|
||||
- Refactored the WebSocket handling in NetworkProxy to use a unified error logging mechanism.
|
||||
|
||||
## 2025-02-04 - 3.1.2 - fix(core)
|
||||
Refactor certificate handling across the project
|
||||
|
||||
- Moved certificate keys and certs to the assets/certs directory.
|
||||
- Updated test utilities to load certificates from the central location.
|
||||
- Cleaned up redundant code and improved error logging regarding certificates.
|
||||
- Ensured correct handling of host header in ProxyRouter class.
|
||||
|
||||
## 2025-02-03 - 3.1.1 - fix(workflow)
|
||||
Update Gitea workflow paths and dependencies
|
||||
|
||||
- Updated registry paths for npmci image and repositories in Gitea workflow files.
|
||||
- Fixed dependency paths in package.json.
|
||||
- Completed adding typescript to the list of devDependencies.
|
||||
|
||||
## 2024-10-07 - 3.1.0 - feat(NetworkProxy)
|
||||
Introduce WebSocket heartbeat to maintain active connections in NetworkProxy
|
||||
|
||||
- Added heartbeat mechanism to WebSocket connections to ensure they remain active.
|
||||
- Terminating WebSocket if no pong is received for 5 minutes.
|
||||
- Set up heartbeat interval to run every 1 minute for connection checks.
|
||||
|
||||
## 2024-10-07 - 3.0.61 - fix(networkproxy)
|
||||
Improve error handling for proxy requests
|
||||
|
||||
- Wrapped proxy request logic in a try-catch block to handle errors gracefully.
|
||||
- Improved error handling for WebSocket communication by checking errors before attempting to send messages.
|
||||
- Added logging for error cases to aid in debugging.
|
||||
|
||||
## 2024-05-29 - 3.0.60 - various updates
|
||||
Maintenance updates and adjustments.
|
||||
|
||||
- Updated project description
|
||||
- Updated tsconfig settings
|
||||
- Updated npmextra.json with new githost info
|
||||
|
||||
## 2023-07-27 - 3.0.58 to 3.0.59 - core improvements
|
||||
Improvements and internal restructuring.
|
||||
|
||||
- Switch to a new organizational scheme
|
||||
- Core updates and adjustments
|
||||
|
||||
## 2022-07-29 - 2.0.16 to 3.0.0 - major transition
|
||||
This release marks a major transition with several breaking changes.
|
||||
|
||||
- BREAKING CHANGE: switched core to ESM (EcmaScript Module)
|
||||
- Major core updates
|
@ -1,17 +1,34 @@
|
||||
{
|
||||
"gitzone": {
|
||||
"projectType": "npm",
|
||||
"module": {
|
||||
"githost": "gitlab.com",
|
||||
"gitscope": "pushrocks",
|
||||
"githost": "code.foss.global",
|
||||
"gitscope": "push.rocks",
|
||||
"gitrepo": "smartproxy",
|
||||
"shortDescription": "a proxy for handling high workloads of proxying",
|
||||
"npmPackagename": "@pushrocks/smartproxy",
|
||||
"description": "a proxy for handling high workloads of proxying",
|
||||
"npmPackagename": "@push.rocks/smartproxy",
|
||||
"license": "MIT",
|
||||
"projectDomain": "push.rocks"
|
||||
"projectDomain": "push.rocks",
|
||||
"keywords": [
|
||||
"proxy",
|
||||
"network traffic",
|
||||
"high workload",
|
||||
"http",
|
||||
"https",
|
||||
"websocket",
|
||||
"network routing",
|
||||
"ssl redirect",
|
||||
"port mapping",
|
||||
"reverse proxy",
|
||||
"authentication"
|
||||
]
|
||||
}
|
||||
},
|
||||
"npmci": {
|
||||
"npmGlobalTools": [],
|
||||
"npmAccessLevel": "public"
|
||||
},
|
||||
"tsdoc": {
|
||||
"legal": "\n## License and Legal Information\n\nThis repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository. \n\n**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.\n\n### Trademarks\n\nThis project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.\n\n### Company Information\n\nTask Venture Capital GmbH \nRegistered at District court Bremen HRB 35230 HB, Germany\n\nFor any legal inquiries or if you require further information, please contact us via email at hello@task.vc.\n\nBy using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.\n"
|
||||
}
|
||||
}
|
1779
package-lock.json
generated
1779
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
83
package.json
83
package.json
@ -1,41 +1,76 @@
|
||||
{
|
||||
"name": "@pushrocks/smartproxy",
|
||||
"version": "1.0.15",
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "3.8.1",
|
||||
"private": false,
|
||||
"description": "a proxy for handling high workloads of proxying",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"main": "dist_ts/index.js",
|
||||
"typings": "dist_ts/index.d.ts",
|
||||
"type": "module",
|
||||
"author": "Lossless GmbH",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"test": "(tstest test/)",
|
||||
"build": "(tsbuild)",
|
||||
"format": "(gitzone format)"
|
||||
"build": "(tsbuild --web --allowimplicitany)",
|
||||
"format": "(gitzone format)",
|
||||
"buildDocs": "tsdoc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@gitzone/tsbuild": "^2.0.22",
|
||||
"@gitzone/tstest": "^1.0.15",
|
||||
"@pushrocks/tapbundle": "^3.0.7",
|
||||
"@types/node": "^12.7.2",
|
||||
"tslint": "^5.19.0",
|
||||
"tslint-config-prettier": "^1.15.0"
|
||||
"@git.zone/tsbuild": "^2.1.66",
|
||||
"@git.zone/tsrun": "^1.2.44",
|
||||
"@git.zone/tstest": "^1.0.77",
|
||||
"@push.rocks/tapbundle": "^5.5.6",
|
||||
"@types/node": "^22.13.0",
|
||||
"typescript": "^5.7.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pushrocks/smartpromise": "^3.0.2",
|
||||
"@pushrocks/smartrequest": "^1.1.20",
|
||||
"@tsclass/tsclass": "^2.0.3",
|
||||
"@types/ws": "^6.0.2",
|
||||
"ws": "^7.1.2"
|
||||
"@push.rocks/lik": "^6.1.0",
|
||||
"@push.rocks/smartdelay": "^3.0.5",
|
||||
"@push.rocks/smartpromise": "^4.2.2",
|
||||
"@push.rocks/smartrequest": "^2.0.23",
|
||||
"@push.rocks/smartstring": "^4.0.15",
|
||||
"@tsclass/tsclass": "^4.4.0",
|
||||
"@types/ws": "^8.5.14",
|
||||
"ws": "^8.18.0",
|
||||
"minimatch": "^9.0.3",
|
||||
"@types/minimatch": "^5.1.2"
|
||||
},
|
||||
"files": [
|
||||
"ts/*",
|
||||
"ts_web/*",
|
||||
"dist/*",
|
||||
"dist_web/*",
|
||||
"dist_ts_web/*",
|
||||
"assets/*",
|
||||
"ts/**/*",
|
||||
"ts_web/**/*",
|
||||
"dist/**/*",
|
||||
"dist_*/**/*",
|
||||
"dist_ts/**/*",
|
||||
"dist_ts_web/**/*",
|
||||
"assets/**/*",
|
||||
"cli.js",
|
||||
"npmextra.json",
|
||||
"readme.md"
|
||||
]
|
||||
],
|
||||
"browserslist": [
|
||||
"last 1 chrome versions"
|
||||
],
|
||||
"keywords": [
|
||||
"proxy",
|
||||
"network traffic",
|
||||
"high workload",
|
||||
"http",
|
||||
"https",
|
||||
"websocket",
|
||||
"network routing",
|
||||
"ssl redirect",
|
||||
"port mapping",
|
||||
"reverse proxy",
|
||||
"authentication"
|
||||
],
|
||||
"homepage": "https://code.foss.global/push.rocks/smartproxy#readme",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://code.foss.global/push.rocks/smartproxy.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://code.foss.global/push.rocks/smartproxy/issues"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {}
|
||||
}
|
||||
}
|
||||
|
9702
pnpm-lock.yaml
generated
Normal file
9702
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
1
readme.hints.md
Normal file
1
readme.hints.md
Normal file
@ -0,0 +1 @@
|
||||
|
132
readme.md
132
readme.md
@ -1,26 +1,120 @@
|
||||
# @pushrocks/smartproxy
|
||||
a proxy for handling high workloads of proxying
|
||||
# @push.rocks/smartproxy
|
||||
|
||||
## Availabililty and Links
|
||||
* [npmjs.org (npm package)](https://www.npmjs.com/package/@pushrocks/smartproxy)
|
||||
* [gitlab.com (source)](https://gitlab.com/pushrocks/smartproxy)
|
||||
* [github.com (source mirror)](https://github.com/pushrocks/smartproxy)
|
||||
* [docs (typedoc)](https://pushrocks.gitlab.io/smartproxy/)
|
||||
A proxy for handling high workloads of proxying.
|
||||
|
||||
## Status for master
|
||||
[](https://gitlab.com/pushrocks/smartproxy/commits/master)
|
||||
[](https://gitlab.com/pushrocks/smartproxy/commits/master)
|
||||
[](https://www.npmjs.com/package/@pushrocks/smartproxy)
|
||||
[](https://snyk.io/test/npm/@pushrocks/smartproxy)
|
||||
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
[](https://nodejs.org/dist/latest-v10.x/docs/api/)
|
||||
[](https://prettier.io/)
|
||||
## Install
|
||||
|
||||
To install `@push.rocks/smartproxy`, run the following command in your project's root directory:
|
||||
|
||||
```bash
|
||||
npm install @push.rocks/smartproxy --save
|
||||
```
|
||||
|
||||
This will add `@push.rocks/smartproxy` to your project's dependencies.
|
||||
|
||||
## Usage
|
||||
|
||||
For further information read the linked docs at the top of this readme.
|
||||
`@push.rocks/smartproxy` is a versatile package for setting up and handling proxies with various capabilities such as SSL redirection, port proxying, and creating network proxies with complex routing rules. Below is a comprehensive guide on using its features.
|
||||
|
||||
> MIT licensed | **©** [Lossless GmbH](https://lossless.gmbh)
|
||||
| By using this npm module you agree to our [privacy policy](https://lossless.gmbH/privacy)
|
||||
### Setting Up a Network Proxy
|
||||
|
||||
[](https://maintainedby.lossless.com)
|
||||
Create a network proxy to route incoming HTTPS requests to different local servers based on the hostname.
|
||||
|
||||
```typescript
|
||||
import { NetworkProxy } from '@push.rocks/smartproxy';
|
||||
|
||||
// Instantiate the NetworkProxy with desired options
|
||||
const myNetworkProxy = new NetworkProxy({ port: 443 });
|
||||
|
||||
// Define your reverse proxy configurations
|
||||
const proxyConfigs = [
|
||||
{
|
||||
destinationIp: '127.0.0.1',
|
||||
destinationPort: '3000',
|
||||
hostName: 'example.com',
|
||||
privateKey: `-----BEGIN PRIVATE KEY-----
|
||||
PRIVATE_KEY_CONTENT
|
||||
-----END PRIVATE KEY-----`,
|
||||
publicKey: `-----BEGIN CERTIFICATE-----
|
||||
CERTIFICATE_CONTENT
|
||||
-----END CERTIFICATE-----`,
|
||||
},
|
||||
// Add more reverse proxy configurations here
|
||||
];
|
||||
|
||||
// Start the network proxy
|
||||
await myNetworkProxy.start();
|
||||
|
||||
// Update proxy configurations dynamically
|
||||
await myNetworkProxy.updateProxyConfigs(proxyConfigs);
|
||||
|
||||
// Optionally, add default headers to all responses
|
||||
await myNetworkProxy.addDefaultHeaders({
|
||||
'X-Powered-By': 'smartproxy',
|
||||
});
|
||||
```
|
||||
|
||||
### Port Proxying
|
||||
|
||||
You can also set up a port proxy to forward traffic from one port to another, which is useful for dynamic port forwarding scenarios.
|
||||
|
||||
```typescript
|
||||
import { PortProxy } from '@push.rocks/smartproxy';
|
||||
|
||||
// Create a PortProxy to forward traffic from port 5000 to port 3000
|
||||
const myPortProxy = new PortProxy(5000, 3000);
|
||||
|
||||
// Start the port proxy
|
||||
await myPortProxy.start();
|
||||
|
||||
// To stop the port proxy, simply call
|
||||
await myPortProxy.stop();
|
||||
```
|
||||
|
||||
### Enabling SSL Redirection
|
||||
|
||||
Easily redirect HTTP traffic to HTTPS using the `SslRedirect` class. This is particularly useful when ensuring all traffic uses encryption.
|
||||
|
||||
```typescript
|
||||
import { SslRedirect } from '@push.rocks/smartproxy';
|
||||
|
||||
// Instantiate the SslRedirect on port 80 (HTTP)
|
||||
const mySslRedirect = new SslRedirect(80);
|
||||
|
||||
// Start listening and redirecting to HTTPS
|
||||
await mySslRedirect.start();
|
||||
|
||||
// To stop the redirection, use
|
||||
await mySslRedirect.stop();
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
The package integrates seamlessly with TypeScript, allowing for advanced use cases, such as implementing custom routing logic, authentication mechanisms, and handling WebSocket connections through the network proxy.
|
||||
|
||||
For a more advanced setup involving WebSocket proxying and dynamic configuration reloading, refer to the network proxy example provided above. The WebSocket support demonstrates how seamless it is to work with real-time applications.
|
||||
|
||||
Remember, when dealing with certificates and private keys for HTTPS configurations, always secure your keys and store them appropriately.
|
||||
|
||||
`@push.rocks/smartproxy` provides a solid foundation for handling high workloads and complex proxying requirements with ease, whether you're implementing SSL redirections, port forwarding, or extensive routing and WebSocket support in your network.
|
||||
|
||||
For more information on how to use the features, refer to the in-depth documentation available in the package's repository or the npm package description.
|
||||
|
||||
## License and Legal Information
|
||||
|
||||
This repository contains open-source code that is licensed under the MIT License. A copy of the MIT License can be found in the [license](license) file within this repository.
|
||||
|
||||
**Please note:** The MIT License does not grant permission to use the trade names, trademarks, service marks, or product names of the project, except as required for reasonable and customary use in describing the origin of the work and reproducing the content of the NOTICE file.
|
||||
|
||||
### Trademarks
|
||||
|
||||
This project is owned and maintained by Task Venture Capital GmbH. The names and logos associated with Task Venture Capital GmbH and any related products or services are trademarks of Task Venture Capital GmbH and are not included within the scope of the MIT license granted herein. Use of these trademarks must comply with Task Venture Capital GmbH's Trademark Guidelines, and any usage must be approved in writing by Task Venture Capital GmbH.
|
||||
|
||||
### Company Information
|
||||
|
||||
Task Venture Capital GmbH
|
||||
Registered at District court Bremen HRB 35230 HB, Germany
|
||||
|
||||
For any legal inquiries or if you require further information, please contact us via email at hello@task.vc.
|
||||
|
||||
By using this repository, you acknowledge that you have read this section, agree to comply with its terms, and understand that the licensing of the code does not imply endorsement by Task Venture Capital GmbH of any derivative works.
|
||||
|
37
test/helpers/certificates.ts
Normal file
37
test/helpers/certificates.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import * as tls from 'tls';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export interface TestCertificates {
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
}
|
||||
|
||||
export function loadTestCertificates(): TestCertificates {
|
||||
const certPath = path.join(__dirname, '..', '..', 'assets', 'certs', 'cert.pem');
|
||||
const keyPath = path.join(__dirname, '..', '..', 'assets', 'certs', 'key.pem');
|
||||
|
||||
// Read certificates
|
||||
const publicKey = fs.readFileSync(certPath, 'utf8');
|
||||
const privateKey = fs.readFileSync(keyPath, 'utf8');
|
||||
|
||||
// Validate certificates
|
||||
try {
|
||||
// Try to create a secure context with the certificates
|
||||
tls.createSecureContext({
|
||||
cert: publicKey,
|
||||
key: privateKey
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(`Invalid certificates: ${error.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
privateKey,
|
||||
publicKey
|
||||
};
|
||||
}
|
253
test/test.portproxy.ts
Normal file
253
test/test.portproxy.ts
Normal file
@ -0,0 +1,253 @@
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { PortProxy } from '../ts/smartproxy.portproxy.js';
|
||||
|
||||
let testServer: net.Server;
|
||||
let portProxy: PortProxy;
|
||||
const TEST_SERVER_PORT = 4000;
|
||||
const PROXY_PORT = 4001;
|
||||
const TEST_DATA = 'Hello through port proxy!';
|
||||
|
||||
// Helper function to create a test TCP server
|
||||
function createTestServer(port: number): Promise<net.Server> {
|
||||
return new Promise((resolve) => {
|
||||
const server = net.createServer((socket) => {
|
||||
socket.on('data', (data) => {
|
||||
// Echo the received data back
|
||||
socket.write(`Echo: ${data.toString()}`);
|
||||
});
|
||||
|
||||
socket.on('error', (error) => {
|
||||
console.error('[Test Server] Socket error:', error);
|
||||
});
|
||||
});
|
||||
|
||||
server.listen(port, () => {
|
||||
console.log(`[Test Server] Listening on port ${port}`);
|
||||
resolve(server);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Helper function to create a test client connection
|
||||
function createTestClient(port: number, data: string): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = new net.Socket();
|
||||
let response = '';
|
||||
|
||||
client.connect(port, 'localhost', () => {
|
||||
console.log('[Test Client] Connected to server');
|
||||
client.write(data);
|
||||
});
|
||||
|
||||
client.on('data', (chunk) => {
|
||||
response += chunk.toString();
|
||||
client.end();
|
||||
});
|
||||
|
||||
client.on('end', () => {
|
||||
resolve(response);
|
||||
});
|
||||
|
||||
client.on('error', (error) => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Setup test environment
|
||||
tap.test('setup port proxy test environment', async () => {
|
||||
testServer = await createTestServer(TEST_SERVER_PORT);
|
||||
portProxy = new PortProxy({
|
||||
fromPort: PROXY_PORT,
|
||||
toPort: TEST_SERVER_PORT,
|
||||
toHost: 'localhost',
|
||||
domains: [],
|
||||
sniEnabled: false,
|
||||
defaultAllowedIPs: ['127.0.0.1']
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should start port proxy', async () => {
|
||||
await portProxy.start();
|
||||
expect(portProxy.netServer.listening).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('should forward TCP connections and data to localhost', async () => {
|
||||
const response = await createTestClient(PROXY_PORT, TEST_DATA);
|
||||
expect(response).toEqual(`Echo: ${TEST_DATA}`);
|
||||
});
|
||||
|
||||
tap.test('should forward TCP connections to custom host', async () => {
|
||||
// Create a new proxy instance with a custom host
|
||||
const customHostProxy = new PortProxy({
|
||||
fromPort: PROXY_PORT + 1,
|
||||
toPort: TEST_SERVER_PORT,
|
||||
toHost: '127.0.0.1',
|
||||
domains: [],
|
||||
sniEnabled: false,
|
||||
defaultAllowedIPs: ['127.0.0.1']
|
||||
});
|
||||
|
||||
await customHostProxy.start();
|
||||
const response = await createTestClient(PROXY_PORT + 1, TEST_DATA);
|
||||
expect(response).toEqual(`Echo: ${TEST_DATA}`);
|
||||
await customHostProxy.stop();
|
||||
});
|
||||
|
||||
tap.test('should forward connections based on domain-specific target IP', async () => {
|
||||
// Create a second test server on a different port
|
||||
const TEST_SERVER_PORT_2 = TEST_SERVER_PORT + 100;
|
||||
const testServer2 = await createTestServer(TEST_SERVER_PORT_2);
|
||||
|
||||
// Create a proxy with domain-specific target IPs
|
||||
const domainProxy = new PortProxy({
|
||||
fromPort: PROXY_PORT + 2,
|
||||
toPort: TEST_SERVER_PORT, // default port
|
||||
toHost: 'localhost', // default host
|
||||
domains: [{
|
||||
domain: 'domain1.test',
|
||||
allowedIPs: ['127.0.0.1'],
|
||||
targetIP: '127.0.0.1'
|
||||
}, {
|
||||
domain: 'domain2.test',
|
||||
allowedIPs: ['127.0.0.1'],
|
||||
targetIP: 'localhost'
|
||||
}],
|
||||
sniEnabled: false, // We'll test without SNI first since this is a TCP proxy test
|
||||
defaultAllowedIPs: ['127.0.0.1']
|
||||
});
|
||||
|
||||
await domainProxy.start();
|
||||
|
||||
// Test default connection (should use default host)
|
||||
const response1 = await createTestClient(PROXY_PORT + 2, TEST_DATA);
|
||||
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
|
||||
|
||||
// Create another proxy with different default host
|
||||
const domainProxy2 = new PortProxy({
|
||||
fromPort: PROXY_PORT + 3,
|
||||
toPort: TEST_SERVER_PORT,
|
||||
toHost: '127.0.0.1',
|
||||
domains: [],
|
||||
sniEnabled: false,
|
||||
defaultAllowedIPs: ['127.0.0.1']
|
||||
});
|
||||
|
||||
await domainProxy2.start();
|
||||
const response2 = await createTestClient(PROXY_PORT + 3, TEST_DATA);
|
||||
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
|
||||
|
||||
await domainProxy.stop();
|
||||
await domainProxy2.stop();
|
||||
await new Promise<void>((resolve) => testServer2.close(() => resolve()));
|
||||
});
|
||||
|
||||
tap.test('should handle multiple concurrent connections', async () => {
|
||||
const concurrentRequests = 5;
|
||||
const requests = Array(concurrentRequests).fill(null).map((_, i) =>
|
||||
createTestClient(PROXY_PORT, `${TEST_DATA} ${i + 1}`)
|
||||
);
|
||||
|
||||
const responses = await Promise.all(requests);
|
||||
|
||||
responses.forEach((response, i) => {
|
||||
expect(response).toEqual(`Echo: ${TEST_DATA} ${i + 1}`);
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should handle connection timeouts', async () => {
|
||||
const client = new net.Socket();
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
client.connect(PROXY_PORT, 'localhost', () => {
|
||||
// Don't send any data, just wait for timeout
|
||||
client.on('close', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should stop port proxy', async () => {
|
||||
await portProxy.stop();
|
||||
expect(portProxy.netServer.listening).toBeFalse();
|
||||
});
|
||||
|
||||
// Cleanup
|
||||
tap.test('should support optional source IP preservation in chained proxies', async () => {
|
||||
// Test 1: Without IP preservation (default behavior)
|
||||
const firstProxyDefault = new PortProxy({
|
||||
fromPort: PROXY_PORT + 4,
|
||||
toPort: PROXY_PORT + 5,
|
||||
toHost: 'localhost',
|
||||
domains: [],
|
||||
sniEnabled: false,
|
||||
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
|
||||
});
|
||||
|
||||
const secondProxyDefault = new PortProxy({
|
||||
fromPort: PROXY_PORT + 5,
|
||||
toPort: TEST_SERVER_PORT,
|
||||
toHost: 'localhost',
|
||||
domains: [],
|
||||
sniEnabled: false,
|
||||
defaultAllowedIPs: ['127.0.0.1', '::ffff:127.0.0.1']
|
||||
});
|
||||
|
||||
await secondProxyDefault.start();
|
||||
await firstProxyDefault.start();
|
||||
|
||||
// This should work because we explicitly allow both IPv4 and IPv6 formats
|
||||
const response1 = await createTestClient(PROXY_PORT + 4, TEST_DATA);
|
||||
expect(response1).toEqual(`Echo: ${TEST_DATA}`);
|
||||
|
||||
await firstProxyDefault.stop();
|
||||
await secondProxyDefault.stop();
|
||||
|
||||
// Test 2: With IP preservation
|
||||
const firstProxyPreserved = new PortProxy({
|
||||
fromPort: PROXY_PORT + 6,
|
||||
toPort: PROXY_PORT + 7,
|
||||
toHost: 'localhost',
|
||||
domains: [],
|
||||
sniEnabled: false,
|
||||
defaultAllowedIPs: ['127.0.0.1'],
|
||||
preserveSourceIP: true
|
||||
});
|
||||
|
||||
const secondProxyPreserved = new PortProxy({
|
||||
fromPort: PROXY_PORT + 7,
|
||||
toPort: TEST_SERVER_PORT,
|
||||
toHost: 'localhost',
|
||||
domains: [],
|
||||
sniEnabled: false,
|
||||
defaultAllowedIPs: ['127.0.0.1'],
|
||||
preserveSourceIP: true
|
||||
});
|
||||
|
||||
await secondProxyPreserved.start();
|
||||
await firstProxyPreserved.start();
|
||||
|
||||
// This should work with just IPv4 because source IP is preserved
|
||||
const response2 = await createTestClient(PROXY_PORT + 6, TEST_DATA);
|
||||
expect(response2).toEqual(`Echo: ${TEST_DATA}`);
|
||||
|
||||
await firstProxyPreserved.stop();
|
||||
await secondProxyPreserved.stop();
|
||||
});
|
||||
|
||||
tap.test('cleanup port proxy test environment', async () => {
|
||||
await new Promise<void>((resolve) => testServer.close(() => resolve()));
|
||||
});
|
||||
|
||||
process.on('exit', () => {
|
||||
if (testServer) {
|
||||
testServer.close();
|
||||
}
|
||||
if (portProxy && portProxy.netServer) {
|
||||
portProxy.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
420
test/test.ts
420
test/test.ts
@ -1,18 +1,422 @@
|
||||
import { expect, tap } from '@pushrocks/tapbundle';
|
||||
import * as smartproxy from '../ts/index';
|
||||
import { expect, tap } from '@push.rocks/tapbundle';
|
||||
import * as smartproxy from '../ts/index.js';
|
||||
import { loadTestCertificates } from './helpers/certificates.js';
|
||||
import * as https from 'https';
|
||||
import * as http from 'http';
|
||||
import { WebSocket, WebSocketServer } from 'ws';
|
||||
|
||||
let testProxy: smartproxy.SmartProxy;
|
||||
let testProxy: smartproxy.NetworkProxy;
|
||||
let testServer: http.Server;
|
||||
let wsServer: WebSocketServer;
|
||||
let testCertificates: { privateKey: string; publicKey: string };
|
||||
|
||||
tap.test('first test', async () => {
|
||||
testProxy = new smartproxy.SmartProxy();
|
||||
// Helper function to make HTTPS requests
|
||||
async function makeHttpsRequest(
|
||||
options: https.RequestOptions,
|
||||
): Promise<{ statusCode: number; headers: http.IncomingHttpHeaders; body: string }> {
|
||||
console.log('[TEST] Making HTTPS request:', {
|
||||
hostname: options.hostname,
|
||||
port: options.port,
|
||||
path: options.path,
|
||||
method: options.method,
|
||||
headers: options.headers,
|
||||
});
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = https.request(options, (res) => {
|
||||
console.log('[TEST] Received HTTPS response:', {
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
});
|
||||
let data = '';
|
||||
res.on('data', (chunk) => (data += chunk));
|
||||
res.on('end', () => {
|
||||
console.log('[TEST] Response completed:', { data });
|
||||
resolve({
|
||||
statusCode: res.statusCode!,
|
||||
headers: res.headers,
|
||||
body: data,
|
||||
});
|
||||
});
|
||||
});
|
||||
req.on('error', (error) => {
|
||||
console.error('[TEST] Request error:', error);
|
||||
reject(error);
|
||||
});
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
|
||||
// Setup test environment
|
||||
tap.test('setup test environment', async () => {
|
||||
// Load and validate certificates
|
||||
console.log('[TEST] Loading and validating certificates');
|
||||
testCertificates = loadTestCertificates();
|
||||
console.log('[TEST] Certificates loaded and validated');
|
||||
|
||||
// Create a test HTTP server
|
||||
testServer = http.createServer((req, res) => {
|
||||
console.log('[TEST SERVER] Received HTTP request:', {
|
||||
url: req.url,
|
||||
method: req.method,
|
||||
headers: req.headers,
|
||||
});
|
||||
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
||||
res.end('Hello from test server!');
|
||||
});
|
||||
|
||||
// Handle WebSocket upgrade requests
|
||||
testServer.on('upgrade', (request, socket, head) => {
|
||||
console.log('[TEST SERVER] Received WebSocket upgrade request:', {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers: {
|
||||
host: request.headers.host,
|
||||
upgrade: request.headers.upgrade,
|
||||
connection: request.headers.connection,
|
||||
'sec-websocket-key': request.headers['sec-websocket-key'],
|
||||
'sec-websocket-version': request.headers['sec-websocket-version'],
|
||||
'sec-websocket-protocol': request.headers['sec-websocket-protocol'],
|
||||
},
|
||||
});
|
||||
|
||||
if (request.headers.upgrade?.toLowerCase() !== 'websocket') {
|
||||
console.log('[TEST SERVER] Not a WebSocket upgrade request');
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[TEST SERVER] Handling WebSocket upgrade');
|
||||
wsServer.handleUpgrade(request, socket, head, (ws) => {
|
||||
console.log('[TEST SERVER] WebSocket connection upgraded');
|
||||
wsServer.emit('connection', ws, request);
|
||||
});
|
||||
});
|
||||
|
||||
// Create a WebSocket server (for the test HTTP server)
|
||||
console.log('[TEST SERVER] Creating WebSocket server');
|
||||
wsServer = new WebSocketServer({
|
||||
noServer: true,
|
||||
perMessageDeflate: false,
|
||||
clientTracking: true,
|
||||
handleProtocols: () => 'echo-protocol',
|
||||
});
|
||||
|
||||
wsServer.on('connection', (ws, request) => {
|
||||
console.log('[TEST SERVER] WebSocket connection established:', {
|
||||
url: request.url,
|
||||
headers: {
|
||||
host: request.headers.host,
|
||||
upgrade: request.headers.upgrade,
|
||||
connection: request.headers.connection,
|
||||
'sec-websocket-key': request.headers['sec-websocket-key'],
|
||||
'sec-websocket-version': request.headers['sec-websocket-version'],
|
||||
'sec-websocket-protocol': request.headers['sec-websocket-protocol'],
|
||||
},
|
||||
});
|
||||
|
||||
// Set up connection timeout
|
||||
const connectionTimeout = setTimeout(() => {
|
||||
console.error('[TEST SERVER] WebSocket connection timed out');
|
||||
ws.terminate();
|
||||
}, 5000);
|
||||
|
||||
// Clear timeout when connection is properly closed
|
||||
const clearConnectionTimeout = () => {
|
||||
clearTimeout(connectionTimeout);
|
||||
};
|
||||
|
||||
ws.on('message', (message) => {
|
||||
const msg = message.toString();
|
||||
console.log('[TEST SERVER] Received message:', msg);
|
||||
try {
|
||||
const response = `Echo: ${msg}`;
|
||||
console.log('[TEST SERVER] Sending response:', response);
|
||||
ws.send(response);
|
||||
// Clear timeout on successful message exchange
|
||||
clearConnectionTimeout();
|
||||
} catch (error) {
|
||||
console.error('[TEST SERVER] Error sending message:', error);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error('[TEST SERVER] WebSocket error:', error);
|
||||
clearConnectionTimeout();
|
||||
});
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
console.log('[TEST SERVER] WebSocket connection closed:', {
|
||||
code,
|
||||
reason: reason.toString(),
|
||||
wasClean: code === 1000 || code === 1001,
|
||||
});
|
||||
clearConnectionTimeout();
|
||||
});
|
||||
|
||||
ws.on('ping', (data) => {
|
||||
try {
|
||||
console.log('[TEST SERVER] Received ping, sending pong');
|
||||
ws.pong(data);
|
||||
} catch (error) {
|
||||
console.error('[TEST SERVER] Error sending pong:', error);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('pong', (data) => {
|
||||
console.log('[TEST SERVER] Received pong');
|
||||
});
|
||||
});
|
||||
|
||||
wsServer.on('error', (error) => {
|
||||
console.error('Test server: WebSocket server error:', error);
|
||||
});
|
||||
|
||||
wsServer.on('headers', (headers) => {
|
||||
console.log('Test server: WebSocket headers:', headers);
|
||||
});
|
||||
|
||||
wsServer.on('close', () => {
|
||||
console.log('Test server: WebSocket server closed');
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => testServer.listen(3000, resolve));
|
||||
console.log('Test server listening on port 3000');
|
||||
});
|
||||
|
||||
tap.test('should start the testproxy', async () => {
|
||||
tap.test('should create proxy instance', async () => {
|
||||
testProxy = new smartproxy.NetworkProxy({
|
||||
port: 3001,
|
||||
});
|
||||
expect(testProxy).toEqual(testProxy); // Instance equality check
|
||||
});
|
||||
|
||||
tap.test('should start the proxy server', async () => {
|
||||
// Ensure any previous server is closed
|
||||
if (testProxy && testProxy.httpsServer) {
|
||||
await new Promise<void>((resolve) =>
|
||||
testProxy.httpsServer.close(() => resolve())
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[TEST] Starting the proxy server');
|
||||
await testProxy.start();
|
||||
console.log('[TEST] Proxy server started');
|
||||
|
||||
// Configure proxy with test certificates
|
||||
// Awaiting the update ensures that the SNI context is added before any requests come in.
|
||||
await testProxy.updateProxyConfigs([
|
||||
{
|
||||
destinationIp: '127.0.0.1',
|
||||
destinationPort: '3000',
|
||||
hostName: 'push.rocks',
|
||||
publicKey: testCertificates.publicKey,
|
||||
privateKey: testCertificates.privateKey,
|
||||
},
|
||||
]);
|
||||
|
||||
console.log('[TEST] Proxy configuration updated');
|
||||
});
|
||||
|
||||
tap.test('should close the testproxy', async () => {
|
||||
tap.test('should route HTTPS requests based on host header', async () => {
|
||||
// IMPORTANT: Connect to localhost (where the proxy is listening) but use the Host header "push.rocks"
|
||||
const response = await makeHttpsRequest({
|
||||
hostname: 'localhost', // changed from 'push.rocks' to 'localhost'
|
||||
port: 3001,
|
||||
path: '/',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'push.rocks', // virtual host for routing
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
expect(response.statusCode).toEqual(200);
|
||||
expect(response.body).toEqual('Hello from test server!');
|
||||
});
|
||||
|
||||
tap.test('should handle unknown host headers', async () => {
|
||||
// Connect to localhost but use an unknown host header.
|
||||
const response = await makeHttpsRequest({
|
||||
hostname: 'localhost', // connecting to localhost
|
||||
port: 3001,
|
||||
path: '/',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'unknown.host', // this should not match any proxy config
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
// Expect a 404 response with the appropriate error message.
|
||||
expect(response.statusCode).toEqual(404);
|
||||
expect(response.body).toEqual('This route is not available on this server.');
|
||||
});
|
||||
|
||||
tap.test('should support WebSocket connections', async () => {
|
||||
console.log('\n[TEST] ====== WebSocket Test Started ======');
|
||||
console.log('[TEST] Test server port:', 3000);
|
||||
console.log('[TEST] Proxy server port:', 3001);
|
||||
console.log('\n[TEST] Starting WebSocket test');
|
||||
|
||||
// Reconfigure proxy with test certificates if necessary
|
||||
await testProxy.updateProxyConfigs([
|
||||
{
|
||||
destinationIp: '127.0.0.1',
|
||||
destinationPort: '3000',
|
||||
hostName: 'push.rocks',
|
||||
publicKey: testCertificates.publicKey,
|
||||
privateKey: testCertificates.privateKey,
|
||||
},
|
||||
]);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
console.log('[TEST] Creating WebSocket client');
|
||||
|
||||
// IMPORTANT: Connect to localhost but specify the SNI servername and Host header as "push.rocks"
|
||||
const wsUrl = 'wss://localhost:3001'; // changed from 'wss://push.rocks:3001'
|
||||
console.log('[TEST] Creating WebSocket connection to:', wsUrl);
|
||||
|
||||
const ws = new WebSocket(wsUrl, {
|
||||
rejectUnauthorized: false, // Accept self-signed certificates
|
||||
handshakeTimeout: 5000,
|
||||
perMessageDeflate: false,
|
||||
headers: {
|
||||
Host: 'push.rocks', // required for SNI and routing on the proxy
|
||||
Connection: 'Upgrade',
|
||||
Upgrade: 'websocket',
|
||||
'Sec-WebSocket-Version': '13',
|
||||
},
|
||||
protocol: 'echo-protocol',
|
||||
agent: new https.Agent({
|
||||
rejectUnauthorized: false, // Also needed for the underlying HTTPS connection
|
||||
}),
|
||||
});
|
||||
|
||||
console.log('[TEST] WebSocket client created');
|
||||
|
||||
let resolved = false;
|
||||
const cleanup = () => {
|
||||
if (!resolved) {
|
||||
resolved = true;
|
||||
try {
|
||||
console.log('[TEST] Cleaning up WebSocket connection');
|
||||
ws.close();
|
||||
resolve();
|
||||
} catch (error) {
|
||||
console.error('[TEST] Error during cleanup:', error);
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
console.error('[TEST] WebSocket test timed out');
|
||||
cleanup();
|
||||
reject(new Error('WebSocket test timed out after 5 seconds'));
|
||||
}, 5000);
|
||||
|
||||
// Connection establishment events
|
||||
ws.on('upgrade', (response) => {
|
||||
console.log('[TEST] WebSocket upgrade response received:', {
|
||||
headers: response.headers,
|
||||
statusCode: response.statusCode,
|
||||
});
|
||||
});
|
||||
|
||||
ws.on('open', () => {
|
||||
console.log('[TEST] WebSocket connection opened');
|
||||
try {
|
||||
console.log('[TEST] Sending test message');
|
||||
ws.send('Hello WebSocket');
|
||||
} catch (error) {
|
||||
console.error('[TEST] Error sending message:', error);
|
||||
cleanup();
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('message', (message) => {
|
||||
console.log('[TEST] Received message:', message.toString());
|
||||
if (
|
||||
message.toString() === 'Hello WebSocket' ||
|
||||
message.toString() === 'Echo: Hello WebSocket'
|
||||
) {
|
||||
console.log('[TEST] Message received correctly');
|
||||
clearTimeout(timeout);
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
ws.on('error', (error) => {
|
||||
console.error('[TEST] WebSocket error:', error);
|
||||
cleanup();
|
||||
reject(error);
|
||||
});
|
||||
|
||||
ws.on('close', (code, reason) => {
|
||||
console.log('[TEST] WebSocket connection closed:', {
|
||||
code,
|
||||
reason: reason.toString(),
|
||||
});
|
||||
cleanup();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('should handle custom headers', async () => {
|
||||
await testProxy.addDefaultHeaders({
|
||||
'X-Proxy-Header': 'test-value',
|
||||
});
|
||||
|
||||
const response = await makeHttpsRequest({
|
||||
hostname: 'localhost', // changed to 'localhost'
|
||||
port: 3001,
|
||||
path: '/',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
host: 'push.rocks', // still routing to push.rocks
|
||||
},
|
||||
rejectUnauthorized: false,
|
||||
});
|
||||
|
||||
expect(response.headers['x-proxy-header']).toEqual('test-value');
|
||||
});
|
||||
|
||||
tap.test('cleanup', async () => {
|
||||
console.log('[TEST] Starting cleanup');
|
||||
|
||||
// Clean up all servers
|
||||
console.log('[TEST] Terminating WebSocket clients');
|
||||
wsServer.clients.forEach((client) => {
|
||||
client.terminate();
|
||||
});
|
||||
|
||||
console.log('[TEST] Closing WebSocket server');
|
||||
await new Promise<void>((resolve) =>
|
||||
wsServer.close(() => {
|
||||
console.log('[TEST] WebSocket server closed');
|
||||
resolve();
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[TEST] Closing test server');
|
||||
await new Promise<void>((resolve) =>
|
||||
testServer.close(() => {
|
||||
console.log('[TEST] Test server closed');
|
||||
resolve();
|
||||
})
|
||||
);
|
||||
|
||||
console.log('[TEST] Stopping proxy');
|
||||
await testProxy.stop();
|
||||
console.log('[TEST] Cleanup complete');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
process.on('exit', () => {
|
||||
console.log('[TEST] Shutting down test server');
|
||||
testServer.close(() => console.log('[TEST] Test server shut down'));
|
||||
wsServer.close(() => console.log('[TEST] WebSocket server shut down'));
|
||||
testProxy.stop().then(() => console.log('[TEST] Proxy server stopped'));
|
||||
});
|
||||
|
||||
tap.start();
|
8
ts/00_commitinfo_data.ts
Normal file
8
ts/00_commitinfo_data.ts
Normal file
@ -0,0 +1,8 @@
|
||||
/**
|
||||
* autocreated commitinfo by @push.rocks/commitinfo
|
||||
*/
|
||||
export const commitinfo = {
|
||||
name: '@push.rocks/smartproxy',
|
||||
version: '3.8.1',
|
||||
description: 'a proxy for handling high workloads of proxying'
|
||||
}
|
@ -1 +1,3 @@
|
||||
export * from './smartproxy.classes.smartproxy';
|
||||
export * from './smartproxy.classes.networkproxy.js';
|
||||
export * from './smartproxy.portproxy.js';
|
||||
export * from './smartproxy.classes.sslredirect.js';
|
||||
|
@ -1,7 +0,0 @@
|
||||
export interface IHostConfig {
|
||||
hostName: string;
|
||||
destinationIp: string;
|
||||
destinationPort: number;
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
}
|
29
ts/plugins.ts
Normal file
29
ts/plugins.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// node native scope
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as net from 'net';
|
||||
import * as tls from 'tls';
|
||||
import * as url from 'url';
|
||||
|
||||
export { http, https, net, tls, url };
|
||||
|
||||
// tsclass scope
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
export { tsclass };
|
||||
|
||||
// pushrocks scope
|
||||
import * as lik from '@push.rocks/lik';
|
||||
import * as smartdelay from '@push.rocks/smartdelay';
|
||||
import * as smartpromise from '@push.rocks/smartpromise';
|
||||
import * as smartrequest from '@push.rocks/smartrequest';
|
||||
import * as smartstring from '@push.rocks/smartstring';
|
||||
|
||||
export { lik, smartdelay, smartrequest, smartpromise, smartstring };
|
||||
|
||||
// third party scope
|
||||
import * as ws from 'ws';
|
||||
import wsDefault from 'ws';
|
||||
import { minimatch } from 'minimatch';
|
||||
|
||||
export { wsDefault, ws, minimatch };
|
369
ts/smartproxy.classes.networkproxy.ts
Normal file
369
ts/smartproxy.classes.networkproxy.ts
Normal file
@ -0,0 +1,369 @@
|
||||
import * as plugins from './plugins.js';
|
||||
import { ProxyRouter } from './smartproxy.classes.router.js';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
export interface INetworkProxyOptions {
|
||||
port: number;
|
||||
}
|
||||
|
||||
interface IWebSocketWithHeartbeat extends plugins.wsDefault {
|
||||
lastPong: number;
|
||||
}
|
||||
|
||||
export class NetworkProxy {
|
||||
public options: INetworkProxyOptions;
|
||||
public proxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||
public httpsServer: plugins.https.Server;
|
||||
public router = new ProxyRouter();
|
||||
public socketMap = new plugins.lik.ObjectMap<plugins.net.Socket>();
|
||||
public defaultHeaders: { [key: string]: string } = {};
|
||||
public heartbeatInterval: NodeJS.Timeout;
|
||||
private defaultCertificates: { key: string; cert: string };
|
||||
|
||||
public alreadyAddedReverseConfigs: {
|
||||
[hostName: string]: plugins.tsclass.network.IReverseProxyConfig;
|
||||
} = {};
|
||||
|
||||
constructor(optionsArg: INetworkProxyOptions) {
|
||||
this.options = optionsArg;
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const certPath = path.join(__dirname, '..', 'assets', 'certs');
|
||||
|
||||
try {
|
||||
this.defaultCertificates = {
|
||||
key: fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8'),
|
||||
cert: fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8')
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading certificates:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async start() {
|
||||
// Instead of marking the callback async (which Node won't await),
|
||||
// we call our async handler and catch errors.
|
||||
this.httpsServer = plugins.https.createServer(
|
||||
{
|
||||
key: this.defaultCertificates.key,
|
||||
cert: this.defaultCertificates.cert
|
||||
},
|
||||
(originRequest, originResponse) => {
|
||||
this.handleRequest(originRequest, originResponse).catch((error) => {
|
||||
console.error('Unhandled error in request handler:', error);
|
||||
try {
|
||||
originResponse.end();
|
||||
} catch (err) {
|
||||
// ignore errors during cleanup
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
// Enable websockets
|
||||
const wsServer = new plugins.ws.WebSocketServer({ server: this.httpsServer });
|
||||
|
||||
// Set up the heartbeat interval
|
||||
this.heartbeatInterval = setInterval(() => {
|
||||
wsServer.clients.forEach((ws: plugins.wsDefault) => {
|
||||
const wsIncoming = ws as IWebSocketWithHeartbeat;
|
||||
if (!wsIncoming.lastPong) {
|
||||
wsIncoming.lastPong = Date.now();
|
||||
}
|
||||
if (Date.now() - wsIncoming.lastPong > 5 * 60 * 1000) {
|
||||
console.log('Terminating websocket due to missing pong for 5 minutes.');
|
||||
wsIncoming.terminate();
|
||||
} else {
|
||||
wsIncoming.ping();
|
||||
}
|
||||
});
|
||||
}, 60000); // runs every 1 minute
|
||||
|
||||
wsServer.on(
|
||||
'connection',
|
||||
(wsIncoming: IWebSocketWithHeartbeat, reqArg: plugins.http.IncomingMessage) => {
|
||||
console.log(
|
||||
`wss proxy: got connection for wsc for https://${reqArg.headers.host}${reqArg.url}`,
|
||||
);
|
||||
|
||||
wsIncoming.lastPong = Date.now();
|
||||
wsIncoming.on('pong', () => {
|
||||
wsIncoming.lastPong = Date.now();
|
||||
});
|
||||
|
||||
let wsOutgoing: plugins.wsDefault;
|
||||
const outGoingDeferred = plugins.smartpromise.defer();
|
||||
|
||||
// --- Improvement 2: Only call routeReq once ---
|
||||
const wsDestinationConfig = this.router.routeReq(reqArg);
|
||||
if (!wsDestinationConfig) {
|
||||
wsIncoming.terminate();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
wsOutgoing = new plugins.wsDefault(
|
||||
`ws://${wsDestinationConfig.destinationIp}:${wsDestinationConfig.destinationPort}${reqArg.url}`,
|
||||
);
|
||||
console.log('wss proxy: initiated outgoing proxy');
|
||||
wsOutgoing.on('open', async () => {
|
||||
outGoingDeferred.resolve();
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error initiating outgoing WebSocket:', err);
|
||||
wsIncoming.terminate();
|
||||
return;
|
||||
}
|
||||
|
||||
wsIncoming.on('message', async (message, isBinary) => {
|
||||
try {
|
||||
await outGoingDeferred.promise;
|
||||
wsOutgoing.send(message, { binary: isBinary });
|
||||
} catch (error) {
|
||||
console.error('Error sending message to wsOutgoing:', error);
|
||||
}
|
||||
});
|
||||
|
||||
wsOutgoing.on('message', async (message, isBinary) => {
|
||||
try {
|
||||
wsIncoming.send(message, { binary: isBinary });
|
||||
} catch (error) {
|
||||
console.error('Error sending message to wsIncoming:', error);
|
||||
}
|
||||
});
|
||||
|
||||
const terminateWsOutgoing = () => {
|
||||
if (wsOutgoing) {
|
||||
wsOutgoing.terminate();
|
||||
console.log('Terminated outgoing ws.');
|
||||
}
|
||||
};
|
||||
wsIncoming.on('error', terminateWsOutgoing);
|
||||
wsIncoming.on('close', terminateWsOutgoing);
|
||||
|
||||
const terminateWsIncoming = () => {
|
||||
if (wsIncoming) {
|
||||
wsIncoming.terminate();
|
||||
console.log('Terminated incoming ws.');
|
||||
}
|
||||
};
|
||||
wsOutgoing.on('error', terminateWsIncoming);
|
||||
wsOutgoing.on('close', terminateWsIncoming);
|
||||
},
|
||||
);
|
||||
|
||||
this.httpsServer.keepAliveTimeout = 600 * 1000;
|
||||
this.httpsServer.headersTimeout = 600 * 1000;
|
||||
|
||||
this.httpsServer.on('connection', (connection: plugins.net.Socket) => {
|
||||
this.socketMap.add(connection);
|
||||
console.log(`Added connection. Now ${this.socketMap.getArray().length} sockets connected.`);
|
||||
const cleanupConnection = () => {
|
||||
if (this.socketMap.checkForObject(connection)) {
|
||||
this.socketMap.remove(connection);
|
||||
console.log(`Removed connection. ${this.socketMap.getArray().length} sockets remaining.`);
|
||||
connection.destroy();
|
||||
}
|
||||
};
|
||||
connection.on('close', cleanupConnection);
|
||||
connection.on('error', cleanupConnection);
|
||||
connection.on('end', cleanupConnection);
|
||||
connection.on('timeout', cleanupConnection);
|
||||
});
|
||||
|
||||
this.httpsServer.listen(this.options.port);
|
||||
console.log(
|
||||
`NetworkProxy -> OK: now listening for new connections on port ${this.options.port}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal async handler for processing HTTP/HTTPS requests.
|
||||
*/
|
||||
private async handleRequest(
|
||||
originRequest: plugins.http.IncomingMessage,
|
||||
originResponse: plugins.http.ServerResponse,
|
||||
): Promise<void> {
|
||||
const endOriginReqRes = (
|
||||
statusArg: number = 404,
|
||||
messageArg: string = 'This route is not available on this server.',
|
||||
headers: plugins.http.OutgoingHttpHeaders = {},
|
||||
) => {
|
||||
originResponse.writeHead(statusArg, messageArg);
|
||||
originResponse.end(messageArg);
|
||||
if (originRequest.socket !== originResponse.socket) {
|
||||
console.log('hey, something is strange.');
|
||||
}
|
||||
originResponse.destroy();
|
||||
};
|
||||
|
||||
console.log(
|
||||
`got request: ${originRequest.headers.host}${plugins.url.parse(originRequest.url).path}`,
|
||||
);
|
||||
const destinationConfig = this.router.routeReq(originRequest);
|
||||
|
||||
if (!destinationConfig) {
|
||||
console.log(
|
||||
`${originRequest.headers.host} can't be routed properly. Terminating request.`,
|
||||
);
|
||||
endOriginReqRes();
|
||||
return;
|
||||
}
|
||||
|
||||
// authentication
|
||||
if (destinationConfig.authentication) {
|
||||
const authInfo = destinationConfig.authentication;
|
||||
switch (authInfo.type) {
|
||||
case 'Basic': {
|
||||
const authHeader = originRequest.headers.authorization;
|
||||
if (!authHeader) {
|
||||
return endOriginReqRes(401, 'Authentication required', {
|
||||
'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"',
|
||||
});
|
||||
}
|
||||
if (!authHeader.includes('Basic ')) {
|
||||
return endOriginReqRes(401, 'Authentication required', {
|
||||
'WWW-Authenticate': 'Basic realm="Access to the staging site", charset="UTF-8"',
|
||||
});
|
||||
}
|
||||
const authStringBase64 = authHeader.replace('Basic ', '');
|
||||
const authString: string = plugins.smartstring.base64.decode(authStringBase64);
|
||||
const userPassArray = authString.split(':');
|
||||
const user = userPassArray[0];
|
||||
const pass = userPassArray[1];
|
||||
if (user === authInfo.user && pass === authInfo.pass) {
|
||||
console.log('Request successfully authenticated');
|
||||
} else {
|
||||
return endOriginReqRes(403, 'Forbidden: Wrong credentials');
|
||||
}
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return endOriginReqRes(
|
||||
403,
|
||||
'Forbidden: unsupported authentication method configured. Please report to the admin.',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let destinationUrl: string;
|
||||
if (destinationConfig) {
|
||||
destinationUrl = `http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${originRequest.url}`;
|
||||
} else {
|
||||
return endOriginReqRes();
|
||||
}
|
||||
console.log(destinationUrl);
|
||||
try {
|
||||
const proxyResponse = await plugins.smartrequest.request(
|
||||
destinationUrl,
|
||||
{
|
||||
method: originRequest.method,
|
||||
headers: {
|
||||
...originRequest.headers,
|
||||
'X-Forwarded-Host': originRequest.headers.host,
|
||||
'X-Forwarded-Proto': 'https',
|
||||
},
|
||||
keepAlive: true,
|
||||
},
|
||||
true, // streaming (keepAlive)
|
||||
(proxyRequest) => {
|
||||
originRequest.on('data', (data) => {
|
||||
proxyRequest.write(data);
|
||||
});
|
||||
originRequest.on('end', () => {
|
||||
proxyRequest.end();
|
||||
});
|
||||
originRequest.on('error', () => {
|
||||
proxyRequest.end();
|
||||
});
|
||||
originRequest.on('close', () => {
|
||||
proxyRequest.end();
|
||||
});
|
||||
originRequest.on('timeout', () => {
|
||||
proxyRequest.end();
|
||||
originRequest.destroy();
|
||||
});
|
||||
proxyRequest.on('error', () => {
|
||||
endOriginReqRes();
|
||||
});
|
||||
},
|
||||
);
|
||||
originResponse.statusCode = proxyResponse.statusCode;
|
||||
console.log(proxyResponse.statusCode);
|
||||
for (const defaultHeader of Object.keys(this.defaultHeaders)) {
|
||||
originResponse.setHeader(defaultHeader, this.defaultHeaders[defaultHeader]);
|
||||
}
|
||||
for (const header of Object.keys(proxyResponse.headers)) {
|
||||
originResponse.setHeader(header, proxyResponse.headers[header]);
|
||||
}
|
||||
proxyResponse.on('data', (data) => {
|
||||
originResponse.write(data);
|
||||
});
|
||||
proxyResponse.on('end', () => {
|
||||
originResponse.end();
|
||||
});
|
||||
proxyResponse.on('error', () => {
|
||||
originResponse.destroy();
|
||||
});
|
||||
proxyResponse.on('close', () => {
|
||||
originResponse.end();
|
||||
});
|
||||
proxyResponse.on('timeout', () => {
|
||||
originResponse.end();
|
||||
originResponse.destroy();
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error while processing request:', error);
|
||||
endOriginReqRes(502, 'Bad Gateway: Error processing the request');
|
||||
}
|
||||
}
|
||||
|
||||
public async updateProxyConfigs(
|
||||
proxyConfigsArg: plugins.tsclass.network.IReverseProxyConfig[],
|
||||
) {
|
||||
console.log(`got new proxy configs`);
|
||||
this.proxyConfigs = proxyConfigsArg;
|
||||
this.router.setNewProxyConfigs(proxyConfigsArg);
|
||||
for (const hostCandidate of this.proxyConfigs) {
|
||||
const existingHostNameConfig = this.alreadyAddedReverseConfigs[hostCandidate.hostName];
|
||||
|
||||
if (!existingHostNameConfig) {
|
||||
this.alreadyAddedReverseConfigs[hostCandidate.hostName] = hostCandidate;
|
||||
} else {
|
||||
if (
|
||||
existingHostNameConfig.publicKey === hostCandidate.publicKey &&
|
||||
existingHostNameConfig.privateKey === hostCandidate.privateKey
|
||||
) {
|
||||
continue;
|
||||
} else {
|
||||
this.alreadyAddedReverseConfigs[hostCandidate.hostName] = hostCandidate;
|
||||
}
|
||||
}
|
||||
|
||||
this.httpsServer.addContext(hostCandidate.hostName, {
|
||||
cert: hostCandidate.publicKey,
|
||||
key: hostCandidate.privateKey,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async addDefaultHeaders(headersArg: { [key: string]: string }) {
|
||||
for (const headerKey of Object.keys(headersArg)) {
|
||||
this.defaultHeaders[headerKey] = headersArg[headerKey];
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
this.httpsServer.close(() => {
|
||||
done.resolve();
|
||||
});
|
||||
for (const socket of this.socketMap.getArray()) {
|
||||
socket.destroy();
|
||||
}
|
||||
await done.promise;
|
||||
clearInterval(this.heartbeatInterval);
|
||||
console.log('NetworkProxy -> OK: Server has been stopped and all connections closed.');
|
||||
}
|
||||
}
|
@ -1,80 +0,0 @@
|
||||
import * as plugins from './smartproxy.plugins';
|
||||
import * as interfaces from './interfaces';
|
||||
import { SmartproxyRouter } from './smartproxy.classes.router';
|
||||
|
||||
export class ProxyWorker {
|
||||
public httpsServer: plugins.https.Server | plugins.http.Server;
|
||||
public router = new SmartproxyRouter();
|
||||
|
||||
/**
|
||||
* starts the proxyInstance
|
||||
*/
|
||||
public async start() {
|
||||
this.httpsServer = plugins.http.createServer(async (req, res) => {
|
||||
req.headers.host = this.router.routeReq(req);
|
||||
const response = await plugins.smartrequest.request(
|
||||
`https://${req.headers.host}${req.url}`,
|
||||
{
|
||||
method: req.method,
|
||||
headers: req.headers
|
||||
},
|
||||
true
|
||||
);
|
||||
res.statusCode = response.statusCode;
|
||||
for (const header of Object.keys(response.headers)) {
|
||||
res.setHeader(header, response.headers[header]);
|
||||
}
|
||||
response.on('data', data => {
|
||||
res.write(data);
|
||||
});
|
||||
response.on('end', () => {
|
||||
res.end();
|
||||
});
|
||||
});
|
||||
for (const hostCandidate of this.hostCandidates) {
|
||||
/* this.httpsServer.addContext(hostCandidate.hostName, {
|
||||
cert: hostCandidate.publicKey,
|
||||
key: hostCandidate.privateKey
|
||||
}); */
|
||||
}
|
||||
|
||||
// Enable websockets
|
||||
const wss = new plugins.ws.Server({ server: this.httpsServer });
|
||||
wss.on('connection', function connection(ws) {
|
||||
const wscConnected = plugins.smartpromise.defer();
|
||||
const wsc = new plugins.ws(`${ws.url}`);
|
||||
wsc.on('open', () => {
|
||||
wscConnected.resolve();
|
||||
});
|
||||
|
||||
ws.on('message', async (message) => {
|
||||
await wscConnected.promise;
|
||||
wsc.emit('message', message);
|
||||
});
|
||||
wsc.on('message', (message) => {
|
||||
ws.emit('message', message);
|
||||
});
|
||||
|
||||
// handle closing
|
||||
ws.on('close', (message) => {
|
||||
wsc.close();
|
||||
});
|
||||
wsc.on('close', (message) => {
|
||||
ws.close();
|
||||
});
|
||||
});
|
||||
|
||||
this.httpsServer.listen(3000);
|
||||
}
|
||||
|
||||
public async update() {
|
||||
await this.start();
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
this.httpsServer.close(() => {
|
||||
done.resolve();
|
||||
});
|
||||
}
|
||||
}
|
@ -1,8 +1,33 @@
|
||||
import * as plugins from './smartproxy.plugins';
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class SmartproxyRouter {
|
||||
export class ProxyRouter {
|
||||
public reverseProxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||
|
||||
public routeReq(req: plugins.http.IncomingMessage) {
|
||||
return 'lossless.gmbh';
|
||||
/**
|
||||
* sets a new set of reverse configs to be routed to
|
||||
* @param reverseCandidatesArg
|
||||
*/
|
||||
public setNewProxyConfigs(reverseCandidatesArg: plugins.tsclass.network.IReverseProxyConfig[]) {
|
||||
this.reverseProxyConfigs = reverseCandidatesArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* routes a request
|
||||
*/
|
||||
public routeReq(req: plugins.http.IncomingMessage): plugins.tsclass.network.IReverseProxyConfig {
|
||||
const originalHost = req.headers.host;
|
||||
if (!originalHost) {
|
||||
console.error('No host header found in request');
|
||||
return undefined;
|
||||
}
|
||||
// Strip port from host if present
|
||||
const hostWithoutPort = originalHost.split(':')[0];
|
||||
const correspodingReverseProxyConfig = this.reverseProxyConfigs.find((reverseConfig) => {
|
||||
return reverseConfig.hostName === hostWithoutPort;
|
||||
});
|
||||
if (!correspodingReverseProxyConfig) {
|
||||
console.error(`No config found for host: ${hostWithoutPort}`);
|
||||
}
|
||||
return correspodingReverseProxyConfig;
|
||||
}
|
||||
}
|
||||
|
@ -1,22 +0,0 @@
|
||||
import * as plugins from './smartproxy.plugins';
|
||||
import * as interfaces from './interfaces';
|
||||
|
||||
export class SmartProxy {
|
||||
|
||||
public hostCandidates: interfaces.IHostConfig[] = [];
|
||||
|
||||
public addHostCandidate(hostCandidate: interfaces.IHostConfig) {
|
||||
// TODO search for old hostCandidates with that target
|
||||
this.hostCandidates.push(hostCandidate);
|
||||
}
|
||||
|
||||
async start () {
|
||||
|
||||
}
|
||||
|
||||
async stop () {
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
32
ts/smartproxy.classes.sslredirect.ts
Normal file
32
ts/smartproxy.classes.sslredirect.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export class SslRedirect {
|
||||
httpServer: plugins.http.Server;
|
||||
port: number;
|
||||
constructor(portArg: number) {
|
||||
this.port = portArg;
|
||||
}
|
||||
|
||||
public async start() {
|
||||
this.httpServer = plugins.http.createServer((request, response) => {
|
||||
const requestUrl = new URL(request.url, `http://${request.headers.host}`);
|
||||
const completeUrlWithoutProtocol = `${requestUrl.host}${requestUrl.pathname}${requestUrl.search}`;
|
||||
const redirectUrl = `https://${completeUrlWithoutProtocol}`;
|
||||
console.log(`Got http request for http://${completeUrlWithoutProtocol}`);
|
||||
console.log(`Redirecting to ${redirectUrl}`);
|
||||
response.writeHead(302, {
|
||||
Location: redirectUrl,
|
||||
});
|
||||
response.end();
|
||||
});
|
||||
this.httpServer.listen(this.port);
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
this.httpServer.close(() => {
|
||||
done.resolve();
|
||||
});
|
||||
await done.promise;
|
||||
}
|
||||
}
|
30
ts/smartproxy.helpers.certificates.ts
Normal file
30
ts/smartproxy.helpers.certificates.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
export interface ICertificates {
|
||||
privateKey: string;
|
||||
publicKey: string;
|
||||
}
|
||||
|
||||
export function loadDefaultCertificates(): ICertificates {
|
||||
try {
|
||||
const certPath = path.join(__dirname, '..', 'assets', 'certs');
|
||||
const privateKey = fs.readFileSync(path.join(certPath, 'key.pem'), 'utf8');
|
||||
const publicKey = fs.readFileSync(path.join(certPath, 'cert.pem'), 'utf8');
|
||||
|
||||
if (!privateKey || !publicKey) {
|
||||
throw new Error('Failed to load default certificates');
|
||||
}
|
||||
|
||||
return {
|
||||
privateKey,
|
||||
publicKey
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error loading default certificates:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
// node native scope
|
||||
import crypto from 'crypto';
|
||||
import http from 'http';
|
||||
import https from 'https';
|
||||
|
||||
export { crypto, http, https };
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartpromise from '@pushrocks/smartpromise';
|
||||
import * as smartrequest from '@pushrocks/smartrequest';
|
||||
|
||||
export {
|
||||
smartrequest,
|
||||
smartpromise
|
||||
};
|
||||
|
||||
// third party scope
|
||||
import ws from 'ws';
|
||||
|
||||
export {
|
||||
ws
|
||||
};
|
321
ts/smartproxy.portproxy.ts
Normal file
321
ts/smartproxy.portproxy.ts
Normal file
@ -0,0 +1,321 @@
|
||||
import * as plugins from './plugins.js';
|
||||
|
||||
export interface IDomainConfig {
|
||||
domain: string; // glob pattern for domain
|
||||
allowedIPs: string[]; // glob patterns for IPs allowed to access this domain
|
||||
targetIP?: string; // Optional target IP for this domain
|
||||
}
|
||||
|
||||
export interface IProxySettings extends plugins.tls.TlsOptions {
|
||||
// Port configuration
|
||||
fromPort: number;
|
||||
toPort: number;
|
||||
toHost?: string; // Target host to proxy to, defaults to 'localhost'
|
||||
|
||||
// Domain and security settings
|
||||
domains: IDomainConfig[];
|
||||
sniEnabled?: boolean;
|
||||
defaultAllowedIPs?: string[]; // Optional default IP patterns if no matching domain found
|
||||
preserveSourceIP?: boolean; // Whether to preserve the client's source IP when proxying
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract SNI (Server Name Indication) from a TLS ClientHello packet.
|
||||
* Returns the server name if found, or undefined.
|
||||
*/
|
||||
function extractSNI(buffer: Buffer): string | undefined {
|
||||
let offset = 0;
|
||||
// We need at least 5 bytes for the record header.
|
||||
if (buffer.length < 5) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// TLS record header
|
||||
const recordType = buffer.readUInt8(0);
|
||||
if (recordType !== 22) { // 22 = handshake
|
||||
return undefined;
|
||||
}
|
||||
// Read record length
|
||||
const recordLength = buffer.readUInt16BE(3);
|
||||
if (buffer.length < 5 + recordLength) {
|
||||
// Not all data arrived yet; in production you might need to accumulate more data.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
offset = 5;
|
||||
// Handshake message type should be 1 for ClientHello.
|
||||
const handshakeType = buffer.readUInt8(offset);
|
||||
if (handshakeType !== 1) {
|
||||
return undefined;
|
||||
}
|
||||
// Skip handshake header (1 byte type + 3 bytes length)
|
||||
offset += 4;
|
||||
|
||||
// Skip client version (2 bytes) and random (32 bytes)
|
||||
offset += 2 + 32;
|
||||
|
||||
// Session ID
|
||||
const sessionIDLength = buffer.readUInt8(offset);
|
||||
offset += 1 + sessionIDLength;
|
||||
|
||||
// Cipher suites
|
||||
const cipherSuitesLength = buffer.readUInt16BE(offset);
|
||||
offset += 2 + cipherSuitesLength;
|
||||
|
||||
// Compression methods
|
||||
const compressionMethodsLength = buffer.readUInt8(offset);
|
||||
offset += 1 + compressionMethodsLength;
|
||||
|
||||
// Extensions length
|
||||
if (offset + 2 > buffer.length) {
|
||||
return undefined;
|
||||
}
|
||||
const extensionsLength = buffer.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
const extensionsEnd = offset + extensionsLength;
|
||||
|
||||
// Iterate over extensions
|
||||
while (offset + 4 <= extensionsEnd) {
|
||||
const extensionType = buffer.readUInt16BE(offset);
|
||||
const extensionLength = buffer.readUInt16BE(offset + 2);
|
||||
offset += 4;
|
||||
|
||||
// Check for SNI extension (type 0)
|
||||
if (extensionType === 0x0000) {
|
||||
// SNI extension: first 2 bytes are the SNI list length.
|
||||
if (offset + 2 > buffer.length) {
|
||||
return undefined;
|
||||
}
|
||||
const sniListLength = buffer.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
const sniListEnd = offset + sniListLength;
|
||||
// Loop through the list; typically there is one entry.
|
||||
while (offset + 3 < sniListEnd) {
|
||||
const nameType = buffer.readUInt8(offset);
|
||||
offset++;
|
||||
const nameLen = buffer.readUInt16BE(offset);
|
||||
offset += 2;
|
||||
if (nameType === 0) { // host_name
|
||||
if (offset + nameLen > buffer.length) {
|
||||
return undefined;
|
||||
}
|
||||
const serverName = buffer.toString('utf8', offset, offset + nameLen);
|
||||
return serverName;
|
||||
}
|
||||
offset += nameLen;
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
offset += extensionLength;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export class PortProxy {
|
||||
netServer: plugins.net.Server;
|
||||
settings: IProxySettings;
|
||||
// Track active incoming connections
|
||||
private activeConnections: Set<plugins.net.Socket> = new Set();
|
||||
private connectionLogger: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(settings: IProxySettings) {
|
||||
this.settings = {
|
||||
...settings,
|
||||
toHost: settings.toHost || 'localhost'
|
||||
};
|
||||
}
|
||||
|
||||
public async start() {
|
||||
const cleanUpSockets = (from: plugins.net.Socket, to: plugins.net.Socket) => {
|
||||
from.end();
|
||||
to.end();
|
||||
from.removeAllListeners();
|
||||
to.removeAllListeners();
|
||||
from.unpipe();
|
||||
to.unpipe();
|
||||
from.destroy();
|
||||
to.destroy();
|
||||
};
|
||||
|
||||
const normalizeIP = (ip: string): string[] => {
|
||||
// Handle IPv4-mapped IPv6 addresses
|
||||
if (ip.startsWith('::ffff:')) {
|
||||
const ipv4 = ip.slice(7); // Remove '::ffff:' prefix
|
||||
return [ip, ipv4];
|
||||
}
|
||||
// Handle IPv4 addresses by adding IPv4-mapped IPv6 variant
|
||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) {
|
||||
return [ip, `::ffff:${ip}`];
|
||||
}
|
||||
return [ip];
|
||||
};
|
||||
|
||||
const isAllowed = (value: string, patterns: string[]): boolean => {
|
||||
// Expand patterns to include both IPv4 and IPv6 variants
|
||||
const expandedPatterns = patterns.flatMap(normalizeIP);
|
||||
// Check if any variant of the IP matches any expanded pattern
|
||||
return normalizeIP(value).some(ip =>
|
||||
expandedPatterns.some(pattern => plugins.minimatch(ip, pattern))
|
||||
);
|
||||
};
|
||||
|
||||
const findMatchingDomain = (serverName: string): IDomainConfig | undefined => {
|
||||
return this.settings.domains.find(config => plugins.minimatch(serverName, config.domain));
|
||||
};
|
||||
|
||||
// Create a plain net server for TLS passthrough.
|
||||
this.netServer = plugins.net.createServer((socket: plugins.net.Socket) => {
|
||||
const remoteIP = socket.remoteAddress || '';
|
||||
|
||||
// Flag to detect if we've received the first data chunk.
|
||||
let initialDataReceived = false;
|
||||
|
||||
// Immediately attach an error handler to catch early errors.
|
||||
socket.on('error', (err: Error) => {
|
||||
if (!initialDataReceived) {
|
||||
console.log(`(Premature) Incoming socket error from ${remoteIP} before data received: ${err.message}`);
|
||||
} else {
|
||||
console.log(`(Immediate) Incoming socket error from ${remoteIP}: ${err.message}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Track the new incoming connection.
|
||||
this.activeConnections.add(socket);
|
||||
console.log(`New connection from ${remoteIP}. Active connections: ${this.activeConnections.size}`);
|
||||
|
||||
// Flag to ensure cleanup happens only once.
|
||||
let connectionClosed = false;
|
||||
const cleanupOnce = () => {
|
||||
if (!connectionClosed) {
|
||||
connectionClosed = true;
|
||||
cleanUpSockets(socket, to);
|
||||
if (this.activeConnections.has(socket)) {
|
||||
this.activeConnections.delete(socket);
|
||||
console.log(`Connection from ${remoteIP} terminated. Active connections: ${this.activeConnections.size}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let to: plugins.net.Socket;
|
||||
|
||||
const handleError = (side: 'incoming' | 'outgoing') => (err: Error) => {
|
||||
const code = (err as any).code;
|
||||
if (code === 'ECONNRESET') {
|
||||
console.log(`ECONNRESET on ${side} side from ${remoteIP}: ${err.message}`);
|
||||
} else {
|
||||
console.log(`Error on ${side} side from ${remoteIP}: ${err.message}`);
|
||||
}
|
||||
cleanupOnce();
|
||||
};
|
||||
|
||||
const handleClose = (side: 'incoming' | 'outgoing') => () => {
|
||||
console.log(`Connection closed on ${side} side from ${remoteIP}`);
|
||||
cleanupOnce();
|
||||
};
|
||||
|
||||
// Setup connection, optionally accepting the initial data chunk.
|
||||
const setupConnection = (serverName: string, initialChunk?: Buffer) => {
|
||||
// Check if the IP is allowed by default.
|
||||
const isDefaultAllowed = this.settings.defaultAllowedIPs && isAllowed(remoteIP, this.settings.defaultAllowedIPs);
|
||||
if (!isDefaultAllowed && serverName) {
|
||||
const domainConfig = findMatchingDomain(serverName);
|
||||
if (!domainConfig) {
|
||||
console.log(`Connection rejected: No matching domain config for ${serverName} from ${remoteIP}`);
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
if (!isAllowed(remoteIP, domainConfig.allowedIPs)) {
|
||||
console.log(`Connection rejected: IP ${remoteIP} not allowed for domain ${serverName}`);
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
} else if (!isDefaultAllowed && !serverName) {
|
||||
console.log(`Connection rejected: No SNI and IP ${remoteIP} not in default allowed list`);
|
||||
socket.end();
|
||||
return;
|
||||
} else {
|
||||
console.log(`Connection allowed: IP ${remoteIP} is in default allowed list`);
|
||||
}
|
||||
|
||||
// Determine target host.
|
||||
const domainConfig = serverName ? findMatchingDomain(serverName) : undefined;
|
||||
const targetHost = domainConfig?.targetIP || this.settings.toHost!;
|
||||
|
||||
// Create connection options.
|
||||
const connectionOptions: plugins.net.NetConnectOpts = {
|
||||
host: targetHost,
|
||||
port: this.settings.toPort,
|
||||
};
|
||||
if (this.settings.preserveSourceIP) {
|
||||
connectionOptions.localAddress = remoteIP.replace('::ffff:', '');
|
||||
}
|
||||
|
||||
// Establish outgoing connection.
|
||||
to = plugins.net.connect(connectionOptions);
|
||||
console.log(`Connection established: ${remoteIP} -> ${targetHost}:${this.settings.toPort}${serverName ? ` (SNI: ${serverName})` : ''}`);
|
||||
|
||||
// Push back the initial chunk if provided.
|
||||
if (initialChunk) {
|
||||
socket.unshift(initialChunk);
|
||||
}
|
||||
socket.setTimeout(120000);
|
||||
socket.pipe(to);
|
||||
to.pipe(socket);
|
||||
|
||||
// Attach error and close handlers for both sockets.
|
||||
socket.on('error', handleError('incoming'));
|
||||
to.on('error', handleError('outgoing'));
|
||||
socket.on('close', handleClose('incoming'));
|
||||
to.on('close', handleClose('outgoing'));
|
||||
socket.on('timeout', handleError('incoming'));
|
||||
to.on('timeout', handleError('outgoing'));
|
||||
socket.on('end', handleClose('incoming'));
|
||||
to.on('end', handleClose('outgoing'));
|
||||
};
|
||||
|
||||
// For SNI-enabled connections, peek at the first chunk.
|
||||
if (this.settings.sniEnabled) {
|
||||
socket.once('data', (chunk: Buffer) => {
|
||||
initialDataReceived = true;
|
||||
// Try to extract the server name from the ClientHello.
|
||||
const serverName = extractSNI(chunk) || '';
|
||||
console.log(`Received connection from ${remoteIP} with SNI: ${serverName}`);
|
||||
setupConnection(serverName, chunk);
|
||||
});
|
||||
} else {
|
||||
// For non-SNI connections, simply check defaultAllowedIPs.
|
||||
initialDataReceived = true;
|
||||
if (!this.settings.defaultAllowedIPs || !isAllowed(remoteIP, this.settings.defaultAllowedIPs)) {
|
||||
console.log(`Connection rejected: IP ${remoteIP} not allowed for non-SNI connection`);
|
||||
socket.end();
|
||||
return;
|
||||
}
|
||||
setupConnection('');
|
||||
}
|
||||
})
|
||||
.on('error', (err: Error) => {
|
||||
console.log(`Server Error: ${err.message}`);
|
||||
})
|
||||
.listen(this.settings.fromPort, () => {
|
||||
console.log(`PortProxy -> OK: Now listening on port ${this.settings.fromPort}${this.settings.sniEnabled ? ' (SNI passthrough enabled)' : ''}`);
|
||||
});
|
||||
|
||||
// Log active connection count every 10 seconds.
|
||||
this.connectionLogger = setInterval(() => {
|
||||
console.log(`(Interval Log) Active connections: ${this.activeConnections.size}`);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
this.netServer.close(() => {
|
||||
done.resolve();
|
||||
});
|
||||
if (this.connectionLogger) {
|
||||
clearInterval(this.connectionLogger);
|
||||
this.connectionLogger = null;
|
||||
}
|
||||
await done.promise;
|
||||
}
|
||||
}
|
16
tsconfig.json
Normal file
16
tsconfig.json
Normal file
@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"experimentalDecorators": true,
|
||||
"useDefineForClassFields": false,
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"esModuleInterop": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {}
|
||||
},
|
||||
"exclude": [
|
||||
"dist_*/**/*.d.ts"
|
||||
]
|
||||
}
|
17
tslint.json
17
tslint.json
@ -1,17 +0,0 @@
|
||||
{
|
||||
"extends": ["tslint:latest", "tslint-config-prettier"],
|
||||
"rules": {
|
||||
"semicolon": [true, "always"],
|
||||
"no-console": false,
|
||||
"ordered-imports": false,
|
||||
"object-literal-sort-keys": false,
|
||||
"member-ordering": {
|
||||
"options":{
|
||||
"order": [
|
||||
"static-method"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultSeverity": "warning"
|
||||
}
|
Reference in New Issue
Block a user