Freitag, 1. Februar 2013

Ein einfacher NodeJS Webserver mit Range-Control

Ein einfacher Webserver - ideal für Testzwecke

Oft braucht man eine Webserver für kleine statische Inhalte. Apache und Co sind zu gross. Doch sollte der Server auch die Möglichkeit bieten grosse Videos abspielen zu können. Wir brauchen also die Möglichkeit den Range Header auszuwerten.

Benötigte Module

  • Express
  • Mime

Features eingebaut

Der Server liefert Inhalte aus einem statischen Verzeichnis. Wenn die Datei dort nicht liegt, wird ein weiteres verstecktes statisches Verzeichnis geprüft. Damit kann man Standardinstallationen ausliefern, bis der Kunde im echten web-Ordner die entsprechenden Dateien hinterlegt. 

Dazu kann man auch eine dynamische Abarbeitung von Anfragen anstossen. Dazu dient die Klasse DynServer, die noch durch entsprechenden Code ausgebaut werden muss. Im Beispiel kann man die Vorgehensweise gut erkennen.

Was kann der Webserver?

  • HTTP-Range (keine Unterstützung für Multi-Range)
  • Upload
  • Auslieferung statischer Inhalte
  • Auslieferung von unsichtbaren statischen Inhalten
  • Starten von dynamischen Prozessen

NodeJS und Express und Mime

