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