var myVersion = "0.64a", myProductName = "PagePark"; //The MIT License (MIT) //Copyright (c) 2014 Dave Winer //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. //structured listing: http://scripting.com/listings/pagepark.html var fs = require ("fs"); var request = require ("request"); var urlpack = require ("url"); var http = require ("http"); var marked = require ("marked"); var dns = require ("dns"); var mime = require ("mime"); //1/8/15 by DW var utils = require ("./lib/utils.js"); //1/18/15 by DW var opmlLib = require ("./lib/opml.js"); //6/23/15 by DW var folderPathFromEnv = process.env.pageparkFolderPath; //1/3/15 by DW var pageparkPrefs = { myPort: 1339, //1/8/15 by DW -- was 80, see note in readme.md indexFilename: "index", flProcessScriptFiles: true, extScriptFiles: "js", //5/5/15 by DW flProcessMarkdownFiles: true, extMarkdownFiles: "md", //5/5/15 by DW flProcessOpmlFiles: true, extOpmlFiles: "opml" //6/23/15 by DW }; var fnamePrefs = "prefs/prefs.json"; var pageparkStats = { ctStarts: 0, whenLastStart: new Date (0), ctHits: 0, ctHitsToday: 0, whenLastHit: new Date (0), hitsByDomain: {} }; var fnameStats = "prefs/stats.json", flStatsDirty = false; var domainsPath = "domains/"; var configFname = "/config.json"; var mdTemplatePath = "prefs/mdTemplate.txt"; var urlDefaultMarkdownTemplate = "http://fargo.io/code/pagepark/defaultmarkdowntemplate.txt"; var opmlTemplatePath = "prefs/opmlTemplate.txt"; var urlDefaultOpmlTemplate = "http://fargo.io/code/pagepark/defaultopmltemplate.txt"; function fsSureFilePath (path, callback) { var splits = path.split ("/"); path = ""; //1/8/15 by DW if (splits.length > 0) { function doLevel (levelnum) { if (levelnum < (splits.length - 1)) { path += splits [levelnum] + "/"; fs.exists (path, function (flExists) { if (flExists) { doLevel (levelnum + 1); } else { fs.mkdir (path, undefined, function () { doLevel (levelnum + 1); }); } }); } else { if (callback != undefined) { callback (); } } } doLevel (0); } else { if (callback != undefined) { callback (); } } } function httpExt2MIME (ext) { //12/24/14 by DW mime.default_type = "text/plain"; return (mime.lookup (ext)); } function httpReadUrl (url, callback) { request (url, function (error, response, body) { if (!error && (response.statusCode == 200)) { callback (body) } }); } function getFullFilePath (relpath) { //1/3/15 by DW var folderpath = folderPathFromEnv; if (folderpath == undefined) { //the environment variable wasn't specified return (relpath); } if (!utils.endsWith (folderpath, "/")) { folderpath += "/"; } if (utils.beginsWith (relpath, "/")) { relpath = utils.stringDelete (relpath, 1, 1); } return (folderpath + relpath); } function getTemplate (myTemplatePath, urlDefaultTemplate, callback) { var f = getFullFilePath (myTemplatePath); fs.readFile (f, function (err, data) { if (err) { httpReadUrl (urlDefaultTemplate, function (s) { fs.writeFile (myTemplatePath, s, function (err) { if (callback != undefined) { callback (s); } }); }); } else { if (callback != undefined) { callback (data.toString ()); } } }); } function getMarkdownTemplate (callback) { getTemplate (mdTemplatePath, urlDefaultMarkdownTemplate, callback); } function getOpmlTemplate (callback) { //6/23/15 by DW getTemplate (opmlTemplatePath, urlDefaultOpmlTemplate, callback); } function checkPathForIllegalChars (path) { function isIllegal (ch) { if (utils.isAlpha (ch) || utils.isNumeric (ch)) { return (false); } switch (ch) { case "/": case "_": case "-": case ".": case " ": case "*": return (false); } return (true); } for (var i = 0; i < path.length; i++) { if (isIllegal (path [i])) { return (false); } } if (utils.stringContains (path, "./")) { return (false); } return (true); } function everySecond () { if (flStatsDirty) { writeStats (fnameStats, pageparkStats); flStatsDirty = false; } } function handleHttpRequest (httpRequest, httpResponse) { function hasAcceptHeader (theHeader) { if (httpRequest.headers.accept === undefined) { return (false); } else { var split = httpRequest.headers.accept.split (", "); for (var i = 0; i < split.length; i++) { if (split [i] == theHeader) { return (true); } } return (false); } } function getDomainFolder (host, callback) { //5/11/15 by DW var folder = getFullFilePath (domainsPath); var domainfolder = folder + host; fs.exists (domainfolder, function (flExists) { if (flExists) { callback (domainfolder, host); } else { if (utils.stringCountFields (host, ".") == 3) { var firstpart = utils.stringNthField (host, ".", 1); var wildcardhost = "*" + utils.stringDelete (host, 1, firstpart.length); domainfolder = folder + wildcardhost; callback (domainfolder, wildcardhost); } else { callback (domainfolder, host); } } }); } function getConfigFile (host, callback) { var config = { urlSiteRedirect: undefined, urlSiteContents: undefined, flProcessScriptFiles: true, flProcessMarkdownFiles: true, flProcessOpmlFiles: true, extScriptFiles: pageparkPrefs.extScriptFiles, extMarkdownFiles: pageparkPrefs.extMarkdownFiles, extOpmlFiles: pageparkPrefs.extOpmlFiles }; var f = getFullFilePath (domainsPath) + host + configFname; fs.readFile (f, function (err, data) { if (err) { callback (config); } else { try { var storedConfig = JSON.parse (data.toString ()); for (var x in storedConfig) { config [x] = storedConfig [x]; } callback (config); } catch (err) { console.log ("getConfigFile: error reading " + configFname + " file for host " + host + ". " + err.message); callback (config); } } }); } function return404 () { httpResponse.writeHead (404, {"Content-Type": "text/plain"}); httpResponse.end ("The file was not found."); } function findIndexFile (folder, callback) { fs.readdir (folder, function (err, list) { for (var i = 0; i < list.length; i++) { var fname = list [i]; if (utils.stringCountFields (fname, ".") == 2) { //something like xxx.yyy if (utils.stringNthField (fname, ".", 1).toLowerCase () == pageparkPrefs.indexFilename) { //something like index.wtf callback (folder + fname); return; } } } return404 (); }); } function serveFile (f, config) { var formatParam; //url ends with ?format=abc -- 6/24/15 by DW if (parsedUrl.query.format !== undefined) { formatParam = parsedUrl.query.format.toLowerCase () } function httpReturn (val, type) { //2/17/15 by DW httpResponse.writeHead (200, {"Content-Type": type}); httpResponse.end (val.toString ()); } function defaultReturn (type, data) { httpResponse.writeHead (200, {"Content-Type": type}); httpResponse.end (data); } fs.readFile (f, function (err, data) { if (err) { return404 (); } else { var ext = utils.stringLastField (f, ".").toLowerCase (), type = httpExt2MIME (ext); switch (ext) { case config.extScriptFiles: if (pageparkPrefs.flProcessScriptFiles && config.flProcessScriptFiles) { try { var val = eval (data.toString ()); if (val !== undefined) { //2/17/15 by DW httpResponse.writeHead (200, {"Content-Type": "text/html"}); httpResponse.end (val.toString ()); } } catch (err) { httpResponse.writeHead (500, {"Content-Type": "text/plain"}); httpResponse.end ("Error running " + parsedUrl.pathname + ": \"" + err.message + "\""); } } else { defaultReturn (type, data); } break; case config.extMarkdownFiles: if (pageparkPrefs.flProcessMarkdownFiles && config.flProcessMarkdownFiles) { getMarkdownTemplate (function (theTemplate) { var mdtext = data.toString (), pagetable = new Object (); pagetable.bodytext = marked (mdtext); pagetable.title = utils.stringLastField (f, "/"); var s = utils.multipleReplaceAll (theTemplate, pagetable, false, "[%", "%]"); httpResponse.writeHead (200, {"Content-Type": "text/html"}); httpResponse.end (s); }); } else { defaultReturn (type, data); } break; case config.extOpmlFiles: //6/23/15 by DW var opmltext = data.toString (); if (formatParam == "json") { opmlLib.readOpmlString (opmltext, function (theOutline) { httpResponse.writeHead (200, {"Content-Type": "application/json", "Access-Control-Allow-Origin": "*"}); httpResponse.end (utils.jsonStringify (theOutline)); }); } else { var flReturnHtml = (!hasAcceptHeader ("text/x-opml")) && (formatParam != "opml"); if (pageparkPrefs.flProcessOpmlFiles && config.flProcessOpmlFiles && flReturnHtml) { //6/24/15 by DW getOpmlTemplate (function (theTemplate) { var pagetable = new Object (); opmlLib.readOpmlString (opmltext, function (theOutline) { pagetable.bodytext = utils.jsonStringify (theOutline); pagetable.title = utils.stringLastField (f, "/"); var s = utils.multipleReplaceAll (theTemplate, pagetable, false, "[%", "%]"); httpResponse.writeHead (200, {"Content-Type": "text/html"}); httpResponse.end (s); }); }); } else { defaultReturn ("text/xml", data); } } break; default: defaultReturn (type, data); break; } } }); } function delegateRequest (urlToDelegateTo) { var theRequest = { url: urlToDelegateTo, followRedirect: false, //5/26/15 by DW headers: { "X-Forwarded-Host": host, "X-Forwarded-For": httpRequest.connection.remoteAddress } }; try { httpRequest.pipe (request (theRequest)).pipe (httpResponse); } catch (tryError) { httpResponse.writeHead (500, {"Content-Type": "text/plain"}); httpResponse.end (tryError.message); } } function findMappedDomain (domain, callback) { //5/23/15 by DW for (var x in pageparkPrefs.domainMap) { if (utils.endsWith (domain, x)) { callback (pageparkPrefs.domainMap [x]); //a mapped domain, delegate to this port return; } } callback (undefined); //it's one of our domains, handle it here } try { var parsedUrl = urlpack.parse (httpRequest.url, true), host, lowerhost, port, referrer; var lowerpath = parsedUrl.pathname.toLowerCase (), now = new Date (); //set host, port host = httpRequest.headers.host; if (utils.stringContains (host, ":")) { port = utils.stringNthField (host, ":", 2); host = utils.stringNthField (host, ":", 1); } else { port = 80; } lowerhost = host.toLowerCase (); //set referrer referrer = httpRequest.headers.referer; if (referrer == undefined) { referrer = ""; } //stats //hits by domain if (pageparkStats.hitsByDomain [lowerhost] == undefined) { pageparkStats.hitsByDomain [lowerhost] = 1; } else { pageparkStats.hitsByDomain [lowerhost]++; } //hits today if (!utils.sameDay (now, pageparkStats.whenLastHit)) { //day rollover pageparkStats.ctHitsToday = 0; } pageparkStats.ctHits++; pageparkStats.ctHitsToday++; pageparkStats.whenLastHit = now; flStatsDirty = true; //log the request dns.reverse (httpRequest.connection.remoteAddress, function (err, domains) { var client = httpRequest.connection.remoteAddress; if (!err) { if (domains.length > 0) { client = domains [0]; } } if (client == undefined) { //1/25/15 by DW client = ""; } console.log (now.toLocaleTimeString () + " " + httpRequest.method + " " + host + ":" + port + " " + lowerpath + " " + referrer + " " + client); }); //handle the request findMappedDomain (host, function (thePort) { if (thePort !== undefined) { var urlRemote; parsedUrl.protocol = "http:"; parsedUrl.host = host + ":" + thePort; parsedUrl.hostname = host; parsedUrl.port = thePort; urlRemote = urlpack.format (parsedUrl); delegateRequest (urlRemote); } else { //no mapping, we handle the request getDomainFolder (host, function (domainfolder, actualhost) { //might be a wildcard folder var f = domainfolder + parsedUrl.pathname; if (checkPathForIllegalChars (f)) { fsSureFilePath (domainsPath, function () { //make sure domains folder exists getConfigFile (actualhost, function (config) { //get config.json, if it exists -- 1/18/15 by DW if (config != undefined) { if (config.urlSiteRedirect != undefined) { var urlRedirect = config.urlSiteRedirect + parsedUrl.pathname; httpResponse.writeHead (302, {"Location": urlRedirect, "Content-Type": "text/plain"}); httpResponse.end ("Temporary redirect to " + urlRedirect + "."); return; } if (config.urlSiteContents != undefined) { //4/26/15 by DW -- v0.55 delegateRequest (config.urlSiteContents + httpRequest.url); return; } if (config.s3Path != undefined) { //5/11/15 PM by DW v0.58 var firstPartOfHost = utils.stringNthField (host, ".", 1); //if it's dave.smallpict.com, this value is "dave" var s3url = "http:/" + config.s3Path + firstPartOfHost + parsedUrl.pathname; //xxx request (s3url, function (error, response, body) { if (error) { httpResponse.writeHead (500, {"Content-Type": "text/plain"}); httpResponse.end ("Error accessing S3 data: " + error.message); } else { httpResponse.writeHead (response.statusCode, {"Content-Type": response.headers ["content-type"]}); httpResponse.end (body); } }); return; } } fs.stat (f, function (err, stats) { if (err) { switch (lowerpath) { case "/version": httpResponse.writeHead (200, {"Content-Type": "text/plain"}); httpResponse.end (myVersion); break; case "/now": httpResponse.writeHead (200, {"Content-Type": "text/plain"}); httpResponse.end (now.toString ()); break; case "/status": var status = { prefs: pageparkPrefs, status: pageparkStats } httpResponse.writeHead (200, {"Content-Type": "text/plain"}); httpResponse.end (utils.jsonStringify (status)); break; default: return404 (); break; } } else { if (stats.isDirectory ()) { if (!utils.endsWith (f, "/")) { f += "/"; } findIndexFile (f, function (findex) { serveFile (findex, config); }); } else { serveFile (f, config); } } }); }); }); } else { httpResponse.writeHead (500, {"Content-Type": "text/plain"}); httpResponse.end ("The file name contains illegal characters."); } }); } }); } catch (tryError) { httpResponse.writeHead (500, {"Content-Type": "text/plain"}); httpResponse.end (tryError.message); } } function writeStats (fname, stats, callback) { var f = getFullFilePath (fname); fsSureFilePath (f, function () { fs.writeFile (f, utils.jsonStringify (stats), function (err) { if (err) { console.log ("writeStats: error == " + err.message); } if (callback != undefined) { callback (); } }); }); } function readStats (fname, stats, callback) { var f = getFullFilePath (fname); fsSureFilePath (f, function () { fs.exists (f, function (flExists) { if (flExists) { fs.readFile (f, function (err, data) { if (err) { console.log ("readStats: error reading file " + f + " == " + err.message) if (callback != undefined) { callback (); } } else { var storedStats = JSON.parse (data.toString ()); for (var x in storedStats) { stats [x] = storedStats [x]; } writeStats (fname, stats, function () { if (callback != undefined) { callback (); } }); } }); } else { writeStats (fname, stats, function () { if (callback != undefined) { callback (); } }); } }); }); } function startup () { readStats (fnamePrefs, pageparkPrefs, function () { readStats (fnameStats, pageparkStats, function () { fsSureFilePath (getFullFilePath (domainsPath) + "x", function () { //make sure domains folder exists var now = new Date (); pageparkStats.ctStarts++; pageparkStats.whenLastStart = now; flStatsDirty = true; http.createServer (handleHttpRequest).listen (pageparkPrefs.myPort); console.log (""); console.log (myProductName + " v" + myVersion + " running on port " + pageparkPrefs.myPort + "."); console.log (""); setInterval (everySecond, 1000); }); }); }); } startup ();