2012-05-11 16:36:04 +00:00
/ * \
title : $ : / c o r e / m o d u l e s / c o m m a n d s / s e r v e r . j s
type : application / javascript
module - type : command
2012-05-19 17:23:14 +00:00
Serve tiddlers over http
2012-05-11 16:36:04 +00:00
\ * /
( function ( ) {
/*jslint node: true, browser: true */
/*global $tw: false */
"use strict" ;
2013-03-17 17:57:46 +00:00
if ( ! $tw . browser ) {
var util = require ( "util" ) ,
fs = require ( "fs" ) ,
url = require ( "url" ) ,
path = require ( "path" ) ,
2014-10-21 18:31:29 +00:00
http = require ( "http" ) ;
2013-03-17 17:57:46 +00:00
}
2012-05-11 16:36:04 +00:00
exports . info = {
name : "server" ,
synchronous : true
} ;
2013-03-17 17:57:46 +00:00
/ *
A simple HTTP server with regexp - based routes
* /
function SimpleServer ( options ) {
this . routes = options . routes || [ ] ;
this . wiki = options . wiki ;
this . variables = options . variables || { } ;
}
SimpleServer . prototype . set = function ( obj ) {
var self = this ;
$tw . utils . each ( obj , function ( value , name ) {
self . variables [ name ] = value ;
} ) ;
2012-05-11 16:36:04 +00:00
} ;
2013-03-17 17:57:46 +00:00
SimpleServer . prototype . get = function ( name ) {
return this . variables [ name ] ;
} ;
SimpleServer . prototype . addRoute = function ( route ) {
this . routes . push ( route ) ;
} ;
2013-11-28 14:03:08 +00:00
SimpleServer . prototype . findMatchingRoute = function ( request , state ) {
2014-06-13 13:44:34 +00:00
var pathprefix = this . get ( "pathprefix" ) || "" ;
2013-11-28 14:03:08 +00:00
for ( var t = 0 ; t < this . routes . length ; t ++ ) {
var potentialRoute = this . routes [ t ] ,
pathRegExp = potentialRoute . path ,
2014-06-13 13:44:34 +00:00
pathname = state . urlInfo . pathname ,
match ;
if ( pathprefix ) {
if ( pathname . substr ( 0 , pathprefix . length ) === pathprefix ) {
pathname = pathname . substr ( pathprefix . length ) ;
match = potentialRoute . path . exec ( pathname ) ;
} else {
match = false ;
}
} else {
match = potentialRoute . path . exec ( pathname ) ;
}
2013-11-28 14:03:08 +00:00
if ( match && request . method === potentialRoute . method ) {
state . params = [ ] ;
for ( var p = 1 ; p < match . length ; p ++ ) {
state . params . push ( match [ p ] ) ;
}
return potentialRoute ;
}
}
return null ;
} ;
SimpleServer . prototype . checkCredentials = function ( request , incomingUsername , incomingPassword ) {
2014-08-30 19:44:26 +00:00
var header = request . headers . authorization || "" ,
2013-11-28 14:03:08 +00:00
token = header . split ( /\s+/ ) . pop ( ) || "" ,
auth = $tw . utils . base64Decode ( token ) ,
parts = auth . split ( /:/ ) ,
username = parts [ 0 ] ,
password = parts [ 1 ] ;
if ( incomingUsername === username && incomingPassword === password ) {
return "ALLOWED" ;
} else {
return "DENIED" ;
}
2014-08-30 19:44:26 +00:00
} ;
2013-11-28 14:03:08 +00:00
2013-12-29 13:07:06 +00:00
SimpleServer . prototype . listen = function ( port , host ) {
2013-03-17 17:57:46 +00:00
var self = this ;
2013-11-28 14:03:08 +00:00
http . createServer ( function ( request , response ) {
2013-03-17 17:57:46 +00:00
// Compose the state object
var state = { } ;
state . wiki = self . wiki ;
state . server = self ;
state . urlInfo = url . parse ( request . url ) ;
// Find the route that matches this path
2013-11-28 14:03:08 +00:00
var route = self . findMatchingRoute ( request , state ) ;
// Check for the username and password if we've got one
var username = self . get ( "username" ) ,
password = self . get ( "password" ) ;
if ( username && password ) {
// Check they match
if ( self . checkCredentials ( request , username , password ) !== "ALLOWED" ) {
2014-03-31 06:58:13 +00:00
var servername = state . wiki . getTiddlerText ( "$:/SiteTitle" ) || "TiddlyWiki5" ;
2013-11-28 14:03:08 +00:00
response . writeHead ( 401 , "Authentication required" , {
2014-03-31 06:58:13 +00:00
"WWW-Authenticate" : 'Basic realm="Please provide your username and password to login to ' + servername + '"'
2013-11-28 14:03:08 +00:00
} ) ;
response . end ( ) ;
return ;
2013-03-17 17:57:46 +00:00
}
}
// Return a 404 if we didn't find a route
if ( ! route ) {
response . writeHead ( 404 ) ;
response . end ( ) ;
return ;
}
2014-02-11 19:42:45 +00:00
// Set the encoding for the incoming request
// TODO: Presumably this would need tweaking if we supported PUTting binary tiddlers
request . setEncoding ( "utf8" ) ;
2013-03-17 17:57:46 +00:00
// Dispatch the appropriate method
2012-05-11 16:36:04 +00:00
switch ( request . method ) {
2013-03-17 17:57:46 +00:00
case "GET" : // Intentional fall-through
case "DELETE" :
route . handler ( request , response , state ) ;
break ;
2012-05-11 16:36:04 +00:00
case "PUT" :
var data = "" ;
request . on ( "data" , function ( chunk ) {
data += chunk . toString ( ) ;
} ) ;
request . on ( "end" , function ( ) {
2013-03-17 17:57:46 +00:00
state . data = data ;
route . handler ( request , response , state ) ;
2012-05-11 16:36:04 +00:00
} ) ;
break ;
2013-03-17 17:57:46 +00:00
}
2013-12-29 13:07:06 +00:00
} ) . listen ( port , host ) ;
2013-03-17 17:57:46 +00:00
} ;
var Command = function ( params , commander , callback ) {
this . params = params ;
this . commander = commander ;
this . callback = callback ;
// Set up server
this . server = new SimpleServer ( {
wiki : this . commander . wiki
} ) ;
// Add route handlers
this . server . addRoute ( {
method : "PUT" ,
path : /^\/recipes\/default\/tiddlers\/(.+)$/ ,
handler : function ( request , response , state ) {
var title = decodeURIComponent ( state . params [ 0 ] ) ,
fields = JSON . parse ( state . data ) ;
// Pull up any subfields in the `fields` object
if ( fields . fields ) {
$tw . utils . each ( fields . fields , function ( field , name ) {
fields [ name ] = field ;
} ) ;
delete fields . fields ;
}
// Remove any revision field
2014-08-30 19:44:26 +00:00
if ( fields . revision ) {
delete fields . revision ;
2013-03-17 17:57:46 +00:00
}
2013-08-06 14:26:48 +00:00
state . wiki . addTiddler ( new $tw . Tiddler ( state . wiki . getCreationFields ( ) , fields , { title : title } ) ) ;
2013-03-17 17:57:46 +00:00
var changeCount = state . wiki . getChangeCount ( title ) . toString ( ) ;
response . writeHead ( 204 , "OK" , {
2014-05-27 08:49:48 +00:00
Etag : "\"default/" + encodeURIComponent ( title ) + "/" + changeCount + ":\"" ,
"Content-Type" : "text/plain"
2013-03-17 17:57:46 +00:00
} ) ;
response . end ( ) ;
}
} ) ;
this . server . addRoute ( {
method : "DELETE" ,
path : /^\/bags\/default\/tiddlers\/(.+)$/ ,
handler : function ( request , response , state ) {
var title = decodeURIComponent ( state . params [ 0 ] ) ;
state . wiki . deleteTiddler ( title ) ;
2014-05-27 08:49:48 +00:00
response . writeHead ( 204 , "OK" , {
"Content-Type" : "text/plain"
} ) ;
2013-03-17 17:57:46 +00:00
response . end ( ) ;
}
} ) ;
this . server . addRoute ( {
method : "GET" ,
path : /^\/$/ ,
handler : function ( request , response , state ) {
response . writeHead ( 200 , { "Content-Type" : state . server . get ( "serveType" ) } ) ;
2013-11-08 08:51:14 +00:00
var text = state . wiki . renderTiddler ( state . server . get ( "renderType" ) , state . server . get ( "rootTiddler" ) ) ;
2013-03-17 17:57:46 +00:00
response . end ( text , "utf8" ) ;
}
} ) ;
this . server . addRoute ( {
method : "GET" ,
path : /^\/status$/ ,
handler : function ( request , response , state ) {
response . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
var text = JSON . stringify ( {
2013-08-21 08:53:45 +00:00
username : state . server . get ( "username" ) ,
2013-03-17 17:57:46 +00:00
space : {
recipe : "default"
} ,
tiddlywiki _version : $tw . version
} ) ;
response . end ( text , "utf8" ) ;
}
} ) ;
2013-12-18 17:27:24 +00:00
this . server . addRoute ( {
method : "GET" ,
path : /^\/favicon.ico$/ ,
handler : function ( request , response , state ) {
response . writeHead ( 200 , { "Content-Type" : "image/x-icon" } ) ;
var buffer = state . wiki . getTiddlerText ( "$:/favicon.ico" , "" ) ;
response . end ( buffer , "base64" ) ;
}
} ) ;
2013-03-17 17:57:46 +00:00
this . server . addRoute ( {
method : "GET" ,
path : /^\/recipes\/default\/tiddlers.json$/ ,
handler : function ( request , response , state ) {
response . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
var tiddlers = [ ] ;
2013-12-11 22:01:20 +00:00
state . wiki . forEachTiddler ( { sortField : "title" } , function ( title , tiddler ) {
2013-03-17 17:57:46 +00:00
var tiddlerFields = { } ;
$tw . utils . each ( tiddler . fields , function ( field , name ) {
if ( name !== "text" ) {
tiddlerFields [ name ] = tiddler . getFieldString ( name ) ;
}
} ) ;
2014-08-30 19:44:26 +00:00
tiddlerFields . revision = state . wiki . getChangeCount ( title ) ;
2013-11-08 20:18:26 +00:00
tiddlerFields . type = tiddlerFields . type || "text/vnd.tiddlywiki" ;
2013-03-17 17:57:46 +00:00
tiddlers . push ( tiddlerFields ) ;
} ) ;
var text = JSON . stringify ( tiddlers ) ;
response . end ( text , "utf8" ) ;
}
} ) ;
this . server . addRoute ( {
method : "GET" ,
path : /^\/recipes\/default\/tiddlers\/(.+)$/ ,
handler : function ( request , response , state ) {
var title = decodeURIComponent ( state . params [ 0 ] ) ,
tiddler = state . wiki . getTiddler ( title ) ,
tiddlerFields = { } ,
knownFields = [
"bag" , "created" , "creator" , "modified" , "modifier" , "permissions" , "recipe" , "revision" , "tags" , "text" , "title" , "type" , "uri"
] ;
if ( tiddler ) {
$tw . utils . each ( tiddler . fields , function ( field , name ) {
2013-07-05 21:37:55 +00:00
var value = tiddler . getFieldString ( name ) ;
2013-03-17 17:57:46 +00:00
if ( knownFields . indexOf ( name ) !== - 1 ) {
tiddlerFields [ name ] = value ;
2013-03-17 15:28:49 +00:00
} else {
2013-03-17 17:57:46 +00:00
tiddlerFields . fields = tiddlerFields . fields || { } ;
tiddlerFields . fields [ name ] = value ;
2013-03-17 15:28:49 +00:00
}
2013-03-17 17:57:46 +00:00
} ) ;
2014-08-30 19:44:26 +00:00
tiddlerFields . revision = state . wiki . getChangeCount ( title ) ;
2013-11-08 20:18:26 +00:00
tiddlerFields . type = tiddlerFields . type || "text/vnd.tiddlywiki" ;
2013-03-17 17:57:46 +00:00
response . writeHead ( 200 , { "Content-Type" : "application/json" } ) ;
response . end ( JSON . stringify ( tiddlerFields ) , "utf8" ) ;
} else {
2013-03-17 15:28:49 +00:00
response . writeHead ( 404 ) ;
response . end ( ) ;
2012-05-11 16:36:04 +00:00
}
2013-03-17 17:57:46 +00:00
}
} ) ;
} ;
Command . prototype . execute = function ( ) {
2014-10-21 18:31:29 +00:00
if ( ! $tw . boot . wikiTiddlersPath ) {
2014-10-22 15:47:20 +00:00
$tw . utils . warning ( "Warning: Wiki folder '" + $tw . boot . wikiPath + "' does not exist or is missing a tiddlywiki.info file" ) ;
2014-10-21 18:31:29 +00:00
}
2013-03-17 17:57:46 +00:00
var port = this . params [ 0 ] || "8080" ,
2013-11-25 21:16:27 +00:00
rootTiddler = this . params [ 1 ] || "$:/core/save/all" ,
2013-03-17 17:57:46 +00:00
renderType = this . params [ 2 ] || "text/plain" ,
2013-08-21 08:53:45 +00:00
serveType = this . params [ 3 ] || "text/html" ,
2013-12-11 07:45:36 +00:00
username = this . params [ 4 ] ,
2013-12-29 13:07:06 +00:00
password = this . params [ 5 ] ,
2014-06-13 13:44:34 +00:00
host = this . params [ 6 ] || "127.0.0.1" ,
pathprefix = this . params [ 7 ] ;
2013-03-17 17:57:46 +00:00
this . server . set ( {
rootTiddler : rootTiddler ,
renderType : renderType ,
2013-08-21 08:53:45 +00:00
serveType : serveType ,
2013-11-28 14:03:08 +00:00
username : username ,
2014-06-13 13:44:34 +00:00
password : password ,
pathprefix : pathprefix
2013-03-17 17:57:46 +00:00
} ) ;
2013-12-29 13:07:06 +00:00
this . server . listen ( port , host ) ;
console . log ( "Serving on " + host + ":" + port ) ;
2013-12-18 21:11:16 +00:00
console . log ( "(press ctrl-C to exit)" ) ;
2015-05-21 07:14:40 +00:00
// Warn if required plugins are missing
2015-07-08 14:48:13 +00:00
if ( ! $tw . wiki . getTiddler ( "$:/plugins/tiddlywiki/tiddlyweb" ) || ! $tw . wiki . getTiddler ( "$:/plugins/tiddlywiki/filesystem" ) ) {
2015-05-21 07:14:40 +00:00
$tw . utils . warning ( "Warning: Plugins required for client-server operation (\"tiddlywiki/filesystem\" and \"tiddlywiki/tiddlyweb\") are missing from tiddlywiki.info file" ) ;
}
2012-05-11 16:36:04 +00:00
return null ;
} ;
exports . Command = Command ;
} ) ( ) ;