You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
204 lines
5.5 KiB
JavaScript
204 lines
5.5 KiB
JavaScript
/* global textsecure */
|
|
const https = require('https');
|
|
const EventEmitter = require('events');
|
|
const natUpnp = require('nat-upnp');
|
|
|
|
const STATUS = {
|
|
OK: 200,
|
|
BAD_REQUEST: 400,
|
|
NOT_FOUND: 404,
|
|
METHOD_NOT_ALLOWED: 405,
|
|
INTERNAL_SERVER_ERROR: 500,
|
|
};
|
|
|
|
class LocalLokiServer extends EventEmitter {
|
|
/**
|
|
* Creates an instance of LocalLokiServer.
|
|
* Sends out a `message` event when a new message is received.
|
|
*/
|
|
constructor(pems, options = {}) {
|
|
super();
|
|
const httpsOptions = {
|
|
key: pems.private,
|
|
cert: pems.cert,
|
|
};
|
|
if (!options.skipUpnp) {
|
|
this.upnpClient = natUpnp.createClient();
|
|
}
|
|
this.server = https.createServer(httpsOptions, (req, res) => {
|
|
let body = [];
|
|
|
|
const sendResponse = (statusCode, message = null) => {
|
|
const headers = message && {
|
|
'Content-Type': 'text/plain',
|
|
};
|
|
res.writeHead(statusCode, headers);
|
|
res.end(message);
|
|
};
|
|
|
|
if (req.method !== 'POST') {
|
|
sendResponse(STATUS.METHOD_NOT_ALLOWED);
|
|
return;
|
|
}
|
|
|
|
// Check endpoints
|
|
req
|
|
.on('error', () => {
|
|
// Internal server error
|
|
sendResponse(STATUS.INTERNAL_SERVER_ERROR);
|
|
})
|
|
.on('data', chunk => {
|
|
body.push(chunk);
|
|
})
|
|
.on('end', () => {
|
|
try {
|
|
body = Buffer.concat(body).toString();
|
|
} catch (e) {
|
|
// Internal server error: failed to convert body to string
|
|
sendResponse(STATUS.INTERNAL_SERVER_ERROR);
|
|
}
|
|
|
|
// Check endpoints here
|
|
if (req.url === '/storage_rpc/v1') {
|
|
try {
|
|
const bodyObject = JSON.parse(body);
|
|
if (bodyObject.method !== 'store') {
|
|
sendResponse(STATUS.NOT_FOUND, 'Invalid endpoint!');
|
|
return;
|
|
}
|
|
this.emit('message', {
|
|
message: bodyObject.params.data,
|
|
onSuccess: () => sendResponse(STATUS.OK),
|
|
onFailure: () => sendResponse(STATUS.NOT_FOUND),
|
|
});
|
|
} catch (e) {
|
|
// Bad Request: Failed to decode json
|
|
sendResponse(STATUS.BAD_REQUEST, 'Failed to decode JSON');
|
|
}
|
|
} else {
|
|
sendResponse(STATUS.NOT_FOUND, 'Invalid endpoint!');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async start(port, ip) {
|
|
// Close the old server
|
|
await this.close();
|
|
|
|
// Start a listening on new server
|
|
return new Promise((res, rej) => {
|
|
this.server.listen(port, ip, async (err) => {
|
|
if (err) {
|
|
rej(err);
|
|
} else if (this.upnpClient) {
|
|
try {
|
|
const publicPort = await this.punchHole();
|
|
res(publicPort);
|
|
} catch (e) {
|
|
if (e instanceof textsecure.HolePunchingError) {
|
|
await this.close();
|
|
}
|
|
rej(e);
|
|
}
|
|
} else {
|
|
res(port);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
async punchHole() {
|
|
const privatePort = this.server.address().port;
|
|
const portStart = 22100;
|
|
const portEnd = 22200;
|
|
const ttl = 60 * 15; // renew upnp every 15 minutes
|
|
const publicPortsInUse = await new Promise((resolve, reject) => {
|
|
this.upnpClient.getMappings({ local: true }, (err, results) => {
|
|
if (err) {
|
|
// We assume an error here means upnp not enabled
|
|
reject(new textsecure.HolePunchingError('Could not get mapping from upnp. Upnp not available?', err));
|
|
}
|
|
else {
|
|
// remove the current private port from the current mapping
|
|
// to allow reusing that port.
|
|
resolve(results
|
|
.filter(entry => entry.private.port !== privatePort)
|
|
.map(entry => entry.public.port));
|
|
}
|
|
});
|
|
});
|
|
|
|
for (let publicPort = portStart; publicPort <= portEnd; publicPort += 1) {
|
|
if (publicPortsInUse.includes(publicPort)) {
|
|
// eslint-disable-next-line no-continue
|
|
continue;
|
|
}
|
|
const p = new Promise((resolve, reject) => {
|
|
this.upnpClient.portMapping({
|
|
public: publicPort,
|
|
private: privatePort,
|
|
ttl,
|
|
}, (err) => {
|
|
if (err)
|
|
reject(err);
|
|
else
|
|
resolve();
|
|
});
|
|
});
|
|
try {
|
|
// eslint-disable-next-line no-await-in-loop
|
|
await p;
|
|
this.publicPort = publicPort;
|
|
this.timerHandler = setTimeout(async () => {
|
|
try {
|
|
this.publicPort = await this.punchHole();
|
|
} catch (e) {
|
|
this.close();
|
|
}
|
|
}, ttl * 1000);
|
|
return publicPort;
|
|
} catch (e) {
|
|
throw new textsecure.HolePunchingError('Could not punch hole. Disabled upnp?', e);
|
|
}
|
|
}
|
|
const e = new Error();
|
|
throw new textsecure.HolePunchingError(`Could not punch hole: no available port. Public ports: ${portStart}-${portEnd}`, e);
|
|
}
|
|
// Async wrapper for http server close
|
|
close() {
|
|
clearInterval(this.timerHandler);
|
|
if (this.upnpClient) {
|
|
this.upnpClient.portUnmapping({
|
|
public: this.publicPort,
|
|
});
|
|
this.publicPort = null;
|
|
}
|
|
if (this.server) {
|
|
return new Promise(res => {
|
|
this.server.close(() => res());
|
|
});
|
|
}
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
getPort() {
|
|
if (this.server.listening) {
|
|
return this.server.address().port;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
getPublicPort() {
|
|
return this.publicPort;
|
|
}
|
|
|
|
isListening() {
|
|
return this.server.listening;
|
|
}
|
|
}
|
|
|
|
module.exports = LocalLokiServer;
|