UNPKG

stremio-addons

Version:
207 lines (173 loc) 7.36 kB
var url = require("url"); var rpc = require("./rpc"); var validate = require("./validate"); // simply console-log warnings in case of wrong args; aimed to aid development var extend = require("extend"); var async = require("async"); var template; var SESSION_LIVE = 10*60*60*1000; // 10 hrs var CACHE_TTL = 2.5 * 60 * 60; // seconds to live for the cache var CENTRAL = "https://api9.strem.io"; var IS_DEVEL = process.env.NODE_ENV !== "production"; function Server(methods, options, manifest) { var self = this; if (options && typeof(manifest) === "undefined") { manifest = options; options = null; } options = extend({ allow: [ CENTRAL ], // default stremio central secret: "8417fe936f0374fbd16a699668e8f3c4aa405d9f" // default secret for testing add-ons }, options || { }); this.methods = methods; this.manifest = manifest; this.options = options; Object.keys(methods).forEach(function(key) { if (typeof(methods[key]) != "function") throw Error(key+" should be a function"); }); // Announce to central self.announced = false; function announce() { self.announced = true; var body = JSON.stringify({ id: manifest.id, manifest: manifest }); var parsed = url.parse(CENTRAL+"/stremio/announce/"+options.secret); var req = (parsed.protocol.match("https") ? require("https") : require("http")).request(extend(parsed, { method: "POST", headers: { "Content-Type": "application/json", "Content-Length": body.length } }), function(res) { if (res.statusCode !== 200) console.error("Announce error for "+manifest.id+", statusCode: "+res.statusCode); }); req.on("error", function(err) { console.error("Announce error for "+manifest.id, err) }); req.end(body); } // Introspect the addon function meta(cb) { cb(null, { methods: Object.keys(methods), manifest: extend({ methods: Object.keys(methods) }, manifest || {}) }); }; // In case we use this in place of endpoint URL this.toString = function() { return self.manifest.id; }; // Direct interface this.request = function(method, params, cb) { if (method == "meta") return meta(cb); if (! methods[method]) return cb({ message: "method not supported", code: -32601 }, null); var auth = params[0], // AUTH is obsolete args = params[1] || { }; return methods[method](args, function(err, res) { if (err) return cb(err); if (IS_DEVEL) validate(method, res); // This would simply console-log warnings in case of wrong args; aimed to aid development cb(null, res); }, { stremioget: true }); // everything is allowed without auth in stremioget mode }; // HTTP middleware this.middleware = function(req, res, next) { if (!self.announced && !manifest.dontAnnounce) announce(); var start = Date.now(), finished = false; req._statsNotes = []; var getInfo = function() { return [req.url].concat(req._statsNotes).filter(function(x) { return x }) }; if (process.env.STREMIO_LOGGING) { res.on("finish", function() { finished = true; console.log("\x1b[34m["+(new Date()).toISOString()+"]\x1b[0m -> \x1b[32m["+(Date.now()-start)+"ms]\x1b[0m "+getInfo().join(", ")+" / "+res.statusCode) }); setTimeout(function() { if (!finished) console.log("-> \x1b[31m[WARNING]\x1b[0m "+getInfo().join(", ")+" taking more than 3000ms to run") }, 3000); } var parsed = url.parse(req.url); req._statsNotes.push(req.method); // HTTP method if (req.method === "OPTIONS") { var headers = {}; headers["Access-Control-Allow-Origin"] = "*"; headers["Access-Control-Allow-Methods"] = "POST, GET, PUT, DELETE, OPTIONS"; headers["Access-Control-Allow-Credentials"] = false; headers["Access-Control-Max-Age"] = "86400"; // 24 hours headers["Access-Control-Allow-Headers"] = "X-Requested-With, X-HTTP-Method-Override, Content-Type, Accept"; res.writeHead(200, headers); res.end(); return; }; if (req.method == "POST" || ( req.method == "GET" && parsed.pathname.match("q.json$") ) ) return serveRPC(req, res, function(method, params, cb) { req._statsNotes.push(method); // stremio method self.request(method, params, cb); }); else if (req.method == "GET") { // unsupported by JSON-RPC, it uses post return landingPage(req, res); } res.writeHead(405); // method not allowed res.end(); }; function serveRPC(req, res, handle) { var isGet = req.url.match("q.json"); var isJson = req.headers["content-type"] && req.headers["content-type"].match("^application/json"); if (!(isGet || isJson)) return res.writeHead(415); // unsupported media type res.setHeader("Access-Control-Allow-Origin", "*"); function formatResp(id, err, body) { var respBody = { jsonrpc: "2.0", id: id }; if (err) respBody.error = { message: err.message, code: err.code || -32603 }; else respBody.result = body; return respBody; }; function send(respBody, ttl) { respBody = JSON.stringify(respBody); res.setHeader("Content-Type", "application/json"); res.setHeader("Content-Length", Buffer.byteLength(respBody, "utf8")); if (! (req.headers.host && req.headers.host.match(/localhost|127.0.0.1/))) { res.setHeader("Cache-Control", "public, max-age="+(ttl || CACHE_TTL) ); // around 2 hours default } res.end(respBody); }; rpc.receiveJSON(req, function(err, body) { if (err) return send({ code: -32700, message: "parse error" }); // TODO: jsonrpc, id prop var ttl = CACHE_TTL; if (!isNaN(options.cacheTTL)) ttl = options.cacheTTL; if (options.cacheTTL && options.cacheTTL[body.method]) ttl = options.cacheTTL[body.method]; if (Array.isArray(body)) { async.map(body, function(b, cb) { // WARNING: same logic as --> if (!b || !b.id || !b.method) return cb(null, formatResp(null, { code: -32700, message: "parse error" })); handle(b.method, b.params, function(err, bb) { cb(null, formatResp(b.id, err, bb)) }); }, function(err, bodies) { send(bodies, ttl) }); } else { // --> THIS if (!body || !body.id || !body.method) return send(formatResp(null, { code: -32700, message: "parse error" })); handle(body.method, body.params, function(err, b) { send(formatResp(body.id, err, b), ttl) }); } }); }; function landingPage(req, res) { var endpoint = manifest.endpoint || "http://"+req.headers.host+req.url; var stats = { }, top = []; // TODO: cache at least stats.get for some time if (! self.methods['stats.get']) return respond(); self.request("stats.get", [{ stremioget: true }], function(err, s) { if (err) console.log(err); if (s) stats = s; if (! self.methods['meta.find']) return respond(); self.request("meta.find", [{stremioget: true}, { query: {}, limit: 10 }], function(err, t) { if (err) return error(err); if (t) top = t; respond(); }); }); function error(e) { console.error("LANDING PAGE ERROR",e); res.writeHead(500); res.end(); } function respond() { try { if (! template) template = require("ejs").compile(require("fs").readFileSync(__dirname+"/addon-template.ejs").toString(), { }); var body = template({ addon: { manifest: manifest, methods: methods }, endpoint: endpoint, stats: stats, top: top }); res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(body); } catch(e) { error(e) } } } }; module.exports = Server;