mirror of
https://github.com/NuSkooler/enigma-bbs.git
synced 2025-07-24 03:30:40 +02:00
* Add FileBaseFilters
* Add HTTP(S) file web server with temp URLs * Get temp web d/l from file list * Add File area filter editor (all file area stuff will be rename to file "base" later) * Concept of "listening servers" vs "login servers" * Ability to get servers by their package name * New MCI: %FN: File Base active filter name * Some ES6 updates * VC resetInitialFocus() to set focus to explicit/detected initial focus field * Limit what is dumped out when logging form data
This commit is contained in:
parent
712cf512f0
commit
a7c0f2b7b0
22 changed files with 1233 additions and 286 deletions
230
core/file_area_web.js
Normal file
230
core/file_area_web.js
Normal file
|
@ -0,0 +1,230 @@
|
|||
/* jslint node: true */
|
||||
'use strict';
|
||||
|
||||
// ENiGMA½
|
||||
const Config = require('./config.js').config;
|
||||
const FileDb = require('./database.js').dbs.file;
|
||||
const getISOTimestampString = require('./database.js').getISOTimestampString;
|
||||
const FileEntry = require('./file_entry.js');
|
||||
const getServer = require('./listening_server.js').getServer;
|
||||
const Errors = require('./enig_error.js').Errors;
|
||||
|
||||
// deps
|
||||
const hashids = require('hashids');
|
||||
const moment = require('moment');
|
||||
const paths = require('path');
|
||||
const async = require('async');
|
||||
const fs = require('fs');
|
||||
const mimeTypes = require('mime-types');
|
||||
|
||||
const WEB_SERVER_PACKAGE_NAME = 'codes.l33t.enigma.web.server';
|
||||
|
||||
/*
|
||||
:TODO:
|
||||
* Load temp download URLs @ startup & set expire timers via scheduler.
|
||||
* At creation, set expire timer via scheduler
|
||||
*
|
||||
*/
|
||||
|
||||
class FileAreaWebAccess {
|
||||
constructor() {
|
||||
this.hashids = new hashids(Config.general.boardName);
|
||||
}
|
||||
|
||||
startup(cb) {
|
||||
const self = this;
|
||||
|
||||
async.series(
|
||||
[
|
||||
function initFromDb(callback) {
|
||||
// :TODO: Init from DB & register expiration timers
|
||||
return callback(null);
|
||||
},
|
||||
function addWebRoute(callback) {
|
||||
const webServer = getServer(WEB_SERVER_PACKAGE_NAME);
|
||||
if(!webServer) {
|
||||
return callback(Errors.DoesNotExist(`Server with package name "${WEB_SERVER_PACKAGE_NAME}" does not exist`));
|
||||
}
|
||||
|
||||
const routeAdded = webServer.instance.addRoute({
|
||||
method : 'GET',
|
||||
path : '/f/[a-zA-Z0-9]+$', // :TODO: allow this to be configurable
|
||||
handler : self.routeWebRequest.bind(self),
|
||||
});
|
||||
|
||||
return callback(routeAdded ? null : Errors.General('Failed adding route'));
|
||||
}
|
||||
],
|
||||
err => {
|
||||
return cb(err);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
shutdown(cb) {
|
||||
return cb(null);
|
||||
}
|
||||
|
||||
load(cb) {
|
||||
return cb(null); // :TODO: Load from db
|
||||
}
|
||||
|
||||
loadServedHashId(hashId, cb) {
|
||||
FileDb.get(
|
||||
`SELECT expire_timestamp FROM
|
||||
file_web_serve
|
||||
WHERE hash_id = ?`,
|
||||
[ hashId ],
|
||||
(err, result) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
const decoded = this.hashids.decode(hashId);
|
||||
if(!result || 2 !== decoded.length) {
|
||||
return cb(Errors.Invalid('Invalid or unknown hash ID'));
|
||||
}
|
||||
|
||||
return cb(
|
||||
null,
|
||||
{
|
||||
hashId : hashId,
|
||||
userId : decoded[0],
|
||||
fileId : decoded[1],
|
||||
expireTimestamp : moment(result.expire_timestamp),
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
getHashId(client, fileEntry) {
|
||||
//
|
||||
// Hashid is a unique combination of userId & fileId
|
||||
//
|
||||
return this.hashids.encode(client.user.userId, fileEntry.fileId);
|
||||
}
|
||||
|
||||
buildTempDownloadLink(client, fileEntry, hashId) {
|
||||
hashId = hashId || this.getHashId(client, fileEntry);
|
||||
|
||||
//
|
||||
// Create a URL such as
|
||||
// https://l33t.codes:44512/f/qFdxyZr
|
||||
//
|
||||
// :TODO: build from config
|
||||
|
||||
//
|
||||
// Prefer HTTPS over HTTP. Be explicit about the port
|
||||
// only if required.
|
||||
//
|
||||
let schema;
|
||||
let port;
|
||||
if(Config.contentServers.web.https.enabled) {
|
||||
schema = 'https://';
|
||||
port = (443 === Config.contentServers.web.https.port) ?
|
||||
'' :
|
||||
`:${Config.contentServers.web.https.port}`;
|
||||
} else {
|
||||
schema = 'http://';
|
||||
port = (80 === Config.contentServers.web.http.port) ?
|
||||
'' :
|
||||
`:${Config.contentServers.web.http.port}`;
|
||||
}
|
||||
|
||||
return `${schema}${Config.contentServers.web.domain}${port}${Config.fileBase.web.path}${hashId}`;
|
||||
}
|
||||
|
||||
getExistingTempDownloadServeItem(client, fileEntry, cb) {
|
||||
const hashId = this.getHashId(client, fileEntry);
|
||||
this.loadServedHashId(hashId, (err, servedItem) => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
servedItem.url = this.buildTempDownloadLink(client, fileEntry);
|
||||
|
||||
return cb(null, servedItem);
|
||||
});
|
||||
}
|
||||
|
||||
createAndServeTempDownload(client, fileEntry, options, cb) {
|
||||
const hashId = this.getHashId(client, fileEntry);
|
||||
const url = this.buildTempDownloadLink(client, fileEntry, hashId);
|
||||
options.expireTime = options.expireTime || moment().add(2, 'days');
|
||||
|
||||
// add/update rec with hash id and (latest) timestamp
|
||||
FileDb.run(
|
||||
`REPLACE INTO file_web_serve (hash_id, expire_timestamp)
|
||||
VALUES (?, ?);`,
|
||||
[ hashId, getISOTimestampString(options.expireTime) ],
|
||||
err => {
|
||||
if(err) {
|
||||
return cb(err);
|
||||
}
|
||||
|
||||
// :TODO: setup tracking of expiration time so we can clean up the entry
|
||||
|
||||
return cb(null, url);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
fileNotFound(resp) {
|
||||
resp.writeHead(404, { 'Content-Type' : 'text/html' } );
|
||||
|
||||
// :TODO: allow custom 404
|
||||
return resp.end('<html><body>Not found</html>');
|
||||
}
|
||||
|
||||
routeWebRequest(req, resp) {
|
||||
const hashId = paths.basename(req.url);
|
||||
|
||||
this.loadServedHashId(hashId, (err, servedItem) => {
|
||||
|
||||
if(err) {
|
||||
return this.fileNotFound(resp);
|
||||
}
|
||||
|
||||
const fileEntry = new FileEntry();
|
||||
fileEntry.load(servedItem.fileId, err => {
|
||||
if(err) {
|
||||
return this.fileNotFound(resp);
|
||||
}
|
||||
|
||||
const filePath = fileEntry.filePath;
|
||||
if(!filePath) {
|
||||
return this.fileNotFound(resp);
|
||||
}
|
||||
|
||||
fs.stat(filePath, (err, stats) => {
|
||||
if(err) {
|
||||
return this.fileNotFound(resp);
|
||||
}
|
||||
|
||||
resp.on('close', () => {
|
||||
// connection closed *before* the response was fully sent
|
||||
// :TODO: Log and such
|
||||
});
|
||||
|
||||
resp.on('finish', () => {
|
||||
// transfer completed fully
|
||||
// :TODO: we need to update the users stats - bytes xferred, credit stuff, etc.
|
||||
});
|
||||
|
||||
const headers = {
|
||||
'Content-Type' : mimeTypes.contentType(paths.extname(filePath)) || mimeTypes.contentType('.bin'),
|
||||
'Content-Length' : stats.size,
|
||||
'Content-Disposition' : `attachment; filename="${fileEntry.fileName}"`,
|
||||
};
|
||||
|
||||
const readStream = fs.createReadStream(filePath);
|
||||
resp.writeHead(200, headers);
|
||||
return readStream.pipe(resp);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new FileAreaWebAccess();
|
Loading…
Add table
Add a link
Reference in a new issue