WebFund 2015W: Assignment 3

From Soma-notes
Revision as of 17:13, 7 February 2015 by Soma (talk | contribs) (→‎Solutions)

Answer the following questions about tinywebserver.js. There are 10 points in 7 questions. There is also 1 point for a bonus question. This assignment is due by 10 AM on Monday, February 2, 2015.

Please submit your answers as a single text or PDF file called "<username>-comp2406-assign3.txt" or "<username>-comp2406-assign3.pdf", where username is your MyCarletonOne username. The first four lines of this file should be "COMP 2406 Assignment 3", your name, student number, and the date. You may wish to format answers.txt in Markdown to improve its appearance. If you do so, you may convert your text file to PDF using pandoc.

No other formats will be accepted. Submitting in another format will likely result in your assignment not being graded and you receiving no marks for this assignment. In particular do not submit an MS Word or OpenOffice file as your answers document!


Questions

  1. [4] Explain how and why the behavior of tinywebserver changes when you delete the following lines:
    • Line 50: 'html': 'text/html',
    • Line 64: docroot: '.'
    • Line 95: return response.end();
    • Line 126: requestpath += options.index;
  2. [1] Who calls exists_callback() defined on lines 115-121? When is this call made?
  3. [1] What does the path.join() call do on line 139? Why call this method rather than implenting this functionality inline?
  4. [1] When serve_file() returns, what work has it accomplished? Specifically what data (if any) has been returned to the requesting web client? Why?
  5. [1] What is the purpose of the code on lines 150-156 (the else clause of the "err != null" if test)?
  6. [1] How can you access the incoming HTTP request headers in tinywebserver?
  7. [1] Which part of tinywebserver would you change to add a new HTTP response header?
  8. [BONUS 1] What do you find most confusing about the material covered so far in COMP 2406? Note you do NOT get a bonus mark for saying "nothing"!

Solutions

    • If you delete the 'html' property of the MIME_TYPES object, then get_mime() will return null for a .html file. The default MIME time will then be return to the browser, which is text/plain. This will cause all .html files returned by tinywebserver to be displayed as text files in the browser (i.e., it will be like viewing the source but without the highlighting).
    • If you delete the docroot property of the options object then there will be no docroot appended to the path for requested files. Thus when path.join() tries to used docroot it will get undefined; this will cause path.join to throw an unhandled exception which will crash tinywebserver.
    • If you omit the response.end() call at the end of respond() then the browser will never finish loading the page. You may see some of the content return but it won't stop on its own until the browser's page load timeout period has passed.
    • If you don't append options.index to the end of requestpath in return_index() then tinywebserver will no longer return index.html when a directory is requested (e.g. /). It will instead try to treat the directory as a file and will log an error.
  1. The fs.exists() method calls exists_callback() after having determined whether or not the file (given in requestpath) does exist.
  2. path_join() prepends the path in request.url with options.docroot. This could have been done using the + for string concatenation, but then the code would only work on UNIX-like and Mac systems (which use forward slashes to separate directories in file paths). The path_join() call converts /'s to \'s on Windows systems. (See http://shapeshed.com/writing-cross-platform-node/)
  3. When serve_file() has returned it has done nothing except call fs.readFile(), a function that returns immediately. fs.readFile() doesn't return anything so the return value of serve_file() is undefined. Data is only returned to the client when the callback given to fs.readFile() is called (and it calls respond()).
  4. The lines 150-156 allows the webserver to serve the index file when a directory is requested. Thus a request for / gets mapped to returning [docroot]/index.html.
  5. The request.headers object contains the (parsed) incoming request headers and their values.
  6. To add response headers you'd modify respond(). Specifically, you'd add properties to the anonymous object defined on lines 89-91 (with the "Content-Type" value).

Code

