diff --git a/.eslintrc.cjs b/.eslintrc.cjs index 3eab0c6..a7d79fa 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -12,5 +12,6 @@ module.exports = { }, rules: { // Add your custom rules here + }, }; \ No newline at end of file diff --git a/.github/workflows/npm-gulp.yml b/.github/workflows/npm-gulp.yml new file mode 100644 index 0000000..190148c --- /dev/null +++ b/.github/workflows/npm-gulp.yml @@ -0,0 +1,31 @@ +name: NodeJS with Gulp + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x, 22.x] + + steps: + - uses: actions/checkout@v4 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Build + run: | + npm install + gulp diff --git a/api-server/package.json b/api-server/package.json index d4fbc1f..a4b415b 100644 --- a/api-server/package.json +++ b/api-server/package.json @@ -12,5 +12,8 @@ "type": "git", "url": "https://github.com/ado24/NAD-C338-REST-API.git" }, - "private": true + "private": true, + "dependencies": { + "cors": "^2.8.5" + } } diff --git a/api-server/properties.env b/api-server/properties.env index 0a0c5a1..1b4b2d5 100644 --- a/api-server/properties.env +++ b/api-server/properties.env @@ -1,3 +1,5 @@ API_SERVER_LISTENER_PORT=3000 BLUOS_TCP_PORT=30001 -BLUOS_IP=10.0.0.251 \ No newline at end of file +BLUOS_IP=10.0.0.251 +KEEPALIVE_TIMEOUT=61000 +HEADERS_TIMEOUT=62000 \ No newline at end of file diff --git a/api-server/server.js b/api-server/server.js index 3a860aa..25b50ef 100644 --- a/api-server/server.js +++ b/api-server/server.js @@ -1,6 +1,9 @@ import express from 'express'; import { NADC338 } from '../model/NAD-C338.js'; import dotenv from 'dotenv'; +import https from 'https'; +import fs from 'fs'; +import cors from 'cors'; dotenv.config({'path': './api-server/properties.env'}); const app = express(); @@ -8,14 +11,52 @@ const app = express(); const port = process.env.API_SERVER_LISTENER_PORT; const ip = process.env.BLUOS_IP; const bluOsPort = parseInt(process.env.BLUOS_TCP_PORT); +const keepAliveTimeout = parseInt(process.env.KEEPALIVE_TIMEOUT); +const headersTimeout = parseInt(process.env.HEADERS_TIMEOUT); -app.use(express.json()); +// Load SSL certificate and private key +//const ca = fs.readFileSync('path/to/your/ca_bundle.crt', 'utf8'); + +const endpointKeyPath = "./keys/server.key"; +const endpointCertPath = "./certs/server.crt"; +const caPath = "./certs/server.crt"; + +const credentials = { + key: fs.readFileSync(endpointKeyPath), + cert: fs.readFileSync(endpointCertPath), + ca: fs.readFileSync(caPath) +}; let nad = new NADC338(ip, bluOsPort); +app.use(cors()); +app.use(express.json()); +app.use((req, res, next) => { + // Set default security headers + res.set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains'); + next(); +}); +app.use((req, res, next) => { + // Add default cache control for modification endpoints + if (req.method !== 'GET') { + res.set({ + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); + } + next(); +}); + // GET endpoints +// Frequently changing state - minimal cache app.get('/power', async (req, res) => { try { const power = await nad.getPower(); + res.set({ + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); res.json({ power }); } catch (error) { res.status(500).send(error.message); @@ -25,6 +66,11 @@ app.get('/power', async (req, res) => { app.get('/volume', async (req, res) => { try { const volume = await nad.getVolume(); + res.set({ + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); res.json({ volume }); } catch (error) { res.status(500).send(error.message); @@ -34,6 +80,11 @@ app.get('/volume', async (req, res) => { app.get('/source', async (req, res) => { try { const source = await nad.getSource(); + res.set({ + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); res.json({ source }); } catch (error) { res.status(500).send(error.message); @@ -43,15 +94,25 @@ app.get('/source', async (req, res) => { app.get('/mute', async (req, res) => { try { const mute = await nad.getMute(); + res.set({ + 'Cache-Control': 'no-cache, no-store, must-revalidate', + 'Pragma': 'no-cache', + 'Expires': '0' + }); res.json({ mute }); } catch (error) { res.status(500).send(error.message); } }); +// Less frequently changing settings - short cache app.get('/brightness', async (req, res) => { try { const brightness = await nad.getBrightness(); + res.set({ + 'Cache-Control': 'public, max-age=5', + 'Vary': 'Accept-Encoding' + }); res.json({ brightness }); } catch (error) { res.status(500).send(error.message); @@ -61,15 +122,24 @@ app.get('/brightness', async (req, res) => { app.get('/bass', async (req, res) => { try { const bass = await nad.getBass(); + res.set({ + 'Cache-Control': 'public, max-age=5', + 'Vary': 'Accept-Encoding' + }); res.json({ bass }); } catch (error) { res.status(500).send(error.message); } }); +// Configuration settings - longer cache app.get('/auto-sense', async (req, res) => { try { const autoSense = await nad.getAutoSense(); + res.set({ + 'Cache-Control': 'public, max-age=60', + 'Vary': 'Accept-Encoding' + }); res.json({ autoSense }); } catch (error) { res.status(500).send(error.message); @@ -79,13 +149,17 @@ app.get('/auto-sense', async (req, res) => { app.get('/auto-standby', async (req, res) => { try { const autoStandby = await nad.getAutoStandby(); + res.set({ + 'Cache-Control': 'public, max-age=60', + 'Vary': 'Accept-Encoding' + }); res.json({ autoStandby }); } catch (error) { res.status(500).send(error.message); } }); -// POST/PUT endpoints +// POST/PUT/PATCH/DELETE endpoints app.post('/power', async (req, res) => { try { const { state } = req.body; @@ -94,7 +168,23 @@ app.post('/power', async (req, res) => { } else if (state === 'Off') { await nad.powerOff(); } - res.sendStatus(200); + const power = await nad.getPower(); + res.json({ power }); + } catch (error) { + res.status(500).send(error.message); + } +}); + +app.patch('/power', async (req, res) => { + try { + const { state } = req.body; + if (state === 'On') { + await nad.powerOn(); + } else if (state === 'Off') { + await nad.powerOff(); + } + //const power = await nad.getPower(); + res.json({ state }); } catch (error) { res.status(500).send(error.message); } @@ -104,7 +194,19 @@ app.put('/volume', async (req, res) => { try { const { level } = req.body; await nad.setVolume(level); - res.sendStatus(200); + const volume = await nad.getVolume(); + res.json({ volume }); + } catch (error) { + res.status(500).send(error.message); + } +}); + +app.patch('/volume', async (req, res) => { + try { + const { level } = req.body; + await nad.setVolume(level); + //const volume = await nad.getVolume(); + res.json({ level }); } catch (error) { res.status(500).send(error.message); } @@ -157,6 +259,15 @@ app.post('/bass', async (req, res) => { } }); +app.patch('/bass', async (req, res) => { + try { + await nad.setBass(); + res.json({ bass: 'On' }); + } catch (error) { + res.status(500).send(error.message); + } +}); + app.delete('/bass', async (req, res) => { try { await nad.unsetBass(); @@ -202,6 +313,14 @@ app.delete('/auto-standby', async (req, res) => { } }); -app.listen(port, () => { - console.log(`Node.js server running at http://localhost:${port}`); -}); \ No newline at end of file +// Create HTTPS server +const httpsServer = https.createServer(credentials, app); + +httpsServer.listen(port, () => { + console.log(`HTTPS server running at https://localhost:${port}`); +}); + +httpsServer.keepAliveTimeout = keepAliveTimeout; +httpsServer.headersTimeout = headersTimeout; + +app.set('timeout', keepAliveTimeout); \ No newline at end of file diff --git a/api/nad-c338-api.yaml b/api/nad-c338-api.yaml index ce060c7..33aa508 100644 --- a/api/nad-c338-api.yaml +++ b/api/nad-c338-api.yaml @@ -10,7 +10,7 @@ servers: ip: default: localhost port: - default: 3000 + default: 3000 paths: /power: get: @@ -27,6 +27,13 @@ paths: type: string '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string post: summary: Set power status requestBody: @@ -42,8 +49,23 @@ paths: responses: '200': description: Power status set + content: + application/json: + schema: + type: object + properties: + power: + type: string '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /volume: get: summary: Get volume level @@ -59,6 +81,13 @@ paths: type: number '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string put: summary: Set volume level requestBody: @@ -73,8 +102,23 @@ paths: responses: '200': description: Volume level set + content: + application/json: + schema: + type: object + properties: + volume: + type: number '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /source: get: summary: Get source @@ -90,6 +134,13 @@ paths: type: string '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string put: summary: Set source requestBody: @@ -104,8 +155,23 @@ paths: responses: '200': description: Source set + content: + application/json: + schema: + type: object + properties: + source: + type: string '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /mute: get: summary: Get mute status @@ -121,6 +187,13 @@ paths: type: string '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string post: summary: Mute responses: @@ -128,6 +201,14 @@ paths: description: Muted '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /unmute: post: summary: Unmute @@ -136,6 +217,14 @@ paths: description: Unmuted '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /brightness: get: summary: Get brightness level @@ -151,6 +240,13 @@ paths: type: number '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string put: summary: Set brightness level requestBody: @@ -165,8 +261,23 @@ paths: responses: '200': description: Brightness level set + content: + application/json: + schema: + type: object + properties: + brightness: + type: number '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /bass: get: summary: Get bass status @@ -182,6 +293,13 @@ paths: type: string '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string post: summary: Set bass responses: @@ -189,6 +307,13 @@ paths: description: Bass set '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string delete: summary: Unset bass responses: @@ -196,6 +321,14 @@ paths: description: Bass unset '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /auto-sense: get: summary: Get auto sense status @@ -211,6 +344,13 @@ paths: type: string '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string post: summary: Set auto sense responses: @@ -218,6 +358,13 @@ paths: description: Auto sense set '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string delete: summary: Unset auto sense responses: @@ -225,6 +372,14 @@ paths: description: Auto sense unset '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string + /auto-standby: get: summary: Get auto standby status @@ -240,6 +395,13 @@ paths: type: string '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string post: summary: Set auto standby responses: @@ -247,10 +409,24 @@ paths: description: Auto standby set '500': description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string delete: summary: Unset auto standby responses: '200': description: Auto standby unset '500': - description: Server error \ No newline at end of file + description: Server error + content: + application/json: + schema: + type: object + properties: + error: + type: string \ No newline at end of file diff --git a/certs/server.crt b/certs/server.crt new file mode 100644 index 0000000..587919b --- /dev/null +++ b/certs/server.crt @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUKoH09iIPk6g9nUwynrD2zBs37WkwDQYJKoZIhvcNAQEN +BQAwPDELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMQ4wDAYDVQQKDAVhZG8yNDEQ +MA4GA1UEAwwHbGVnaW9uNzAgFw0yNTAzMjAxMzQzNDFaGA8yMTI1MDIyNDEzNDM0 +MVowPDELMAkGA1UEBhMCQ0ExCzAJBgNVBAgMAk9OMQ4wDAYDVQQKDAVhZG8yNDEQ +MA4GA1UEAwwHbGVnaW9uNzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +AOATNCkWRD6M4T4G8ThZmI7QNtBZ+4xQ9Fc1OqLgI5813N+/GM+g40HmjUq++5X3 +p91KbR802oSGl6/s+Hy5YWzffA8NhcjbJRY4A0Gehtqw4Wr8NUIbF+utADAMAREU +1DFRmx2NhRj9A+5yXo4QgQ53ZO75SO8zWk1piiXnBKthufOtBMwNR23//ronb9U9 +TCvzEnuRMl2bObcVVjyMWxbe27kyb/0DFFfabownt3a8yUtoIVuBec/k3g+giOnQ +jhcyyDYoUf+7sHu6jKXl9/ZZBk6LEEu6gLmin+0y6gVxRm6jSQCkZZAlHNpOB6R+ +PHEjGjMcJmiXu3/oZCWdQjamtAW9dWhD0eB34kc6A2dOMcNcVEKqfGwnlQGKRRfw +io/I/+/DoZfMMF31G0qB5A2rKfw0r+1f5GUaivY4GpoOsUdvL13re0Y0j5UVlmtF +WvNkIW4GTA0Sao+FPQ+TTfj8JrgxrvBx21Wqb3/ogbFiNWtVzdb5v4s2gpFSsTAR +zPsRmLZBNsBf2DN7RAFC3zIf0ngmrOf/J5nYlBM1yTUf7y9OsHcVPtbk65b/Wj4v +K1MpPFKsBcQSuNc1JH2qBT1ddrgLG6KW4Cn91WUORjS5x4RkvO/AHUtldThdgUF3 +cHLwkt43TZPECxBoZjZuOVnnlVS+Araz41Zj2MWr5Q8jAgMBAAGjeDB2MB0GA1Ud +DgQWBBRrZFXoDcG6qNHDlNhjj+/36SHK2DAfBgNVHSMEGDAWgBRrZFXoDcG6qNHD +lNhjj+/36SHK2DAPBgNVHRMBAf8EBTADAQH/MCMGA1UdEQQcMBqCB2xlZ2lvbjeC +CWxvY2FsaG9zdIcECgAABDANBgkqhkiG9w0BAQ0FAAOCAgEAdbP9JqGjWTr53zME +cYEpcAuexXOUJngoJ3RI756L++IDARQdI6K+VchrITQEe/btbks7C8YAp6LkfREZ +x7+IX47nUCAyTp+CKWe2CbxZS+7WWVS/vb+DnmTw69nKUaWrA4bEyq4MNbt5qWFf +DdCw6uZiNvKvkM/4yJrSriQ+cYa0BZ2moL4iJbxa37700rlJaBKWzBC86GXntDa7 +RxB+lbKN+J28wADS/5JrnZFjbAKsvjx7pGiO5HY2j0CzHX9Cv0zmFCqYTO+29l0v +2PwO0MHMKNBI9kS/QFfUD5fQ5Z6K/rdNpE+C7Imy/ftHRtP4ROeJgScP55x2Y2F/ +eDPkkbRhfS7P3RTClKFsOSnvdQEsIEZIJSx27sN5tf8EO4d2+vF2SohnJyGJ/SEd +haNoQYvD6vZVH8/WBQoU268CE5Qs3FDBLF/toWFKU7efI+NvHfe5YR9t//akFetJ +A3Hk146v5ocFoqTvdQosw+cjAs4hoXZIxAI3o2mCfnXxCpjtCaPHHI5GTUQArpIR +Ztt+zCv7jMkqqUx+vMC+n/euLRnYpeJf5UWnB6OOTkpuxzNXZiFC22PTWD6lNCPU +7JnSz+DEntbLtBazFxI/3cPkZ6VZSvWzUGF7Gs4huZ5d+vb1XfxN7P+nYWrjQkmK +lr1F98szsX/HYZXOpPcM2E9pnjY= +-----END CERTIFICATE----- diff --git a/gulpfile.js b/gulpfile.js new file mode 100644 index 0000000..5d47a5a --- /dev/null +++ b/gulpfile.js @@ -0,0 +1,106 @@ +import gulp from 'gulp'; +import babel from 'gulp-babel'; +import ts from 'gulp-typescript'; +import { deleteAsync } from 'del'; + +// Clean the dist directory +const clean = () => deleteAsync(['dist']); + +// Transpile JavaScript files for api-server +const transpileApiServer = () => { + return gulp.src('api-server/**/*.js') + .pipe(babel({ + presets: [['@babel/preset-env', { modules: false }]] + })) + .pipe(gulp.dest('dist/api-server')); +}; + +// Transpile JavaScript files for tcp-server +const transpileTcpServer = () => { + return gulp.src('tcp-server/**/*.js') + .pipe(babel({ + presets: [['@babel/preset-env', { modules: false }]] + })) + .pipe(gulp.dest('dist/tcp-server')); +}; + +// Transpile model files +const transpileModel = () => { + return gulp.src('model/**/*.js') + .pipe(babel({ + presets: [['@babel/preset-env', { modules: false }]] + })) + .pipe(gulp.dest('dist/model')); +} + +// Copy non-JS files for api-server +const copyApiServer = () => { + return gulp.src(['api-server/**/*.json', 'api-server/properties.env']) + .pipe(gulp.dest('dist/api-server')); +}; + +// Copy non-JS files for tcp-server +const copyTcpServer = () => { + return gulp.src(['tcp-server/**/*.json', 'tcp-server/properties.env']) + .pipe(gulp.dest('dist/tcp-server')); +}; + +// Copy non-JS files for model +const copyModel = () => { + return gulp.src('model/**/*.json') + .pipe(gulp.dest('dist/model')); +}; + +// Copy keys and certs +const copyKeys = () => { + return gulp.src('keys/**/*') + .pipe(gulp.dest('dist/keys')); +} + +const copyCerts = () => { + return gulp.src('certs/**/*') + .pipe(gulp.dest('dist/certs')); +} + +// Generate package.json +const generatePackageJson = () => { + return gulp.src('package.json') + .pipe(gulp.dest('dist')); +}; + + +// Create a TypeScript project +const tsProject = ts.createProject('tsconfig.json'); + +// Transpile TypeScript files for api-server +const transpileApiServerTS = () => { + return gulp.src('api-server/**/*.ts') + .pipe(tsProject()) + .pipe(gulp.dest('dist/api-server-ts')); +}; + +// Transpile TypeScript files for tcp-server +const transpileTcpServerTS = () => { + return gulp.src('tcp-server/**/*.ts') + .pipe(tsProject()) + .pipe(gulp.dest('dist/tcp-server-ts')); +}; + +// Wrapper for the clean task +gulp.task('clean', clean); + +// Wrapper for transpiling JavaScript +gulp.task('transpile', gulp.parallel(transpileApiServer, transpileTcpServer,transpileModel)); + +// Wrapper for copying non-JS files +gulp.task('copy', gulp.parallel(copyApiServer, copyTcpServer, copyModel, copyKeys, copyCerts)); + + +// Task to transpile JavaScript files +gulp.task('package-js', gulp.series(transpileApiServer, transpileTcpServer, transpileModel, "copy")); // gulp.parallel(copyApiServer, copyTcpServer, copyModel))); + +// Task to transpile TypeScript files +gulp.task('package-ts', gulp.parallel(transpileApiServerTS, transpileTcpServerTS)); + +// Default task +gulp.task('default', gulp.series(clean, gulp.series(transpileApiServer, transpileTcpServer, transpileModel, "copy", generatePackageJson))); \ No newline at end of file diff --git a/keys/server.key b/keys/server.key new file mode 100644 index 0000000..2538789 --- /dev/null +++ b/keys/server.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +TwScgdOyIqBWRmFz8S+MxK1kZhYJIXyrbA== +-----END PRIVATE KEY----- diff --git a/model/NAD-C338.js b/model/NAD-C338.js index 2ade2a8..00b84a6 100644 --- a/model/NAD-C338.js +++ b/model/NAD-C338.js @@ -3,7 +3,7 @@ export class NADC338 { this.ip = ip; this.port = port; this.powerState = null; - this.volume = null; + this.volume = 0; this.source = null; this.mute = null; this.brightness = null; diff --git a/package.json b/package.json index 336622e..4a250c0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,8 @@ "start-servers": "concurrently \"npm run api-server\" \"npm run tcp-server\"", "api-server": "node api-server/server.js", "tcp-server": "node tcp-server/server.js", - "run-test": "jest --verbose --coverage" + "run-test": "jest --coverage", + "test": "node --experimental-vm-modules node_modules/jest/bin/jest.js" }, "workspaces": [ "api-server", @@ -17,15 +18,20 @@ "dependencies": { "concurrently": "^9.1.2", "dotenv": "^16.4.7", - "express": "^4.21.2" + "express": "^5.1.0" }, "devDependencies": { + "@babel/core": "^7.26.7", + "@babel/preset-env": "^7.26.7", "@jest/globals": "^29.7.0", - "@babel/preset-env": "^7.26.0", "babel-jest": "^29.7.0", + "del": "^8.0.0", + "gulp": "^5.0.0", + "gulp-typescript": "^6.0.0-alpha.1", + "gulp-babel": "^8.0.0", "jest": "^29.7.0", "nock": "^14.0.0", - "node": "^23.7.0", + "node": "^24.0.2", "supertest": "^7.0.0" } } diff --git a/tcp-server/properties.env b/tcp-server/properties.env index 703ac67..71ce032 100644 --- a/tcp-server/properties.env +++ b/tcp-server/properties.env @@ -2,3 +2,6 @@ MAX_RECONNECT_ATTEMPTS=5 RECONNECT_INTERVAL=5000 MAX_LISTENERS=30 NAD_TCP_PORT=30001 +NAD_HOSTNAME=localhost +SERVER_LISTENER_IP=0.0.0.0 +LOG_LEVEL=none \ No newline at end of file diff --git a/tcp-server/server.js b/tcp-server/server.js index cb4f388..c554bb9 100644 --- a/tcp-server/server.js +++ b/tcp-server/server.js @@ -10,6 +10,8 @@ const nadTcpPort = parseInt(process.env.NAD_TCP_PORT); const maxReconnectAttempts = process.env.MAX_RECONNECT_ATTEMPTS; const reconnectInterval = process.env.RECONNECT_INTERVAL; // 5 seconds const maxListeners = parseInt(process.env.MAX_LISTENERS); +const serverListenerIp = process.env.SERVER_LISTENER_IP; +const logLevel = process.env.LOG_LEVEL; const errorCodes = ["ECONNRESET", "ECONNREFUSED", "ENETUNREACH", "ETIMEDOUT"]; @@ -17,12 +19,13 @@ export const connectToServer = async (ip, port) => { return new Promise((resolve, reject) => { client = net.createConnection({ port: port, host: ip }, () => { client.setMaxListeners(maxListeners); - console.log(`Connected to ${ip}:${port}`); + if (logLevel !== "none") + console.log(`Connected to ${ip}:${port}`); reconnectAttempts = 0; // Reset attempts on successful connection resolve(client); }); - client.on('error', err => { + client.on('error', /** @param {Error & { code?: string }} err */ err => { if (errorCodes.includes(err.code)) { console.error(`Connection error: ${err.message}`); client.destroy(); @@ -34,14 +37,16 @@ export const connectToServer = async (ip, port) => { }); client.on('close', () => { - console.log('Connection closed'); + if (logLevel !== "none") + console.log('Connection closed'); }); }); }; export const attemptReconnect = (ip, port) => { if (reconnectAttempts < maxReconnectAttempts) { - console.log(`Reconnection attempt ${++reconnectAttempts}...`); + if (logLevel !== "none") + console.log(`Reconnection attempt ${++reconnectAttempts}...`); setTimeout(() => connectToServer(ip, port), reconnectInterval); } else { console.error('Max reconnection attempts reached. Giving up.'); @@ -60,7 +65,7 @@ const requestHandler = async (req, res) => { return; } - if (req.method === 'POST') { + if (req.method === 'POST' || req.method === 'PUT' || req.method === 'DELETE') { let body = ''; req.on('data', chunk => { body += chunk.toString(); @@ -102,14 +107,16 @@ const requestHandler = async (req, res) => { const server = http.createServer(requestHandler); -server.listen(nadTcpPort, '0.0.0.0', () => { - console.log('Server is listening on port 30001'); +server.listen(nadTcpPort, serverListenerIp, () => { + if (logLevel !== "none") + console.log(`TCP server running at ${serverListenerIp}:${nadTcpPort}`); }); process.on('SIGINT', () => { if (client) { client.end(); - console.log('TCP connection closed'); + if (logLevel !== "none") + console.log('TCP connection closed'); } process.exit(); });