'use strict';
const fs = require('fs'),
http = require('http'),
https = require('https'),
path = require('path'),
stream = require('stream');
const { normalizeAddress } = require('../core/utils/ip-util');
const { handleHttpPipe } = require('../core/utils/pipe-util');
function splitQueryString(url) {
const i = url.indexOf('?');
if (i >= 0) {
return [
stripTrailingSlashes(unescape(url.slice(0, i))),
url.slice(i + 1)
];
} else {
return [
stripTrailingSlashes(unescape(url)),
null
];
}
}
function stripTrailingSlashes(s) {
while (s.length > 1 && s.endsWith('/')) {
s = s.slice(0, s.length - 1);
}
return s || '/';
}
function toBase64(str) {
return Buffer.from(str, 'utf-8').toString('base64');
}
function fromBase64(str) {
return Buffer.from(str, 'base64').toString('utf-8');
}
function streamToBuffer(s) {
return new Promise(resolve => {
const data = [];
function onData(chunk) {
data.push(chunk);
}
function onClose() {
if (!s.readableEnded) {
doResolve({ error: 'stream closed unexpectedly' });
}
}
function onError(e) {
doResolve({ error: e.toString() });
}
function onEnd() {
doResolve({
buffer: Buffer.concat(data)
});
}
s.on('data', onData);
s.on('close', onClose);
s.on('error', onError);
s.on('end', onEnd);
function doResolve(res) {
s.removeListener('data', onData);
s.removeListener('close', onClose);
s.removeListener('error', onError);
s.removeListener('end', onEnd);
s.addListener('error', function() { });
resolve(res);
}
});
}
class MediaServer {
constructor({ credentials, allowlist, webdavAddress, webdavExternalUrl }, newznabClients, transferClients) {
this._authHeader = credentials ? 'Basic ' + toBase64(credentials) : null;
if (!this._authHeader) {
console.log('Warning: Authentication is disabled!');
}
this._connectAllowlist = new Set(allowlist);
console.log('Allowed IPs:', [...this._connectAllowlist]);
const match = /^(https?):\/\/([^:]+):([^@]+)@([^:]+):([0-9]+)$/.exec(webdavAddress);
if (!match) {
throw new Error('Invalid webdav address: ' + webdavAddress);
}
const protocol = match[1], username = match[2], password = match[3], host = match[4], port = match[5];
this._webdavAuthHeader = 'Basic ' + toBase64(username + ':' + password);
this._webdavUrl = `${protocol}://${host}:${port}`;
this._webdavExternalUrl = webdavExternalUrl;
this._actionPath = '/action';
this._newznabClients = newznabClients;
this._transferClients = transferClients;
this._staticAssets = {};
this._staticAssets['/favicon.ico'] = {
contentType: 'image/x-icon',
cache: true,
data: fs.readFileSync(path.join(__dirname, 'assets', 'cloud.ico'))
};
this._staticAssets['/bootstrap.min.css'] = {
contentType: 'text/css; charset=utf-8',
cache: true,
data: fs.readFileSync(path.join(__dirname, 'assets', 'bootstrap.min.css'))
};
this._staticAssets['/bootstrap.bundle.min.js'] = {
contentType: 'application/x-javascript',
cache: true,
data: fs.readFileSync(path.join(__dirname, 'assets', 'bootstrap.bundle.min.js'))
};
this._staticAssets['/util.js'] = {
contentType: 'application/x-javascript',
cache: true,
data: fs.readFileSync(path.join(__dirname, 'assets', 'util.js'))
};
this._staticAssets['/app.js'] = {
contentType: 'application/x-javascript',
cache: true,
data: fs.readFileSync(path.join(__dirname, 'assets', 'app.js'))
};
this._staticAssets['/'] = {
contentType: 'text/html; charset=utf-8',
cache: true,
data: fs.readFileSync(path.join(__dirname, 'assets', 'app.html'))
};
this.handleConnect = this.handleConnect.bind(this);
this.handleRequest = this.handleRequest.bind(this);
}
async init() {
for (const name in this._transferClients) {
await this._transferClients[name].init();
}
}
handleConnect(socket) {
const remoteAddress = normalizeAddress(socket.remoteAddress);
if (!this._connectAllowlist.has(remoteAddress)) {
console.log('* Denying connection from ' + remoteAddress);
socket.destroy();
}
}
handleRequest(req, res) {
const { method, url, headers } = req;
const [reqPath, queryStr] = splitQueryString(url);
if (this._authHeader && headers['authorization'] !== this._authHeader) {
res.writeHead(401, 'Unauthorized', {
'Www-Authenticate': 'Basic realm="transfer"'
});
res.end();
return;
}
console.log('DEBUG: handleRequest', method, url);
if (method == 'OPTIONS') {
res.writeHead(200, 'OK', {
'Cache-Control': 'no-store, no-cache, must-revalidate',
'Allow': 'OPTIONS, GET, DELETE, PROPFIND, PUT, MOVE',
'DAV': '1, 3, extended-mkcol, 2',
'MS-Author-Via': 'DAV',
'Accept-Ranges': 'bytes'
});
res.end();
return;
} else if (method === 'HEAD') {
this._handleHead(headers, reqPath, queryStr, res);
} else if (method === 'GET') {
this._handleGet(headers, reqPath, queryStr, req, res);
} else if (method === 'POST') {
this._handlePost(headers, reqPath, req, res);
} else {
console.log('Unknown method: ' + method + ' ' + url);
res.writeHead(501, 'Not Implemented');
res.end();
}
}
_handleHead(headers, reqPath, queryStr, res) {
if (reqPath in this._staticAssets) {
const { contentType, cache } = this._staticAssets[reqPath];
res.writeHead(200, 'OK', {
'Cache-Control': cache ? 'public, max-age=31536000, immutable' : 'no-store, no-cache',
'Content-Type': contentType
});
res.end();
} else {
res.writeHead(404, 'Not Found');
res.end();
}
}
_handleGet(headers, reqPath, queryStr, req, res) {
if (reqPath in this._staticAssets) {
const { contentType, cache, path, data } = this._staticAssets[reqPath];
if (path) {
res.writeHead(200, 'OK', {
'Cache-Control': cache ? 'public, max-age=31536000, immutable' : 'no-store, no-cache',
'Content-Type': contentType
});
const stream = fs.createReadStream(path);
handleHttpPipe(stream, req, res);
} else if (data) {
const buf = Buffer.from(data);
res.writeHead(200, 'OK', {
'Cache-Control': cache ? 'public, max-age=31536000, immutable' : 'no-store, no-cache',
'Content-Type': contentType,
'Content-Length': buf.length
});
res.end(buf);
} else {
throw new Error('Invalic static asset entry: ' + reqPath);
}
return;
} else if (reqPath === this._actionPath || reqPath.startsWith(this._actionPath + '?')) {
this._handleGetAction(headers, queryStr, res);
} else {
res.writeHead(404, 'Not Found');
res.end();
}
}
[truncated]
}