1 |
|
2 | "use strict";
|
3 |
|
4 | const assert = require('assert');
|
5 | const Mime = require('mime');
|
6 | const fs = require('fs');
|
7 | const Path = require('path');
|
8 | const request = require('request');
|
9 | const crypto = require('crypto');
|
10 | const Async = require('async');
|
11 | const Url = require('url');
|
12 | const https = require('https');
|
13 |
|
14 | const debug = require('debug')('upnpserver:contentProviders:1Fichier');
|
15 | const logger = require('../logger');
|
16 |
|
17 | const URL = require('../util/url');
|
18 |
|
19 | const ContentProvider = require('./contentProvider');
|
20 |
|
21 | const DIRECTORY_MIME_TYPE = "inode/directory";
|
22 | const USER_AGENT = 'Upnpserver/1.0';
|
23 |
|
24 | var streamID=0;
|
25 |
|
26 | class OneFichierContentProvider extends ContentProvider {
|
27 |
|
28 | constructor(configuration) {
|
29 | super(configuration);
|
30 |
|
31 | this._cache={};
|
32 |
|
33 | this._userAgent = USER_AGENT;
|
34 |
|
35 | this._requestQueue = Async.queue((task, callback) => task(callback), configuration.maxRequest || 2);
|
36 |
|
37 | if (!configuration.username) {
|
38 | throw new Error("Username must be specified !");
|
39 | }
|
40 | if (!configuration.password) {
|
41 | throw new Error("Password must be specified !");
|
42 | }
|
43 |
|
44 | this._baseURL=configuration.baseURL || "https://1fichier.com/";
|
45 | this._username=this.normalizeParameter(configuration.username);
|
46 | var password=this.normalizeParameter(configuration.password);
|
47 | this._password=password;
|
48 | this._passwordMD5=crypto.createHash('md5').update(password).digest("hex");
|
49 |
|
50 | if (!this._username || !this._password) {
|
51 | throw new Error("Username or password must be defined !");
|
52 | }
|
53 |
|
54 | debug("Set baseURL to", this._baseURL, "username=", this._username, "password=", this._passwordMD5);
|
55 | }
|
56 |
|
57 | initialize(service, callback) {
|
58 | super.initialize(service, (error) => {
|
59 | if (error) {
|
60 | return callback(error);
|
61 | }
|
62 |
|
63 | this._userAgent=[ "Node/" + process.versions.node, "UPnP/1.0",
|
64 | "UPnPServer/" + service.upnpServer.packageDescription.version ].join(' ');
|
65 |
|
66 | callback();
|
67 | });
|
68 | }
|
69 |
|
70 | |
71 |
|
72 |
|
73 | _convertURL(contentURL) {
|
74 | var url=contentURL.substring(this.protocol.length+1);
|
75 |
|
76 | if (!url || url==='/') {
|
77 | url=this._baseURL+"console/get_folder_content.pl";
|
78 |
|
79 | } else {
|
80 | var reg=/([^/]+)\/([^\/]+)$/.exec(url);
|
81 |
|
82 | if (reg) {
|
83 | url = this._baseURL+"console/get_folder_content.pl?id="+reg[2];
|
84 | } else {
|
85 | reg=/\?([^?]+)$/.exec(url);
|
86 | url = this._baseURL+"?"+reg[1];
|
87 | }
|
88 | }
|
89 |
|
90 | debug("Convert content URL of",contentURL,"=>",url);
|
91 |
|
92 | return url;
|
93 | }
|
94 |
|
95 | |
96 |
|
97 |
|
98 | readdir(contentURL, callback) {
|
99 | var url=this._convertURL(contentURL);
|
100 |
|
101 | var folderId="0";
|
102 |
|
103 | var reg=/\/([^\/]+)$/.exec(contentURL);
|
104 | if (reg) {
|
105 | folderId=reg[1];
|
106 | }
|
107 |
|
108 | debug("Readdir",contentURL,"folderId=",folderId);
|
109 |
|
110 | this._requestQueue.push((callback) => this._readdir(url, folderId, callback), callback);
|
111 | }
|
112 |
|
113 | _readdir(url, folderId, callback) {
|
114 |
|
115 | if (this._badPassword) {
|
116 | return callback(new Error("Bad password detected !"));
|
117 | }
|
118 |
|
119 | var options = {
|
120 | qs: {
|
121 | user: this._username,
|
122 | pass: this._passwordMD5
|
123 | }
|
124 | };
|
125 |
|
126 | request(url, options, (error, response, body) => {
|
127 |
|
128 | if (error) {
|
129 | logger.error("Can not read directory ",url,error);
|
130 | error.url=url;
|
131 | return callback(error);
|
132 | }
|
133 |
|
134 | if (response.statusCode===403) {
|
135 | this._badPassword=true;
|
136 | return callback(new Error("Bad password detected !"));
|
137 | }
|
138 |
|
139 | var json;
|
140 | try {
|
141 | json = JSON.parse(body);
|
142 | } catch (x) {
|
143 | x.url=url;
|
144 | x.body=body;
|
145 | return callback(x);
|
146 | }
|
147 |
|
148 | debug("_readDir", "json=", json);
|
149 |
|
150 | if (!(json instanceof Array)) {
|
151 | var err=new Error("Invalid readdir response for url="+folderId);
|
152 | err.body=body;
|
153 |
|
154 | return callback(err);
|
155 | }
|
156 |
|
157 | var ret=json.map((f) => {
|
158 |
|
159 | var stat=this._createStat(f, folderId);
|
160 |
|
161 | this._cache[stat.url]=stat;
|
162 |
|
163 | debug("_readDir", "stat=", stat);
|
164 |
|
165 | return this.newURL(stat.url);
|
166 | });
|
167 |
|
168 | callback(null, ret);
|
169 | });
|
170 | }
|
171 |
|
172 | _createStat(f, parentId) {
|
173 | var stat={
|
174 | name: f.name,
|
175 | mtime: new Date(f.date),
|
176 | type: f.type ,
|
177 | isDirectory: () => f.type==="d",
|
178 | isFile: () => f.type!=="d"
|
179 | };
|
180 |
|
181 | if (f.type==="d") {
|
182 | var reg=/console\/get_folder_content\.pl\?id=(.+)$/.exec(f.url);
|
183 | stat.url=this.protocol+":"+parentId+"/"+(reg && reg[1]);
|
184 | stat.mimeType=DIRECTORY_MIME_TYPE;
|
185 |
|
186 | } else {
|
187 | stat.size=parseInt(f.size, 10);
|
188 |
|
189 | var reg2=/\?(.+)$/.exec(f.url);
|
190 | stat.url=this.protocol+":"+parentId+"?"+reg2[1];
|
191 | stat.mimeType=f.mimeType || Mime.lookup(f.name);
|
192 | }
|
193 |
|
194 | return stat;
|
195 | }
|
196 |
|
197 | |
198 |
|
199 |
|
200 | stat(contentURL, callback) {
|
201 |
|
202 | var stat=this._cache[contentURL];
|
203 | if (stat) {
|
204 | debug("stat", "Stat is in cache",stat);
|
205 | return callback(null, stat);
|
206 | }
|
207 |
|
208 | if (true) {
|
209 |
|
210 |
|
211 | var reg=/:([^/]+)\/([^\/]+)$/.exec(contentURL);
|
212 | if (!reg) {
|
213 | reg=/:([^?]*)\?(\/.+)$/.exec(contentURL);
|
214 | }
|
215 |
|
216 | var url=this._baseURL+"console/get_folder_content.pl?id="+reg[1];
|
217 |
|
218 | this._readdir(url, reg[1], (error, stats) => {
|
219 | if (error) {
|
220 | return callback(error);
|
221 | }
|
222 |
|
223 | var stat=stats.find((s) => s.url===contentURL);
|
224 |
|
225 | callback(null, stat);
|
226 | });
|
227 |
|
228 | return;
|
229 | }
|
230 |
|
231 | this._requestQueue.push((callback) => this._stat(contentURL, callback), callback);
|
232 | }
|
233 |
|
234 | _stat(contentURL, callback) {
|
235 |
|
236 | if (this._badPassword) {
|
237 | return callback(new Error("Bad password detected !"));
|
238 | }
|
239 |
|
240 | var url=this._convertURL(contentURL);
|
241 |
|
242 | debug("_stat", "contentURL=", contentURL);
|
243 | var options = {
|
244 | method: "HEAD",
|
245 | followRedirect: false,
|
246 | qs: {
|
247 | user: this._username,
|
248 | pass: this._passwordMD5
|
249 | }
|
250 | };
|
251 |
|
252 | debug("_stat", "Http request", options);
|
253 |
|
254 | request(url, options, (error, response, body) => {
|
255 | debug("_stat", "Body=",body);
|
256 | if (error) {
|
257 | return callback(error);
|
258 | }
|
259 |
|
260 | if (response.statusCode===403) {
|
261 | this._badPassword=true;
|
262 | return callback(new Error("Bad password detected !"));
|
263 | }
|
264 |
|
265 | var ret=[];
|
266 |
|
267 | callback(null, ret);
|
268 | });
|
269 | }
|
270 |
|
271 | |
272 |
|
273 |
|
274 | createReadStream(session, contentURL, options, callback) {
|
275 | debug("createReadStream", "Create url=", contentURL, "options=",options);
|
276 |
|
277 | var url=this._convertURL(contentURL);
|
278 |
|
279 | this._requestQueue.push((callback) => this._createReadStream(url, options, callback), (error, stream) => {
|
280 | if (error) {
|
281 | logger.error("_createReadStream from url="+url+" throws an error", error);
|
282 | return callback(error);
|
283 | }
|
284 |
|
285 | debug("createReadStream", "returns stream");
|
286 | callback(null, stream);
|
287 | });
|
288 | }
|
289 |
|
290 | |
291 |
|
292 |
|
293 | _createReadStream(url, options, callback) {
|
294 | debug("_createReadStream", "url=",url,"options=",options);
|
295 |
|
296 | if (this._badPassword) {
|
297 | return callback(new Error("Bad password detected !"));
|
298 | }
|
299 |
|
300 | var requestOptions = Url.parse(url);
|
301 | requestOptions.headers=requestOptions.headers || {};
|
302 | requestOptions.headers.Authorization="Basic "+new Buffer(this._username + ":" + this._password).toString("base64");
|
303 | requestOptions.headers['User-Agent']=this._userAgent;
|
304 |
|
305 | if (options) {
|
306 | if (options.start) {
|
307 | var bs="bytes "+options.start;
|
308 | if (options.end) {
|
309 | bs+=options.end+"/"+(options.end-options.start+1);
|
310 | } else {
|
311 | bs+="*/*";
|
312 | }
|
313 | requestOptions.headers['content-range']=bs;
|
314 | }
|
315 | }
|
316 | debug("Request options=", requestOptions);
|
317 |
|
318 | var req = https.request(requestOptions, (response) => {
|
319 |
|
320 | var sid=streamID++;
|
321 |
|
322 | debug("_createReadStream", "Get response url=", url, "statusCode=",response.statusCode,"stream=#",sid);
|
323 |
|
324 | if (response.statusCode===403) {
|
325 | this._badPassword=true;
|
326 | return callback(new Error("Bad password detected !"));
|
327 | }
|
328 |
|
329 | if (response.statusCode===302) {
|
330 | var location=response.headers.location;
|
331 | debug("_createReadStream", "Redirect to",location);
|
332 |
|
333 | if (location && location!==url) {
|
334 | setImmediate(() => {
|
335 | this._createReadStream(location, options, callback);
|
336 | });
|
337 | }
|
338 | return;
|
339 | }
|
340 |
|
341 | if (Math.floor(response.statusCode/100)!=2) {
|
342 | logger.error("Invalid status code "+response.statusCode+" for url "+url);
|
343 | var ex=new Error("Invalid status code "+response.statusCode);
|
344 | return callback(ex);
|
345 | }
|
346 |
|
347 | if (debug.enabled) {
|
348 | var count=0;
|
349 |
|
350 | response.on('data', (chunk) => {
|
351 | count+=chunk.length;
|
352 | debug('Stream #',sid,'(',url,') Load',count,'bytes');
|
353 | });
|
354 |
|
355 | response.on('end', (chunk) => {
|
356 | debug('Stream #',sid,'(',url,') CLOSED');
|
357 | });
|
358 | }
|
359 |
|
360 | callback(null, response);
|
361 | });
|
362 |
|
363 |
|
364 | req.on("error", (error) => {
|
365 | logger.error("_createReadStream: Catch error for url=",url,"error=",error,error.stack);
|
366 | error.url=url;
|
367 | callback(error);
|
368 | });
|
369 | req.end();
|
370 | }
|
371 |
|
372 | |
373 |
|
374 |
|
375 | toString() {
|
376 | return "[1Fichier ContentProvider name='"+this.name+"' username='"+this._username+"']";
|
377 | }
|
378 | }
|
379 |
|
380 | module.exports = OneFichierContentProvider;
|