*** empty log message ***
authorMatthew Mondor <mmondor@pulsar-zone.net>
Wed, 13 Jul 2005 22:05:34 +0000 (22:05 +0000)
committerMatthew Mondor <mmondor@pulsar-zone.net>
Wed, 13 Jul 2005 22:05:34 +0000 (22:05 +0000)
tests/js-test/js/httpd/httpd.js
tests/js-test/js/httpd/options.js
tests/js-test/src/classes/js_fd.c

index 08407bb..b8dc977 100644 (file)
@@ -1,4 +1,4 @@
-/* $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.
@@ -50,7 +56,7 @@
  * 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 $';
 
 
 
@@ -84,7 +90,7 @@ function file_read(file)
                } else
                        fd = file;
                for (;;) {
-                       data = fd.read(8192);
+                       data = fd.read(16384);
                        if (data.length == 0)
                                break;
                        contents += data;
@@ -282,6 +288,7 @@ function HTTPReply(code, desc, type)
        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
@@ -332,7 +339,7 @@ HTTPReply.prototype = {
                        headers += 'Content-Type: ' + this.type + "\r\n";
                headers += "\r\n";
 
-               fd.write(headers + contents);
+               fd.bwrite(headers + contents);
        }
 }
 
@@ -469,6 +476,10 @@ function process_transfer(time)
         * 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) {
@@ -489,11 +500,19 @@ function process_transfer(time)
                        }
                } 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;
@@ -508,7 +527,7 @@ function process_transfer(time)
                                close = true;
                        else {
                                try {
-                                       this.write(this.transfer_data);
+                                       this.bwrite(this.transfer_data);
                                        this.transfer_state =
                                            STATE_TRANSFER_READ;
                                        this.updateTimeout(time);
@@ -584,7 +603,7 @@ FD.prototype.init = function(time, idx)
        /* 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
@@ -602,7 +621,7 @@ FD.prototype.parseRequest = function(time)
 {
        var close = false;
        var valid = false;
-       var evil = false;
+       var evil_browser = evil_os = false;
        var vhost = '';
        var lines;
        var words;
@@ -621,6 +640,7 @@ FD.prototype.parseRequest = function(time)
        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");
@@ -646,15 +666,12 @@ FD.prototype.parseRequest = function(time)
        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)
@@ -664,12 +681,18 @@ FD.prototype.parseRequest = function(time)
                                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);
                }
        }
 
@@ -686,13 +709,20 @@ FD.prototype.parseRequest = function(time)
        /*
         * 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
@@ -789,7 +819,7 @@ FD.prototype.parsePost = function(time)
 
 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
@@ -808,7 +838,7 @@ FD.prototype.httpRespond = function(time)
         * 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',
@@ -992,7 +1022,57 @@ FD.prototype.httpRespond = function(time)
        }
 
        /*
-        * 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;
@@ -1000,12 +1080,9 @@ FD.prototype.httpRespond = function(time)
        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;
 }
 
@@ -1146,6 +1223,59 @@ FD.prototype.httpDebug = function()
 }
 
 /*
+ * 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.
@@ -1437,8 +1567,14 @@ function main() {
                        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();
index a9d7645..071f30d 100644 (file)
@@ -1,9 +1,10 @@
-/* $Id: options.js,v 1.11 2005/07/10 20:12:21 mmondor Exp $ */
+/* $Id: options.js,v 1.12 2005/07/13 22:05:25 mmondor Exp $ */
 
 var options = {
        max_connections:        32,
        max_connections_addr:   4,
        io_timeout:             60,
+//     readbuf_size:           65536,
        readbuf_size:           16384,
        default_vhost:          "chat.pulsar-zone.net",
        default_mimetype:       "application/octet-stream",
@@ -11,7 +12,8 @@ var options = {
        default_session_exp:    1800,
        sess_gc_interval:       600,
        sess_id_size:           64,
-       ban_msie:               false
+       ban_msie:               false,
+       ban_windows:            false
 };
 
 /* Address:port combinations to listen to */
@@ -46,8 +48,8 @@ var vhosts = [
 
        /* Static only test virtual host */
        {
-               name:           "test.hal.xisop",
-               root:           "/home/mmondor/jswww/test/htdocs"
+               name:           "test.localhost",
+               root:           "/home/mmondor/jswww/test"
        }
 ];
 
index 73502f1..1fd660d 100644 (file)
@@ -1,4 +1,4 @@
-/* $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
@@ -129,6 +129,14 @@ struct poll_fds {
        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 *,
@@ -212,26 +220,26 @@ enum fd_methods_args_enum {
 };
 
 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 */
@@ -251,8 +259,8 @@ static JSFunctionSpec fd_methods[] = {
        { "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 },
@@ -1223,8 +1231,9 @@ static JSBool
 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);
 
@@ -1232,7 +1241,12 @@ fd_m_truncate(JSContext *cx, JSObject *obj, uintN argc, jsval *argv,
            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));
@@ -1860,6 +1874,7 @@ fd_m_lseek(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
        jsfd_t          *jsfd;
        off_t           off, newoff;
        int             whence;
+       jsdouble        doff;
 
        *rval = OBJECT_TO_JSVAL(NULL);
 
@@ -1867,7 +1882,11 @@ fd_m_lseek(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
            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) {
@@ -1875,7 +1894,11 @@ fd_m_lseek(JSContext *cx, JSObject *obj, uintN argc, jsval *argv, jsval *rval)
                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;
 }
@@ -2298,12 +2321,8 @@ fd_methods_args_check(JSContext *cx, JSObject *obj, const char *fun, int id,
        }
 
        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",
@@ -2312,7 +2331,17 @@ fd_methods_args_check(JSContext *cx, JSObject *obj, const char *fun, int id,
                                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",