// tinywebserver.js
//
// A modification of Rod Waldhoff's tiny node.js webserver
// original written in coffeescript
// simplified and made more native-ish by Anil Somayaji
// March 19, 2014
//
// original headers of coffeescript version:
//
// A simple static-file web server implemented as a stand-alone
// Node.js/CoffeeScript app.
//---------------------------------------------------------------------
// For more information, see:
// <https://github.com/rodw/tiny-node.js-webserver>
//---------------------------------------------------------------------
// This program is distributed under the "MIT License".
// (See <http://www.opensource.org/licenses/mit-license.php>.)
//---------------------------------------------------------------------
// Copyright (c) 2012 Rodney Waldhoff
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without restriction,
// including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software,
// and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
// BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
// ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//---------------------------------------------------------------------

var path = require('path');
var http = require('http');
var fs = require('fs');

var MIME_TYPES = {
    'css': 'text/css',
    'gif': 'image/gif',
    'htm': 'text/html',
    'html': 'text/html',
    'ico': 'image/x-icon',
    'jpeg': 'image/jpeg',
    'jpg': 'image/jpeg',
    'js': 'text/javascript',
    'json': 'application/json',
    'png': 'image/png',
    'txt': 'text/text'
};

var options = {
    host: 'localhost',
    port: 8080,
    index: 'index.html',
    docroot: '.'
};

var get_mime = function(filename) {
    var ext, type;
    for (ext in MIME_TYPES) {
        type = MIME_TYPES[ext];
        if (filename.indexOf(ext, filename.length - ext.length) !== -1) {
            return type;
        }
    }
    return null;
};


var respond = function(request, response, status, content, content_type) {
    if (!status) {
        status = 200;
    }

    if (!content_type) {
        content_type = 'text/plain';
    }
    console.log("" + status + "\t" +
                request.method + "\t" + request.url);
    response.writeHead(status, {
        "Content-Type": content_type
    });
    if (content) {
        response.write(content);
    }
    return response.end();
};

var serve_file = function(request, response, requestpath) {
    return fs.readFile(requestpath, function(error, content) {
        if (error != null) {
            console.error("ERROR: Encountered error while processing " +
                          request.method + " of \"" + request.url + 
                          "\".", error);
            return respond(request, response, 500);
        } else {
            return respond(request, response, 200, 
                           content, get_mime(requestpath));
        }
    });
};


var return_index = function(request, response, requestpath)  {

    var exists_callback = function(file_exists) {
        if (file_exists) {
            return serve_file(request, response, requestpath);
        } else {
            return respond(request, response, 404);
        }
    }
    
    if (requestpath.substr(-1) !== '/') {
        requestpath += "/";
    }
    requestpath += options.index;
    return fs.exists(requestpath, exists_callback);
}

var request_handler = function(request, response) {
    var requestpath;

    if (request.url.match(/((\.|%2E|%2e)(\.|%2E|%2e))|(~|%7E|%7e)/) != null) {
        console.warn("WARNING: " + request.method +
                     " of \"" + request.url + 
                     "\" rejected as insecure.");
        return respond(request, response, 403);
    } else {
        requestpath = path.normalize(path.join(options.docroot, request.url));
        return fs.exists(requestpath, function(file_exists) {
            if (file_exists) {
                return fs.stat(requestpath, function(err, stat) {
                    if (err != null) {
                        console.error("ERROR: Encountered error calling" +
                                      "fs.stat on \"" + requestpath + 
                                      "\" while processing " + 
                                      request.method + " of \"" + 
                                      request.url + "\".", err);
                        return respond(request, response, 500);
                    } else {
                        if ((stat != null) && stat.isDirectory()) {
                            return return_index(request, response, requestpath);
                        } else {
                            return serve_file(request, response, requestpath);
                        }
                    }
                });
            } else {
                return respond(request, response, 404);
            }
        });
    }
};

var server = http.createServer(request_handler);

server.listen(options.port, options.host, function() {
    return console.log("Server listening at http://" +
                       options.host + ":" + options.port + "/");
});