forked from jeffpar/pcjs.v1
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnetlib.js
402 lines (386 loc) · 17.1 KB
/
netlib.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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
/**
* @fileoverview Net-related functions
* @author <a href="mailto:[email protected]">Jeff Parsons</a> (@jeffpar)
* @copyright © 2012-2020 Jeff Parsons
*
* This file is part of PCjs, a computer emulation software project at <https://www.pcjs.org>.
*
* PCjs is free software: you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation, either version 3
* of the License, or (at your option) any later version.
*
* PCjs is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
* even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with PCjs. If not,
* see <http://www.gnu.org/licenses/gpl.html>.
*
* You are required to include the above copyright notice in every modified copy of this work
* and to display that copyright notice when the software starts running; see COPYRIGHT in
* <https://www.pcjs.org/modules/shared/lib/defines.js>.
*
* Some PCjs files also attempt to load external resource files, such as character-image files,
* ROM files, and disk image files. Those external resource files are not considered part of PCjs
* for purposes of the GNU General Public License, and the author does not claim any copyright
* as to their contents.
*/
"use strict";
if (typeof module !== "undefined") {
var fs = require("fs");
var http = require("http");
var path = require("path");
var url = require("url");
}
class Net {
/**
* hasParm(sParm, sValue, req)
*
* @param {string} sParm
* @param {string} [sValue] (pass undefined to check for the presence of ANY sParm)
* @param {Object} [req] is the web server's (ie, Express) request object, if any
* @return {boolean} true if the request Object contains the specified parameter/value, false if not
*
* TODO: Consider whether sParm === null should check for the presence of ANY parameter in Net.asPropagate.
*/
static hasParm(sParm, sValue, req)
{
return (req && req.query && req.query[sParm] && (!sValue || req.query[sParm] == sValue));
}
/**
* propagateParms(sURL, req)
*
* Propagates any "special" query parameters (as listed in Net.asPropagate) from the given
* request object (req) to the given URL (sURL).
*
* We do not modify an sURL that already contains a '?' OR that begins with a protocol
* (eg, http:, mailto:, etc), in order to keep this function simple, since it's only for
* debugging purposes anyway. I also considered blowing off any URLs with a '#' for the
* same reason, since any hash string must follow any query parameters, but stripping
* and re-appending the hash string is pretty trivial, so we do handle that.
*
* TODO: Make propagateParms() more general-purpose (eg, capable of detecting any URL
* to the same site, and capable of merging any of our "special" query parameters with any
* existing query parameters.
*
* @param {string|null} sURL
* @param {Object} [req] is the web server's (ie, Express) request object, if any
* @return {string} massaged sURL
*/
static propagateParms(sURL, req)
{
if (sURL !== null && sURL.indexOf('?') < 0) {
let i;
let sHash = "";
if ((i = sURL.indexOf('#')) >= 0) {
sHash = sURL.substr(i);
sURL = sURL.substr(0, i);
}
let match = sURL.match(/^([a-z])+:(.*)/);
if (!match && req && req.query) {
for (i = 0; i < Net.asPropagate.length; i++) {
let sQuery = Net.asPropagate[i];
let sValue;
if ((sValue = req.query[sQuery])) {
let sParm = (sURL.indexOf('?') < 0 ? '?' : '&');
sParm += sQuery + '=';
if (sURL.indexOf(sParm) < 0) sURL += sParm + encodeURIComponent(sValue);
}
}
}
sURL += sHash;
}
return sURL;
}
/**
* encodeURL(sURL, req, fDebug)
*
* Used to encodes any URLs presented on the current page, using this, um, simple 5-step process:
*
* 1) Replace any backslashes with slashes, in case the URL was derived from a file system path
* 2) Remap links that begin with "archive/" to the corresponding URL at "http://archive.pcjs.org/"
* 3) Use decodeURI() to eliminate escape sequences (like "%20") so that encodeURI() won't re-encode the "%"
* 4) Use encodeURI() to transform all "htmlspecialchars" and reserved characters into the appropriate sequences
* 5) Massage the result with Net.propagateParms(), so that any special parameters are passed along
*
* @param {string} sURL
* @param {Object} req is the web server's (ie, Express) request object, if any
* @param {boolean} [fDebug]
* @return {string} encoded URL
*/
static encodeURL(sURL, req, fDebug)
{
if (sURL) {
sURL = sURL.replace(/\\/g, '/');
if (!fDebug) {
if (sURL.match(/^[^:?]*archive\//)) {
if (sURL.charAt(0) != '/') sURL = path.join(req.path, sURL);
/*
* NOTE: "http://archive.pcjs.org" is now "https://s3-us-west-2.amazonaws.com/archive.pcjs.org"
*/
sURL = "https://s3-us-west-2.amazonaws.com/archive.pcjs.org" + sURL.replace("/archive/", "/");
}
}
/*
* If the incoming URL already contains URI-style escape sequences (eg, "%20" instead of spaces),
* calling decodeURI() first will eliminate them, preventing encodeURI() from converting leading
* "%" into "%25" and corrupting sequences like "%20" by turning them into "%2520".
*/
return Net.propagateParms(encodeURI(decodeURI(sURL)), req);
}
return sURL;
}
/**
* isRemote(sPath)
*
* TODO: Add support for FTP? HTTPS? Anything else?
*
* @param {string} sPath
* @return {boolean} true if sPath is a (supported) remote path, false if not
*/
static isRemote(sPath)
{
return (sPath.indexOf("http:") === 0);
}
/**
* getStat(sURL, done)
*
* @param {string} sURL
* @param {function(Error,Object)} done
*/
static getStat(sURL, done)
{
let options = url.parse(sURL);
options.method = "HEAD";
options.path = options.pathname; // TODO: Determine the necessity of aliasing this
let req = http.request(options, function(res)
{
let err = null;
let stat = null;
// console.log(JSON.stringify(res.headers));
if (res.statusCode == 200) {
/*
* Apparently Node lower-cases response headers (at least incoming headers, despite
* lots of amusing whining by certain people in the Node community), which seems like
* a good thing, because that means I can do two simple key look-ups.
*/
let sLength = res.headers['content-length'];
let sModified = res.headers['last-modified'];
stat = {
size: sLength ? parseInt(sLength, 10) : -1,
mtime: sModified ? new Date(sModified) : null,
remote: true // an additional property we provide to indicate this is not your normal stats object
};
} else {
err = new Error("unexpected response code: " + res.statusCode);
}
done(err, stat);
});
req.on('error', function(err)
{
done(err, null);
});
req.end();
}
/**
* getFile(sURL, sEncoding, done)
*
* TODO: Add support for FTP? HTTPS? Anything else?
*
* @param {string} sURL is the source file
* @param {string|null} sEncoding is the encoding to assume, if any
* @param {function(Error,number,(string|Buffer))} done receives an Error, an HTTP status code, and a Buffer (if any)
*/
static getFile(sURL, sEncoding, done)
{
/*
* Buffer objects are a fixed size, so my choices are: 1) call getStat() first, hope it returns
* the true size, and then preallocate a buffer; or 2) create a new, larger buffer every time a new
* chunk arrives. The latter seems best.
*
* However, if an encoding is given, we'll simply concatenate all the data into a String and return
* that instead. Note that the incoming data is always a Buffer, but concatenation with a String
* performs an implied "toString()" on the Buffer.
*
* WARNING: Even when an encoding is provided, we don't make any attempt to verify that the incoming
* data matches that encoding.
*/
let sFile = "";
let bufFile = null;
http.get(sURL, function(res)
{
res.on('data', function(data)
{
if (sEncoding) {
sFile += data;
return;
}
if (!bufFile) {
bufFile = data;
return;
}
/*
* We need to grow bufFile. I used to do this myself, using the "copy" method:
*
* buf.copy(targetBuffer, [targetStart], [sourceStart], [sourceEnd])
*
* which defaults to 0 for [targetStart] and [sourceStart], but the docs don't clearly
* define the default value for [sourceEnd]. They say "buffer.length", but there is no
* parameter here named "buffer". Let's hope that in the case of "bufFile.copy(buf)"
* they meant "bufFile.length".
*
* However, it turns out this is moot, because there's a new kid in town: Buffer.concat().
*
* buf = new Buffer(bufFile.length + data.length);
* bufFile.copy(buf);
* data.copy(buf, bufFile.length);
* bufFile = buf;
*/
bufFile = Buffer.concat([bufFile, data], bufFile.length + data.length);
}).on('end', function()
{
/*
* TODO: Decide what to do when res.statusCode is actually an error code (eg, 404), because
* in such cases, the file content will likely just be an HTML error page.
*/
if (res.statusCode < 400) {
done(null, res.statusCode, sEncoding? sFile : bufFile);
} else {
done(new Error(sEncoding? sFile : bufFile), res.statusCode, null);
}
}).on('error', function(err)
{
done(err, res.statusCode, null);
});
});
}
/**
* downloadFile(sURL, sFile, done)
*
* @param {string} sURL is the source file
* @param {string} sFile is a fully-qualified target file
* @param {function(Error,number)} done is a callback that receives an Error and a HTTP status code
*/
static downloadFile(sURL, sFile, done)
{
let file = fs.createWriteStream(sFile);
/*
* http.get() accepts a "url" string in lieu of an "options" object; it automatically builds
* the latter from the former using url.parse(). This is good, because it relieves me from
* building my own "options" object, and also from wondering why http functions expect "options"
* to contain a "path" property, whereas url.parse() returns a "pathname" property.
*
* Either the documentation isn't quite right for url.parse() or http.request() (the big brother
* of http.get), or one of those "options" properties is aliased to the other, or...?
*/
http.get(sURL, function(res)
{
res.on('data', function(data)
{
file.write(data);
}).on('end', function()
{
file.end();
/*
* TODO: We should try to update the file's modification time to match the 'last-modified'
* response header value, if any.
*
* TODO: Decide what to do when res.statusCode is actually an error code (eg, 404), because
* in such cases, the file content will likely just be an HTML error page.
*/
done(null, res.statusCode);
}).on('error', function(err)
{
done(err, res.statusCode);
});
});
}
/**
* getResource(sURL, dataPost, fAsync, done)
*
* Request the specified resource (sURL), and once the request is complete, notify done().
*
* @param {string} sURL
* @param {Object|null} [dataPost] for a POST request (default is a GET request)
* @param {boolean} [fAsync] is true for an asynchronous request
* @param {function(string,string|null,number)} [done]
* @return {Array|null} Array containing [sResource, nErrorCode], or null if no response yet
*/
static getResource(sURL, dataPost, fAsync, done)
{
let nErrorCode = -1, sResource = null, response = null;
if (Net.isRemote(sURL)) {
/*
* TODO: This code is nothing more than a band-aid. It assumes the URL uses "http:"
* (hence the call to getFile(), which only supports HTTP GET operations), it assumes
* the requested data is UTF-8 string data (which is normally the case, because nearly
* all our requests are for JSON files), it doesn't deal with dataPost, it assumes
* that fAsync is true, and it performs very simplistic error code mapping.
*
* But, it gets the job done for what little we actually ask of it, when our machines
* are running in the Node environment.
*/
Net.getFile(sURL, "utf8", function(err, status, data) {
if (done) done(sURL, data, err? status : 0);
});
} else {
if (!Net.sServerRoot) {
Net.sServerRoot = path.join(path.dirname(fs.realpathSync(__filename)), "../../../");
}
let sFile = path.join(Net.sServerRoot, sURL);
if (fAsync) {
fs.readFile(sFile, {encoding: "utf8"}, function(err, s)
{
/*
* TODO: If err is set, is there an error code we should return (instead of -1)?
*/
if (!err) {
sResource = s;
nErrorCode = 0;
}
if (done) done(sURL, sResource, nErrorCode);
});
} else {
try {
sResource = fs.readFileSync(sFile, {encoding: "utf8"});
nErrorCode = 0;
} catch (err) {
/*
* TODO: If err is set, is there an error code we should return (instead of -1)?
*/
console.log(err.message);
}
if (done) done(sURL, sResource, nErrorCode);
response = [sResource, nErrorCode];
}
}
return response;
}
}
/*
* The following are (super-secret) commands that can be added to the URL to enable special features.
*
* Our super-secret command processor is affectionately call Gort, and while Gort doesn't understand commands
* like "Klaatu barada nikto", it does understand commands like "debug" and "rebuild"; eg:
*
* http://www.pcjs.org/?gort=debug
*
* hasParm() detects the presence of the specified command, and propagateParms() is a URL filter that ensures
* any commands listed in Net.asPropagate are passed through to all other URLs on the same page; using any of these
* commands also forces the page to be rebuilt and not cached (since we would never want a cached "index.html" to
* contain/expose any of these commands).
*/
Net.GORT_COMMAND = "gort";
Net.GORT_DEBUG = "debug"; // use this to force uncompiled JavaScript even on a Release server
Net.GORT_NODEBUG = "nodebug"; // use this to force uncompiled JavaScript but with DEBUG code disabled
Net.GORT_RELEASE = "release"; // use this to force the use of compiled JavaScript even on a Debug server
Net.GORT_REBUILD = "rebuild"; // use this to force the "index.html" in the current directory to be rebuilt
Net.REVEAL_COMMAND = "reveal";
Net.REVEAL_PDFS = "pdfs";
/*
* This is a list of the URL parameters that propagateParms() will propagate from the requester's URL to
* other URLs provided by the requester.
*/
Net.asPropagate = [Net.GORT_COMMAND, "autostart"];
Net.sServerRoot = null;
if (typeof module !== "undefined") module.exports = Net;