diff --git a/.dockerignore b/.dockerignore index c2658d7..53f7ef0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1 +1,6 @@ +.git/ +.nogit/ +dist/ +dist_*/ node_modules/ +rust/target/ diff --git a/.gitea/workflows/default_nottags.yaml b/.gitea/workflows/default_nottags.yaml deleted file mode 100644 index 9f4e743..0000000 --- a/.gitea/workflows/default_nottags.yaml +++ /dev/null @@ -1,66 +0,0 @@ -name: Default (not tags) - -on: - push: - tags-ignore: - - '**' - -env: - IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci - NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{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 @shipzone/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 diff --git a/.gitea/workflows/default_tags.yaml b/.gitea/workflows/default_tags.yaml deleted file mode 100644 index e27ad69..0000000 --- a/.gitea/workflows/default_tags.yaml +++ /dev/null @@ -1,124 +0,0 @@ -name: Default (tags) - -on: - push: - tags: - - '*' - -env: - IMAGE: registry.gitlab.com/hosttoday/ht-docker-node:npmci - NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{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 @shipzone/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 @shipzone/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 @shipzone/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 @shipzone/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 diff --git a/.gitea/workflows/docker_nottags.yaml b/.gitea/workflows/docker_nottags.yaml new file mode 100644 index 0000000..ea056d9 --- /dev/null +++ b/.gitea/workflows/docker_nottags.yaml @@ -0,0 +1,36 @@ +name: Docker (non-tag pushes) + +on: + push: + tags-ignore: + - '**' + +env: + IMAGE: code.foss.global/host.today/ht-docker-node:szci + NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git + NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }} + +jobs: + test: + runs-on: ubuntu-latest + container: + image: ${{ env.IMAGE }} + + steps: + - uses: actions/checkout@v3 + + - name: Prepare + run: | + pnpm install -g pnpm + pnpm install -g @git.zone/tsdocker@latest + pnpm config set registry https://verdaccio.lossless.digital + pnpm install + + - name: Test + run: pnpm test + + - name: Build image + run: tsdocker build + + - name: Test image + run: tsdocker test diff --git a/.gitea/workflows/docker_tags.yaml b/.gitea/workflows/docker_tags.yaml new file mode 100644 index 0000000..7bacc2a --- /dev/null +++ b/.gitea/workflows/docker_tags.yaml @@ -0,0 +1,42 @@ +name: Docker (tags) + +on: + push: + tags: + - '*' + +env: + IMAGE: code.foss.global/host.today/ht-docker-node:szci + NPMCI_COMPUTED_REPOURL: https://${{gitea.repository_owner}}:${{secrets.GITEA_TOKEN}}@gitea.lossless.digital/${{gitea.repository}}.git + NPMCI_LOGIN_DOCKER_DOCKERREGISTRY: ${{ secrets.NPMCI_LOGIN_DOCKER_DOCKERREGISTRY }} + +jobs: + release: + runs-on: ubuntu-latest + container: + image: code.foss.global/host.today/ht-docker-dbase:szci + + steps: + - uses: actions/checkout@v3 + + - name: Prepare + run: | + pnpm install -g pnpm + pnpm install -g @git.zone/tsdocker@latest + pnpm config set registry https://verdaccio.lossless.digital + pnpm install + + - name: Login to registries + run: tsdocker login + + - name: List images + run: tsdocker list + + - name: Build images + run: tsdocker build + + - name: Test images + run: tsdocker test + + - name: Push to code.foss.global + run: tsdocker push code.foss.global diff --git a/.smartconfig.json b/.smartconfig.json index 17e97d4..9ceedf3 100644 --- a/.smartconfig.json +++ b/.smartconfig.json @@ -5,6 +5,13 @@ "linux_arm64" ] }, + "@git.zone/tsdocker": { + "registries": ["code.foss.global"], + "registryRepoMap": { + "code.foss.global": "serve.zone/remoteingress" + }, + "platforms": ["linux/amd64", "linux/arm64"] + }, "@git.zone/cli": { "projectType": "npm", "module": { @@ -47,4 +54,4 @@ "@ship.zone/szci": { "npmGlobalTools": [] } -} \ No newline at end of file +} diff --git a/Dockerfile b/Dockerfile index e85966b..6063bd3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,46 +1,38 @@ # gitzone dockerfile_service ## STAGE 1 // BUILD -FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node1 -COPY ./ /app -WORKDIR /app -ARG NPMCI_TOKEN_NPM2 -ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2 -RUN npmci npm prepare -RUN pnpm config set store-dir .pnpm-store -RUN rm -rf node_modules && pnpm install -RUN pnpm run build +FROM code.foss.global/host.today/ht-docker-node:lts AS build -# gitzone dockerfile_service -## STAGE 2 // install production -FROM registry.gitlab.com/hosttoday/ht-docker-node:npmci as node2 WORKDIR /app -COPY --from=node1 /app /app + +COPY package.json pnpm-lock.yaml ./ +RUN pnpm config set store-dir .pnpm-store +RUN pnpm config set registry https://verdaccio.lossless.digital +RUN pnpm install --frozen-lockfile + +COPY . ./ +# The npm package builds both Rust targets; each OCI image only needs its native binary. +RUN node -e "const fs=require('node:fs');const p='.smartconfig.json';const c=JSON.parse(fs.readFileSync(p,'utf8'));c['@git.zone/tsrust']={...(c['@git.zone/tsrust']||{}),targets:[]};fs.writeFileSync(p,JSON.stringify(c));" \ + && pnpm exec tsbuild tsfolders --allowimplicitany \ + && pnpm exec tsrust RUN rm -rf .pnpm-store -ARG NPMCI_TOKEN_NPM2 -ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2 -RUN npmci npm prepare -RUN pnpm config set store-dir .pnpm-store -RUN rm -rf node_modules/ && pnpm install --prod +RUN pnpm prune --prod +## STAGE 2 // PRODUCTION +FROM code.foss.global/host.today/ht-docker-node:alpine-node AS production -## STAGE 3 // rebuild dependencies for alpine -FROM registry.gitlab.com/hosttoday/ht-docker-node:alpinenpmci as node3 WORKDIR /app -COPY --from=node2 /app /app -ARG NPMCI_TOKEN_NPM2 -ENV NPMCI_TOKEN_NPM2 $NPMCI_TOKEN_NPM2 -RUN npmci npm prepare -RUN pnpm config set store-dir .pnpm-store -RUN pnpm rebuild -r -## STAGE 4 // the final production image with all dependencies in place -FROM registry.gitlab.com/hosttoday/ht-docker-node:alpine as node4 -WORKDIR /app -COPY --from=node3 /app /app +ENV NODE_ENV=production -### Healthchecks -RUN pnpm install -g @servezone/healthy -HEALTHCHECK --interval=30s --timeout=30s --start-period=30s --retries=3 CMD [ "healthy" ] +COPY --from=build /app/package.json ./package.json +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/cli.js ./cli.js +COPY --from=build /app/dist_ts ./dist_ts +COPY --from=build /app/dist_rust ./dist_rust -EXPOSE 80 -CMD ["npm", "start"] +LABEL org.opencontainers.image.title="remoteingress" \ + org.opencontainers.image.description="serve.zone edge ingress tunnel" \ + org.opencontainers.image.source="https://code.foss.global/serve.zone/remoteingress" + +EXPOSE 80 443 8443 53/udp +CMD ["node", "cli.js"] diff --git a/package.json b/package.json index 5a37f28..5de8046 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "author": "Task Venture Capital GmbH", "license": "MIT", "scripts": { + "start": "(node ./cli.js)", "test": "(pnpm run build && tstest test/ --verbose --logfile --timeout 60)", "build": "(tsbuild tsfolders --allowimplicitany && tsrust)", "buildDocs": "(tsdoc)" diff --git a/test/test_latest.sh b/test/test_latest.sh new file mode 100755 index 0000000..fbc1cd1 --- /dev/null +++ b/test/test_latest.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +set -euo pipefail + +node --input-type=module <<'NODE' +import fs from 'node:fs'; +import { execFileSync } from 'node:child_process'; + +const readJson = (path) => JSON.parse(fs.readFileSync(path, 'utf8')); +const arch = process.arch === 'x64' ? 'amd64' : process.arch; + +const checks = { + packageVersion: readJson('/app/package.json').version, + hasCli: fs.existsSync('/app/cli.js'), + hasRustBinary: fs.existsSync(`/app/dist_rust/remoteingress-bin_linux_${arch}`) || fs.existsSync('/app/dist_rust/remoteingress-bin'), +}; + +await import('/app/dist_ts/index.js'); +execFileSync('node', ['/app/cli.js', '--help'], { stdio: 'pipe' }); + +if (checks.packageVersion !== '4.17.1') { + throw new Error(`Unexpected remoteingress package version ${checks.packageVersion}`); +} +if (!checks.hasCli) { + throw new Error('Missing cli.js'); +} +if (!checks.hasRustBinary) { + throw new Error(`Missing Rust binary for ${arch}`); +} + +console.log(JSON.stringify(checks)); +NODE diff --git a/ts/classes.remoteingresshub.ts b/ts/classes.remoteingresshub.ts index 243aa38..a0e7322 100644 --- a/ts/classes.remoteingresshub.ts +++ b/ts/classes.remoteingresshub.ts @@ -135,7 +135,7 @@ export interface IUdpStatus { droppedDatagrams: number; } -type TAllowedEdge = { id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number; firewallConfig?: IFirewallConfig; performance?: IPerformanceConfig }; +export type TAllowedEdge = { id: string; secret: string; listenPorts?: number[]; listenPortsUdp?: number[]; stunIntervalSecs?: number; firewallConfig?: IFirewallConfig; performance?: IPerformanceConfig }; const MAX_RESTART_ATTEMPTS = 10; const MAX_RESTART_BACKOFF_MS = 30_000; diff --git a/ts/index.ts b/ts/index.ts index dab56af..f34dae5 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,3 +1,147 @@ +import { RemoteIngressEdge } from './classes.remoteingressedge.js'; +import { RemoteIngressHub, type IHubConfig, type TAllowedEdge } from './classes.remoteingresshub.js'; + export * from './classes.remoteingresshub.js'; export * from './classes.remoteingressedge.js'; export * from './classes.token.js'; + +const usage = `remoteingress + +Usage: + remoteingress hub [--tunnel-port 8443] [--target-host 127.0.0.1] + remoteingress edge --token + remoteingress edge --hub-host --edge-id --secret [--hub-port 8443] + +Environment: + REMOTEINGRESS_MODE=hub|edge + REMOTEINGRESS_TOKEN= + REMOTEINGRESS_HUB_HOST= + REMOTEINGRESS_HUB_PORT=8443 + REMOTEINGRESS_EDGE_ID= + REMOTEINGRESS_SECRET= + REMOTEINGRESS_TARGET_HOST=127.0.0.1 + REMOTEINGRESS_ALLOWED_EDGES_JSON='[{"id":"edge-1","secret":"secret","listenPorts":[80,443]}]' +`; + +const readArg = (args: string[], name: string): string | undefined => { + const prefix = `--${name}=`; + const inlineValue = args.find((arg) => arg.startsWith(prefix)); + if (inlineValue) { + return inlineValue.slice(prefix.length); + } + + const index = args.indexOf(`--${name}`); + if (index >= 0) { + return args[index + 1]; + } + + return undefined; +}; + +const readNumber = (value: string | undefined, fallback: number): number => { + if (!value) { + return fallback; + } + + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) { + throw new Error(`Invalid port: ${value}`); + } + + return parsed; +}; + +const readJson = (value: string | undefined, fallback: T): T => { + if (!value) { + return fallback; + } + + return JSON.parse(value) as T; +}; + +const waitForever = async (stop: () => Promise) => { + let stopping = false; + const handleStop = async () => { + if (stopping) { + return; + } + stopping = true; + await stop(); + process.exit(0); + }; + + process.once('SIGINT', () => void handleStop()); + process.once('SIGTERM', () => void handleStop()); + + await new Promise(() => {}); +}; + +export const runCli = async () => { + const args = process.argv.slice(2); + if (args.includes('--help') || args.includes('-h')) { + console.log(usage); + return; + } + + const positionalMode = args[0]?.startsWith('--') ? undefined : args[0]; + const mode = readArg(args, 'mode') ?? positionalMode ?? process.env.REMOTEINGRESS_MODE; + + if (mode === 'hub') { + const hub = new RemoteIngressHub(); + const config: IHubConfig = { + tunnelPort: readNumber(readArg(args, 'tunnel-port') ?? process.env.REMOTEINGRESS_TUNNEL_PORT, 8443), + targetHost: readArg(args, 'target-host') ?? process.env.REMOTEINGRESS_TARGET_HOST ?? '127.0.0.1', + tls: { + certPem: readArg(args, 'tls-cert-pem') ?? process.env.REMOTEINGRESS_TLS_CERT_PEM, + keyPem: readArg(args, 'tls-key-pem') ?? process.env.REMOTEINGRESS_TLS_KEY_PEM, + }, + performance: readJson(readArg(args, 'performance-json') ?? process.env.REMOTEINGRESS_PERFORMANCE_JSON, undefined), + }; + + await hub.start(config); + + const allowedEdges = readJson( + readArg(args, 'allowed-edges-json') ?? process.env.REMOTEINGRESS_ALLOWED_EDGES_JSON, + [], + ); + if (allowedEdges.length > 0) { + await hub.updateAllowedEdges(allowedEdges); + } + + console.log(`RemoteIngress hub listening on ${config.tunnelPort}`); + await waitForever(() => hub.stop()); + return; + } + + if (mode === 'edge') { + const edge = new RemoteIngressEdge(); + const token = readArg(args, 'token') ?? process.env.REMOTEINGRESS_TOKEN; + + if (token) { + await edge.start({ token }); + } else { + const hubHost = readArg(args, 'hub-host') ?? process.env.REMOTEINGRESS_HUB_HOST; + const edgeId = readArg(args, 'edge-id') ?? process.env.REMOTEINGRESS_EDGE_ID; + const secret = readArg(args, 'secret') ?? process.env.REMOTEINGRESS_SECRET; + + if (!hubHost || !edgeId || !secret) { + throw new Error('Edge mode requires --token or --hub-host, --edge-id, and --secret'); + } + + await edge.start({ + hubHost, + hubPort: readNumber(readArg(args, 'hub-port') ?? process.env.REMOTEINGRESS_HUB_PORT, 8443), + edgeId, + secret, + bindAddress: readArg(args, 'bind-address') ?? process.env.REMOTEINGRESS_BIND_ADDRESS, + transportMode: readArg(args, 'transport-mode') as any, + }); + } + + console.log('RemoteIngress edge started'); + await waitForever(() => edge.stop()); + return; + } + + console.log(usage); +};