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.
		
		
		
		
		
			
		
			
				
	
	
		
			220 lines
		
	
	
		
			5.7 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			220 lines
		
	
	
		
			5.7 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;
 |