Compare commits
209 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 |
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-----
|
55
changelog.md
Normal file
55
changelog.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Changelog
|
||||
|
||||
## 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"
|
||||
}
|
||||
}
|
2098
package-lock.json
generated
2098
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
84
package.json
84
package.json
@ -1,44 +1,74 @@
|
||||
{
|
||||
"name": "@pushrocks/smartproxy",
|
||||
"version": "1.0.20",
|
||||
"name": "@push.rocks/smartproxy",
|
||||
"version": "3.1.3",
|
||||
"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/smartnetwork": "^1.1.6",
|
||||
"@pushrocks/smartpromise": "^3.0.2",
|
||||
"@pushrocks/smartrequest": "^1.1.23",
|
||||
"@pushrocks/smartspawn": "^2.0.8",
|
||||
"@pushrocks/smartsystem": "^2.0.7",
|
||||
"@tsclass/tsclass": "^2.0.5",
|
||||
"@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"
|
||||
},
|
||||
"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": {}
|
||||
}
|
||||
}
|
||||
|
9696
pnpm-lock.yaml
generated
Normal file
9696
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
|
||||
};
|
||||
}
|
420
test/test.ts
420
test/test.ts
@ -1,26 +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 };
|
||||
|
||||
if (process.env.CI) {
|
||||
process.exit(0);
|
||||
// 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();
|
||||
});
|
||||
}
|
||||
|
||||
tap.test('first test', async () => {
|
||||
testProxy = new smartproxy.SmartProxy();
|
||||
// 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 wait for 5 seconds', async (tools) => {
|
||||
await tools.delayFor(1000);
|
||||
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 close the testproxy', async () => {
|
||||
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.1.3',
|
||||
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';
|
||||
|
369
ts/smartproxy.classes.networkproxy.ts
Normal file
369
ts/smartproxy.classes.networkproxy.ts
Normal file
@ -0,0 +1,369 @@
|
||||
import * as plugins from './smartproxy.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,106 +0,0 @@
|
||||
import { expose } from '@pushrocks/smartspawn';
|
||||
import * as plugins from './smartproxy.plugins';
|
||||
import { SmartproxyRouter } from './smartproxy.classes.router';
|
||||
|
||||
export class ProxyWorker {
|
||||
public hostCandidates: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||
public httpsServer: plugins.https.Server; // | plugins.http.Server;
|
||||
public port = 8001;
|
||||
public router = new SmartproxyRouter();
|
||||
|
||||
public async setPort(portArg: number) {
|
||||
this.port = portArg;
|
||||
}
|
||||
|
||||
/**
|
||||
* starts the proxyInstance
|
||||
*/
|
||||
public async start() {
|
||||
this.httpsServer = plugins.https.createServer(async (req, res) => {
|
||||
const destinationConfig = this.router.routeReq(req);
|
||||
const response = await plugins.smartrequest.request(
|
||||
`http://${destinationConfig.destinationIp}:${destinationConfig.destinationPort}${req.url}`,
|
||||
{
|
||||
method: req.method,
|
||||
headers: req.headers
|
||||
},
|
||||
true // lets make this streaming
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
// 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(this.port);
|
||||
console.log(`OK: now listening for new connections on port ${this.port}`);
|
||||
}
|
||||
|
||||
public async updateCandidates(arrayOfReverseCandidates: plugins.tsclass.IReverseProxyConfig[]) {
|
||||
this.hostCandidates = arrayOfReverseCandidates;
|
||||
this.router.setNewCandidates(arrayOfReverseCandidates);
|
||||
for (const hostCandidate of this.hostCandidates) {
|
||||
this.httpsServer.addContext(hostCandidate.hostName, {
|
||||
cert: hostCandidate.publicKey,
|
||||
key: hostCandidate.privateKey
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
this.httpsServer.close(() => {
|
||||
done.resolve();
|
||||
});
|
||||
await done.promise;
|
||||
}
|
||||
}
|
||||
|
||||
const proxyWorkerInstance = new ProxyWorker();
|
||||
|
||||
// the following is interesting for the master process only
|
||||
const proxyWorkerCalls = {
|
||||
stop: async () => {
|
||||
await proxyWorkerInstance.stop();
|
||||
},
|
||||
start: async () => {
|
||||
await proxyWorkerInstance.start();
|
||||
},
|
||||
updateReverseConfigs: async (configArray: plugins.tsclass.network.IReverseProxyConfig[]) => {}
|
||||
};
|
||||
|
||||
export type TProxyWorkerCalls = typeof proxyWorkerCalls;
|
||||
expose(proxyWorkerCalls);
|
||||
console.log('ProxyWorker initialized');
|
@ -1,20 +1,33 @@
|
||||
import * as plugins from './smartproxy.plugins';
|
||||
import * as plugins from './smartproxy.plugins.js';
|
||||
|
||||
export class SmartproxyRouter {
|
||||
public reverseCandidates: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||
export class ProxyRouter {
|
||||
public reverseProxyConfigs: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||
|
||||
/**
|
||||
* sets a new set of reverse configs to be routed to
|
||||
* @param reverseCandidatesArg
|
||||
*/
|
||||
public setNewCandidates(reverseCandidatesArg: plugins.tsclass.network.IReverseProxyConfig[]) {
|
||||
this.reverseCandidates = 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;
|
||||
const correspodingReverseProxyConfig = this.reverseCandidates.find(reverseConfig => {
|
||||
return reverseConfig.hostName === originalHost;
|
||||
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,35 +0,0 @@
|
||||
import * as plugins from './smartproxy.plugins';
|
||||
|
||||
import { TProxyWorkerCalls } from './smartproxy.classes.proxyworker';
|
||||
import { TPortProxyCalls } from './smartproxy.portproxy';
|
||||
|
||||
export class SmartProxy {
|
||||
public smartsystem = new plugins.smartsystem.Smartsystem();
|
||||
public hostCandidates: plugins.tsclass.network.IReverseProxyConfig[] = [];
|
||||
public proxyWorkerFunctions: plugins.smartspawn.ModuleThread<TProxyWorkerCalls>;
|
||||
public portProxyFunctions: plugins.smartspawn.ModuleThread<TPortProxyCalls>;
|
||||
|
||||
public addHostCandidate(hostCandidate: plugins.tsclass.network.IReverseProxyConfig) {
|
||||
// TODO search for old hostCandidates with that target
|
||||
this.hostCandidates.push(hostCandidate);
|
||||
}
|
||||
|
||||
public async start () {
|
||||
this.proxyWorkerFunctions = await plugins.smartspawn.spawn<TProxyWorkerCalls>(new plugins.smartspawn.Worker('./smartproxy.classes.proxyworker'));
|
||||
this.portProxyFunctions = await plugins.smartspawn.spawn<TPortProxyCalls>(new plugins.smartspawn.Worker('./smartproxy.portproxy'));
|
||||
console.log('successfully spawned proxymaster');
|
||||
await this.proxyWorkerFunctions.start();
|
||||
}
|
||||
|
||||
public async stop () {
|
||||
await this.proxyWorkerFunctions.stop();
|
||||
await plugins.smartspawn.Thread.terminate(this.portProxyFunctions);
|
||||
console.log('proxy worker stopped');
|
||||
await this.portProxyFunctions.stop();
|
||||
await plugins.smartspawn.Thread.terminate(this.proxyWorkerFunctions);
|
||||
console.log('portproxy stopped');
|
||||
console.log('Terminated all childs!');
|
||||
}
|
||||
|
||||
|
||||
}
|
32
ts/smartproxy.classes.sslredirect.ts
Normal file
32
ts/smartproxy.classes.sslredirect.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import * as plugins from './smartproxy.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,32 +1,27 @@
|
||||
// node native scope
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
import * as net from 'net';
|
||||
import * as url from 'url';
|
||||
|
||||
export { http, https };
|
||||
export { http, https, net, url };
|
||||
|
||||
// tsclass scope
|
||||
import * as tsclass from '@tsclass/tsclass';
|
||||
|
||||
export {
|
||||
tsclass
|
||||
};
|
||||
export { tsclass };
|
||||
|
||||
// pushrocks scope
|
||||
import * as smartpromise from '@pushrocks/smartpromise';
|
||||
import * as smartrequest from '@pushrocks/smartrequest';
|
||||
import * as smartspawn from '@pushrocks/smartspawn';
|
||||
import * as smartsystem from '@pushrocks/smartsystem';
|
||||
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 {
|
||||
smartrequest,
|
||||
smartpromise,
|
||||
smartspawn,
|
||||
smartsystem,
|
||||
};
|
||||
export { lik, smartdelay, smartrequest, smartpromise, smartstring };
|
||||
|
||||
// third party scope
|
||||
import * as ws from 'ws';
|
||||
import wsDefault from 'ws';
|
||||
|
||||
export {
|
||||
ws
|
||||
};
|
||||
export { wsDefault, ws };
|
||||
|
@ -1,26 +1,70 @@
|
||||
import * as plugins from './smartproxy.plugins';
|
||||
import { expose } from '@pushrocks/smartspawn';
|
||||
import * as plugins from './smartproxy.plugins.js';
|
||||
import * as net from 'net';
|
||||
const server = net.createServer(from => {
|
||||
const to = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: 8001
|
||||
});
|
||||
from.pipe(to);
|
||||
to.pipe(from);
|
||||
}).listen(8000);
|
||||
|
||||
const portProxyCalls = {
|
||||
stop: async () => {
|
||||
export class PortProxy {
|
||||
netServer: plugins.net.Server;
|
||||
fromPort: number;
|
||||
toPort: number;
|
||||
|
||||
constructor(fromPortArg: number, toPortArg: number) {
|
||||
this.fromPort = fromPortArg;
|
||||
this.toPort = toPortArg;
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
this.netServer = net
|
||||
.createServer((from) => {
|
||||
const to = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: this.toPort,
|
||||
});
|
||||
from.setTimeout(120000);
|
||||
from.pipe(to);
|
||||
to.pipe(from);
|
||||
from.on('error', () => {
|
||||
cleanUpSockets(from, to);
|
||||
});
|
||||
to.on('error', () => {
|
||||
cleanUpSockets(from, to);
|
||||
});
|
||||
from.on('close', () => {
|
||||
cleanUpSockets(from, to);
|
||||
});
|
||||
to.on('close', () => {
|
||||
cleanUpSockets(from, to);
|
||||
});
|
||||
from.on('timeout', () => {
|
||||
cleanUpSockets(from, to);
|
||||
});
|
||||
to.on('timeout', () => {
|
||||
cleanUpSockets(from, to);
|
||||
});
|
||||
from.on('end', () => {
|
||||
cleanUpSockets(from, to);
|
||||
});
|
||||
to.on('end', () => {
|
||||
cleanUpSockets(from, to);
|
||||
});
|
||||
})
|
||||
.listen(this.fromPort);
|
||||
console.log(`PortProxy -> OK: Now listening on port ${this.fromPort}`);
|
||||
}
|
||||
|
||||
public async stop() {
|
||||
const done = plugins.smartpromise.defer();
|
||||
server.close(() => {
|
||||
this.netServer.close(() => {
|
||||
done.resolve();
|
||||
});
|
||||
await done.promise;
|
||||
}
|
||||
};
|
||||
|
||||
export type TPortProxyCalls = typeof portProxyCalls;
|
||||
expose(portProxyCalls);
|
||||
|
||||
console.log('PortProxy Initialized');
|
||||
}
|
||||
|
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