UNPKG

13.5 kBJavaScriptView Raw
1/*jslint node: true, nomen: true, esversion: 6 */
2"use strict";
3
4const assert = require('assert');
5const events = require('events');
6const http = require('http');
7const ip = require('ip');
8const SSDP = require('node-ssdp');
9const url = require('url');
10const util = require('util');
11
12const debug = require('debug')('upnpserver:api');
13const logger = require('./lib/logger');
14
15const UPNPServer = require('./lib/upnpServer');
16const Repository = require('./lib/repositories/repository');
17
18class API extends events.EventEmitter {
19
20 /**
21 * upnpserver API.
22 *
23 * @param {object}
24 * configuration
25 * @param {array}
26 * paths
27 *
28 * @constructor
29 */
30 constructor(configuration, paths) {
31 super();
32
33 this.configuration = Object.assign({}, this.defaultConfiguration, configuration);
34 this.repositories = [];
35 this._upnpClasses = {};
36 this._contentHandlers = [];
37 this._contentProviders = {};
38 this._contentHandlersKey = 0;
39
40 if (typeof (paths) === "string") {
41 this.addDirectory("/", paths);
42
43 } else if (util.isArray(paths)) {
44 paths.forEach((path) => this.initPaths(path));
45 }
46
47 if (this.configuration.noDefaultConfig !== true) {
48 this.loadConfiguration("./default-config.json");
49 }
50
51 var cf = this.configuration.configurationFiles;
52 if (typeof (cf) === "string") {
53 var toks = cf.split(',');
54 toks.forEach((tok) => this.loadConfiguration(tok));
55
56 } else if (util.isArray(cf)) {
57 cf.forEach((c) => this.loadConfiguration(c));
58 }
59 }
60
61 /**
62 * Default server configuration.
63 *
64 * @type {object}
65 */
66 get defaultConfiguration() {
67 return {
68 "dlnaSupport": true,
69 "httpPort": 10293,
70 "name": "Node Server",
71 "version": require("./package.json").version
72 };
73 }
74
75 /**
76 * Initialize paths.
77 *
78 * @param {string|object}
79 * path
80 * @returns {Repository} the created repository
81 */
82 initPaths(path) {
83 if (typeof (path) === "string") {
84 return this.addDirectory("/", path);
85 }
86 if (typeof(path) === "object") {
87 if (path.type === "video") {
88 path.type = "movie";
89 }
90
91 return this.declareRepository(path);
92 }
93
94 throw new Error("Invalid path '" + util.inspect(path) + "'");
95 }
96
97 /**
98 * Declare a repository
99 *
100 * @param {object}
101 * the configuration
102 * @returns {Repository} the new repository
103 */
104 declareRepository(configuration) {
105 var config = Object.assign({}, configuration);
106
107 var mountPath = config.mountPoint || config.mountPath || "/";
108
109 var type = config.type;
110 if (!type) {
111 logger.error("Type is not specified, assume it is a 'directory' type");
112 type = "directory";
113 }
114
115 var requirePath = configuration.require;
116 if (!requirePath) {
117 requirePath = "./lib/repositories/" + type;
118 }
119
120 debug("declareRepository", "requirePath=", requirePath, "mountPath=", mountPath, "config=", config);
121
122 var clazz = require(requirePath);
123 if (!clazz) {
124 logger.error("Class of repository must be specified");
125 return;
126 }
127
128 var repository = new clazz(mountPath, config);
129
130 return this.addRepository(repository);
131 }
132
133 /**
134 * Add a repository.
135 *
136 * @param {Repository}
137 * repository
138 *
139 * @returns {Repository} a Repository object
140 */
141 addRepository(repository) {
142 assert(repository instanceof Repository, "Invalid repository parameter '" + repository + "'");
143
144 this.repositories.push(repository);
145
146 return repository;
147 }
148
149 /**
150 * Add simple directory.
151 *
152 * @param {string}
153 * mountPath
154 * @param {string}
155 * path
156 *
157 * @returns {Repository} a Repository object
158 */
159 addDirectory(mountPath, path, configuration) {
160 assert.equal(typeof (mountPath), "string", "Invalid mountPoint parameter '" +
161 mountPath + "'");
162
163 assert.equal(typeof (path), "string", "Invalid path parameter '" + path + "'");
164
165 configuration = Object.assign({}, configuration, {mountPath, path, type: "directory"});
166
167 return this.declareRepository(configuration);
168 }
169
170 /**
171 * Add music directory.
172 *
173 * @param {string}
174 * mountPath
175 * @param {string}
176 * path
177 *
178 * @returns {Repository} a Repository object
179 */
180 addMusicDirectory(mountPath, path, configuration) {
181 assert.equal(typeof mountPath, "string", "Invalid mountPath parameter '" +
182 mountPath + "'");
183 assert.equal(typeof path, "string", "Invalid path parameter '" + mountPath + "'");
184
185 configuration = Object.assign({}, configuration, {mountPath, path, type: "music"});
186
187 return this.declareRepository(configuration);
188 }
189
190 /**
191 * Add video directory.
192 *
193 * @param {string}
194 * mountPath
195 * @param {string}
196 * path
197 *
198 * @returns {Repository} a Repository object
199 */
200 addVideoDirectory(mountPath, path, configuration) {
201 assert.equal(typeof mountPath, "string", "Invalid mountPoint parameter '" + mountPath + "'");
202 assert.equal(typeof path, "string", "Invalid path parameter '" + path + "'");
203
204 configuration = Object.assign({}, configuration, {mountPath, path, type: "movie"});
205
206 return this.declareRepository(configuration);
207 }
208
209 /**
210 * Add history directory.
211 *
212 * @param {string}
213 * mountPath
214 *
215 * @returns {Repository} a Repository object
216 */
217 addHistoryDirectory(mountPath, configuration) {
218 assert.equal(typeof mountPath, "string", "Invalid mountPoint parameter '" + mountPath + "'");
219
220 configuration = Object.assign({}, configuration, {mountPath, type: "history"});
221
222 return this.declareRepository(configuration);
223 }
224
225 /**
226 * Add iceCast.
227 *
228 * @param {string}
229 * mountPath
230 * @param {object}
231 * configuration
232 *
233 * @returns {Repository} a Repository object
234 */
235 addIceCast(mountPath, configuration) {
236 assert.equal(typeof mountPath, "string", "Invalid mountPoint parameter '" +
237 mountPath + "'");
238
239 configuration = Object.assign({}, configuration, {mountPath, type: "iceCast"});
240
241 return this.declareRepository(configuration);
242 }
243
244 /**
245 * Load a JSON configuration
246 *
247 * @param {string}
248 * path - The path of the JSON file
249 */
250 loadConfiguration(path) {
251 var config = require(path);
252
253 var upnpClasses = config.upnpClasses;
254 if (upnpClasses) {
255 for (var upnpClassName in upnpClasses) {
256 var p = upnpClasses[upnpClassName];
257
258 var clazz = require(p);
259
260 this._upnpClasses[upnpClassName] = new clazz();
261 }
262 }
263
264 var contentHandlers = config.contentHandlers;
265 if (contentHandlers instanceof Array) {
266 contentHandlers.forEach((contentHandler) => {
267
268 var mimeTypes = contentHandler.mimeTypes || [];
269
270 if (contentHandler.mimeType) {
271 mimeTypes = mimeTypes.slice(0);
272 mimeTypes.push(contentHandler.mimeType);
273 }
274
275 var requirePath = contentHandler.require;
276 if (!requirePath) {
277 requirePath = "./lib/contentHandlers/" + contentHandler.type;
278 }
279 if (!requirePath) {
280 logger.error("Require path is not defined !");
281 return;
282 }
283
284 var clazz = require(requirePath);
285 if (!clazz) {
286 logger.error("Class of contentHandler must be specified");
287 return;
288 }
289
290 var configuration = contentHandler.configuration || {};
291
292 var ch = new clazz(configuration, mimeTypes);
293 ch.priority = contentHandler.priority || 0;
294 ch.mimeTypes = mimeTypes;
295
296 this._contentHandlers.push(ch);
297 });
298 }
299
300 var contentProviders = config.contentProviders;
301 if (contentProviders instanceof Array) {
302 contentProviders.forEach((contentProvider) => {
303 var protocol = contentProvider.protocol;
304 if (!protocol) {
305 logger.error("Protocol property must be defined for contentProvider " + contentProvider.id + "'.");
306 return;
307 }
308 if (protocol in this._contentProviders) {
309 logger.error("Protocol '" + protocol + "' is already known");
310 return;
311 }
312
313 var name = contentProvider.name || protocol;
314
315 var requirePath = contentProvider.require;
316 if (!requirePath) {
317 var type = contentProvider.type || protocol;
318
319 requirePath = "./lib/contentProviders/" + type;
320 }
321 if (!requirePath) {
322 logger.error("Require path is not defined !");
323 return;
324 }
325
326 var clazz = require(requirePath);
327 if (!clazz) {
328 logger.error("Class of contentHandler must be specified");
329 return;
330 }
331
332 var configuration = Object.assign({}, contentProvider);
333
334 var ch = new clazz(configuration, protocol);
335 ch.protocol = protocol;
336 ch.name = name;
337
338 this._contentProviders[protocol] = ch;
339 });
340 }
341
342 var repositories = config.repositories;
343 if (repositories) {
344 repositories.forEach((configuration) => this.declareRepository(configuration));
345 }
346 }
347
348 /**
349 * Start server.
350 */
351 start() {
352 this.stop(() => {
353 this.startServer();
354 });
355 }
356
357 /**
358 * Start server callback.
359 *
360 * @return {UPNPServer}
361 */
362 startServer(callback) {
363 callback = callback || (() => {
364 });
365
366 debug("startServer", "Start the server");
367
368 if (!this.repositories.length) {
369 return callback(new Error("No directories defined !"));
370 }
371
372 var configuration = this.configuration;
373 configuration.repositories = this.repositories;
374 configuration.upnpClasses = this._upnpClasses;
375 configuration.contentHandlers = this._contentHandlers;
376 configuration.contentProviders = this._contentProviders;
377
378 if (!callback) {
379 callback = (error) => {
380 if (error) {
381 logger.error(error);
382 }
383 };
384 }
385
386 var upnpServer = new UPNPServer(configuration.httpPort, configuration, (error, upnpServer) => {
387 if (error) {
388 logger.error("Can not start UPNPServer", error);
389
390 return callback(error);
391 }
392
393 debug("startServer", "Server started ...");
394
395 this._upnpServerStarted(upnpServer, callback);
396 });
397
398 return upnpServer;
399 }
400
401 /**
402 * After the server start.
403 *
404 * @param {object}
405 * upnpServer
406 */
407 _upnpServerStarted(upnpServer, callback) {
408
409 this.emit("starting");
410
411 this.upnpServer = upnpServer;
412
413 var locationURL = 'http://' + ip.address() + ':' + this.configuration.httpPort + "/description.xml";
414
415 var config = {
416 udn: this.upnpServer.uuid,
417 description: "/description.xml",
418 location: locationURL,
419 ssdpSig: "Node/" + process.versions.node + " UPnP/1.0 " + "UPnPServer/" +
420 require("./package.json").version
421 };
422
423 debug("_upnpServerStarted", "New SSDP server config=", config);
424
425 var ssdpServer = new SSDP.Server(config);
426 this.ssdpServer = ssdpServer;
427
428 ssdpServer.addUSN('upnp:rootdevice');
429 ssdpServer.addUSN(upnpServer.type);
430
431 var services = upnpServer.services;
432 if (services) {
433 for (var route in services) {
434 ssdpServer.addUSN(services[route].type);
435 }
436 }
437
438 debug("_upnpServerStarted", "New Http server port=", upnpServer.port);
439
440 var httpServer = http.createServer();
441 this.httpServer = httpServer;
442
443 httpServer.on('request', this._processRequest.bind(this));
444
445 httpServer.listen(upnpServer.port, (error) => {
446 if (error) {
447 return callback(error);
448 }
449
450 this.ssdpServer.start();
451
452 this.emit("waiting");
453
454 var address = httpServer.address();
455
456 debug("_upnpServerStarted", "Http server is listening on address=", address);
457
458 var hostname = address.address;
459 if (address.family === 'IPv6') {
460 hostname = '[' + hostname + ']';
461 }
462
463 console.log('Ready http://' + hostname + ':' + address.port);
464
465 callback();
466 });
467 }
468
469 /**
470 * Process request
471 *
472 * @param {object}
473 * request
474 * @param {object}
475 * response
476 */
477 _processRequest(request, response) {
478
479 var path = url.parse(request.url).pathname;
480
481 // logger.debug("Uri=" + request.url);
482
483 var now = Date.now();
484 try {
485 this.upnpServer.processRequest(request, response, path, (error, processed) => {
486
487 var stats = {
488 request: request,
489 response: response,
490 path: path,
491 processTime: Date.now() - now,
492 };
493
494 if (error) {
495 response.writeHead(500, 'Server error: ' + error);
496 response.end();
497
498 this.emit("code:500", error, stats);
499 return;
500 }
501
502 if (!processed) {
503 response.writeHead(404, 'Resource not found: ' + path);
504 response.end();
505
506 this.emit("code:404", stats);
507 return;
508 }
509
510 this.emit("code:200", stats);
511 });
512
513 } catch (error) {
514 logger.error("Process request exception", error);
515 this.emit("error", error);
516 }
517 }
518
519 /**
520 * Stop server.
521 *
522 * @param {function|undefined}
523 * callback
524 */
525 stop(callback) {
526 debug("stop", "Stopping ...");
527
528 callback = callback || (() => {
529 return false;
530 });
531
532 var httpServer = this.httpServer;
533 var ssdpServer = this.ssdpServer;
534 var stopped = false;
535
536 if (this.ssdpServer) {
537 this.ssdpServer = undefined;
538 stopped = true;
539
540 try {
541 debug("stop", "Stop ssdp server ...");
542
543 ssdpServer.stop();
544
545 } catch (error) {
546 logger.error(error);
547 }
548 }
549
550 if (httpServer) {
551 this.httpServer = undefined;
552 stopped = true;
553
554 try {
555 debug("stop", "Stop http server ...");
556
557 httpServer.close();
558
559 } catch (error) {
560 logger.error(error);
561 }
562 }
563
564 debug("stop", "Stopped");
565
566 if (stopped) {
567 this.emit("stopped");
568 }
569
570 callback(null, stopped);
571 }
572}
573
574module.exports = API;