This repository has been archived by the owner on Feb 16, 2019. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 47
/
Copy pathserver.js
206 lines (173 loc) · 7.19 KB
/
server.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
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;