}
*/
HTTPAPI.parseDiskValues = function(s, a)
{
if (s) {
var as = s.split(':');
for (var i = 0; i < as.length && i < a.length; i++) {
if (!str.isValidInt(as[i])) break;
a[i] = parseInt(as[i]);
}
}
return a;
};
/**
* processDiskAPI(req, res)
*
* @param {Object} req is an Express request object (http://expressjs.com/api.html#req.params)
* @param {Object} res is an Express response object (http://expressjs.com/api.html#res.status)
* @return {boolean} true if the request was handled as an API request, false if not
*/
HTTPAPI.processDiskAPI = function(req, res)
{
/*
* For every volume, we must maintain the active machine+user that currently has access.
*/
var nResponse = 400; // default to "Bad Request"
var reqParms = req.method == "GET"? req.query : req.body;
var sAction = reqParms[DiskAPI.QUERY.ACTION];
var sPath = reqParms[DiskAPI.QUERY.VOLUME];
var sMode = reqParms[DiskAPI.QUERY.MODE]; // one of DiskAPI.MODE.* (eg, DiskAPI.MODE.DEMANDRW)
var sCHS = reqParms[DiskAPI.QUERY.CHS];
var sAddr = reqParms[DiskAPI.QUERY.ADDR];
var sData = reqParms[DiskAPI.QUERY.DATA];
var sMachine = reqParms[DiskAPI.QUERY.MACHINE];
var sUser = reqParms[DiskAPI.QUERY.USER];
/*
* aCHS is filled in as follows:
*
* [0]: total cylinders
* [1]: total heads
* [2]: total sectors per track
* [3]: total bytes per sector (generally 512)
*
* aAddr is filled in as follows:
*
* [0]: 0-based cylinder number
* [1]: 0-based head number
* [2]: 1-based sector number
* [3]: sector count
*/
var aCHS = HTTPAPI.parseDiskValues(sCHS, [0, 0, 0, 512]);
var aAddr = HTTPAPI.parseDiskValues(sAddr, [0, 0, 0, 0]);
HTMLOut.logDebug('HTTPAPI.processDiskAPI("' + sPath + '"): action=' + sAction + ', chs=' + sCHS + ', addr=' + sAddr);
if (sPath && sPath.indexOf('*/') === 0 && sPath.indexOf("..") < 0) {
HTTPAPI.verifyUserDir(sUser, function(sDir) {
if (sDir) {
var cbInit = aCHS[0] * aCHS[1] * aCHS[2] * aCHS[3];
sPath = path.join(sDir, sPath.replace('*', ''));
switch(sAction) {
case DiskAPI.ACTION.OPEN:
HTTPAPI.openUserVolume(sPath, sMachine, sUser, sMode, cbInit, function(nResponse, sResponse, fd) {
res.status(nResponse).send(sResponse);
});
break;
case DiskAPI.ACTION.READ:
HTTPAPI.openUserVolume(sPath, sMachine, sUser, sMode, cbInit, function(nResponse, sResponse, fd) {
if (fd) {
HTTPAPI.readUserVolume(sPath, fd, aCHS, aAddr, function(nResponse, sResponse) {
/*
* Without the addition of "no-store", Chrome will assume that a previous response to
* a previously seen URL can be re-used without hitting the server again, which would be
* bad if the requested sector(s) had been written in the meantime.
*
* Perhaps I should be using different HTTP verbs, or perhaps I should switch to sockets,
* but in the meantime, this is absolutely necessary.
*/
res.set("Cache-Control", "no-cache, no-store");
res.status(nResponse).send(sResponse);
});
} else {
res.status(nResponse).send(sResponse);
}
});
break;
case DiskAPI.ACTION.WRITE:
HTTPAPI.openUserVolume(sPath, sMachine, sUser, sMode, cbInit, function(nResponse, sResponse, fd) {
if (fd) {
HTTPAPI.writeUserVolume(sPath, fd, aCHS, aAddr, sData, function(nResponse, sResponse) {
res.status(nResponse).send(sResponse);
});
} else {
res.status(nResponse).send(sResponse);
}
});
break;
case DiskAPI.ACTION.CLOSE:
HTTPAPI.closeUserVolume(sPath, sMachine, sUser, function(err) {
var sResponse = null;
if (err) {
sResponse = err.message;
} else {
nResponse = 200;
}
res.status(nResponse).send(sResponse);
});
break;
default:
res.status(nResponse).send(DiskAPI.FAIL.BADACTION);
break;
}
return;
}
res.status(nResponse).send(DiskAPI.FAIL.BADUSER);
});
return true;
}
res.status(nResponse).send(DiskAPI.FAIL.BADVOL);
return true;
};
/**
* processDumpAPI(req, res)
*
* @param {Object} req is an Express request object (http://expressjs.com/api.html#req.params)
* @param {Object} res is an Express response object (http://expressjs.com/api.html#res.status)
* @return {boolean} true if the request was handled as an API request, false if not
*/
HTTPAPI.processDumpAPI = function(req, res)
{
var aCommand;
var nResponse = 400; // default to "Bad Request"
var sDisk, sFile, sFormat, fComments;
if ((aCommand = HTTPAPI.hasAPICommand(req, DumpAPI.asDiskCommands))) {
sDisk = aCommand[1];
HTMLOut.logDebug('HTTPAPI.processDumpAPI("' + sDisk + '"): type=' + aCommand[0]);
/*
* Allowing ".." in a path component is risky, unless we're running locally...
*/
if (sDisk.indexOf("..") < 0 || req.app.settings.port == 8088) {
sFormat = req.query[DumpAPI.QUERY.FORMAT] || DumpAPI.FORMAT.JSON;
fComments = (req.query[DumpAPI.QUERY.COMMENTS]? true : false);
var mbHD = req.query[DumpAPI.QUERY.MBHD];
/*
* TODO: Consider adding support for DiskDump's "exclusion" option to the API interface
* (the command-line interface supports it).
*/
var disk = new DiskDump(sDisk, null, sFormat, fComments, mbHD, sServerRoot);
if (aCommand[0] == DumpAPI.QUERY.DISK || aCommand[0] == DumpAPI.QUERY.IMG) {
disk.loadFile(function(err) {
HTTPAPI.dumpDisk(err, disk, res);
});
} else {
disk.buildImage(aCommand[0] == DumpAPI.QUERY.DIR, function(err) {
HTTPAPI.dumpDisk(err, disk, res);
});
}
return true;
}
}
else if ((aCommand = HTTPAPI.hasAPICommand(req, DumpAPI.asFileCommands))) {
sFile = aCommand[1];
HTMLOut.logDebug('HTTPAPI.processDumpAPI("' + sFile + '"): type=' + aCommand[0]);
/*
* Allowing ".." in a path component is too risky, unless we're running locally.
*/
if (sFile.indexOf("..") < 0 || req.app.settings.port == 8088) {
sFormat = req.query[DumpAPI.QUERY.FORMAT] || DumpAPI.FORMAT.JSON;
fComments = (req.query[DumpAPI.QUERY.COMMENTS]? true : false);
var fDecimal;
if (req.query[DumpAPI.QUERY.DECIMAL]) {
fDecimal = (req.query[DumpAPI.QUERY.DECIMAL] == "true");
}
var file = new FileDump(sFormat, fComments, fDecimal, sServerRoot);
file.loadFile(sFile, 0, 0, function(err) {
HTTPAPI.dumpFile(err, file, res);
});
return true;
}
}
return false;
};
/**
* dumpDisk(err, disk, res)
*
* @param {Error} err
* @param {DiskDump} disk
* @param {Object} res
*/
HTTPAPI.dumpDisk = function(err, disk, res)
{
var sResponse = "";
var nResponse = 400; // default to "Bad Request"
var sMIMEType = null;
var sAttachment = null;
if (err) {
/*
* TODO: Do a better job of mapping the underlying error (eg, err.errno) to an appropriate HTTP response error.
*/
nResponse = 404;
sResponse = err.message;
} else {
if (disk.sFormat == DumpAPI.FORMAT.IMG) {
// sMIMEType = "application/octet-stream";
sMIMEType = "application/x-download";
sAttachment = path.basename(disk.sDiskPath);
var i = sAttachment.lastIndexOf('.');
if (i > 0) sAttachment = sAttachment.substring(0, i+1) + DumpAPI.FORMAT.IMG;
sResponse = disk.convertToIMG();
} else {
// res.charset = "utf-8";
sMIMEType = "application/json; charset=utf-8";
sResponse = disk.convertToJSON();
}
/*
* Return a successful response ONLY if the disk conversion call returned any data
*/
if (sResponse) {
nResponse = 200;
} else {
sResponse = "unable to convert disk image: " + disk.sDiskPath;
}
}
if (sMIMEType) {
res.set("Content-Type", sMIMEType);
}
if (sAttachment) {
res.set("Content-Disposition", 'attachment; filename="' + sAttachment + '"');
}
res.status(nResponse).send(sResponse);
};
/**
* dumpFile(err, file, res)
*
* @param {Error} err
* @param {FileDump} file
* @param {Object} res
*/
HTTPAPI.dumpFile = function(err, file, res)
{
var sResponse = "";
var nResponse = 400; // default to "Bad Request"
var sMIMEType = null;
var sAttachment = null;
if (err) {
/*
* TODO: Do a better job of mapping the underlying error (eg, err.errno) to an appropriate HTTP response error.
*/
nResponse = 404;
sResponse = err.message;
} else {
if (file.sFormat == DumpAPI.FORMAT.ROM) {
// sMIMEType = "application/octet-stream";
sMIMEType = "application/x-download";
sAttachment = path.basename(file.sFilePath);
var i = sAttachment.lastIndexOf('.');
if (i > 0) sAttachment = sAttachment.substring(0, i+1) + DumpAPI.FORMAT.ROM;
sResponse = file.getData();
} else {
file.convertToJSON(function(err, str) {
if (!err) {
nResponse = 200;
} else {
str = err.message;
}
res.status(nResponse).send(str);
});
return;
}
/*
* Return a successful response ONLY if the file conversion call returned any data
*/
if (sResponse) {
nResponse = 200;
} else {
sResponse = "unable to convert file image: " + file.sFilePath;
}
}
if (sMIMEType) {
res.set("Content-Type", sMIMEType);
}
if (sAttachment) {
res.set("Content-Disposition", 'attachment; filename="' + sAttachment + '"');
}
res.status(nResponse).send(sResponse);
};
/**
* processReportAPI(req, res)
*
* @param {Object} req is an Express request object (http://expressjs.com/api.html#req.params)
* @param {Object} res is an Express response object (http://expressjs.com/api.html#res.status)
* @return {boolean} true if the request was handled as an API request, false if not
*/
HTTPAPI.processReportAPI = function(req, res)
{
var sApp = req.body[ReportAPI.QUERY.APP];
var sVer = req.body[ReportAPI.QUERY.VER];
var sURL = req.body[ReportAPI.QUERY.URL];
var sUser = req.body[ReportAPI.QUERY.USER];
var sType = req.body[ReportAPI.QUERY.TYPE];
var sData = req.body[ReportAPI.QUERY.DATA];
HTMLOut.logDebug('HTTPAPI.processReportAPI("' + sApp + '"): ver=' + sVer + ', url=' + sURL + ', type=' + sType);
if (sApp && sVer && sType == ReportAPI.TYPE.BUG && sData) {
/*
* Generate a random number x (where 0 <= x < 1), add 0.1 so that it's guaranteed to be
* non-zero, convert to base 36, and chop off the leading digit and "decimal" point.
*/
var sRemoteIP = req? req.ip : "";
var sDataFile = "/logs/" + sType + "s/" + usr.formatDate("Ymd") + '-' + (Math.random() + 0.1).toString(36).substr(2,8) + ".json";
var sReport = "" + sApp + ' v' + sVer + ' ' + usr.formatDate("Y-m-d H:i:s") + ' ' + sRemoteIP + ' ' + sDataFile + "
\n";
sDataFile = path.join(sServerRoot, sDataFile);
fs.writeFile(sDataFile, sData);
var sTypeFile = path.join(sServerRoot, "/logs/" + sType + "s.html");
fs.appendFile(sTypeFile, sReport);
res.status(200).send(ReportAPI.RES.OK);
return true;
}
return false;
};
/**
* processUserAPI(req, res)
*
* @param {Object} req is an Express request object (http://expressjs.com/api.html#req.params)
* @param {Object} res is an Express response object (http://expressjs.com/api.html#res.status)
* @return {boolean} true if the request was handled as an API request, false if not
*/
HTTPAPI.processUserAPI = function(req, res)
{
var reqParms = req.method == "GET"? req.query : req.body;
var sReq = reqParms[UserAPI.QUERY.REQ];
var sUser = reqParms[UserAPI.QUERY.USER];
var sState = reqParms[UserAPI.QUERY.STATE];
var sData = reqParms[UserAPI.QUERY.DATA];
HTMLOut.logDebug('HTTPAPI.processUserAPI("' + sUser + '"): req=' + sReq + (sState? (', state=' + sState) : ''));
if (sUser) {
switch(sReq) {
case UserAPI.REQ.CREATE:
if (HTTPAPI.createUserID(sUser, res)) return true;
break;
case UserAPI.REQ.VERIFY:
if (HTTPAPI.verifyUserID(sUser, res)) return true;
break;
case UserAPI.REQ.STORE:
if (HTTPAPI.storeUserData(sUser, sState, sData, res)) return true;
break;
case UserAPI.REQ.LOAD:
if (HTTPAPI.loadUserData(sUser, sState, res)) return true;
break;
default:
break;
}
}
return false;
};
/**
* createUserID(sUser, res)
*
* sUser must consist of the authorizing key, a colon, and the key to be authorized.
*
* @param {string} sUser
* @param {Object} res is an Express response object (http://expressjs.com/api.html#res.status)
* @return {boolean} true if request was valid (does not imply success), false if invalid
*/
HTTPAPI.createUserID = function(sUser, res)
{
HTMLOut.logDebug('HTTPAPI.createUserID("' + sUser + '")');
var asUsers = sUser.split(':');
if (asUsers[0] && asUsers[1]) {
HTTPAPI.verifyUserID(asUsers[0], res, function doneVerifyAuthorizedID(iVerified, result, res) {
HTMLOut.logDebug('HTTPAPI.doneVerifyAuthorizedID("' + asUsers[0] + '"): ' + iVerified);
/*
* The authorizing key has been verified, but it may authorize another key only
* if it is the first key in the list (or there are no keys at all yet and the authorizing
* key matches GORT_COMMAND).
*/
if (iVerified == 1 || iVerified <= 0 && asUsers[0] == net.GORT_COMMAND) {
HTTPAPI.verifyUserID(asUsers[1], res, function doneVerifyUserID(iVerified, resultSecond, res) {
HTMLOut.logDebug('HTTPAPI.doneVerifyUserID("' + asUsers[1] + '"): ' + iVerified);
if (iVerified <= 0) {
var sUserFile = path.join(sServerRoot, "/logs/users.log");
fs.appendFile(sUserFile, asUsers[1] + "\n");
result[UserAPI.RES.CODE] = UserAPI.CODE.OK;
result[UserAPI.RES.DATA] = asUsers[1];
} else {
result[UserAPI.RES.CODE] = UserAPI.CODE.FAIL;
result[UserAPI.RES.DATA] = UserAPI.FAIL.DUPLICATE;
}
if (res) res.status(200).send(JSON.stringify(result) + "\n");
});
return;
}
result[UserAPI.RES.CODE] = UserAPI.CODE.FAIL;
result[UserAPI.RES.DATA] = UserAPI.FAIL.VERIFY;
if (res) res.status(200).send(JSON.stringify(result) + "\n");
});
return true;
}
return false;
};
/**
* verifyUserID(sUser, res, done)
*
* If a done() handler is specified, the first parameter it receives is iVerified, which
* will be -1 if the "users.log" file hasn't been initialized yet, 0 if the key doesn't exist,
* or a positive number representing the line number at which the key appears.
*
* Moreover, when a done() handler is provided, it simply passes the response object (res) to
* done(), which must actually send the response, based on the provided result; this function sends
* a response only if done() is NOT provided.
*
* @param {string} sUser
* @param {Object} res is an Express response object (http://expressjs.com/api.html#res.status)
* @param {function(number, Object, Object)} [done]
* @return {boolean} true if request was valid (does not imply success), false if invalid
*/
HTTPAPI.verifyUserID = function(sUser, res, done)
{
HTMLOut.logDebug('HTTPAPI.verifyUserID("' + sUser + '")');
/*
* If a colon separator is present, this is an implicit REQ.CREATE call
* (in fact, at present, PCjs does not issue any explicit REQ.CREATE calls).
*/
if (sUser.indexOf(':') > 0 && HTTPAPI.createUserID(sUser, res)) {
return true;
}
var iVerified = -1;
var sUserFile = path.join(sServerRoot, "/logs/users.log");
fs.readFile(sUserFile, {encoding: "utf8"}, function doneReadUserIDs(err, sData) {
var sResCode = UserAPI.CODE.FAIL;
var sResData = UserAPI.FAIL.VERIFY;
if (err) {
HTMLOut.logError(err);
} else {
var asUsers = sData.split("\n");
iVerified = asUsers.indexOf(sUser);
if (iVerified >= 0) {
if (HTTPAPI.createUserDir(sUser)) {
sResCode = UserAPI.CODE.OK;
sResData = sUser;
}
}
iVerified++;
}
var result = {};
result[UserAPI.RES.CODE] = sResCode;
result[UserAPI.RES.DATA] = sResData;
if (done) {
done(iVerified, result, res);
return;
}
if (res) res.status(200).send(JSON.stringify(result) + "\n");
});
return true;
};
/**
* getUserDir(sUser)
*
* @param {string} sUser
* @return {string}
*/
HTTPAPI.getUserDir = function(sUser)
{
return path.join(sServerRoot, "/logs/users/" + /* sUser.substr(0, 2) + "/" + */ sUser);
};
/**
* createUserDir(sUser)
*
* TODO: Creation is relatively rare, so I'm lazy and use synchronous calls, but fix this someday.
*
* @param {string} sUser
* @return {boolean} true if successful, false if not
*/
HTTPAPI.createUserDir = function(sUser)
{
var sDir = HTTPAPI.getUserDir(sUser);
return (fs.existsSync(sDir) || !!mkdirp.sync(sDir));
};
/**
* verifyUserDir(sUser, done)
*
* @param {string} sUser
* @param {function(string|null)} done
*/
HTTPAPI.verifyUserDir = function(sUser, done)
{
HTMLOut.logDebug('HTTPAPI.verifyUserDir("' + sUser + '")');
if (sUser) {
var sDir = HTTPAPI.getUserDir(sUser);
fs.exists(sDir, function(fExists) {
if (!fExists) {
HTTPAPI.verifyUserID(sUser, null, function(iVerified, result, res) {
done(iVerified > 0? sDir : null);
});
return;
}
done(sDir);
});
return;
}
done(null);
};
/**
* loadUserData(sUser, sState, res)
*
* @param {string} sUser
* @param {string} sState is a state ID
* @param {Object} res is an Express response object (http://expressjs.com/api.html#res.status)
* @return {boolean} true if request was valid (does not imply success), false if invalid
*/
HTTPAPI.loadUserData = function(sUser, sState, res)
{
HTMLOut.logDebug('HTTPAPI.loadUserData("' + sUser + '","' + sState + '")');
if (sState) {
HTTPAPI.verifyUserDir(sUser, function(sDir) {
var result = {};
result[UserAPI.RES.CODE] = UserAPI.CODE.FAIL;
result[UserAPI.RES.DATA] = UserAPI.FAIL.VERIFY;
if (sDir) {
if (sState.indexOf("..") >= 0) {
result[UserAPI.RES.DATA] = UserAPI.FAIL.BADSTATE;
}
else {
var sFile = path.join(sDir, sState);
fs.readFile(sFile, {encoding: "utf8"}, function doneReadUserData(err, sData) {
if (err) {
HTMLOut.logError(err);
/*
* We'll assume that any error here means the file doesn't exist (ie, NOSTATE instead
* of BADLOAD).
*/
result[UserAPI.RES.DATA] = UserAPI.FAIL.NOSTATE;
} else {
/*
* Suppress the normal RES_CODE+RES_DATA output format and simply return the "raw" state;
* this makes life simpler for the client app.
*/
result = sData;
/*
* Without the addition of "no-store", Chrome (and perhaps other browsers) will assume that
* a previous response to a previously seen URL can be re-used without hitting the server
* again, which would be bad if the requested state has been modified in the meantime.
*
* Here's the scenario: the user loads a web page with a machine that uses server-side state,
* the user changes the state of that machine and switches away from the machine (eg, clicks
* a link to a different page), which causes the state to be updated on the server; then they
* click the Back button, and the browser, seeing the same server-side state request, simply
* uses the state data it originally retrieved, instead of requesting the updated state from the
* server.
*/
res.set("Cache-Control", "no-cache, no-store");
}
res.status(200).send(typeof result == "string"? result : JSON.stringify(result) + "\n");
});
return;
}
}
res.status(200).send(JSON.stringify(result) + "\n");
});
return true;
}
return false;
};
/**
* storeUserData(sUser, sState, sData, res)
*
* @param {string} sUser
* @param {string} sState is a state ID
* @param {string} sData
* @param {Object} res is an Express response object (http://expressjs.com/api.html#res.status)
* @return {boolean} true if request was valid (does not imply success), false if invalid
*/
HTTPAPI.storeUserData = function(sUser, sState, sData, res)
{
HTMLOut.logDebug('HTTPAPI.storeUserData("' + sUser + '","' + sState + '")');
if (sState && sData) {
HTTPAPI.verifyUserDir(sUser, function(sDir) {
var result = {};
result[UserAPI.RES.CODE] = UserAPI.CODE.FAIL;
result[UserAPI.RES.DATA] = UserAPI.FAIL.VERIFY;
if (sDir) {
if (sState.indexOf("..") >= 0) {
result[UserAPI.RES.DATA] = UserAPI.FAIL.BADSTATE;
}
else {
var sFile = path.join(sDir, sState);
fs.writeFile(sFile, sData);
result[UserAPI.RES.CODE] = UserAPI.CODE.OK;
result[UserAPI.RES.DATA] = sData.length + " bytes stored";
}
}
res.status(200).send(JSON.stringify(result) + "\n");
});
return true;
}
return false;
};
module.exports = HTTPAPI;