Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Dependencies
node_modules/
views/node_modules/

# Build output
views/dist/
views/.angular/

# Lock files (generated; let contributors run their own installs)
package-lock.json
views/package-lock.json

# OS / editor
.DS_Store
Thumbs.db
.vscode/
*.swp
*.swo
341 changes: 188 additions & 153 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,162 +22,197 @@
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.
*/
'use strict'
const bodyParser = require('body-parser');
const express = require('express');
const app = express();
var cors = require('cors');
var http = require('http');
var path = require('path');
var request = require('request');
var port = '4200';
http.createServer(function(req, res) {});
app.use(bodyParser.json());
app.use(cors());
app.set('port',port);
app.use(express.static(path.join(__dirname, "views/dist")));
app.use(express.static(path.join(__dirname, '/node_modules')));
app.use(function(req, res, next) {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept,X-Auth-Token,Cookie-Headers');
res.header('Access-Control-Allow-Methods', 'POST,GET,OPTIONS,PUT,DELETE');
res.header('Allow','HEAD, GET, PATCH, POST, OPTIONS, DELETE');
next();
});
app.get('/getCollectionData', function(req, response){
var urlString = req.query.Ip;
request({
headers: {
'X-Auth-Token':req.get('X-Auth-Token'),
'Cookie':req.get('Cookie-Headers')
},
uri: urlString,
method: 'GET'
}, function (err, res, body) {
if(err) {
if(err.code == 'ECONNREFUSED') {
return response.status(502).send({ error: err.code + ': Connection refused' })
}
else if(err.code == 'ETIMEDOUT ') {
return response.status(502).send({ error: err.code + ': Connection Timed Out' })
}
else{
return response.status(502).send({ error: err.code })
}
}
try {
return response.send(JSON.parse(body));
} catch (e) {
return response.status(404).send({ error: ' Connection refused' })
}
});
});
app.post('/addCollection', function(req, response) {
request({
uri: req.body.Ip,
method: 'POST',
headers: {
'X-Auth-Token':req.get('X-Auth-Token'),
'Cookie':req.get('Cookie-Headers')
},
}, function (err, res) {
if(err || res.statusCode !== 200 || res.body == 500) {
return response.status(404).send({error:'Adding a new collection/service failed at this moment'});
}
return response.send(res);
});
});
app.post('/updateCollection', function(req, response) {
var urlString = req.query.Ip;
request({
headers: {
'X-Auth-Token':req.get('X-Auth-Token'),
'Cookie':req.get('Cookie-Headers')
},
uri: urlString,
method: 'PATCH',
json:req.body
}, function (err, res) {
if(err || res.statusCode !== 200 || res.body == 500) {
return response.status(404).send({error:'Updating a collection/service failed.'});
}
return response.send(res);
});
});
app.post('/getCookie', function(req, response) {
var cred = {
UserName:req.body.UserName,
Password:req.body.Password
};
request({
uri: req.body.postUrl,
method: 'POST',
json: cred
}, function (err, res) {
if(err) {
if(err.code == 'ENOTFOUND') {
return response.status(502).send({ error: err.code + ': Invalid IPAddress/port' })
}
else if(err.code == 'ECONNREFUSED') {
return response.status(502).send({ error: err.code + ': Connection refused' })
}
else if(err.code == 'ETIMEDOUT ') {
return response.status(502).send({ error: err.code + ': Error Connection Timed Out' })
}
else{
return response.status(502).send({ error: err.code })
}
}
if(res.headers['x-auth-token'] != null ){
response.setHeader('X-Auth-Token',res.headers['x-auth-token']);
}
if(res.headers['set-cookie'] != null){
response.setHeader('Cookie-Headers',res.headers['set-cookie']);
}
if(res.headers['location'] != null) {
response.setHeader('location',res.headers['location']);
}
return response.send(res);
});
});
app.delete('/deleteService',function(req,response) {
var urlString = req.query.Ip + req.body['@odata.id'];
request({
uri: urlString,
method: 'DELETE',
headers:{
'Content-Type':'application/json'
}
}, function (err, res) {
'use strict';
const express = require('express');
const app = express();
const cors = require('cors');
const path = require('path');
const axios = require('axios');
const https = require('https');
const port = '4200';

if(err || res.statusCode !== 200 || res.body == 500) {
return response.status(405).send({error:'Deletion Failed'});
}
return response.send(res);
// Shared HTTPS agent that bypasses certificate validation (for self-signed certs).
const selfSignedAgent = new https.Agent({ rejectUnauthorized: false });

});
});
app.delete('/deleteSession',function(req,response) {
var urlString = req.query.Ip;
request({
uri: urlString,
method: 'DELETE',
headers:{
'Content-Type':'application/json',
'X-Auth-Token':req.get('X-Auth-Token'),
'Cookie':req.get('Cookie-Headers')
}
}, function (err, res) {
// Build an axios config that forwards auth headers and optionally bypasses TLS
// certificate verification for self-signed certs (X-Allow-Self-Signed: true).
// Per DSP0266 §13.3.3, HTTP Basic auth requires HTTPS; the caller is responsible
// for choosing the correct scheme. Self-signed bypass is intended for lab/dev use only.
function buildAxiosConfig(req, extraHeaders) {
const headers = Object.assign({}, extraHeaders);
const authorization = req.get('Authorization');
const xAuthToken = req.get('X-Auth-Token');
const cookieHeaders = req.get('Cookie-Headers');
const allowSelfSigned = (req.get('X-Allow-Self-Signed') || '').toLowerCase() === 'true';
if (authorization) headers['Authorization'] = authorization;
if (xAuthToken) headers['X-Auth-Token'] = xAuthToken;
if (cookieHeaders) headers['Cookie'] = cookieHeaders;
console.log('[proxy] allowSelfSigned:', allowSelfSigned, '| auth:', authorization ? 'Basic' : xAuthToken ? 'Token' : 'none');
const config = { headers, timeout: 30000 };
if (allowSelfSigned) {
config.httpsAgent = selfSignedAgent;
}
return config;
}

if(err || res.statusCode !== 200 || res.body == 500) {
app.use(express.json());
app.use(cors());
app.set('port', port);
app.use(express.static(path.join(__dirname, 'views/dist/SNIA/browser')));
app.use(express.static(path.join(__dirname, '/node_modules')));

return response.send(err)
}
return response.send(res);
});
});
app.listen(3300, function(){
console.log('Listening on port 3000, Live http://localhost:3000');
});
// Unwraps Node.js v17+ chained errors (err.cause) to find the real TLS/network code.
function getRootCode(err) {
return err?.cause?.code || err?.cause?.cause?.code || err.code || '';
}