Diese Variante benötigt das Express und Mime Modul. Damit hat man weniger Code, muss jedoch in der Lage sein, dieses Modul auf dem Server installieren zu können.

 var express = require('express');  
 var app = express();  
 var PATH = require('path');  
 var FS = require('fs');  
 var MIME = require('mime');  
 var system = {  
     port:3000  
     ,tmpdir:"tmp"  
     ,wwwdir:"www"  
     ,wwwtemplatesdir:"wwwtemplates" // hier liegen die dateien zum ausliefern  
     ,uploaddir:"uploads"  
     ,zaehler:0  
     ,JSTeile:{}  
     ,args:[]  
 }  
 checkARGs();  
 starten();  
 function starten() {  
     if (!FS.existsSync(system.uploaddir)) {  
         FS.mkdirSync(system.uploaddir);  
     }  
     if (!FS.existsSync(system.tmpdir)) {  
         FS.mkdirSync(system.tmpdir);  
     }  
     // leere die tmpdir und uploaddir  
         // auch per timeout  
     // json-callback-namen aus parameter cbf nehmen  
     app.set('jsonp callback name', 'cbf');   
     //app.use(express.logger());  
     app.use(function(req,res,next) { // mein logger  
         // req.params  
         // req.body  
         // reg.query  
         // req.files  
         // req.cookies  
         system.zaehler++;  
         res.locals.zaehler = system.zaehler;  
         console.log("["+Date2Text()+"]", req.ip+":"+req.connection.remotePort, req.host, req.protocol, req.method, req.originalUrl);  
         next();      
         });  
     // zuerst static content  
     app.use(express.bodyParser({ keepExtensions: true, uploadDir: system.uploaddir }));  
     app.use(express.cookieParser());  
     app.use(function(req, res,next){  
         var range = req.header('Range');  
         if (!range)  
             return next();  
         var file = PATH.join(system.wwwdir,req.path);  
         if (!FS.existsSync(file))  
             return next();  
      var stat = FS.statSync(file);  
      if (!stat.isFile()) return next();  
      var start = parseInt(range.slice(range.indexOf('bytes=')+6, range.indexOf('-')));  
      var end = parseInt(range.slice(range.indexOf('-')+1, range.length));  
      if (isNaN(end) || end == 0) end = stat.size-1;  
      if (start > end) return;  
      console.log("["+Date2Text()+"]",'Browser requested bytes from ' + start + ' to ' +end + ' of file ' + file);  
      var date = new Date();  
      res.writeHead(206, { // NOTE: a partial http response  
       // 'Date':date.toUTCString(),  
       'Connection':'close',  
       // 'Cache-Control':'private',  
        'Content-Type':MIME.lookup(file),  
        'Content-Length':end - start,  
       'Content-Range':'bytes '+start+'-'+end+'/'+stat.size,  
        'Accept-Ranges':'bytes',  
       // 'Server':'CustomStreamer/0.0.1',  
       'Transfer-Encoding':'chunked'  
       });  
      var stream = FS.createReadStream(file,  
       { flags: 'r', start: start, end: end});  
      stream.on("open", function (fd) {  
             stream.pipe(res);  
             });  
             stream.on("error", function(){  
                 res.end();  
                 //console.log("#"+res.locals.zaehler+" Fehler beim ausliefern der Datei");  
             });  
             stream.on("data", function(data){ // daten kommen hier vorbei  
                 res.bytezaehler += data.length;  
                 //console.log("#"+res.locals.zaehler, data.length, bytezaehler, stats.size);  
             });  
             stream.on("end", function(){  
                 res.end();  
                 //console.log("#"+res.locals.zaehler+" Datei-Auslieferung beendet. Gesendet:",res.bytezaehler);  
             });  
      });  
     app.use(express.static(system.wwwdir));  
     app.use(express.static(system.wwwtemplatesdir)); // vom system bereit gestellte elemente  
     app.use(function(req,res,next) {  
             // auswerten req  
             if (req.query['dynserver'] || req.body['dynserver']) {  
           if (!system.DynServer)  
             system.DynServer = new DynServer(req,res,system);  
           system.DynServer.auswerten();  
             }  
             else {  
                 next();  
             }  
         });  
     app.use(function(req,res){  
         res.send(404, 'Not found');  
         });  
     app.listen(system.port);  
     console.log("server auf ",system.port," - wwwdir:",system.wwwdir);  
 }  
 function JSONPErrorAnworten(req,res,antwort,status){  
     if (!status)  
         status = 200;  
     res.jsonp(status, { cbid: req.query.cbid, error: antwort });      
 }  
 // hilfsfunktionen  
 time = function(){  
     var t = new Date();  
     return t.getTime();   
 }  
 Date2Text = function(millisek, format) {  
     if (!millisek) {  
         var t = new Date();  
         millisek = t.getTime();  
     }  
     var d = new Date(millisek);  
     if (!format)  
         format = "%d.%m.%Y %H:%i";  
     var tage = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];  
     var monate = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dez'];  
     var formate = {'d':((d.getDate()<10)?'0'+d.getDate():d.getDate()),  
             'j':d.getDate(),'D':tage[d.getDay()],'w':d.getDate(),'m':((d.getMonth()+1<10)?'0'+(d.getMonth()+1):d.getMonth()+1),'M':monate[d.getMonth()],  
             'n':d.getMonth()+1,'Y':d.getFullYear(),'y':((d.getYear()>100)?(d.getYear().toString().substr(d.getYear().toString().length-2)):d.getYear()),  
             'H':((d.getHours()<10)?'0'+d.getHours():d.getHours()),'h':((d.getHours()>12)?(d.getHours()-12):d.getHours()),  
             'i':((d.getMinutes()<10)?'0'+d.getMinutes():d.getMinutes()),'s':((d.getSeconds()<10)?'0'+d.getSeconds():d.getSeconds())  
             }  
   for (var akey in formate) {  
     var rg = new RegExp('%'+akey, "g");  
     format = format.replace(rg, formate[akey]);  
   }  
     return format;  
 }  
 // analysieren der consolen Parameter  
 function checkARGs() {  
     if (process.argv.length > 2) {  
         system.args = process.argv;  
         for (var a=0;a<system.args.length;a++){  
             if (system.args[a].substr(0,7) == "wwwdir=") {  
                 system.wwwdir = system.args[a].substr(7);  
             }  
         }  
     }  
 }  
 var DynServer = function (req,res,system) {  
     this.req = req;  
     this.res = res;      
     this.system = system;  
     this.paras = {};  
     if (req.query) {  
         for (key in req.query) {  
             this.paras[key] = req.query[key];  
         }  
     }  
     if (req.body) {  
         for (key in req.body) {  
             this.paras[key] = req.body[key];  
         }  
     }  
     this.upload = function () {  
         console.log("["+Date2Text()+"] upload starten",req.path);  
         // handle upload  
         if (req.files && !isEmptyObject(req.files)) {  
             //console.log(req.files);  
             var self = this;  
             FS.exists(PATH.join(self.system.wwwdir, req.path), function(exists) {  
                 if (!exists) {  
                     return res.send(403, 'upload does not folder exists '+req.path);  
                     return;  
                 }  
                 var dateien = [];  
                 for (var key in req.files) {  
                     var file = req.files[key];  
                     if (!file.filename || file.filename == "") {  
                         FS.unlink(file.path);  
                         res.send(403, "file upload has no name");  
                         return;  
                     }  
                     // upload path  
                     var datei = PATH.join(req.path, file.filename);   
                     var pfad = PATH.join(self.system.wwwdir, datei);  
                     try {  
                         FS.renameSync(file.path, pfad);  
                         console.log("["+Date2Text()+"] upload:",file.length, pfad);  
                     } catch(e) {  
                         console.log(e);  
                         console.log("["+Date2Text()+"] Fehler beim upload verschieben - versuche tempupload zu entfernen");  
                         FS.unlink(file.path);  
                         continue;  
                     }  
                     dateien.push(datei);  
                 }  
                 if (self.paras.data && self.paras.data.redirect) {  
                     return res.redirect(self.paras.data.redirect);   
                 } else  
                 if (dateien.length > 0) {  
                     return res.send(200, 'upload ok '+JSON.stringify(dateien));                                              
                 } else {  
                     return res.send(403, 'problems with upload');  
                 }  
             });  
         } else {  
             return res.send(403, 'no files uploaded');  
         }      
     }  
     this.auswerten = function(){  
         if (!this.paras['func'])  
             return JSONPErrorAnworten(this.req, this.res, "no func recognised");      
         switch(this.paras['func']) {  
             case "upload":  
                 return this.upload();  
                 break;  
             default:  
                 return JSONPErrorAnworten(this.req, this.res, "func not supported");  
         }  
         // keine antwort gesendet, also fehler bei den clientdaten  
         return JSONPErrorAnworten(this.req, this.res, "func error");  
     }  
     return this;  
 }  
 function isEmptyObject(obj) {  
   // This works for arrays too.  
   for(var name in obj) {  
     return false  
   }  
   return true  
 }  

Viel Spass damit
Saso Nikolov