-/* $Id: httpd.js,v 1.55 2005/07/12 13:00:14 mmondor Exp $ */
+/* $Id: httpd.js,v 1.56 2005/07/13 22:05:25 mmondor Exp $ */
/*
* Copyright (c) 2005, Matthew Mondor
* priority at current time.
*
* TODO:
- * - Support partial content to allow resuming uploads for clients
- * - Report error if requested offset is too high, but seek otherwise.
- * Do I need to report the actual file length in Content-Length, or
- * only the remaining?
+ * - There is a problem if the remote socket closes when we're sending
+ * alot of data, poll(2) apparently doesn't always save us from this
+ * despite checking POLLHUP/POLLERR events. A SIGPIPE signal is sent
+ * to the process, and we must be able to handle some common signals.
+ * This would also allow an opportunity for applications to register
+ * server cleanup handlers when SIGTERM is received, i.e. to save to
+ * disk in-memory cached data, etc.
+ * - Read http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
+ * and see if we meet conformance, adjust as needed.
+ * - We might want to check Accept-Language: for multilingual sites...
* - See what to do for HEAD and PUT
* - Possibly limit rate of connections per address like I did in
* mmftpd/mmsmtpd/mmpop3d/mmspawnd, a requested feature of 3s4i.
* Server identification
*/
SERVER_VERSION = 'mmondor_js_httpd/0.0.1 (NetBSD)';
-SERVER_CVSID = '$Id: httpd.js,v 1.55 2005/07/12 13:00:14 mmondor Exp $';
+SERVER_CVSID = '$Id: httpd.js,v 1.56 2005/07/13 22:05:25 mmondor Exp $';
} else
fd = file;
for (;;) {
- data = fd.read(8192);
+ data = fd.read(16384);
if (data.length == 0)
break;
contents += data;
this.headers.push('Date: ' + this.gmttime);
this.headers.push('Server: ' + SERVER_VERSION);
this.headers.push('Connection: close');
+ this.headers.push('Accept-Ranges: bytes');
}
/*
* HTTPReply prototype object
headers += 'Content-Type: ' + this.type + "\r\n";
headers += "\r\n";
- fd.write(headers + contents);
+ fd.bwrite(headers + contents);
}
}
* polling. On EOF reading from either end, exit transfer
* state after writing to the other and syncing/closing,
* and order to close client descriptor.
+ * We take care not to transfer more than this.transfer_size bytes
+ * outbound.
+ * XXX We might also want to observe it for inbound, where it could
+ * be set to the user-provided Content-Length...
*/
if (this.transfer_state == STATE_TRANSFER_READ) {
if (this == this.transfer_src) {
}
} else {
/* Reading from file */
+ var bufsiz = this.transfer_size;
+ if (bufsiz > options.readbuf_size)
+ bufsiz = options.readbuf_size;
try {
- this.transfer_data = this.transfer_src.read(
- options.readbuf_size);
+ if (bufsiz > 0)
+ this.transfer_data =
+ this.transfer_src.read(bufsiz);
+ else
+ this.transfer_data = '';
if (this.transfer_data.length == 0)
this.transfer_eof = true;
+ this.transfer_size -=
+ this.transfer_data.length;
this.transfer_state = STATE_TRANSFER_WRITE;
} catch (x) {
close = true;
close = true;
else {
try {
- this.write(this.transfer_data);
+ this.bwrite(this.transfer_data);
this.transfer_state =
STATE_TRANSFER_READ;
this.updateTimeout(time);
/* Initial input timeout */
this.updateTimeout(time);
/* For process_request()/process_post() */
- this.bread_buffer = '';
+ this.bread_buffer = this.bwrite_buffer = '';
/*
* Note that fcntl(2) and setsockopt(2) flags applied to the bound
{
var close = false;
var valid = false;
- var evil = false;
+ var evil_browser = evil_os = false;
var vhost = '';
var lines;
var words;
this.http_modified_since = undefined;
this.http_sessid = undefined;
this.http_vars_session = {};
+ this.http_range = undefined;
/* Split request lines */
lines = this.request_data.split("\r\n");
if (valid) {
for (i in lines) {
words = lines[i].split(' ');
- if (words[0] == 'Host:') {
- if (words.length == 2) {
- var i2;
-
- if ((i2 = words[1].indexOf(':')) != -1)
- words[1] =
- words[1].substr(0, i2);
- vhost = words[1];
- }
+ if (words[0] == 'Host:' && words.length == 2) {
+ var i2;
+
+ if ((i2 = words[1].indexOf(':')) != -1)
+ words[1] = words[1].substr(0, i2);
+ vhost = words[1];
} else if (words[0] == 'Cookie:') {
words = (lines[i].substr(8)).split('=');
if (words.length == 2)
this.http_agent = lines[i].substr(12);
if (options.ban_msie == true &&
this.http_agent.indexOf('MSIE') != -1)
- evil = true;
- } else if (words[0] == 'Content-Length:')
+ evil_browser = true;
+ if (options.ban_windows == true &&
+ this.http_agent.indexOf('Windows') != -1)
+ evil_os = true;
+ } else if (words[0] == 'Content-Length:' &&
+ words.length == 2)
this.http_content_length = words[1].valueOf();
else if (words[0] == 'If-Modified-Since:')
this.http_modified_since = Math.round(
Date.parse(lines[i].substr(19)) / 1000);
+ else if (words[0] == 'Range:')
+ this.http_range = lines[i].substr(7);
}
}
/*
* Block out evil Microsoft products
*/
- if (evil) {
+ if (evil_browser) {
http_error(this, 666, 'Evil Browser Banished!',
- 'Your browser is evil born.<br> At least ' +
+ 'Your browser is evil born.<br>At least ' +
'<b>upgrade</b> to a <a href="http://mozilla.org">' +
'decent</a> browser to survive on these grounds.<br>');
return true;
}
+ if (evil_os) {
+ http_error(this, 666, 'Evil Operating System Banished!',
+ 'Your Operating System is evil born.<br>At least ' +
+ '<b>upgrade</b> to a <a href="http://netbsd.org">' +
+ 'decent</a> OS to survive on these grounds.<br>');
+ return true;
+ }
/*
* Filter out definitely invalid requests
FD.prototype.httpRespond = function(time)
{
- var path, fd, st, res, ext, mimetype, i, sess;
+ var path, fd, st, res, ext, mimetype, i, sess, size;
/*
* Verify if requested path is valid
* instructions to attempt to reload the originally supplied URL,
* and with a new session cookie.
*/
- if (this.http_sessid == undefined && this.http_vhost.scripts == true) {
+ if (this.http_vhost.scripts == true && this.http_sessid == undefined) {
var doc, sess;
doc = new HTTPReply(200, 'OK',
}
/*
- * We really need to transfer it, so switch to outbound transfer mode.
+ * If client only requested a range of bytes of the file, verify if
+ * the range is valid, and if so, arrange to only transfer the
+ * requested part of the file.
+ * In any other case, simply transfer the whole file.
+ */
+ if (this.http_range != undefined) {
+ var w;
+
+ if (this.http_range.startsWith('bytes'))
+ this.http_range = this.http_range.substr(5);
+ if (this.http_range.length > 0 &&
+ this.http_range.charAt(0) == '=')
+ this.http_range = this.http_range.substr(1);
+ if ((w = this.http_range.split('-')).length == 2) {
+ var from, to;
+
+ if (w[0] == '')
+ w[0] = 0;
+ if (w[1] == '')
+ w[1] = st.st_size - 1;
+ from = Number(w[0]);
+ to = Number(w[1]);
+
+ if (from >= 0 && to < st.st_size && to >= from) {
+ res = new HTTPReply(206, 'Partial Content',
+ mimetype);
+ res.addHeader('Content-Range: bytes ' +
+ from + '-' + to + '/' + st.st_size);
+ this.transfer_size = Math.abs((to - from) + 1);
+ if (from > 0) {
+ try {
+ fd.lseek(from, FD.SEEK_SET);
+ } catch(x) {
+ err.put(x + " at lseek()\n");
+ }
+ }
+ }
+ }
+ }
+ if (res == undefined) {
+ res = new HTTPReply(200, 'OK', mimetype);
+ this.transfer_size = Math.abs(st.st_size);
+ }
+
+ /*
+ * Flush HTTP header, sending it
+ */
+ res.flush(this, this.transfer_size);
+
+ /*
+ * Switch to outbound transfer mode.
*/
this.transfer_src = fd;
this.transfer_dst = this;
this.events = FD.POLLOUT;
/*
- * Flush HTTP header. Then return with close=false, to delegate
- * operations to process_transfer().
+ * Return with close=false, to delegate operations to
+ * process_transfer().
*/
- res = new HTTPReply(200, 'OK', mimetype);
- res.flush(this, st.st_size);
-
return false;
}
}
/*
+ * Since we are using nonblocking mode, it is possible for write(2) to return
+ * a short count, in which case we must be able to resume writing the
+ * remaining buffer after poll(2). There are currently two write paths,
+ * HTTPResponse.flush() and process_transfer(). Both can use this function
+ * instead of write(2). The main poll(2) based loop can then handle buffer
+ * flushing. However, we first attempt to immediately write as much as we can
+ * before queuing what needs to be written again.
+ */
+FD.prototype.bwrite = function(data)
+{
+ var size;
+
+ if ((size = this.write(data)) == data.length)
+ return data.length;
+
+ this.bwrite_buffer += data.substr(size);
+ return size;
+}
+
+/*
+ * Called by the main loop to know if there exists queued data, and to write
+ * it out if needed. Returns an object with two properties, done which if
+ * true tells the caller that there is no more data to flush, and close
+ * which if true means that an error occurred in which case connection should
+ * be closed.
+ */
+FD.prototype.bwrite_flush = function()
+{
+ var size;
+ var obj = new Object();
+
+ obj.done = obj.close = false;
+
+ if (this.bwrite_buffer.length == 0) {
+ obj.done = true;
+ return obj;
+ }
+
+ b = true;
+ try {
+ size = this.write(this.bwrite_buffer);
+ this.bwrite_buffer = this.bwrite_buffer.substr(size);
+ if (this.bwrite_buffer.length == 0)
+ obj.done = true;
+ } catch (x) {
+ if (this.error != FD.EAGAIN)
+ obj.close = true;
+ }
+
+ return obj;
+}
+
+/*
* Verifies if property name ends with [], which considers it as an array of
* values which are then pushed into that array.
* Sets the property normally otherwise.
if ((e[i].revents & (FD.POLLHUP | FD.POLLERR)) != 0)
close = true;
else if ((e[i].revents & (FD.POLLIN | FD.POLLOUT))
- != 0)
- close = e[i].process(cur);
+ != 0) {
+ var o = e[i].bwrite_flush();
+
+ if (o.close == true)
+ close = true;
+ else if (o.done == true)
+ close = e[i].process(cur);
+ }
if (close) {
e[i].close();
-/* $Id: js_fd.c,v 1.34 2005/07/12 12:14:08 mmondor Exp $ */
+/* $Id: js_fd.c,v 1.35 2005/07/13 22:05:34 mmondor Exp $ */
/*
* Copyright (c) 2005, Matthew Mondor
int count, size;
};
+/* Functions arguments types */
+enum jsarg_types {
+ JSAT_INTEGER = 1,
+ JSAT_DOUBLE,
+ JSAT_STRING,
+ JSAT_OBJECT
+};
+
/* Prototypes */
static JSBool fd_constructor(JSContext *, JSObject *, uintN, jsval *,
};
static int fd_methods_args_array[FDMA_MAX][6] = {
- { 1, JSTYPE_NUMBER },
- { 0 },
- { 1, JSTYPE_NUMBER },
- { 0 },
- { 3, JSTYPE_NUMBER, JSTYPE_NUMBER, JSTYPE_NUMBER },
- { 2, JSTYPE_STRING, JSTYPE_NUMBER },
- { 2, JSTYPE_STRING, JSTYPE_NUMBER },
- { 1, JSTYPE_NUMBER },
- { 0 },
- { 2, JSTYPE_NUMBER, JSTYPE_NUMBER },
- { 2, JSTYPE_NUMBER, JSTYPE_NUMBER },
- { 2, JSTYPE_NUMBER, JSTYPE_NUMBER },
- { 2, JSTYPE_NUMBER, JSTYPE_NUMBER },
- { 1, JSTYPE_NUMBER },
- { 1, JSTYPE_STRING },
- { 2, JSTYPE_NUMBER, JSTYPE_NUMBER },
- { 1, JSTYPE_NUMBER },
- { 1, JSTYPE_NUMBER },
- { 0 },
- { 0 }
+ { 1, JSAT_INTEGER }, /* SET */
+ { 0 }, /* CLOSE */
+ { 1, JSAT_DOUBLE }, /* TRUNCATE */
+ { 0 }, /* GET */
+ { 3, JSAT_INTEGER, JSAT_INTEGER, JSAT_INTEGER },/* SOCKET */
+ { 2, JSAT_STRING, JSAT_INTEGER }, /* CONNECT */
+ { 2, JSAT_STRING, JSAT_INTEGER }, /* BIND */
+ { 1, JSAT_INTEGER }, /* LISTEN */
+ { 0 }, /* ACCEPT */
+ { 1, JSAT_INTEGER }, /* SHUTDOWN */
+ { 2, JSAT_INTEGER, JSAT_INTEGER }, /* SETSOCKOPT */
+ { 1, JSAT_INTEGER }, /* GETSOCKOPT */
+ { 2, JSAT_INTEGER, JSAT_INTEGER }, /* FCNTL */
+ { 1, JSAT_INTEGER }, /* READ */
+ { 1, JSAT_STRING }, /* WRITE */
+ { 0 }, /* FDATASYNC */
+ { 2, JSAT_DOUBLE, JSAT_INTEGER }, /* LSEEK */
+ { 1, JSAT_INTEGER }, /* FCHMOD */
+ { 1, JSAT_INTEGER }, /* FLOCK */
+ { 0 } /* FSTAT */
};
/* Provided methods/functions */
{ "setsockopt", fd_m_setsockopt, 2, 0, 0 },
{ "getsockopt", fd_m_getsockopt, 1, 0, 0 },
{ "fcntl", fd_m_fcntl, 2, 0, 0 },
- { "read", fd_m_read, 2, 0, 0 },
- { "write", fd_m_write, 2, 0, 0 },
+ { "read", fd_m_read, 1, 0, 0 },
+ { "write", fd_m_write, 1, 0, 0 },
{ "fdatasync", fd_m_fdatasync, 0, 0, 0 },
{ "lseek", fd_m_lseek, 2, 0, 0 },
{ "fchmod", fd_m_fchmod, 1, 0, 0 },
fd_m_truncate(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
jsval *rval)
{
- jsfd_t *jsfd;
- off_t size;
+ jsfd_t *jsfd;
+ off_t size;
+ jsdouble dsize;
*rval = OBJECT_TO_JSVAL(NULL);
argc, argv, JSFD_FILE)) == NULL)
return JS_FALSE;
- size = (off_t)JSVAL_TO_INT(*argv);
+ if (!JS_ValueToNumber(cx, *argv, &dsize)) {
+ QUEUE_EXCEPTION("Internal error");
+ return JS_FALSE;
+ }
+ size = (off_t)dsize;
+
if (ftruncate(jsfd->fd, size) == -1) {
jsfd->error = errno;
QUEUE_EXCEPTION(strerror(errno));
jsfd_t *jsfd;
off_t off, newoff;
int whence;
+ jsdouble doff;
*rval = OBJECT_TO_JSVAL(NULL);
argc, argv, JSFD_FILE)) == NULL)
return JS_FALSE;
- off = (off_t)JSVAL_TO_INT(argv[0]);
+ if (!JS_ValueToNumber(cx, argv[0], &doff)) {
+ QUEUE_EXCEPTION("Internal error");
+ return JS_FALSE;
+ }
+ off = (off_t)doff;
whence = JSVAL_TO_INT(argv[1]);
if ((newoff = lseek(jsfd->fd, off, whence)) == -1) {
QUEUE_EXCEPTION(strerror(errno));
return JS_FALSE;
}
- *rval = INT_TO_JSVAL((int)newoff);
+
+ if (!JS_NewDoubleValue(cx, (jsdouble)newoff, rval)) {
+ QUEUE_EXCEPTION("Internal error");
+ return JS_FALSE;
+ }
return JS_TRUE;
}
}
for (p++, i = 0; i < argc; i++) {
- /*
- * Let's use switch/case here since JSTYPE_NUMBER isn't
- * exactly the same as JSVAL_IS_INT().
- */
switch (p[i]) {
- case JSTYPE_NUMBER:
+ case JSAT_INTEGER:
if (!JSVAL_IS_INT(argv[i])) {
(void) snprintf(line, 1023,
"%s() - argument #%d not an integer",
return NULL;
}
break;
- case JSTYPE_STRING:
+ case JSAT_DOUBLE:
+ if (!JSVAL_IS_DOUBLE(argv[i]) &&
+ !JSVAL_IS_INT(argv[i])) {
+ (void) snprintf(line, 1023,
+ "%s() - argument #%d not a double",
+ fun, i + 1);
+ QUEUE_EXCEPTION(line);
+ return NULL;
+ }
+ break;
+ case JSAT_STRING:
if (!JSVAL_IS_STRING(argv[i])) {
(void) snprintf(line, 1023,
"%s() - argument #%d not a string",