function axiosErrorResponse(err, response) {
const code = err.code || '';
const rootCode = getRootCode(err);
console.error('[proxy] error code:', code, '| root cause:', rootCode, '| message:', err.message);
if (code === 'ENOTFOUND' || rootCode === 'ENOTFOUND') {
return response.status(502).send({ error: 'ENOTFOUND: Invalid IP address or hostname' });
}
if (code === 'ECONNREFUSED' || rootCode === 'ECONNREFUSED') {
return response.status(502).send({ error: 'ECONNREFUSED: Connection refused — is the Swordfish service running?' });
}
if (code === 'ETIMEDOUT' || rootCode === 'ETIMEDOUT') {
return response.status(502).send({ error: 'ETIMEDOUT: Connection timed out' });
}
// TLS certificate errors — surface even when Node.js wraps them as ECONNABORTED
const tlsCodes = [
'DEPTH_ZERO_SELF_SIGNED_CERT', 'SELF_SIGNED_CERT_IN_CHAIN',
'UNABLE_TO_VERIFY_LEAF_SIGNATURE', 'ERR_TLS_CERT_ALTNAME_INVALID',
'CERT_HAS_EXPIRED', 'UNABLE_TO_GET_ISSUER_CERT_LOCALLY'
];
if (tlsCodes.includes(code) || tlsCodes.includes(rootCode)) {
const tlsCode = tlsCodes.includes(rootCode) ? rootCode : code;
return response.status(502).send({ error: 'TLS error (' + tlsCode + '): enable "Allow Self-Signed Cert" in the Add dialog.' });
}
if (code === 'ECONNABORTED') {
// Could be a timeout OR a TLS abort — include both code and cause in the message.
const detail = rootCode && rootCode !== code ? ` (cause: ${rootCode})` : '';
return response.status(502).send({ error: `ECONNABORTED${detail}: connection aborted — check URL, port, and TLS settings` });
}
if (code === 'ECONNRESET' || rootCode === 'ECONNRESET') {
return response.status(502).send({ error: 'ECONNRESET: connection reset by server' });
}
return response.status(502).send({ error: (rootCode || code || err.message) });
}

