diff --git a/backend/internal/stream.js b/backend/internal/stream.js index f69a5c1..4d49bc3 100644 --- a/backend/internal/stream.js +++ b/backend/internal/stream.js @@ -9,7 +9,7 @@ const internalHost = require('./host'); const {castJsonIfNeed} = require('../lib/helpers'); function omissions () { - return ['is_deleted']; + return ['is_deleted', 'owner.is_deleted', 'certificate.is_deleted']; } const internalStream = { diff --git a/backend/models/stream.js b/backend/models/stream.js index 40fd601..dbec2dc 100644 --- a/backend/models/stream.js +++ b/backend/models/stream.js @@ -8,6 +8,7 @@ const now = require('./now_helper'); Model.knex(db); const boolFields = [ + 'enabled', 'is_deleted', 'tcp_forwarding', 'udp_forwarding', diff --git a/backend/schema/components/stream-object.json b/backend/schema/components/stream-object.json index 0ab8f90..848c30e 100644 --- a/backend/schema/components/stream-object.json +++ b/backend/schema/components/stream-object.json @@ -19,9 +19,7 @@ "incoming_port": { "type": "integer", "minimum": 1, - "maximum": 65535, - "if": {"properties": {"tcp_forwarding": {"const": true}}}, - "then": {"not": {"oneOf": [{"const": 80}, {"const": 443}]}} + "maximum": 65535 }, "forwarding_host": { "anyOf": [ @@ -60,6 +58,19 @@ }, "meta": { "type": "object" + }, + "owner": { + "$ref": "./user-object.json" + }, + "certificate": { + "oneOf": [ + { + "type": "null" + }, + { + "$ref": "./certificate-object.json" + } + ] } } } diff --git a/docker/docker-compose.ci.yml b/docker/docker-compose.ci.yml index 022f281..280a054 100644 --- a/docker/docker-compose.ci.yml +++ b/docker/docker-compose.ci.yml @@ -22,6 +22,10 @@ services: test: ["CMD", "/usr/bin/check-health"] interval: 10s timeout: 3s + expose: + - '80-81/tcp' + - '443/tcp' + - '1500-1503/tcp' networks: fulltest: aliases: @@ -97,7 +101,7 @@ services: HTTP_PROXY: 'squid:3128' HTTPS_PROXY: 'squid:3128' volumes: - - 'cypress_logs:/results' + - 'cypress_logs:/test/results' - './dev/resolv.conf:/etc/resolv.conf:ro' - '/etc/localtime:/etc/localtime:ro' command: cypress run --browser chrome --config-file=cypress/config/ci.js diff --git a/docker/scripts/install-s6 b/docker/scripts/install-s6 index 2922735..5f3b73e 100755 --- a/docker/scripts/install-s6 +++ b/docker/scripts/install-s6 @@ -8,7 +8,7 @@ BLUE='\E[1;34m' GREEN='\E[1;32m' RESET='\E[0m' -S6_OVERLAY_VERSION=3.1.5.0 +S6_OVERLAY_VERSION=3.2.0.2 TARGETPLATFORM=${1:-linux/amd64} # Determine the correct binary file for the architecture given diff --git a/test/cypress/Dockerfile b/test/cypress/Dockerfile index c0bc0ba..9b835fe 100644 --- a/test/cypress/Dockerfile +++ b/test/cypress/Dockerfile @@ -1,6 +1,4 @@ -FROM cypress/included:13.9.0 - -COPY --chown=1000 ./test /test +FROM cypress/included:14.0.1 # Disable Cypress CLI colors ENV FORCE_COLOR=0 @@ -12,12 +10,13 @@ RUN wget "https://github.com/testssl/testssl.sh/archive/refs/tags/v3.2rc4.tar.gz && mv /tmp/testssl.sh-3.2rc4 /testssl \ && rm /tmp/testssl.tgz \ && apt-get update \ - && apt-get install -y bsdmainutils \ + && apt-get install -y bsdmainutils curl dnsutils \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* \ && wget "https://github.com/FiloSottile/mkcert/releases/download/v1.4.4/mkcert-v1.4.4-linux-amd64" -O /bin/mkcert \ && chmod +x /bin/mkcert +COPY --chown=1000 ./test /test WORKDIR /test RUN yarn install && yarn cache clean ENTRYPOINT [] diff --git a/test/cypress/e2e/api/Streams.cy.js b/test/cypress/e2e/api/Streams.cy.js new file mode 100644 index 0000000..9de4315 --- /dev/null +++ b/test/cypress/e2e/api/Streams.cy.js @@ -0,0 +1,213 @@ +/// + +describe('Streams', () => { + let token; + + before(() => { + cy.getToken().then((tok) => { + token = tok; + // Set default site content + cy.task('backendApiPut', { + token: token, + path: '/api/settings/default-site', + data: { + value: 'html', + meta: { + html: '

yay it works

' + }, + }, + }).then((data) => { + cy.validateSwaggerSchema('put', 200, '/settings/{settingID}', data); + }); + }); + + // Create a custom cert pair + cy.exec('mkcert -cert-file=/test/cypress/fixtures/website1.pem -key-file=/test/cypress/fixtures/website1.key.pem website1.example.com').then((result) => { + expect(result.code).to.eq(0); + // Install CA + cy.exec('mkcert -install').then((result) => { + expect(result.code).to.eq(0); + }); + }); + + cy.exec('rm -f /test/results/testssl.json'); + }); + + it('Should be able to create TCP Stream', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/streams', + data: { + incoming_port: 1500, + forwarding_host: '127.0.0.1', + forwarding_port: 80, + certificate_id: 0, + meta: { + dns_provider_credentials: "", + letsencrypt_agree: false, + dns_challenge: true + }, + tcp_forwarding: true, + udp_forwarding: false + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property('enabled', true); + expect(data).to.have.property('tcp_forwarding', true); + expect(data).to.have.property('udp_forwarding', false); + + cy.exec('curl --noproxy -- http://website1.example.com:1500').then((result) => { + expect(result.code).to.eq(0); + expect(result.stdout).to.contain('yay it works'); + }); + }); + }); + + it('Should be able to create UDP Stream', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/streams', + data: { + incoming_port: 1501, + forwarding_host: '127.0.0.1', + forwarding_port: 80, + certificate_id: 0, + meta: { + dns_provider_credentials: "", + letsencrypt_agree: false, + dns_challenge: true + }, + tcp_forwarding: false, + udp_forwarding: true + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property('enabled', true); + expect(data).to.have.property('tcp_forwarding', false); + expect(data).to.have.property('udp_forwarding', true); + }); + }); + + it('Should be able to create TCP/UDP Stream', function() { + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/streams', + data: { + incoming_port: 1502, + forwarding_host: '127.0.0.1', + forwarding_port: 80, + certificate_id: 0, + meta: { + dns_provider_credentials: "", + letsencrypt_agree: false, + dns_challenge: true + }, + tcp_forwarding: true, + udp_forwarding: true + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property('enabled', true); + expect(data).to.have.property('tcp_forwarding', true); + expect(data).to.have.property('udp_forwarding', true); + + cy.exec('curl --noproxy -- http://website1.example.com:1502').then((result) => { + expect(result.code).to.eq(0); + expect(result.stdout).to.contain('yay it works'); + }); + }); + }); + + it('Should be able to create SSL TCP Stream', function() { + let certID = 0; + + // Create custom cert + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/certificates', + data: { + provider: "other", + nice_name: "Custom Certificate for SSL Stream", + }, + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/certificates', data); + expect(data).to.have.property('id'); + certID = data.id; + + // Upload files + cy.task('backendApiPostFiles', { + token: token, + path: `/api/nginx/certificates/${certID}/upload`, + files: { + certificate: 'website1.pem', + certificate_key: 'website1.key.pem', + }, + }).then((data) => { + cy.validateSwaggerSchema('post', 200, '/nginx/certificates/{certID}/upload', data); + expect(data).to.have.property('certificate'); + expect(data).to.have.property('certificate_key'); + + // Create the stream + cy.task('backendApiPost', { + token: token, + path: '/api/nginx/streams', + data: { + incoming_port: 1503, + forwarding_host: '127.0.0.1', + forwarding_port: 80, + certificate_id: certID, + meta: { + dns_provider_credentials: "", + letsencrypt_agree: false, + dns_challenge: true + }, + tcp_forwarding: true, + udp_forwarding: false + } + }).then((data) => { + cy.validateSwaggerSchema('post', 201, '/nginx/streams', data); + expect(data).to.have.property('id'); + expect(data.id).to.be.greaterThan(0); + expect(data).to.have.property("enabled", true); + expect(data).to.have.property('tcp_forwarding', true); + expect(data).to.have.property('udp_forwarding', false); + expect(data).to.have.property('certificate_id', certID); + + // Check the ssl termination + cy.task('log', '[testssl.sh] Running ...'); + cy.exec('/testssl/testssl.sh --quiet --add-ca="$(/bin/mkcert -CAROOT)/rootCA.pem" --jsonfile=/test/results/testssl.json website1.example.com:1503', { + timeout: 120000, // 2 minutes + }).then((result) => { + cy.task('log', '[testssl.sh] ' + result.stdout); + + const allowedSeverities = ["INFO", "OK", "LOW", "MEDIUM"]; + const ignoredIDs = [ + 'cert_chain_of_trust', + 'cert_extlifeSpan', + 'cert_revocation', + 'overall_grade', + ]; + + cy.readFile('/test/results/testssl.json').then((data) => { + // Parse each array item + for (let i = 0; i < data.length; i++) { + const item = data[i]; + if (ignoredIDs.includes(item.id)) { + continue; + } + expect(item.severity).to.be.oneOf(allowedSeverities); + } + }); + }); + }); + }); + }); + }); + +}); diff --git a/test/package.json b/test/package.json index b52ecfb..a3a3cad 100644 --- a/test/package.json +++ b/test/package.json @@ -4,18 +4,18 @@ "description": "", "main": "index.js", "dependencies": { - "@jc21/cypress-swagger-validation": "^0.3.1", - "axios": "^1.7.7", - "cypress": "^13.15.0", - "cypress-multi-reporters": "^1.6.4", + "@jc21/cypress-swagger-validation": "^0.3.2", + "axios": "^1.7.9", + "cypress": "^14.0.1", + "cypress-multi-reporters": "^2.0.5", "cypress-wait-until": "^3.0.2", - "eslint": "^9.12.0", + "eslint": "^9.19.0", "eslint-plugin-align-assignments": "^1.1.2", "eslint-plugin-chai-friendly": "^1.0.1", - "eslint-plugin-cypress": "^3.5.0", + "eslint-plugin-cypress": "^4.1.0", "form-data": "^4.0.1", "lodash": "^4.17.21", - "mocha": "^10.7.3", + "mocha": "^11.1.0", "mocha-junit-reporter": "^2.2.1" }, "scripts": {