├── dislocate ├── templates │ └── index.tpl ├── utils.js ├── interfaces │ ├── generic.js │ └── http.js ├── SConscript ├── schema.js ├── interfaces.js ├── version.js ├── health.js ├── templates.js ├── log.js ├── entry.js ├── config.js ├── pubsub.js ├── client.js ├── services.js └── auth.js ├── NOTICE ├── extern ├── README-EXTERN ├── json-schema.js ├── stacktrace.js ├── node-router.js └── json-template.js ├── ndislocate.js ├── test └── add_service.js ├── TODO ├── README ├── SConstruct └── LICENSE /dislocate/templates/index.tpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | dislocate index 5 | 6 | 7 |

Welcome to Dislocate.

8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Dislocate 2 | Copyright (c) 2010, Cloudkick, Inc. 3 | 4 | This product includes software developed by 5 | Cloudkick (http://www.cloudkick.com/). 6 | 7 | For extern/node-router.js: 8 | Copyright (c) 2010 Tim Caswell 9 | 10 | for extern/json-template.js: 11 | Copyright (C) 2009 Andy Chu -------------------------------------------------------------------------------- /extern/README-EXTERN: -------------------------------------------------------------------------------- 1 | This directory contains Third Party code and libraries used by ndislocate. 2 | 3 | node-router 304c0a56e819 (April 30, 2010) 4 | http://github.com/creationix/node-router 5 | License: MIT 6 | Library used to route URLs to handlers in HTTP server. 7 | 8 | json-template 761f60ca03e (December 5, 2009) 9 | http://github.com/andychu/json-template/ 10 | License: Apache License 2.0 11 | Library used to render templates with variables. 12 | 13 | json-schema f5a14181aa1 (April 21, 2010) 14 | http://github.com/kriszyp/commonjs-utils/raw/master/lib/ 15 | License: MIT 16 | Library used to validate input JSON objects. 17 | 18 | stacktrace fe9d84729c5 (May 29, 2010) 19 | http://github.com/emwendelin/javascript-stacktrace 20 | License: BSD 21 | Utility function to make a stack trace. Slightly modified to use CommonJS style exports. -------------------------------------------------------------------------------- /ndislocate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* 3 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 4 | * contributor license agreements. See the NOTICE file distributed with 5 | * this work for additional information regarding copyright ownership. 6 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 7 | * (the "License"); you may not use this file except in compliance with 8 | * the License. You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | /** 20 | * Wrapper around dislocate entry point. 21 | */ 22 | 23 | var dislocate = require('./dislocate/entry'); 24 | 25 | dislocate.run(); 26 | -------------------------------------------------------------------------------- /dislocate/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Short utility functions that otherwise wouldn't have a home. 20 | */ 21 | 22 | /* 23 | * very simple object merging 24 | * TODO: import mixin from jquery / dojo for real objects 25 | */ 26 | exports.merge = function (a, b) 27 | { 28 | for (var attrname in a) { 29 | if (a.hasOwnProperty(attrname)) { 30 | b[attrname] = a[attrname]; 31 | } 32 | } 33 | return b; 34 | }; 35 | -------------------------------------------------------------------------------- /dislocate/interfaces/generic.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /* 19 | * Generic 'interface' utility code. Creates a standard object model 20 | * between the different interfaces. 21 | */ 22 | 23 | var log = require('../log'); 24 | var services = require('../services'); 25 | 26 | exports.list = function() 27 | { 28 | var rv = {'services': services.all()}; 29 | return rv; 30 | }; 31 | 32 | exports.register = function(obj) 33 | { 34 | services.register(obj.name, obj); 35 | }; 36 | -------------------------------------------------------------------------------- /dislocate/SConscript: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | src = [ 'auth.js', 19 | 'config.js', 20 | 'client.js', 21 | 'entry.js', 22 | 'health.js', 23 | 'interfaces.js', 24 | 'log.js', 25 | 'pubsub.js', 26 | 'schema.js', 27 | 'services.js', 28 | 'templates.js', 29 | 'utils.js', 30 | 'version.js', 31 | 'interfaces/generic.js', 32 | 'interfaces/http.js'] 33 | 34 | source = [File(x) for x in src] 35 | 36 | Return("source") 37 | -------------------------------------------------------------------------------- /dislocate/schema.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Validates a JSON object against a prepared set of types. 20 | */ 21 | 22 | var jsch = require('../extern/json-schema'); 23 | var log = require('./log'); 24 | var sys = require('sys'); 25 | 26 | var schemas = { 27 | 'service': { 28 | type:"object", 29 | properties:{ 30 | name: {tyoe: 'string'}, 31 | type: {type:"string"}, 32 | address: {type:"string"}, 33 | port: {type:'number'} 34 | } 35 | } 36 | }; 37 | 38 | exports.validate = function(name, inst) 39 | { 40 | /* TODO: provide better message on errors ?*/ 41 | return jsch.validate(inst, schemas[name]); 42 | }; 43 | 44 | -------------------------------------------------------------------------------- /test/add_service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | var client = require('../dislocate/client'); 20 | var log = require('../dislocate/log'); 21 | var ps = require('../dislocate/pubsub') 22 | var config = require('../dislocate/config'); 23 | 24 | (function() { 25 | 26 | config.init(); 27 | 28 | ps.sub(ps.CONFIG_DONE, function() { 29 | 30 | client.request({'host': '127.0.0.1', 'port': 1099}, 31 | 'PUT', '/d/service', 32 | function(res) { 33 | log.info('all done!', res.res.statusCode, res.body); 34 | }, 35 | JSON.stringify({'name':'test.sshd', 36 | 'type': 'static', 37 | 'address': '127.0.0.1', 38 | 'port': 22})); 39 | }); 40 | })(); -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | just do it: 2 | 3 | - http server 4 | - service registration (semi done, validation side is done) 5 | - Need to spec out API design soonish 6 | - JSON-Schema auto-generated docs helpful? 7 | 8 | - services heartbeating/checkup (see if tcp port is alive, modules for this) 9 | - static / ttl 10 | - tcp alive 11 | - custom every X seconds 12 | - thrift alive 13 | - http & body match 14 | - databases (mysql, pgsql) 15 | - nosql (cassandra, redis, mongo) 16 | - http & https 17 | - ajp (?) 18 | - amqp 19 | 20 | - on disk services json file (atomic replacement) 21 | - on disk state storage for internal use 22 | 23 | - client clients: 24 | - node.js 25 | - python 26 | - Djnago 1.2 example for database routing: 27 | 28 | - Cassandra Clusters 29 | - php 30 | - Java (?) (ck/esper needs this) 31 | 32 | - dislocate console 33 | - Base on REPLServer 34 | - status, peers, local services, all services, globs, subscribe, disable, etc 35 | 36 | other: 37 | 38 | - documentation! 39 | 40 | - test cases / testing framework 41 | - maybe just assert's for now? 42 | 43 | design: 44 | 45 | - design peer to peer network protocol 46 | - http (?) 47 | - sha512 HMAC on data 48 | - think about SSL crap / hate CAs / etc 49 | 50 | - think about gossip protocol more 51 | - do we need it initially? 52 | - network partitions 53 | - partial tree updates 54 | - how do you update efficiently without transmitting the entire tree. (tree diffs between timestamps?) (there have to be some academic papers about this part) 55 | 56 | - Convince Dan -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Dislocate - A distributed service locator 2 | ------ 3 | 4 | Dislocate aims to eliminate the need to configure 'where' a service is 5 | available on a network, and enable auto discovery of new services, and high 6 | performance location of these services by clients. 7 | 8 | Dislocate is a combination of what monitoring, DNS and load balancers are meant 9 | to do -- but maintaining those services on a large scale distributed system 10 | is a significant toll on sysadmins. Dislocate aims to reduce the amount of 11 | work that sysadmins need to do to maintain properly scaling architectures on 12 | distributed, disperse and dynamic networks. 13 | 14 | Clients on the localhost ask Dislocate for a service, via either HTTP or a file 15 | on disk. Dislocate provides information about both the service, and the machine 16 | hosting it, so that the clients can make load balancing decisions, like 17 | using the least loaded slave SQL server. 18 | 19 | Dislocate itself bootstraps based upon a few seed nodes, and then forms a 20 | peer to peer network over TCP with other Dislocate nodes. Communication 21 | between each node is authenticated by a shared secret. 22 | 23 | Services register themselves with their local Dislcoate instance on startup, and 24 | then Dislocate will advertise them to other peers. The service can register 25 | how Dislocate should see if its still alive, including TCP health checks 26 | or other custom functions. 27 | 28 | Status 29 | ------ 30 | Pre-Alpha. Lots of work to do, I've been trying to hack down the TODO file 31 | whenever I get a chance. 32 | 33 | I welcome help: 34 | IRC: pquerna on Freenode (#bloglines or other channels). 35 | E-Mail: 36 | 37 | -------------------------------------------------------------------------------- /dislocate/interfaces.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Manages the external interfaces used to communicate with dislocate. 20 | * 21 | * This includes the http server and local json file storage. 22 | * 23 | * No implementations are in this file, just management of them. 24 | */ 25 | 26 | var ps = require('./pubsub'); 27 | var interfaces = { 28 | 'http': require('./interfaces/http') 29 | }; 30 | var running = []; 31 | 32 | exports.start = function() 33 | { 34 | for (var key in interfaces) { 35 | if (interfaces.hasOwnProperty(key)) { 36 | mod = interfaces[key]; 37 | mod.start(); 38 | running.push(mod); 39 | } 40 | } 41 | 42 | ps.sub(ps.STATE_STOP, function() { 43 | exports.stop(); 44 | }); 45 | }; 46 | 47 | exports.stop = function() 48 | { 49 | running.forEach(function(entry) { 50 | entry.stop(); 51 | }); 52 | running = []; 53 | }; 54 | -------------------------------------------------------------------------------- /dislocate/version.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Provides the current version of dislocate. 20 | * 21 | * These are written in an easily parsable format for use by 22 | * other (non-js) scripts to easily grep out the version number. 23 | */ 24 | 25 | var DISLOCATE_VERSION_MAJOR = 0; 26 | var DISLOCATE_VERSION_MINOR = 1; 27 | var DISLOCATE_VERSION_PATCH = 0; 28 | 29 | /* change this on release tags */ 30 | var DISLCOATE_IS_DEV = true; 31 | 32 | /* Used in the network protocol for compatibility testing. */ 33 | var DISLOCATE_MAGIC = 2010052200; 34 | 35 | exports.MAJOR = DISLOCATE_VERSION_MAJOR; 36 | exports.MINOR = DISLOCATE_VERSION_MINOR; 37 | exports.PATCH = DISLOCATE_VERSION_PATCH; 38 | exports.IS_DEV = DISLCOATE_IS_DEV; 39 | exports.MAGIC = DISLOCATE_MAGIC; 40 | 41 | exports.toString = function() 42 | { 43 | dstr = '-dev'; 44 | 45 | if (DISLCOATE_IS_DEV === false) { 46 | dstr = '-release'; 47 | } 48 | 49 | return 'dislcoate-'+ DISLOCATE_VERSION_MAJOR +'.'+ DISLOCATE_VERSION_MINOR +'.'+ DISLOCATE_VERSION_PATCH +''+dstr; 50 | }; 51 | -------------------------------------------------------------------------------- /dislocate/health.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * This module keeps track of the health and status of the local machine. 20 | * 21 | * This state information is used by other nodes to determine how much load 22 | * to place on it, but this module itself only cares about local state. 23 | */ 24 | 25 | var ps = require('./pubsub'); 26 | var log = require('./log'); 27 | var update_frequency = 5; 28 | var status = null; 29 | var timerId = null; 30 | 31 | function getloadaverage() 32 | { 33 | /* TODO: getloadaverage */ 34 | return 0.1; 35 | } 36 | 37 | function get_status() 38 | { 39 | var r = {'load': getloadaverage()}; 40 | 41 | status = r; 42 | 43 | setTimeout(get_status, update_frequency * 1000); 44 | } 45 | 46 | 47 | exports.status = function() 48 | { 49 | return status; 50 | }; 51 | 52 | (function(){ 53 | ps.sub(ps.CONFIG_DONE, function(){ 54 | get_status(); 55 | timerId = setTimeout(get_status, update_frequency * 1000); 56 | ps.sub(ps.STATE_STOP, function() { 57 | clearTimeout(timerId); 58 | }); 59 | }); 60 | })(); 61 | -------------------------------------------------------------------------------- /dislocate/templates.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Provides JSON based templates for use by other modules. 20 | */ 21 | 22 | var Template = require("../extern/json-template").Template; 23 | var sys = require('sys'); 24 | var fs = require('fs'); 25 | var path = require('path'); 26 | var utils = require("./utils"); 27 | var template_dir = null; 28 | 29 | function load(name, cb) 30 | { 31 | p = path.join(template_dir, name + ".tpl"); 32 | fs.readFile(p, function(err, data) { 33 | if (err) { 34 | msg = "Loading "+ p +" got: "+ err + "\n"; 35 | cb(msg); 36 | return; 37 | } 38 | /* TODO: Cache parsed template files? */ 39 | t = Template(data.toString()); 40 | cb(null, t); 41 | }); 42 | } 43 | 44 | exports.render = function(name, ctx, cb) 45 | { 46 | default_vars = {}; 47 | ctx = utils.merge(ctx, default_vars); 48 | load(name, function(err, tpl) { 49 | if (err) { 50 | cb(err); 51 | return; 52 | } 53 | tpl.render(ctx, function(result) { 54 | cb(null, result); 55 | }); 56 | }); 57 | }; 58 | 59 | (function() { 60 | template_dir = path.join(__dirname, "templates"); 61 | })(); 62 | -------------------------------------------------------------------------------- /dislocate/log.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | /** 20 | * Logging subsytem, providing log filtering and a simple set of log message 21 | * types. 22 | */ 23 | 24 | var sys = require("sys"); 25 | var stacktrace = require('../extern/stacktrace'); 26 | /** 27 | * TODO: format strings 28 | */ 29 | 30 | var loglevels = { 31 | 'nothing': 0, 32 | 'crit': 1, 33 | 'err': 2, 34 | 'warn': 3, 35 | 'info': 4, 36 | 'debug': 5 37 | }; 38 | 39 | var loglevel_strs = []; 40 | var loglevel = loglevels.debug; 41 | 42 | function logit (level, inargs) { 43 | if (level <= loglevel) { 44 | sys.log(loglevel_strs[level]+ ": "+ Array.apply({}, inargs).join(" ")); 45 | } 46 | } 47 | 48 | exports.set_loglevel = function(level) { 49 | loglevel = level; 50 | }; 51 | 52 | exports.trace = function() { 53 | return stacktrace.trace(); 54 | }; 55 | 56 | (function() { 57 | for (var attrname in loglevels) { 58 | if (loglevels.hasOwnProperty(attrname)) { 59 | loglevel_strs[loglevels[attrname]] = attrname; 60 | exports[attrname] = (function (level) { 61 | return function () { 62 | return logit(level, arguments); 63 | }; 64 | })(loglevels[attrname]); 65 | } 66 | } 67 | })(); 68 | -------------------------------------------------------------------------------- /SConstruct: -------------------------------------------------------------------------------- 1 | # 2 | # Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | # contributor license agreements. See the NOTICE file distributed with 4 | # this work for additional information regarding copyright ownership. 5 | # Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | # (the "License"); you may not use this file except in compliance with 7 | # the License. You may obtain a copy of the License at 8 | # 9 | # http://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, software 12 | # distributed under the License is distributed on an "AS IS" BASIS, 13 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | # See the License for the specific language governing permissions and 15 | # limitations under the License. 16 | # 17 | 18 | EnsureSConsVersion(1, 1, 0) 19 | 20 | import os 21 | import re 22 | from os.path import join as pjoin 23 | 24 | opts = Variables('build.py') 25 | 26 | env = Environment(options=opts, 27 | ENV = os.environ.copy(), 28 | tools=['default']) 29 | 30 | #TODO: convert this to a configure builder, so it gets cached 31 | def read_version(prefix, path): 32 | version_re = re.compile("(.*)%s_VERSION_(?PMAJOR|MINOR|PATCH)(\s+)=(\s+)(?P\d)(.*)" % prefix) 33 | versions = {} 34 | fp = open(path, 'rb') 35 | for line in fp.readlines(): 36 | m = version_re.match(line) 37 | if m: 38 | versions[m.group('id')] = int(m.group('num')) 39 | fp.close() 40 | return (versions['MAJOR'], versions['MINOR'], versions['PATCH']) 41 | 42 | env['version_major'], env['version_minor'], env['version_patch'] = read_version('DISLOCATE', 'dislocate/version.js') 43 | env['version_string'] = "%d.%d.%d" % (env['version_major'], env['version_minor'], env['version_patch']) 44 | 45 | conf = Configure(env, custom_tests = {}) 46 | 47 | conf.env.AppendUnique(RPATH = conf.env.get('LIBPATH')) 48 | env = conf.Finish() 49 | 50 | Export("env") 51 | 52 | source = SConscript("dislocate/SConscript") 53 | 54 | jslint = [env.Command(str(x)+".jslint", x, ["jslint $SOURCE || exit 0"]) for x in source] 55 | #env.AlwaysBuild(jslint) 56 | 57 | Alias('jslint', jslint) 58 | 59 | targets = [] 60 | 61 | env.Default(targets) 62 | -------------------------------------------------------------------------------- /dislocate/entry.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * This module provides a top level entry point and provides flow control to 20 | * startup, run, and shutdown the dislocate service. 21 | */ 22 | 23 | var sys = require("sys"); 24 | var log = require("./log"); 25 | var config = require("./config"); 26 | var ps = require('./pubsub'); 27 | var services = require('./services'); 28 | var auth = require('./auth'); 29 | var interfaces = require('./interfaces'); 30 | var version = require('./version'); 31 | 32 | exports.run = function() { 33 | var called_stop = false; 34 | log.info(version.toString()); 35 | var rv = config.init(); 36 | 37 | if (!rv) { 38 | return; 39 | } 40 | 41 | ps.sub(ps.STATE_STOP, function() { 42 | called_stop = true; 43 | }, true); 44 | 45 | ps.sub(ps.STATE_EXIT, function() { 46 | if (called_stop === false) { 47 | ps.pub(ps.STATE_STOP); 48 | called_stop = true; 49 | } 50 | }, true); 51 | 52 | ps.sub(ps.CONFIG_DONE, function() { 53 | process.addListener('SIGINT', function () { 54 | log.debug("Caught SIGINT, exiting...."); 55 | ps.pub(ps.STATE_EXIT, {'why': 'signal', 'value': "SIGINT"}); 56 | process.exit(); 57 | }); 58 | services.start(); 59 | interfaces.start(); 60 | 61 | services.register('test.sshd', {'type': 'static', 'address': '127.0.0.1', 'port': 22}); 62 | services.register('test.webservers.mine', {'type': 'http', 'address': '127.0.0.1', 'port': 80}); 63 | services.register('test.webservers.mine', {'type': 'http', 'address': '127.0.0.1', 'port': 8080}); 64 | }, true); 65 | }; 66 | -------------------------------------------------------------------------------- /dislocate/config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Configuration subsytem, providing a set of defaults, and merging of a configuration 20 | * JSON file. 21 | */ 22 | 23 | var fs = require('fs'); 24 | var sys = require('sys'); 25 | var log = require('./log'); 26 | var ps = require('./pubsub'); 27 | var utils = require('./utils'); 28 | 29 | var config = { 30 | 'user': 'nobody', 31 | 'port': 49700, 32 | 'address': '0.0.0.0', 33 | 'secret': null, 34 | /* TODO: pick a better default / prefix directory */ 35 | 'state_directory': '/var/tmp/dislocate', 36 | 'authentication_format': { 37 | 'algorithm': 'sha256', 38 | 'encoding': 'base64' 39 | } 40 | }; 41 | 42 | function drop_uid(uid) 43 | { 44 | /* TOOD: username string -> UID */ 45 | cuid = process.getuid(); 46 | /* TODO: setgid? */ 47 | if (cuid === 0) { 48 | process.setuid(uid); 49 | } 50 | else { 51 | log.warn('Currently running as', ''+cuid, 'not changing to', ''+uid); 52 | } 53 | } 54 | 55 | function parse_config(path) { 56 | fs.readFile('test.json', function(err, data) { 57 | parsed = JSON.parse(data.toString()); 58 | log.err(parsed); 59 | config = utils.merge(parsed, config); 60 | ps.pub(ps.CONFIG_DONE); 61 | }); 62 | } 63 | 64 | exports.init = function() { 65 | if (process.argv.length != 3) { 66 | log.err('3rd argument must be configuration file.'); 67 | return false; 68 | } 69 | cfile = process.argv[2]; 70 | log.debug('Reading configuration from', cfile); 71 | parse_config(cfile); 72 | return true; 73 | }; 74 | 75 | exports.get = function() { 76 | return config; 77 | }; 78 | -------------------------------------------------------------------------------- /dislocate/pubsub.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | var log = require('./log'); 19 | var pending = {}; 20 | /* 21 | 22 | TODO: consider rewriting pubsub to use EventEmitter internally, I'm not sure 23 | it can do 'single' event subs in an easy way though, nor i'm not sure 24 | about the performance difference. 25 | 26 | var sys = require('sys'); 27 | var events = require('events').EventEmitter; 28 | var emitter = null; 29 | sys.inherits(PubSubEmitter, events.EventEmitter); 30 | 31 | (function(){ 32 | emitter = new PubSubEmitter(); 33 | })(); 34 | 35 | */ 36 | 37 | exports.CONFIG_DONE = "dislocate.config.done"; 38 | exports.STATE_START ='dislocate.state.start'; 39 | exports.STATE_STOP ='dislocate.state.stop'; 40 | exports.STATE_EXIT ='dislocate.state.exit'; 41 | exports.LOCAL_SERVICE_REGISTER = "dislocate.local.service.register"; 42 | 43 | 44 | exports.pub = function(path, data) 45 | { 46 | if (path === undefined) { 47 | throw "pubsub: path must be defined, did you forget to add a new event type?"; 48 | } 49 | 50 | //log.debug("pub:", path, data); 51 | if (pending[path] !== undefined) { 52 | var arr = pending[path]; 53 | pending[path] = arr.filter(function(i) { 54 | i.cb(data); 55 | if (i.once === true) { 56 | return false; 57 | } 58 | return true; 59 | }); 60 | } 61 | }; 62 | 63 | exports.sub = function(path, cb, once) 64 | { 65 | if (path === undefined) { 66 | throw "pubsub: path must be defined, did you forget to add a new event type?"; 67 | } 68 | 69 | if (once === undefined) { 70 | once = false; 71 | } 72 | 73 | if (pending[path] === undefined) { 74 | pending[path] = []; 75 | } 76 | 77 | //log.debug("sub:", path, cb); 78 | pending[path].push({'cb': cb, 'once': once}); 79 | }; 80 | -------------------------------------------------------------------------------- /dislocate/client.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * 'low level' Peer Client interface. Only provides thin wrapper around 20 | * http + the tokens used for authentication. 21 | */ 22 | var auth = require('./auth'); 23 | var http = require('http'); 24 | var log = require('./log'); 25 | 26 | exports.request = function(peer, method, path, cb, body) 27 | { 28 | var h = http.createClient(peer.port, peer.host); 29 | var preq = { 30 | 'method': method, 31 | 'url': path, 32 | 'headers': {'Host': peer.host, 33 | 'Transfer-Encoding': 'chunked'} 34 | }; 35 | 36 | if (preq.headers.Date === undefined) { 37 | preq.headers.Date = (new Date()).toUTCString(); 38 | } 39 | 40 | /* TODO: auth header here */ 41 | var sig = auth.generateFromRequest(preq, body); 42 | 43 | if (sig.err !== false) { 44 | throw sig.err; 45 | } 46 | 47 | preq.headers['X-Dislocate-Signature'] = sig.hmac; 48 | 49 | var req = h.request(method, path, preq.headers); 50 | 51 | req.addListener('response', function (res) { 52 | var buf = ""; 53 | res.setEncoding('utf8'); 54 | res.addListener('data', function (chunk) { 55 | if (chunk !== undefined) { 56 | buf += chunk; 57 | } 58 | }); 59 | res.addListener('end', function() { 60 | var sig = auth.generateFromResponse(res, res.statusCode, res.headers, buf); 61 | var rv = {'err': false, 'body': buf, 'res': res}; 62 | var hmac = res.headers['x-dislocate-signature']; 63 | if (!auth.validateRaw(sig.hmac, hmac)) { 64 | rv.err = "Invalid HMAC"; 65 | log.err('Invalid response HMAC. Expected ', sig.hmac, ' got ', hmac); 66 | } 67 | cb(rv); 68 | }); 69 | }); 70 | if (body !== undefined) { 71 | req.write(body); 72 | } 73 | req.end(); 74 | }; 75 | 76 | -------------------------------------------------------------------------------- /dislocate/services.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Manages the state of services. 20 | * 21 | * Dislocate is built around a hierachical list of objects. 22 | * 23 | * Each object has a set of metadata. 24 | * 25 | * the ^ prefix is used for internal metadata 26 | * and should not be used or set by clients 27 | * 28 | * names are seperated by a period ('.'), so this 29 | * lets clients subscribe to a part of the tree. 30 | * 31 | **/ 32 | 33 | var log = require('./log'); 34 | var utils = require('./utils'); 35 | var ps = require('./pubsub'); 36 | 37 | 38 | var services = { 39 | '^': { 40 | 'dislocate': { 41 | 'services': [] 42 | } 43 | } 44 | }; 45 | 46 | // metdata type implies how you check if the service is alive 47 | /* 48 | var native_types = { 49 | 'tcp': require('./services/tcp') 50 | } 51 | */ 52 | 53 | function preptree(name) 54 | { 55 | var last = null; 56 | var parts = name.split('.'); 57 | parts.forEach(function(p) { 58 | if (services[p] === undefined) { 59 | services[p] = {}; 60 | } 61 | last = services[p]; 62 | }); 63 | if (last.services === undefined) { 64 | last.services = []; 65 | } 66 | return last; 67 | } 68 | 69 | var checkup_interval = 10000; 70 | var runchecks_timer = null; 71 | 72 | function runchecks() 73 | { 74 | runchecks_timer = setTimeout(runchecks, checkup_interval); 75 | } 76 | 77 | exports.start = function() 78 | { 79 | runchecks_timer = setTimeout(runchecks, checkup_interval); 80 | ps.sub(ps.STATE_STOP, function() { 81 | exports.stop(); 82 | }); 83 | }; 84 | 85 | exports.stop = function() 86 | { 87 | clearTimeout(runchecks_timer); 88 | }; 89 | 90 | exports.register = function(name, metadata) { 91 | var last = preptree(name); 92 | 93 | last.services.push(metadata); 94 | 95 | log.debug("Registered new service: ", name); 96 | }; 97 | 98 | exports.find = function(name) { 99 | var last = preptree(name); 100 | return last.services; 101 | }; 102 | 103 | exports.all = function() 104 | { 105 | var r = {}; 106 | return utils.merge(services, r); 107 | }; 108 | -------------------------------------------------------------------------------- /dislocate/auth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | /** 19 | * Provides a HMAC authentcation of a message, using the configured secret, 20 | * and authentication formats. Currently defaults to a SHA256 HAMC formated 21 | * in base64. 22 | */ 23 | 24 | var crypto = require('crypto'); 25 | var log = require('./log'); 26 | var config = require('./config'); 27 | 28 | exports.validate = function(input, hmac) 29 | { 30 | var correct = exports.generate(input); 31 | return exports.validateRaw(correct, hmac); 32 | }; 33 | 34 | exports.validateRaw = function(correct, hmac) 35 | { 36 | if (correct.length != hmac.length) { 37 | return false; 38 | } 39 | 40 | var rv = 0; 41 | 42 | for (var i = 0; i < correct.length; i++) { 43 | rv = rv | (correct[i] ^ hmac[i]); 44 | } 45 | 46 | if (rv === 0) { 47 | return true; 48 | } 49 | 50 | return false; 51 | }; 52 | 53 | exports.generate = function(input) 54 | { 55 | /* TODO: better handle inputs of objects / non-string types in a consistent way */ 56 | var c = config.get(); 57 | var h = crypto.createHmac(c.authentication_format.algorithm, c.secret); 58 | h.update(input); 59 | return h.digest(c.authentication_format.encoding); 60 | }; 61 | 62 | exports.generateFromRequest = function(req, body) 63 | { 64 | /** 65 | * TODO: Add other HTTP headers 66 | * TODO: talk over this scheme with someone else 67 | * 68 | * Generates HMAC of the following: 69 | * HTTP Method 70 | * URL of the request 71 | * HTTP Date Header 72 | * Entire Request Body 73 | */ 74 | 75 | var rv = {'err': false, 'hmac': null}; 76 | var inputs = [req.method]; 77 | inputs.push(req.url); 78 | 79 | var d = req.headers.date; 80 | if (d === undefined) { 81 | d = req.headers.Date; 82 | } 83 | 84 | if (d === undefined) { 85 | rv.err = 'Date http header must be sent'; 86 | return rv; 87 | } 88 | 89 | inputs.push(d); 90 | inputs.push(body); 91 | 92 | var input = inputs.join(""); 93 | rv.hmac = exports.generate(input); 94 | return rv; 95 | }; 96 | 97 | exports.generateFromResponse = function(res, code, headers, body) 98 | { 99 | var rv = {'err': false, 'hmac': null}; 100 | var inputs = [code]; 101 | var d = headers.date; 102 | if (d === undefined) { 103 | d = headers.Date; 104 | } 105 | 106 | if (d === undefined) { 107 | rv.err = 'Date http header must be sent'; 108 | return rv; 109 | } 110 | 111 | inputs.push(d); 112 | inputs.push(body); 113 | 114 | var input = inputs.join(""); 115 | 116 | rv.hmac = exports.generate(input); 117 | 118 | return rv; 119 | }; 120 | -------------------------------------------------------------------------------- /dislocate/interfaces/http.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Licensed to Cloudkick, Inc ('Cloudkick') under one or more 3 | * contributor license agreements. See the NOTICE file distributed with 4 | * this work for additional information regarding copyright ownership. 5 | * Cloudkick licenses this file to You under the Apache License, Version 2.0 6 | * (the "License"); you may not use this file except in compliance with 7 | * the License. You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | 19 | /** 20 | * HTTP Interface to Dislocate. 21 | * 22 | * Provides read and write access to the state of the dislocate node. 23 | * 24 | * Only requests from 127.0.0.1 or peers with the correct secret are 25 | * allowed to modify any settings. 26 | */ 27 | 28 | var log = require('../log'); 29 | var config = require('../config'); 30 | var nr = require('../../extern/node-router'); 31 | var templates = require("../templates"); 32 | var generic = require('./generic'); 33 | var schema = require('../schema'); 34 | var auth = require('../auth'); 35 | var sys = require('sys'); 36 | var server = null; 37 | 38 | function addResponseAuth(res) 39 | { 40 | res.writeAll = function (code, headers, body) { 41 | if (headers.Date === undefined) { 42 | /* TODO: not correct */ 43 | headers.Date = (new Date()).toUTCString(); 44 | } 45 | var sig = auth.generateFromResponse(res, code, headers, body); 46 | headers['X-Dislocate-Signature'] = sig.hmac; 47 | res.writeHead(code, headers); 48 | res.write(body); 49 | res.end(); 50 | }; 51 | } 52 | 53 | function shortResponse(res, status, message) 54 | { 55 | message = message + ""; 56 | res.writeAll(status, { 57 | "Content-Type": "text/plain; charset=utf-8", 58 | "Content-Length": message.length 59 | }, message); 60 | } 61 | 62 | function renderResponse(res, name, context) 63 | { 64 | templates.render(name, context, 65 | function (err, result) { 66 | if (err) { 67 | shortResponse(res, 500, "Exception rendering template "+ name +": "+ err); 68 | return; 69 | } 70 | shortResponse(res, 200, result); 71 | }); 72 | } 73 | 74 | function renderJSON(res, context) 75 | { 76 | data = JSON.stringify(context); 77 | res.writeAll(200, { 78 | 'Content-Type': 'application/json; charset=utf-8', 79 | "Content-Length": data.length 80 | }, data); 81 | } 82 | 83 | function renderSuccess(res) 84 | { 85 | /* TODO: make this a consistent JSON */ 86 | shortResponse(res, 200, "great success!\n"); 87 | } 88 | 89 | function checkAuth(req, res, body, success, failure) 90 | { 91 | addResponseAuth(res); 92 | 93 | if (failure === undefined) { 94 | failure = function(reason) { 95 | shortResponse(res, 401, reason + '\n'); 96 | }; 97 | } 98 | 99 | var proposed = req.headers["x-dislocate-signature"]; 100 | if (proposed !== undefined) { 101 | /* TODO: this is bogus. Should NEVER reconvert the body to JSON again. */ 102 | var good = auth.generateFromRequest(req, JSON.stringify(body)); 103 | 104 | if (good.err !== false) { 105 | log.info('sending failure'); 106 | return failure("request failed to authenticate: "+ good.err); 107 | } 108 | 109 | if (auth.validateRaw(good.hmac, proposed) === true) { 110 | return success(); 111 | } 112 | 113 | return failure("request failed to authenticate"); 114 | } 115 | 116 | /* TODO: this is just wrong for IPb6 enabled machines */ 117 | if (req.connection.remoteAddress == "127.0.0.1") { 118 | return success(); 119 | } 120 | 121 | return failure("must be from localhost"); 122 | } 123 | 124 | function checkBody(req, res, object_name, object_to_check, success, failure) 125 | { 126 | if (failure === undefined) { 127 | failure = function(reason) { 128 | shortResponse(res, 400, reason + '\n'); 129 | }; 130 | } 131 | 132 | /* TODO: Figure out how much this hurts performance */ 133 | var v = schema.validate(object_name, object_to_check); 134 | if (!v.valid) { 135 | var msg = ""; 136 | for (var i = 0; i < v.errors.length; i++) { 137 | msg += v.errors[i].property +": "+ v.errors[i].message + "\n"; 138 | } 139 | failure('Invalid JSON Object: '+ msg); 140 | return; 141 | } 142 | 143 | return success(); 144 | } 145 | 146 | exports.start = function() 147 | { 148 | var c = config.get(); 149 | server = nr.getServer(); 150 | 151 | server.get("/", function (req, res, match) { 152 | return renderResponse(res, 'index', {}); 153 | }); 154 | 155 | server.get("/d/services", function (req, res, match) { 156 | var services = generic.list(); 157 | return renderJSON(res, services); 158 | }); 159 | 160 | server.put("/d/service", function (req, res, body) { 161 | checkAuth(req, res, body, function() { 162 | checkBody(req, res, 'service', body, function(){ 163 | var rv = generic.register(body); 164 | if (rv === true) { 165 | return renderSuccess(res); 166 | } 167 | else { 168 | return shortResponse(res, 500, rv + '\n'); 169 | } 170 | }); 171 | }); 172 | }, "json"); 173 | 174 | log.info("Starting HTTP server on", c.port); 175 | server.listen(c.port, "0.0.0.0"); 176 | }; 177 | 178 | exports.stop = function() 179 | { 180 | if (server !== null) { 181 | server.close(); 182 | server = null; 183 | } 184 | }; 185 | -------------------------------------------------------------------------------- /extern/json-schema.js: -------------------------------------------------------------------------------- 1 | /** 2 | * JSONSchema Validator - Validates JavaScript objects using JSON Schemas 3 | * (http://www.json.com/json-schema-proposal/) 4 | * 5 | * Copyright (c) 2007 Kris Zyp SitePen (www.sitepen.com) 6 | * Licensed under the MIT (MIT-LICENSE.txt) license. 7 | To use the validator call JSONSchema.validate with an instance object and an optional schema object. 8 | If a schema is provided, it will be used to validate. If the instance object refers to a schema (self-validating), 9 | that schema will be used to validate and the schema parameter is not necessary (if both exist, 10 | both validations will occur). 11 | The validate method will return an array of validation errors. If there are no errors, then an 12 | empty list will be returned. A validation error will have two properties: 13 | "property" which indicates which property had the error 14 | "message" which indicates what the error was 15 | */ 16 | 17 | // setup primitive classes to be JSON Schema types 18 | String.type = "string"; 19 | Boolean.type = "boolean"; 20 | Number.type = "number"; 21 | exports.Integer = {type:"integer"}; 22 | Object.type = "object"; 23 | Array.type = "array"; 24 | Date.type = "string"; 25 | Date.format = "date-time"; 26 | 27 | exports.validate = function(/*Any*/instance,/*Object*/schema) { 28 | // Summary: 29 | // To use the validator call JSONSchema.validate with an instance object and an optional schema object. 30 | // If a schema is provided, it will be used to validate. If the instance object refers to a schema (self-validating), 31 | // that schema will be used to validate and the schema parameter is not necessary (if both exist, 32 | // both validations will occur). 33 | // The validate method will return an object with two properties: 34 | // valid: A boolean indicating if the instance is valid by the schema 35 | // errors: An array of validation errors. If there are no errors, then an 36 | // empty list will be returned. A validation error will have two properties: 37 | // property: which indicates which property had the error 38 | // message: which indicates what the error was 39 | // 40 | return validate(instance,schema,false); 41 | }; 42 | exports.checkPropertyChange = function(/*Any*/value,/*Object*/schema, /*String*/ property) { 43 | // Summary: 44 | // The checkPropertyChange method will check to see if an value can legally be in property with the given schema 45 | // This is slightly different than the validate method in that it will fail if the schema is readonly and it will 46 | // not check for self-validation, it is assumed that the passed in value is already internally valid. 47 | // The checkPropertyChange method will return the same object type as validate, see JSONSchema.validate for 48 | // information. 49 | // 50 | return validate(value,schema, property || "property"); 51 | }; 52 | var validate = exports._validate = function(/*Any*/instance,/*Object*/schema,/*Boolean*/ _changing) { 53 | 54 | var errors = []; 55 | // validate a value against a property definition 56 | function checkProp(value, schema, path,i){ 57 | var l; 58 | path += path ? typeof i == 'number' ? '[' + i + ']' : typeof i == 'undefined' ? '' : '.' + i : i; 59 | function addError(message){ 60 | errors.push({property:path,message:message}); 61 | } 62 | 63 | if((typeof schema != 'object' || schema instanceof Array) && (path || typeof schema != 'function') && !(schema && schema.type)){ 64 | if(typeof schema == 'function'){ 65 | if(!(value instanceof schema)){ 66 | addError("is not an instance of the class/constructor " + schema.name); 67 | } 68 | }else if(schema){ 69 | addError("Invalid schema/property definition " + schema); 70 | } 71 | return null; 72 | } 73 | if(_changing && schema.readonly){ 74 | addError("is a readonly field, it can not be changed"); 75 | } 76 | if(schema['extends']){ // if it extends another schema, it must pass that schema as well 77 | checkProp(value,schema['extends'],path,i); 78 | } 79 | // validate a value against a type definition 80 | function checkType(type,value){ 81 | if(type){ 82 | if(typeof type == 'string' && type != 'any' && 83 | (type == 'null' ? value !== null : typeof value != type) && 84 | !(value instanceof Array && type == 'array') && 85 | !(type == 'integer' && value%1===0)){ 86 | return [{property:path,message:(typeof value) + " value found, but a " + type + " is required"}]; 87 | } 88 | if(type instanceof Array){ 89 | var unionErrors=[]; 90 | for(var j = 0; j < type.length; j++){ // a union type 91 | if(!(unionErrors=checkType(type[j],value)).length){ 92 | break; 93 | } 94 | } 95 | if(unionErrors.length){ 96 | return unionErrors; 97 | } 98 | }else if(typeof type == 'object'){ 99 | var priorErrors = errors; 100 | errors = []; 101 | checkProp(value,type,path); 102 | var theseErrors = errors; 103 | errors = priorErrors; 104 | return theseErrors; 105 | } 106 | } 107 | return []; 108 | } 109 | if(value === undefined){ 110 | if(!schema.optional && !schema.get){ 111 | addError("is missing and it is not optional"); 112 | } 113 | }else{ 114 | errors = errors.concat(checkType(schema.type,value)); 115 | if(schema.disallow && !checkType(schema.disallow,value).length){ 116 | addError(" disallowed value was matched"); 117 | } 118 | if(value !== null){ 119 | if(value instanceof Array){ 120 | if(schema.items){ 121 | if(schema.items instanceof Array){ 122 | for(i=0,l=value.length; i schema.maxItems){ 135 | addError("There must be a maximum of " + schema.maxItems + " in the array"); 136 | } 137 | }else if(schema.properties || schema.additionalProperties){ 138 | errors.concat(checkObj(value, schema.properties, path, schema.additionalProperties)); 139 | } 140 | if(schema.pattern && typeof value == 'string' && !value.match(schema.pattern)){ 141 | addError("does not match the regex pattern " + schema.pattern); 142 | } 143 | if(schema.maxLength && typeof value == 'string' && value.length > schema.maxLength){ 144 | addError("may only be " + schema.maxLength + " characters long"); 145 | } 146 | if(schema.minLength && typeof value == 'string' && value.length < schema.minLength){ 147 | addError("must be at least " + schema.minLength + " characters long"); 148 | } 149 | if(typeof schema.minimum !== undefined && typeof value == typeof schema.minimum && 150 | schema.minimum > value){ 151 | addError("must have a minimum value of " + schema.minimum); 152 | } 153 | if(typeof schema.maximum !== undefined && typeof value == typeof schema.maximum && 154 | schema.maximum < value){ 155 | addError("must have a maximum value of " + schema.maximum); 156 | } 157 | if(schema['enum']){ 158 | var enumer = schema['enum']; 159 | l = enumer.length; 160 | var found; 161 | for(var j = 0; j < l; j++){ 162 | if(enumer[j]===value){ 163 | found=1; 164 | break; 165 | } 166 | } 167 | if(!found){ 168 | addError("does not have a value in the enumeration " + enumer.join(", ")); 169 | } 170 | } 171 | if(typeof schema.maxDecimal == 'number' && 172 | (value.toString().match(new RegExp("\\.[0-9]{" + (schema.maxDecimal + 1) + ",}")))){ 173 | addError("may only have " + schema.maxDecimal + " digits of decimal places"); 174 | } 175 | } 176 | } 177 | return null; 178 | } 179 | // validate an object against a schema 180 | function checkObj(instance,objTypeDef,path,additionalProp){ 181 | 182 | if(typeof objTypeDef =='object'){ 183 | if(typeof instance != 'object' || instance instanceof Array){ 184 | errors.push({property:path,message:"an object is required"}); 185 | } 186 | 187 | for(var i in objTypeDef){ 188 | if(objTypeDef.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_')){ 189 | var value = instance[i]; 190 | var propDef = objTypeDef[i]; 191 | checkProp(value,propDef,path,i); 192 | } 193 | } 194 | } 195 | for(i in instance){ 196 | if(instance.hasOwnProperty(i) && !(i.charAt(0) == '_' && i.charAt(1) == '_') && objTypeDef && !objTypeDef[i] && additionalProp===false){ 197 | errors.push({property:path,message:(typeof value) + "The property " + i + 198 | " is not defined in the schema and the schema does not allow additional properties"}); 199 | } 200 | var requires = objTypeDef && objTypeDef[i] && objTypeDef[i].requires; 201 | if(requires && !(requires in instance)){ 202 | errors.push({property:path,message:"the presence of the property " + i + " requires that " + requires + " also be present"}); 203 | } 204 | value = instance[i]; 205 | if(additionalProp && (!(objTypeDef && typeof objTypeDef == 'object') || !(i in objTypeDef))){ 206 | checkProp(value,additionalProp,path,i); 207 | } 208 | if(!_changing && value && value.$schema){ 209 | errors = errors.concat(checkProp(value,value.$schema,path,i)); 210 | } 211 | } 212 | return errors; 213 | } 214 | if(schema){ 215 | checkProp(instance,schema,'',_changing || ''); 216 | } 217 | if(!_changing && instance && instance.$schema){ 218 | checkProp(instance,instance.$schema,'',''); 219 | } 220 | return {valid:!errors.length,errors:errors}; 221 | }; 222 | exports.mustBeValid = function(result){ 223 | // summary: 224 | // This checks to ensure that the result is valid and will throw an appropriate error message if it is not 225 | // result: the result returned from checkPropertyChange or validate 226 | if(!result.valid){ 227 | throw new TypeError(result.errors.map(function(error){return "for property " + error.property + ': ' + error.message;}).join(", \n")); 228 | } 229 | } 230 | /* will add this later 231 | newFromSchema : function() { 232 | } 233 | */ 234 | 235 | -------------------------------------------------------------------------------- /extern/stacktrace.js: -------------------------------------------------------------------------------- 1 | // Domain Public by Eric Wendelin http://eriwen.com/ (2008) 2 | // Luke Smith http://lucassmith.name/ (2008) 3 | // Loic Dachary (2008) 4 | // Johan Euphrosine (2008) 5 | // Øyvind Sean Kinsey http://kinsey.no/blog (2010) 6 | // 7 | // Information and discussions 8 | // http://jspoker.pokersource.info/skin/test-printstacktrace.html 9 | // http://eriwen.com/javascript/js-stack-trace/ 10 | // http://eriwen.com/javascript/stacktrace-update/ 11 | // http://pastie.org/253058 12 | // http://browsershots.org/http://jspoker.pokersource.info/skin/test-printstacktrace.html 13 | // 14 | 15 | // 16 | // guessFunctionNameFromLines comes from firebug 17 | // 18 | // Software License Agreement (BSD License) 19 | // 20 | // Copyright (c) 2007, Parakey Inc. 21 | // All rights reserved. 22 | // 23 | // Redistribution and use of this software in source and binary forms, with or without modification, 24 | // are permitted provided that the following conditions are met: 25 | // 26 | // * Redistributions of source code must retain the above 27 | // copyright notice, this list of conditions and the 28 | // following disclaimer. 29 | // 30 | // * Redistributions in binary form must reproduce the above 31 | // copyright notice, this list of conditions and the 32 | // following disclaimer in the documentation and/or other 33 | // materials provided with the distribution. 34 | // 35 | // * Neither the name of Parakey Inc. nor the names of its 36 | // contributors may be used to endorse or promote products 37 | // derived from this software without specific prior 38 | // written permission of Parakey Inc. 39 | // 40 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR 41 | // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 42 | // FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 43 | // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 44 | // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 45 | // DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 46 | // IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 47 | // OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 48 | 49 | /** 50 | * 51 | * @cfg {Error} e The error to create a stacktrace from (optional) 52 | * @cfg {Boolean} guess If we should try to resolve the names of anonymous functions 53 | */ 54 | function printStackTrace(options) { 55 | var ex = (options && options.e) ? options.e : null; 56 | var guess = options ? !!options.guess : true; 57 | 58 | var p = new printStackTrace.implementation(); 59 | var result = p.run(ex); 60 | return (guess) ? p.guessFunctions(result) : result; 61 | } 62 | 63 | printStackTrace.implementation = function() {}; 64 | 65 | printStackTrace.implementation.prototype = { 66 | run: function(ex) { 67 | // Use either the stored mode, or resolve it 68 | var mode = this._mode || this.mode(); 69 | if (mode === 'other') { 70 | return this.other(arguments.callee); 71 | } else { 72 | ex = ex || 73 | (function() { 74 | try { 75 | (0)(); 76 | } catch (e) { 77 | return e; 78 | } 79 | })(); 80 | return this[mode](ex); 81 | } 82 | }, 83 | 84 | mode: function() { 85 | try { 86 | (0)(); 87 | } catch (e) { 88 | if (e.arguments) { 89 | return (this._mode = 'chrome'); 90 | } else if (e.stack) { 91 | return (this._mode = 'firefox'); 92 | } else if (window.opera && !('stacktrace' in e)) { //Opera 9- 93 | return (this._mode = 'opera'); 94 | } 95 | } 96 | return (this._mode = 'other'); 97 | }, 98 | 99 | chrome: function(e) { 100 | return e.stack.replace(/^.*?\n/, ''). 101 | replace(/^.*?\n/, ''). 102 | replace(/^.*?\n/, ''). 103 | replace(/^[^\(]+?[\n$]/gm, ''). 104 | replace(/^\s+at\s+/gm, ''). 105 | replace(/^Object.\s*\(/gm, '{anonymous}()@'). 106 | split('\n'); 107 | }, 108 | 109 | firefox: function(e) { 110 | return e.stack.replace(/^.*?\n/, ''). 111 | replace(/(?:\n@:0)?\s+$/m, ''). 112 | replace(/^\(/gm, '{anonymous}('). 113 | split('\n'); 114 | }, 115 | 116 | // Opera 7.x and 8.x only! 117 | opera: function(e) { 118 | var lines = e.message.split('\n'), ANON = '{anonymous}', 119 | lineRE = /Line\s+(\d+).*?script\s+(http\S+)(?:.*?in\s+function\s+(\S+))?/i, i, j, len; 120 | 121 | for (i = 4, j = 0, len = lines.length; i < len; i += 2) { 122 | if (lineRE.test(lines[i])) { 123 | lines[j++] = (RegExp.$3 ? RegExp.$3 + '()@' + RegExp.$2 + RegExp.$1 : ANON + '()@' + RegExp.$2 + ':' + RegExp.$1) + 124 | ' -- ' + 125 | lines[i + 1].replace(/^\s+/, ''); 126 | } 127 | } 128 | 129 | lines.splice(j, lines.length - j); 130 | return lines; 131 | }, 132 | 133 | // Safari, Opera 9+, IE, and others 134 | other: function(curr) { 135 | var ANON = '{anonymous}', fnRE = /function\s*([\w\-$]+)?\s*\(/i, stack = [], j = 0, fn, args; 136 | 137 | var maxStackSize = 10; 138 | while (curr && stack.length < maxStackSize) { 139 | fn = fnRE.test(curr.toString()) ? RegExp.$1 || ANON : ANON; 140 | args = Array.prototype.slice.call(curr['arguments']); 141 | stack[j++] = fn + '(' + printStackTrace.implementation.prototype.stringifyArguments(args) + ')'; 142 | 143 | //Opera bug: if curr.caller does not exist, Opera returns curr (WTF) 144 | if (curr === curr.caller && window.opera) { 145 | //TODO: check for same arguments if possible 146 | break; 147 | } 148 | curr = curr.caller; 149 | } 150 | return stack; 151 | }, 152 | 153 | /** 154 | * @return given arguments array as a String, subsituting type names for non-string types. 155 | */ 156 | stringifyArguments: function(args) { 157 | for (var i = 0; i < args.length; ++i) { 158 | var argument = args[i]; 159 | if (typeof argument == 'object') { 160 | args[i] = '#object'; 161 | } else if (typeof argument == 'function') { 162 | args[i] = '#function'; 163 | } else if (typeof argument == 'string') { 164 | args[i] = '"' + argument + '"'; 165 | } 166 | } 167 | return args.join(','); 168 | }, 169 | 170 | sourceCache: {}, 171 | 172 | /** 173 | * @return the text from a given URL. 174 | */ 175 | ajax: function(url) { 176 | var req = this.createXMLHTTPObject(); 177 | if (!req) { 178 | return; 179 | } 180 | req.open('GET', url, false); 181 | req.setRequestHeader('User-Agent', 'XMLHTTP/1.0'); 182 | req.send(''); 183 | return req.responseText; 184 | }, 185 | 186 | createXMLHTTPObject: function() { 187 | // Try XHR methods in order and store XHR factory 188 | var xmlhttp, XMLHttpFactories = [ 189 | function() { 190 | return new XMLHttpRequest(); 191 | }, function() { 192 | return new ActiveXObject('Msxml2.XMLHTTP'); 193 | }, function() { 194 | return new ActiveXObject('Msxml3.XMLHTTP'); 195 | }, function() { 196 | return new ActiveXObject('Microsoft.XMLHTTP'); 197 | } 198 | ]; 199 | for (var i = 0; i < XMLHttpFactories.length; i++) { 200 | try { 201 | xmlhttp = XMLHttpFactories[i](); 202 | // Use memoization to cache the factory 203 | this.createXMLHTTPObject = XMLHttpFactories[i]; 204 | return xmlhttp; 205 | } catch (e) {} 206 | } 207 | }, 208 | 209 | getSource: function(url) { 210 | if (!(url in this.sourceCache)) { 211 | this.sourceCache[url] = this.ajax(url).split('\n'); 212 | } 213 | return this.sourceCache[url]; 214 | }, 215 | 216 | guessFunctions: function(stack) { 217 | for (var i = 0; i < stack.length; ++i) { 218 | var reStack = /{anonymous}\(.*\)@(\w+:\/\/([-\w\.]+)+(:\d+)?[^:]+):(\d+):?(\d+)?/; 219 | var frame = stack[i], m = reStack.exec(frame); 220 | if (m) { 221 | var file = m[1], lineno = m[4]; //m[7] is character position in Chrome 222 | if (file && lineno) { 223 | var functionName = this.guessFunctionName(file, lineno); 224 | stack[i] = frame.replace('{anonymous}', functionName); 225 | } 226 | } 227 | } 228 | return stack; 229 | }, 230 | 231 | guessFunctionName: function(url, lineNo) { 232 | try { 233 | return this.guessFunctionNameFromLines(lineNo, this.getSource(url)); 234 | } catch (e) { 235 | return 'getSource failed with url: ' + url + ', exception: ' + e.toString(); 236 | } 237 | }, 238 | 239 | guessFunctionNameFromLines: function(lineNo, source) { 240 | var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/; 241 | var reGuessFunction = /['"]?([0-9A-Za-z_]+)['"]?\s*[:=]\s*(function|eval|new Function)/; 242 | // Walk backwards from the first line in the function until we find the line which 243 | // matches the pattern above, which is the function definition 244 | var line = "", maxLines = 10; 245 | for (var i = 0; i < maxLines; ++i) { 246 | line = source[lineNo - i] + line; 247 | if (line !== undefined) { 248 | var m = reGuessFunction.exec(line); 249 | if (m && m[1]) { 250 | return m[1]; 251 | } else { 252 | m = reFunctionArgNames.exec(line); 253 | if (m && m[1]) { 254 | return m[1]; 255 | } 256 | } 257 | } 258 | } 259 | return '(?)'; 260 | } 261 | }; 262 | 263 | exports.trace = printStackTrace; 264 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /extern/node-router.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2010 Tim Caswell 3 | 4 | Permission is hereby granted, free of charge, to any person 5 | obtaining a copy of this software and associated documentation 6 | files (the "Software"), to deal in the Software without 7 | restriction, including without limitation the rights to use, 8 | copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the 10 | Software is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 18 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 20 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 21 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 23 | OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | var sys = require('sys'); 27 | var fs = require('fs'); 28 | var path = require('path'); 29 | var http = require('http'); 30 | var url_parse = require("url").parse; 31 | 32 | // Used as a simple, convient 404 handler. 33 | function notFound(req, res, message) { 34 | message = (message || "Not Found\n") + ""; 35 | res.writeHead(404, { 36 | "Content-Type": "text/plain", 37 | "Content-Length": message.length 38 | }); 39 | res.write(message); 40 | res.end(); 41 | } 42 | 43 | function badRequest(req, res, message) { 44 | message = (message || "Bad Request\n") + ""; 45 | res.writeHead(400, { 46 | "Content-Type": "text/plain", 47 | "Content-Length": message.length 48 | }); 49 | res.write(message); 50 | res.end(); 51 | } 52 | 53 | // Modifies req and res to call logger with a log line on each res.end 54 | // Think of it as "middleware" 55 | function logify(req, res, logger) { 56 | var end = res.end; 57 | res.end = function () { 58 | // Common Log Format (mostly) 59 | logger((req.socket && req.socket.remoteAddress) + " - - [" + (new Date()).toUTCString() + "]" 60 | + " \"" + req.method + " " + req.url 61 | + " HTTP/" + req.httpVersionMajor + "." + req.httpVersionMinor + "\" " 62 | + res.statusCode + " - \"" 63 | + (req.headers['referer'] || "") + "\" \"" + (req.headers["user-agent"] ? req.headers["user-agent"].split(' ')[0] : '') + "\""); 64 | return end.apply(this, arguments); 65 | } 66 | var writeHead = res.writeHead; 67 | res.writeHead = function (code) { 68 | res.statusCode = code; 69 | return writeHead.apply(this, arguments); 70 | } 71 | } 72 | 73 | exports.getServer = function getServer(logger) { 74 | 75 | logger = logger || sys.puts; 76 | 77 | var routes = []; 78 | 79 | // Adds a route the the current server 80 | function addRoute(method, pattern, handler, format) { 81 | if (typeof pattern === 'string') { 82 | pattern = new RegExp("^" + pattern + "$"); 83 | } 84 | var route = { 85 | method: method, 86 | pattern: pattern, 87 | handler: handler 88 | }; 89 | if (format !== undefined) { 90 | route.format = format; 91 | } 92 | routes.push(route); 93 | } 94 | 95 | // The four verbs are wrappers around addRoute 96 | function get(pattern, handler) { 97 | return addRoute("GET", pattern, handler); 98 | } 99 | function post(pattern, handler, format) { 100 | return addRoute("POST", pattern, handler, format); 101 | } 102 | function put(pattern, handler, format) { 103 | return addRoute("PUT", pattern, handler, format); 104 | } 105 | function del(pattern, handler) { 106 | return addRoute("DELETE", pattern, handler); 107 | } 108 | 109 | // This is a meta pattern that expands to a common RESTful mapping 110 | function resource(name, controller, format) { 111 | get(new RegExp('^/' + name + '$'), controller.index); 112 | get(new RegExp('^/' + name + '/([^/]+)$'), controller.show); 113 | post(new RegExp('^/' + name + '$'), controller.create, format); 114 | put(new RegExp('^/' + name + '/([^/]+)$'), controller.update, format); 115 | del(new RegExp('^/' + name + '/([^/]+)$'), controller.destroy); 116 | }; 117 | 118 | function resourceController(name, data, on_change) { 119 | data = data || []; 120 | on_change = on_change || function () {}; 121 | return { 122 | index: function (req, res) { 123 | res.simpleJson(200, {content: data, self: '/' + name}); 124 | }, 125 | show: function (req, res, id) { 126 | var item = data[id]; 127 | if (item) { 128 | res.simpleJson(200, {content: item, self: '/' + name + '/' + id}); 129 | } else { 130 | res.notFound(); 131 | } 132 | }, 133 | create: function (req, res) { 134 | req.jsonBody(function (json) { 135 | var item, id, url; 136 | item = json && json.content; 137 | if (!item) { 138 | res.notFound(); 139 | } else { 140 | data.push(item); 141 | id = data.length - 1; 142 | on_change(id); 143 | url = "/" + name + "/" + id; 144 | res.simpleJson(201, {content: item, self: url}, [["Location", url]]); 145 | } 146 | }); 147 | }, 148 | update: function (req, res, id) { 149 | req.jsonBody(function (json) { 150 | var item = json && json.content; 151 | if (!item) { 152 | res.notFound(); 153 | } else { 154 | data[id] = item; 155 | on_change(id); 156 | res.simpleJson(200, {content: item, self: "/" + name + "/" + id}); 157 | } 158 | }); 159 | }, 160 | destroy: function (req, res, id) { 161 | delete(data[id]); 162 | on_change(id); 163 | res.simpleJson(200, "200 Destroyed"); 164 | } 165 | }; 166 | }; 167 | 168 | // Create the http server object 169 | var server = http.createServer(function (req, res) { 170 | 171 | // Enable logging on all requests using common-logger style 172 | logify(req, res, logger); 173 | 174 | var uri, path; 175 | 176 | // Performs an HTTP 302 redirect 177 | res.redirect = function redirect(location) { 178 | res.writeHead(302, {"Location": location}); 179 | res.end(); 180 | } 181 | 182 | // Performs an internal redirect 183 | res.innerRedirect = function innerRedirect(location) { 184 | logger("Internal Redirect: " + req.url + " -> " + location); 185 | req.url = location; 186 | doRoute(); 187 | } 188 | 189 | function simpleResponse(code, body, content_type, extra_headers) { 190 | res.writeHead(code, (extra_headers || []).concat( 191 | [ ["Content-Type", content_type], 192 | ["Content-Length", body.length] 193 | ])); 194 | res.write(body); 195 | res.end(); 196 | } 197 | 198 | res.simpleText = function (code, body, extra_headers) { 199 | simpleResponse(code, body, "text/plain", extra_headers); 200 | }; 201 | 202 | res.simpleHtml = function (code, body, extra_headers) { 203 | simpleResponse(code, body, "text/html", extra_headers); 204 | }; 205 | 206 | res.simpleJson = function (code, json, extra_headers) { 207 | simpleResponse(code, JSON.stringify(json), "application/json", extra_headers); 208 | }; 209 | 210 | res.notFound = function (message) { 211 | notFound(req, res, message); 212 | }; 213 | 214 | function doRoute() { 215 | uri = url_parse(req.url); 216 | path = uri.pathname; 217 | 218 | for (var i = 0, l = routes.length; i < l; i += 1) { 219 | var route = routes[i]; 220 | if (req.method === route.method) { 221 | var match = path.match(route.pattern); 222 | if (match && match[0].length > 0) { 223 | match.shift(); 224 | match = match.map(function (part) { 225 | return part ? unescape(part) : part; 226 | }); 227 | match.unshift(res); 228 | match.unshift(req); 229 | if (route.format !== undefined) { 230 | var body = ""; 231 | req.setEncoding('utf8'); 232 | req.addListener('data', function (chunk) { 233 | body += chunk; 234 | }); 235 | req.addListener('end', function () { 236 | if (route.format === 'json') { 237 | try { 238 | body = JSON.parse(unescape(body)); 239 | } 240 | catch (err) { 241 | badRequest(req, res, "JSON.parse failed: "+err); 242 | return; 243 | } 244 | } 245 | match.push(body); 246 | route.handler.apply(null, match); 247 | }); 248 | return; 249 | } 250 | var result = route.handler.apply(null, match); 251 | switch (typeof result) { 252 | case "string": 253 | res.simpleHtml(200, result); 254 | break; 255 | case "object": 256 | res.simpleJson(200, result); 257 | break; 258 | } 259 | 260 | return; 261 | } 262 | } 263 | } 264 | 265 | notFound(req, res); 266 | } 267 | doRoute(); 268 | 269 | }); 270 | 271 | function listen(port, host) { 272 | port = port || 8080; 273 | if (typeof port === 'number') { 274 | host = host || "127.0.0.1"; 275 | } 276 | server.listen(port, host); 277 | if (typeof port === 'number') { 278 | logger("node-router server instance at http://" + host + ":" + port + "/"); 279 | } else { 280 | logger("node-router server instance at unix:" + port); 281 | } 282 | } 283 | 284 | function close() { 285 | return server.close(); 286 | } 287 | 288 | // Return a handle to the public facing functions from this closure as the 289 | // server object. 290 | return { 291 | get: get, 292 | post: post, 293 | put: put, 294 | del: del, 295 | resource: resource, 296 | resourceController: resourceController, 297 | listen: listen, 298 | close: close, 299 | }; 300 | } 301 | 302 | 303 | 304 | 305 | exports.staticHandler = function (filename) { 306 | var body, headers; 307 | var content_type = mime.getMime(filename) 308 | var encoding = (content_type.slice(0,4) === "text" ? "utf8" : "binary"); 309 | 310 | function loadResponseData(req, res, callback) { 311 | if (body && headers) { 312 | callback(); 313 | return; 314 | } 315 | 316 | fs.readFile(filename, encoding, function (err, data) { 317 | if (err) { 318 | notFound(req, res, "Cannot find file: " + filename); 319 | return; 320 | } 321 | body = data; 322 | headers = [ [ "Content-Type" , content_type ], 323 | [ "Content-Length" , body.length ] 324 | ]; 325 | headers.push(["Cache-Control", "public"]); 326 | 327 | callback(); 328 | }); 329 | } 330 | 331 | return function (req, res) { 332 | loadResponseData(req, res, function () { 333 | res.writeHead(200, headers); 334 | res.write(body, encoding); 335 | res.end(); 336 | }); 337 | }; 338 | }; 339 | 340 | exports.staticDirHandler = function(root, prefix) { 341 | function loadResponseData(req, res, filename, callback) { 342 | var content_type = mime.getMime(filename); 343 | var encoding = (content_type.slice(0,4) === "text" ? "utf8" : "binary"); 344 | 345 | fs.readFile(filename, encoding, function(err, data) { 346 | if(err) { 347 | notFound(req, res, "Cannot find file: " + filename); 348 | return; 349 | } 350 | var headers = [ [ "Content-Type" , content_type ], 351 | [ "Content-Length" , data.length ], 352 | [ "Cache-Control" , "public" ] 353 | ]; 354 | callback(headers, data, encoding); 355 | }); 356 | } 357 | 358 | return function (req, res) { 359 | // trim off any query/anchor stuff 360 | var filename = req.url.replace(/[\?|#].*$/, ''); 361 | if (prefix) filename = filename.replace(new RegExp('^'+prefix), ''); 362 | // make sure nobody can explore our local filesystem 363 | filename = path.join(root, filename.replace(/\.\.+/g, '.')); 364 | if (filename == root) filename = path.join(root, 'index.html'); 365 | loadResponseData(req, res, filename, function(headers, body, encoding) { 366 | res.writeHead(200, headers); 367 | res.write(body, encoding); 368 | res.end(); 369 | }); 370 | }; 371 | }; 372 | 373 | 374 | // Mini mime module for static file serving 375 | var DEFAULT_MIME = 'application/octet-stream'; 376 | var mime = exports.mime = { 377 | 378 | getMime: function getMime(path) { 379 | var index = path.lastIndexOf("."); 380 | if (index < 0) { 381 | return DEFAULT_MIME; 382 | } 383 | return mime.TYPES[path.substring(index).toLowerCase()] || DEFAULT_MIME; 384 | }, 385 | 386 | TYPES : { ".3gp" : "video/3gpp", 387 | ".a" : "application/octet-stream", 388 | ".ai" : "application/postscript", 389 | ".aif" : "audio/x-aiff", 390 | ".aiff" : "audio/x-aiff", 391 | ".asc" : "application/pgp-signature", 392 | ".asf" : "video/x-ms-asf", 393 | ".asm" : "text/x-asm", 394 | ".asx" : "video/x-ms-asf", 395 | ".atom" : "application/atom+xml", 396 | ".au" : "audio/basic", 397 | ".avi" : "video/x-msvideo", 398 | ".bat" : "application/x-msdownload", 399 | ".bin" : "application/octet-stream", 400 | ".bmp" : "image/bmp", 401 | ".bz2" : "application/x-bzip2", 402 | ".c" : "text/x-c", 403 | ".cab" : "application/vnd.ms-cab-compressed", 404 | ".cc" : "text/x-c", 405 | ".chm" : "application/vnd.ms-htmlhelp", 406 | ".class" : "application/octet-stream", 407 | ".com" : "application/x-msdownload", 408 | ".conf" : "text/plain", 409 | ".cpp" : "text/x-c", 410 | ".crt" : "application/x-x509-ca-cert", 411 | ".css" : "text/css", 412 | ".csv" : "text/csv", 413 | ".cxx" : "text/x-c", 414 | ".deb" : "application/x-debian-package", 415 | ".der" : "application/x-x509-ca-cert", 416 | ".diff" : "text/x-diff", 417 | ".djv" : "image/vnd.djvu", 418 | ".djvu" : "image/vnd.djvu", 419 | ".dll" : "application/x-msdownload", 420 | ".dmg" : "application/octet-stream", 421 | ".doc" : "application/msword", 422 | ".dot" : "application/msword", 423 | ".dtd" : "application/xml-dtd", 424 | ".dvi" : "application/x-dvi", 425 | ".ear" : "application/java-archive", 426 | ".eml" : "message/rfc822", 427 | ".eps" : "application/postscript", 428 | ".exe" : "application/x-msdownload", 429 | ".f" : "text/x-fortran", 430 | ".f77" : "text/x-fortran", 431 | ".f90" : "text/x-fortran", 432 | ".flv" : "video/x-flv", 433 | ".for" : "text/x-fortran", 434 | ".gem" : "application/octet-stream", 435 | ".gemspec" : "text/x-script.ruby", 436 | ".gif" : "image/gif", 437 | ".gz" : "application/x-gzip", 438 | ".h" : "text/x-c", 439 | ".hh" : "text/x-c", 440 | ".htm" : "text/html", 441 | ".html" : "text/html", 442 | ".ico" : "image/vnd.microsoft.icon", 443 | ".ics" : "text/calendar", 444 | ".ifb" : "text/calendar", 445 | ".iso" : "application/octet-stream", 446 | ".jar" : "application/java-archive", 447 | ".java" : "text/x-java-source", 448 | ".jnlp" : "application/x-java-jnlp-file", 449 | ".jpeg" : "image/jpeg", 450 | ".jpg" : "image/jpeg", 451 | ".js" : "application/javascript", 452 | ".json" : "application/json", 453 | ".log" : "text/plain", 454 | ".m3u" : "audio/x-mpegurl", 455 | ".m4v" : "video/mp4", 456 | ".man" : "text/troff", 457 | ".mathml" : "application/mathml+xml", 458 | ".mbox" : "application/mbox", 459 | ".mdoc" : "text/troff", 460 | ".me" : "text/troff", 461 | ".mid" : "audio/midi", 462 | ".midi" : "audio/midi", 463 | ".mime" : "message/rfc822", 464 | ".mml" : "application/mathml+xml", 465 | ".mng" : "video/x-mng", 466 | ".mov" : "video/quicktime", 467 | ".mp3" : "audio/mpeg", 468 | ".mp4" : "video/mp4", 469 | ".mp4v" : "video/mp4", 470 | ".mpeg" : "video/mpeg", 471 | ".mpg" : "video/mpeg", 472 | ".ms" : "text/troff", 473 | ".msi" : "application/x-msdownload", 474 | ".odp" : "application/vnd.oasis.opendocument.presentation", 475 | ".ods" : "application/vnd.oasis.opendocument.spreadsheet", 476 | ".odt" : "application/vnd.oasis.opendocument.text", 477 | ".ogg" : "application/ogg", 478 | ".p" : "text/x-pascal", 479 | ".pas" : "text/x-pascal", 480 | ".pbm" : "image/x-portable-bitmap", 481 | ".pdf" : "application/pdf", 482 | ".pem" : "application/x-x509-ca-cert", 483 | ".pgm" : "image/x-portable-graymap", 484 | ".pgp" : "application/pgp-encrypted", 485 | ".pkg" : "application/octet-stream", 486 | ".pl" : "text/x-script.perl", 487 | ".pm" : "text/x-script.perl-module", 488 | ".png" : "image/png", 489 | ".pnm" : "image/x-portable-anymap", 490 | ".ppm" : "image/x-portable-pixmap", 491 | ".pps" : "application/vnd.ms-powerpoint", 492 | ".ppt" : "application/vnd.ms-powerpoint", 493 | ".ps" : "application/postscript", 494 | ".psd" : "image/vnd.adobe.photoshop", 495 | ".py" : "text/x-script.python", 496 | ".qt" : "video/quicktime", 497 | ".ra" : "audio/x-pn-realaudio", 498 | ".rake" : "text/x-script.ruby", 499 | ".ram" : "audio/x-pn-realaudio", 500 | ".rar" : "application/x-rar-compressed", 501 | ".rb" : "text/x-script.ruby", 502 | ".rdf" : "application/rdf+xml", 503 | ".roff" : "text/troff", 504 | ".rpm" : "application/x-redhat-package-manager", 505 | ".rss" : "application/rss+xml", 506 | ".rtf" : "application/rtf", 507 | ".ru" : "text/x-script.ruby", 508 | ".s" : "text/x-asm", 509 | ".sgm" : "text/sgml", 510 | ".sgml" : "text/sgml", 511 | ".sh" : "application/x-sh", 512 | ".sig" : "application/pgp-signature", 513 | ".snd" : "audio/basic", 514 | ".so" : "application/octet-stream", 515 | ".svg" : "image/svg+xml", 516 | ".svgz" : "image/svg+xml", 517 | ".swf" : "application/x-shockwave-flash", 518 | ".t" : "text/troff", 519 | ".tar" : "application/x-tar", 520 | ".tbz" : "application/x-bzip-compressed-tar", 521 | ".tci" : "application/x-topcloud", 522 | ".tcl" : "application/x-tcl", 523 | ".tex" : "application/x-tex", 524 | ".texi" : "application/x-texinfo", 525 | ".texinfo" : "application/x-texinfo", 526 | ".text" : "text/plain", 527 | ".tif" : "image/tiff", 528 | ".tiff" : "image/tiff", 529 | ".torrent" : "application/x-bittorrent", 530 | ".tr" : "text/troff", 531 | ".ttf" : "application/x-font-ttf", 532 | ".txt" : "text/plain", 533 | ".vcf" : "text/x-vcard", 534 | ".vcs" : "text/x-vcalendar", 535 | ".vrml" : "model/vrml", 536 | ".war" : "application/java-archive", 537 | ".wav" : "audio/x-wav", 538 | ".wma" : "audio/x-ms-wma", 539 | ".wmv" : "video/x-ms-wmv", 540 | ".wmx" : "video/x-ms-wmx", 541 | ".wrl" : "model/vrml", 542 | ".wsdl" : "application/wsdl+xml", 543 | ".xbm" : "image/x-xbitmap", 544 | ".xhtml" : "application/xhtml+xml", 545 | ".xls" : "application/vnd.ms-excel", 546 | ".xml" : "application/xml", 547 | ".xpm" : "image/x-xpixmap", 548 | ".xsl" : "application/xml", 549 | ".xslt" : "application/xslt+xml", 550 | ".yaml" : "text/yaml", 551 | ".yml" : "text/yaml", 552 | ".zip" : "application/zip" 553 | } 554 | }; 555 | -------------------------------------------------------------------------------- /extern/json-template.js: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2009 Andy Chu 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | // 16 | // JavaScript implementation of json-template. 17 | // 18 | 19 | // This is predefined in tests, shouldn't be defined anywhere else. TODO: Do 20 | // something nicer. 21 | var log = log || function() {}; 22 | var repr = repr || function() {}; 23 | 24 | 25 | // The "module" exported by this script is called "jsontemplate": 26 | 27 | var jsontemplate = function() { 28 | 29 | 30 | // Regex escaping for metacharacters 31 | function EscapeMeta(meta) { 32 | return meta.replace(/([\{\}\(\)\[\]\|\^\$\-\+\?])/g, '\\$1'); 33 | } 34 | 35 | var token_re_cache = {}; 36 | 37 | function _MakeTokenRegex(meta_left, meta_right) { 38 | var key = meta_left + meta_right; 39 | var regex = token_re_cache[key]; 40 | if (regex === undefined) { 41 | var str = '(' + EscapeMeta(meta_left) + '.*?' + EscapeMeta(meta_right) + 42 | '\n?)'; 43 | regex = new RegExp(str, 'g'); 44 | } 45 | return regex; 46 | } 47 | 48 | // 49 | // Formatters 50 | // 51 | 52 | function HtmlEscape(s) { 53 | return s.replace(/&/g,'&'). 54 | replace(/>/g,'>'). 55 | replace(//g,'>'). 61 | replace(/ 1) ? p : s; 93 | } 94 | 95 | function _Cycle(value, unused_context, args) { 96 | // Cycle between various values on consecutive integers. 97 | // @index starts from 1, so use 1-based indexing. 98 | return args[(value - 1) % args.length]; 99 | } 100 | 101 | var DEFAULT_FORMATTERS = { 102 | 'html': HtmlEscape, 103 | 'htmltag': HtmlTagEscape, 104 | 'html-attr-value': HtmlTagEscape, 105 | 'str': ToString, 106 | 'raw': function(x) { return x; }, 107 | 'AbsUrl': function(value, context) { 108 | // TODO: Normalize leading/trailing slashes 109 | return context.get('base-url') + '/' + value; 110 | }, 111 | 'plain-url': function(x) { 112 | return '' + HtmlEscape(x) + '' ; 113 | } 114 | }; 115 | 116 | var DEFAULT_PREDICATES = { 117 | 'singular?': function(x) { return x == 1; }, 118 | 'plural?': function(x) { return x > 1; }, 119 | 'Debug?': function(unused, context) { 120 | try { 121 | return context.get('debug'); 122 | } catch(err) { 123 | if (err.name == 'UndefinedVariable') { 124 | return false; 125 | } else { 126 | throw err; 127 | } 128 | } 129 | } 130 | }; 131 | 132 | var FunctionRegistry = function() { 133 | return { 134 | lookup: function(user_str) { 135 | return [null, null]; 136 | } 137 | }; 138 | }; 139 | 140 | var SimpleRegistry = function(obj) { 141 | return { 142 | lookup: function(user_str) { 143 | var func = obj[user_str] || null; 144 | return [func, null]; 145 | } 146 | }; 147 | }; 148 | 149 | var CallableRegistry = function(callable) { 150 | return { 151 | lookup: function(user_str) { 152 | var func = callable(user_str); 153 | return [func, null]; 154 | } 155 | }; 156 | }; 157 | 158 | // Default formatters which can't be expressed in DEFAULT_FORMATTERS 159 | var PrefixRegistry = function(functions) { 160 | return { 161 | lookup: function(user_str) { 162 | for (var i = 0; i < functions.length; i++) { 163 | var name = functions[i].name, func = functions[i].func; 164 | if (user_str.slice(0, name.length) == name) { 165 | // Delimiter is usually a space, but could be something else 166 | var args; 167 | var splitchar = user_str.charAt(name.length); 168 | if (splitchar === '') { 169 | args = []; // No arguments 170 | } else { 171 | args = user_str.split(splitchar).slice(1); 172 | } 173 | return [func, args]; 174 | } 175 | } 176 | return [null, null]; // No formatter 177 | } 178 | }; 179 | }; 180 | 181 | var ChainedRegistry = function(registries) { 182 | return { 183 | lookup: function(user_str) { 184 | for (var i=0; i 0) { 460 | // TODO: check that items is an array; apparently this is hard in JavaScript 461 | //if type(items) is not list: 462 | // raise EvaluationError('Expected a list; got %s' % type(items)) 463 | 464 | // Execute the statements in the block for every item in the list. 465 | // Execute the alternate block on every iteration except the last. Each 466 | // item could be an atom (string, integer, etc.) or a dictionary. 467 | 468 | var last_index = items.length - 1; 469 | var statements = block.Statements(); 470 | var alt_statements = block.Statements('alternate'); 471 | 472 | for (var i=0; context.next() !== undefined; i++) { 473 | _Execute(statements, context, callback); 474 | if (i != last_index) { 475 | _Execute(alt_statements, context, callback); 476 | } 477 | } 478 | } else { 479 | _Execute(block.Statements('or'), context, callback); 480 | } 481 | 482 | context.pop(); 483 | } 484 | 485 | 486 | var _SECTION_RE = /(repeated)?\s*(section)\s+(\S+)?/; 487 | var _OR_RE = /or(?:\s+(.+))?/; 488 | var _IF_RE = /if(?:\s+(.+))?/; 489 | 490 | 491 | // Turn a object literal, function, or Registry into a Registry 492 | function MakeRegistry(obj) { 493 | if (!obj) { 494 | // if null/undefined, use a totally empty FunctionRegistry 495 | return new FunctionRegistry(); 496 | } else if (typeof obj === 'function') { 497 | return new CallableRegistry(obj); 498 | } else if (obj.lookup !== undefined) { 499 | // TODO: Is this a good pattern? There is a namespace conflict where get 500 | // could be either a formatter or a method on a FunctionRegistry. 501 | // instanceof might be more robust. 502 | return obj; 503 | } else if (typeof obj === 'object') { 504 | return new SimpleRegistry(obj); 505 | } 506 | } 507 | 508 | // TODO: The compile function could be in a different module, in case we want to 509 | // compile on the server side. 510 | function _Compile(template_str, options) { 511 | var more_formatters = MakeRegistry(options.more_formatters); 512 | 513 | // default formatters with arguments 514 | var default_formatters = PrefixRegistry([ 515 | {name: 'pluralize', func: _Pluralize}, 516 | {name: 'cycle', func: _Cycle} 517 | ]); 518 | var all_formatters = new ChainedRegistry([ 519 | more_formatters, 520 | SimpleRegistry(DEFAULT_FORMATTERS), 521 | default_formatters 522 | ]); 523 | 524 | var more_predicates = MakeRegistry(options.more_predicates); 525 | 526 | // TODO: Add defaults 527 | var all_predicates = new ChainedRegistry([ 528 | more_predicates, SimpleRegistry(DEFAULT_PREDICATES) 529 | ]); 530 | 531 | // We want to allow an explicit null value for default_formatter, which means 532 | // that an error is raised if no formatter is specified. 533 | var default_formatter; 534 | if (options.default_formatter === undefined) { 535 | default_formatter = 'str'; 536 | } else { 537 | default_formatter = options.default_formatter; 538 | } 539 | 540 | function GetFormatter(format_str) { 541 | var pair = all_formatters.lookup(format_str); 542 | if (!pair[0]) { 543 | throw { 544 | name: 'BadFormatter', 545 | message: format_str + ' is not a valid formatter' 546 | }; 547 | } 548 | return pair; 549 | } 550 | 551 | function GetPredicate(pred_str) { 552 | var pair = all_predicates.lookup(pred_str); 553 | if (!pair[0]) { 554 | throw { 555 | name: 'BadPredicate', 556 | message: pred_str + ' is not a valid predicate' 557 | }; 558 | } 559 | return pair; 560 | } 561 | 562 | var format_char = options.format_char || '|'; 563 | if (format_char != ':' && format_char != '|') { 564 | throw { 565 | name: 'ConfigurationError', 566 | message: 'Only format characters : and | are accepted' 567 | }; 568 | } 569 | 570 | var meta = options.meta || '{}'; 571 | var n = meta.length; 572 | if (n % 2 == 1) { 573 | throw { 574 | name: 'ConfigurationError', 575 | message: meta + ' has an odd number of metacharacters' 576 | }; 577 | } 578 | var meta_left = meta.substring(0, n/2); 579 | var meta_right = meta.substring(n/2, n); 580 | 581 | var token_re = _MakeTokenRegex(meta_left, meta_right); 582 | var current_block = _Section({}); 583 | var stack = [current_block]; 584 | 585 | var strip_num = meta_left.length; // assume they're the same length 586 | 587 | var token_match; 588 | var last_index = 0; 589 | 590 | while (true) { 591 | token_match = token_re.exec(template_str); 592 | if (token_match === null) { 593 | break; 594 | } else { 595 | var token = token_match[0]; 596 | } 597 | 598 | // Add the previous literal to the program 599 | if (token_match.index > last_index) { 600 | var tok = template_str.slice(last_index, token_match.index); 601 | current_block.Append(tok); 602 | } 603 | last_index = token_re.lastIndex; 604 | 605 | var had_newline = false; 606 | if (token.slice(-1) == '\n') { 607 | token = token.slice(null, -1); 608 | had_newline = true; 609 | } 610 | 611 | token = token.slice(strip_num, -strip_num); 612 | 613 | if (token.charAt(0) == '#') { 614 | continue; // comment 615 | } 616 | 617 | if (token.charAt(0) == '.') { // Keyword 618 | token = token.substring(1, token.length); 619 | 620 | var literal = { 621 | 'meta-left': meta_left, 622 | 'meta-right': meta_right, 623 | 'space': ' ', 624 | 'tab': '\t', 625 | 'newline': '\n' 626 | }[token]; 627 | 628 | if (literal !== undefined) { 629 | current_block.Append(literal); 630 | continue; 631 | } 632 | 633 | var new_block, func; 634 | 635 | var section_match = token.match(_SECTION_RE); 636 | if (section_match) { 637 | var repeated = section_match[1]; 638 | var section_name = section_match[3]; 639 | 640 | if (repeated) { 641 | func = _DoRepeatedSection; 642 | new_block = _RepeatedSection({section_name: section_name}); 643 | } else { 644 | func = _DoSection; 645 | new_block = _Section({section_name: section_name}); 646 | } 647 | current_block.Append([func, new_block]); 648 | stack.push(new_block); 649 | current_block = new_block; 650 | continue; 651 | } 652 | 653 | var pred_str, pred; 654 | 655 | // Check {.or pred?} before {.pred?} 656 | var or_match = token.match(_OR_RE); 657 | if (or_match) { 658 | pred_str = or_match[1]; 659 | pred = pred_str ? GetPredicate(pred_str) : null; 660 | current_block.NewOrClause(pred); 661 | continue; 662 | } 663 | 664 | // Match either {.pred?} or {.if pred?} 665 | var matched = false; 666 | 667 | var if_match = token.match(_IF_RE); 668 | if (if_match) { 669 | pred_str = if_match[1]; 670 | matched = true; 671 | } else if (token.charAt(token.length-1) == '?') { 672 | pred_str = token; 673 | matched = true; 674 | } 675 | if (matched) { 676 | pred = pred_str ? GetPredicate(pred_str) : null; 677 | new_block = _PredicateSection(); 678 | new_block.NewOrClause(pred); 679 | current_block.Append([_DoPredicates, new_block]); 680 | stack.push(new_block); 681 | current_block = new_block; 682 | continue; 683 | } 684 | 685 | if (token == 'alternates with') { 686 | current_block.AlternatesWith(); 687 | continue; 688 | } 689 | 690 | if (token == 'end') { 691 | // End the block 692 | stack.pop(); 693 | if (stack.length > 0) { 694 | current_block = stack[stack.length-1]; 695 | } else { 696 | throw { 697 | name: 'TemplateSyntaxError', 698 | message: 'Got too many {end} statements' 699 | }; 700 | } 701 | continue; 702 | } 703 | } 704 | 705 | // A variable substitution 706 | var parts = token.split(format_char); 707 | var formatters; 708 | var name; 709 | if (parts.length == 1) { 710 | if (default_formatter === null) { 711 | throw { 712 | name: 'MissingFormatter', 713 | message: 'This template requires explicit formatters.' 714 | }; 715 | } 716 | // If no formatter is specified, use the default. 717 | formatters = [GetFormatter(default_formatter)]; 718 | name = token; 719 | } else { 720 | formatters = []; 721 | for (var j=1; j