app.get('/getCollectionData', async function(req, response) {
const urlString = req.query.Ip;
console.log('[proxy] GET', urlString);
try {
const res = await axios.get(urlString, buildAxiosConfig(req));
console.log('[proxy] GET', urlString, '->', res.status, '| body keys:', Object.keys(res.data || {}).join(', '));
return response.send(res.data);
} catch (err) {
console.error('[proxy] GET error', urlString, err.code || err.message);
if (err.response) {
console.error('[proxy] GET upstream status:', err.response.status, err.response.data);
return response.status(err.response.status).send({ error: err.message });
}
return axiosErrorResponse(err, response);
}
});

app.post('/addCollection', async function(req, response) {
const urlString = req.body.Ip;
console.log('[proxy] POST addCollection', urlString);
try {
const res = await axios.post(urlString, {}, buildAxiosConfig(req));
console.log('[proxy] POST addCollection', urlString, '->', res.status);
if (res.status !== 200) {
return response.status(404).send({ error: 'Adding a new collection/service failed at this moment' });
}
return response.send(res.data);
} catch (err) {
console.error('[proxy] POST addCollection error', urlString, err.code || err.message);
return response.status(404).send({ error: 'Adding a new collection/service failed at this moment' });
}
});

app.post('/updateCollection', async function(req, response) {
const urlString = req.query.Ip;
console.log('[proxy] PATCH updateCollection', urlString);
try {
const res = await axios.patch(urlString, req.body, buildAxiosConfig(req));
console.log('[proxy] PATCH updateCollection', urlString, '->', res.status);
if (res.status !== 200) {
return response.status(404).send({ error: 'Updating a collection/service failed.' });
}
return response.send(res.data);
} catch (err) {
console.error('[proxy] PATCH updateCollection error', urlString, err.code || err.message);
return response.status(404).send({ error: 'Updating a collection/service failed.' });
}
});

app.post('/getCookie', async function(req, response) {
const cred = {
UserName: req.body.UserName,
Password: req.body.Password
};
console.log('[proxy] POST getCookie', req.body.postUrl, '| user:', req.body.UserName);
try {
// Session login MUST use HTTPS per DSP0266 §13.3.4.2
const axiosCfg = buildAxiosConfig(req, { 'Content-Type': 'application/json' });
const res = await axios.post(req.body.postUrl, cred, axiosCfg);
console.log('[proxy] POST getCookie ->', res.status, '| X-Auth-Token:', !!res.headers['x-auth-token'], '| Location:', res.headers['location']);
if (res.headers['x-auth-token'] != null) {
response.setHeader('X-Auth-Token', res.headers['x-auth-token']);
}
if (res.headers['set-cookie'] != null) {
response.setHeader('Cookie-Headers', res.headers['set-cookie']);
}
if (res.headers['location'] != null) {
response.setHeader('location', res.headers['location']);
}
return response.send(res.data);
} catch (err) {
// Forward the Swordfish service's own HTTP error (e.g., 401 bad credentials)
// so the UI can show a meaningful message rather than a generic 502.
if (err.response) {
console.error('[proxy] POST getCookie upstream error:', err.response.status, err.response.data);
return response.status(err.response.status).send(err.response.data);
}
return axiosErrorResponse(err, response);
}
});

app.delete('/deleteService', async function(req, response) {
const urlString = req.query.Ip + req.body['@odata.id'];
console.log('[proxy] DELETE deleteService', urlString);
try {
const res = await axios.delete(urlString, buildAxiosConfig(req, { 'Content-Type': 'application/json' }));
console.log('[proxy] DELETE deleteService', urlString, '->', res.status);
if (res.status !== 200) {
return response.status(405).send({ error: 'Deletion Failed' });
}
return response.send(res.data);
} catch (err) {
console.error('[proxy] DELETE deleteService error', urlString, err.code || err.message);
return response.status(405).send({ error: 'Deletion Failed' });
}
});

app.delete('/deleteSession', async function(req, response) {
const urlString = req.query.Ip;
console.log('[proxy] DELETE deleteSession', urlString);
try {
const res = await axios.delete(urlString, buildAxiosConfig(req, { 'Content-Type': 'application/json' }));
console.log('[proxy] DELETE deleteSession', urlString, '->', res.status);
return response.send(res.data);
} catch (err) {
console.error('[proxy] DELETE deleteSession error', urlString, err.code || err.message);
return response.send({ error: err.message });
}
});

app.listen(3300, function() {
console.log('Listening on port 3300, Live http://localhost:3300');
});

module.exports = app;

Loading