├── test ├── web │ └── index.html ├── test.js └── test-api.js ├── doc ├── img │ ├── bg1.png │ ├── bg2.png │ └── bg3.png ├── screen.css └── Readme.md ├── web ├── img │ └── web.jpg ├── 404.html ├── flip.html ├── template.html ├── doctor.html ├── index.html ├── ws-chat.html ├── socket.io-chat.html ├── chat.html └── js │ ├── EventSource.js │ ├── additions.js │ ├── json2.js │ └── scout.js ├── .gitignore ├── .npmignore ├── .travis.yml ├── TODO.md ├── README.md ├── CHANGELOG.md ├── package.json ├── LICENSE.md ├── lib ├── Walkthrough ├── License └── camp.js ├── index.html ├── app.js ├── Makefile └── scout.js /test/web/index.html: -------------------------------------------------------------------------------- 1 | 404 -------------------------------------------------------------------------------- /doc/img/bg1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/sc/master/doc/img/bg1.png -------------------------------------------------------------------------------- /doc/img/bg2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/sc/master/doc/img/bg2.png -------------------------------------------------------------------------------- /doc/img/bg3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/sc/master/doc/img/bg3.png -------------------------------------------------------------------------------- /web/img/web.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/badges/sc/master/web/img/web.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | *.dat 3 | *.out 4 | *.pid 5 | 6 | node_modules 7 | https.* 8 | 9 | publish 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /TODO.md 2 | /README.md 3 | /index.html 4 | /doc/img/ 5 | /doc/manual.html 6 | /doc/screen.css 7 | -------------------------------------------------------------------------------- /web/404.html: -------------------------------------------------------------------------------- 1 | 2 | ONOES 3 |

LOL

4 |

All urls ending in ".lol" are routed to this page.

5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 6 4 | - 10 5 | 6 | branches: 7 | only: 8 | - master 9 | - /^greenkeeper/.*$/ 10 | 11 | notifications: 12 | email: 13 | - thaddee.tyl@gmail.com 14 | irc: "irc.freenode.org#tree" 15 | -------------------------------------------------------------------------------- /web/flip.html: -------------------------------------------------------------------------------- 1 | 4 | 10 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | WHAT SHOULD WE DO? 2 | ================== 3 | 4 | Add HTTP digest access authentification. 5 | 6 | Add cache via the Expires header. 7 | 8 | Make the logging system configurable. A single server should be able to log 9 | warnings and errors in a configurable way. We can achieve that by allowing to 10 | set a function that takes a string. By default, that function is console.error. 11 | 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Fork of Scoutcamp maintained for [Shields][] 2 | 3 | [shields]: https://shields.io 4 | 5 | ## Contribute 6 | 7 | Discussions in the [main repo][issues], please. 8 | 9 | [issues]: https://github.com/badges/shields/issues 10 | 11 | ## License 12 | 13 | MIT. See LICENSE.md for more. 14 | 15 | The "Red spiders" comic is CC BY-NC 2.5 from Randall Munroe, XKCD. 16 | Link in `web/index.html`. 17 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 18.1.1 4 | 5 | - Fix crash on malformed auth header 6 | 7 | ## 18.1.0 8 | 9 | - Add add `staticMaxAge` param for setting `Cache-Control` header on static files. 10 | 11 | ## 18.0.0 12 | 13 | - Add top-level `.create()` method which has the same signature as `.start()` 14 | but does not start the server. Invoke `.startAsConfigured()` to use the 15 | configured host and port name. 16 | -------------------------------------------------------------------------------- /web/template.html: -------------------------------------------------------------------------------- 1 | 2 | {{ var title = title || 'Success!' 3 | var info = info || 'This document has been templated.' }} 4 | {{= title in html}} 5 | 6 | 7 |

{{= title in html}}

8 |

{{= info in html}}

9 | 10 |

Give it a try

11 |
12 |

13 | 14 |

15 | 16 |

17 | 18 |
19 | -------------------------------------------------------------------------------- /web/doctor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | doctor 4 | 5 |
6 |
7 | 8 |
9 | 10 | 11 |
12 | 13 | 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Thaddee Tyl (http://espadrine.github.com/)", 3 | "contributors": [ 4 | "Thaddee Tyl ", 5 | "Jan Keromnes " 6 | ], 7 | "name": "@shields_io/camp", 8 | "description": "Fork of Scoutcamp maintained for Shields", 9 | "version": "18.1.1", 10 | "homepage": "https://github.com/badges/sc", 11 | "repository": "badges/sc", 12 | "main": "lib/camp.js", 13 | "scripts": { 14 | "test": "make test" 15 | }, 16 | "dependencies": { 17 | "cookies": "~0.7.1", 18 | "fleau": "~16.2.0", 19 | "formidable": "~1.2.0", 20 | "multilog": "~14.11.22", 21 | "socket.io": "^2.0.4", 22 | "spdy": "^4.0.2", 23 | "handle-thing": "^2.0.1", 24 | "ws": "^6.1.0" 25 | }, 26 | "devDependencies": {}, 27 | "optionalDependencies": {}, 28 | "engines": { 29 | "node": ">=0.11.8" 30 | }, 31 | "license": "MIT", 32 | "directories": { 33 | "lib": "./lib", 34 | "doc": "./doc", 35 | "examples": "node app 1234" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright © 2011-present Thaddée Tyl 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | ScoutCamp 3 | 4 | 22 | 23 | 33 |
34 | 35 |

Success

36 | 37 |

You're on the web!

38 |

Please tinker, chop, thrash and fool around!

39 | -------------------------------------------------------------------------------- /web/ws-chat.html: -------------------------------------------------------------------------------- 1 | 2 | Chat 3 | 4 |
5 | 6 |
7 |
8 |
9 | 10 | 20 | 21 | 44 | -------------------------------------------------------------------------------- /web/socket.io-chat.html: -------------------------------------------------------------------------------- 1 | 2 | Chat 3 | 4 |
5 | 6 |
7 |
8 |
9 | 10 | 20 | 21 | 22 | 46 | -------------------------------------------------------------------------------- /web/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Chat 4 | 5 |
6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 23 | 24 | 49 | 50 | -------------------------------------------------------------------------------- /lib/Walkthrough: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 888888b, 8888 8888B 8888B 888888888 6 | 88 `*8; dP `88' `38^ 88P^^^^^` 7 | 88 88 dP `88. ;8" 88 8 | 88 88 dP `88. ;8P 88 9 | 88 88 dP `88. .8P 888888 10 | 88 .88 dP `88. .8? 8P```' 11 | 88 .;8p dP .X8i8Y. 8R........ 12 | 8888884P 8888 8888888 8888888888 13 | 14 | 15 | __ __ __ 16 | into the /_ / / /_ /_ 17 | / / /__ /__ __/ 18 | 19 | 20 | of 21 | ____ __ ___ _ _ _____ __ ___ _ _ ___ 22 | / __// _/ _ \| || |_ _| / _/ \_ | \/ | _ \ 23 | \__ \ |_| _ || || | | | | |_ /_ | | _/ 24 | /___/\__\___/\____/ |_| \__\|___|_||_|_| 25 | 26 | 27 | lib/ Major server-side files. 28 | camp.js Main export point. 29 | mime.json List of default mime types associated with file extensions. 30 | plate.js Default template engine. 31 | 32 | doc/ Documentation folder (including the website). 33 | Readme.md Must-read. 34 | 35 | app.js Example server. Run it with `sudo node app` 36 | web/ Example document root folder. 37 | js/ 38 | *.js Files necessary to build scout.js. 39 | 40 | scout.js Compressed client-side library. 41 | Designed to be `cp .node_modules/camp/scout.js web/`. 42 | 43 | test/ Testing directory. Feel free to add your own stuff. 44 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ScoutCamp 5 | 6 |

ScoutCamp

7 |
8 | /skoutkamp/ Web apps done right. 9 |
10 | 11 |

12 | "Hassle-free Node.js server framework.
13 | Arbitrary routers. Cookies. Forms. Auth.
14 | Compiled templates. Fetch. WebSockets. Server-Side Events." 15 |

16 | 17 | 18 | 19 |
DOWNLOAD 22 | SOURCES 25 |
26 | 27 |

Instructions

28 | 29 |
30 | $ npm install camp
31 | 
32 | 33 |

34 | Start with the 35 | readme. 36 |
37 | Look at an 38 | example. 39 |
40 | Read the 41 | reference. 42 |

43 | 44 | 60 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | // Server demo. `node app [PORT [https]]` 2 | // © 2011-2017 Thaddée Tyl, Jan Keromnes. LGPL. 3 | 4 | const port = +process.argv[2] || +process.env.PORT || 1234 5 | let camp = require('./lib/camp.js') 6 | let sc = camp.start({port: port, secure: process.argv[3] === 'https'}) 7 | console.log('http://[::1]:' + port) 8 | 9 | // Templating demo: /template.html?title=Hello&info=[Redacted]. 10 | sc.path('/template.html') 11 | 12 | // Templating demo with multiple templates and path parameter. 13 | // /html.template/Hello/World 14 | let flip = sc.template(['web/template.html', 'web/flip.html']) 15 | sc.path('/html.template/:title/:info', (req, res) => { 16 | res.template(req.data, flip) 17 | }) 18 | 19 | // Doctor demo: /doctor?text=… 20 | let replies = ['Ok.', 'Oh⁉', 'Is that so?', 'How interesting!', 21 | 'Hm…', 'What do you mean?', 'So say we all.'] 22 | sc.post('/doctor', (req, res) => { 23 | replies.push(req.query.text) 24 | res.json({reply: replies[Math.random() * replies.length|0]}) 25 | }) 26 | 27 | // Chat demo 28 | let chat = sc.eventSource('/all') 29 | sc.post('/talk', (req, res) => {chat.send(req.data); res.end()}) 30 | 31 | // WebSocket chat demo 32 | sc.wsBroadcast('/chat', (req, res) => res.send(req.data)) 33 | 34 | // Socket.io chat demo 35 | const ioChat = sc.io.of('/chat'); 36 | ioChat.on('connection', socket => 37 | socket.on('message', msg => ioChat.send(msg))); 38 | 39 | // Not found demo 40 | sc.notFound('/*.lol', (req, res) => res.file('/404.html')) 41 | 42 | // Basic authentication demo 43 | sc.get('/secret', (req, res) => { 44 | if (req.username === 'Caesar' && req.password === '1234') { 45 | res.end('Congrats, you found it!') 46 | } else { 47 | res.statusCode = 401 48 | res.setHeader('WWW-Authenticate', 'Basic') 49 | res.end('Nothing to hide here!') 50 | } 51 | }) 52 | 53 | // Low-level handler 54 | sc.handle((req, res, down) => { 55 | res.setHeader('X-Open-Source', 'https://github.com/espadrine/sc/') 56 | down() 57 | }) 58 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // test.js: A module for unit tests. 2 | // Copyright © 2011-2013 Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | // Code covered by the LGPL license. 4 | 5 | var Test = function () { this.n = 0; this.errors = 0; }; 6 | 7 | // Deep inequality. 8 | function notEqual(a, b) { 9 | if (a instanceof Array || a instanceof Object) { 10 | if (typeof a === typeof b) { 11 | for (var key in a) { 12 | if (notEqual(a[key], b[key])) { 13 | return true; 14 | } 15 | } 16 | for (var key in b) { 17 | if (notEqual(a[key], b[key])) { 18 | return true; 19 | } 20 | } 21 | return false; 22 | } else { 23 | return true; 24 | } 25 | } else { 26 | return a !== b; 27 | } 28 | } 29 | 30 | Test.prototype.eq = function (a, b, msg) { 31 | this.n ++; 32 | if (notEqual(a, b)) { 33 | console.log ('#' + this.n + ' failed: got ' + JSON.stringify (a) + 34 | ' instead of ' + JSON.stringify (b)); 35 | if (msg) { 36 | console.log (msg.split('\n').map(function(e){return ' '+e;}).join('\n')); 37 | } 38 | this.errors ++; 39 | } 40 | }; 41 | 42 | Test.prototype.each = function (tests, result) { 43 | var len = tests.length; 44 | var i = tests.length; 45 | function endTest () { 46 | i++; 47 | // When all tests have been performed… 48 | if (i === len) { result (); } 49 | } 50 | for (var j = 0; j < tests.length; j++) { 51 | tests[j] (endTest); 52 | } 53 | }; 54 | 55 | Test.prototype.seq = function (tests, result) { 56 | var len = tests.length; 57 | var i = 0; 58 | function endTest() { 59 | i++; 60 | // When all tests have been performed… 61 | if (i === len) { result (); } 62 | else { tests[i] (endTest); } 63 | } 64 | tests[i] (endTest); 65 | }; 66 | 67 | Test.prototype.tldr = function () { 68 | if (this.errors === 0) { console.log ('All ' + this.n + ' tests passed.'); 69 | } else if (this.errors === this.n) { console.log ('All tests failed.'); 70 | } else { 71 | console.log ((this.n - this.errors) + ' tests passed out of ' + 72 | this.n + ' (' + 73 | (100 * (1 - this.errors/this.n)).toFixed (2) + '%).'); 74 | } 75 | }; 76 | 77 | Test.prototype.exit = function () { 78 | process.exit((this.errors > 0)? 1: 0); 79 | }; 80 | 81 | module.exports = Test; 82 | 83 | -------------------------------------------------------------------------------- /doc/screen.css: -------------------------------------------------------------------------------- 1 | html { 2 | color: #444; 3 | text-shadow: #EC7979 0px 1px 4px; 4 | background: url('img/bg1.png'), url('img/bg3.png'),url('img/bg2.png'), linen; /* linen = #faf0e6. */ 5 | font-family: "Droid Sans", sans-serif; 6 | overflow-y: scroll; 7 | } 8 | 9 | ::-moz-selection { background: #ff5e99; color: #fff; text-shadow: none; } 10 | ::selection { background: #ff5e99; color: #fff; text-shadow: none; } 11 | 12 | h1 { 13 | text-align: right; 14 | font-family: "Ubuntu Mono", monospace; 15 | border-bottom: solid 1px #222; 16 | } 17 | span.purple { 18 | color: purple; 19 | } 20 | 21 | #subtitle { 22 | text-align: center; 23 | } 24 | 25 | h2, h3, table, form { 26 | margin: auto 20px; 27 | } 28 | p, pre, ol, ul, dl { 29 | margin: 12pt 70px; 30 | } 31 | 32 | ol>li, ul>li, dl>dt { 33 | margin-top: 4pt; 34 | } 35 | 36 | h2, h3 { 37 | color: purple; 38 | font-family: 'IM Fell DW Pica', serif; 39 | font-style: italic; 40 | margin-top: 40px; 41 | } 42 | 43 | pre, code, kbd { 44 | font-family: "Ubuntu Mono", monospace; 45 | background-color: #faffe6; 46 | } 47 | pre { 48 | padding: 10px; 49 | border: 1px dashed purple; 50 | letter-spacing: 1px; 51 | text-indent: 0px; 52 | } 53 | 54 | a, a:visited { 55 | color: purple; 56 | text-shadow: 777 0px 1px 4px; 57 | text-decoration: none; 58 | } 59 | a:hover { 60 | color: rgba(1,1,1,0.4); 61 | text-shadow: purple 0px 0px 1px; 62 | } 63 | 64 | textarea { 65 | width: 500px; 66 | height: 150px; 67 | } 68 | 69 | hr { 70 | height: 1px; 71 | border: 0px; 72 | background-color: #423; 73 | margin: 3em; 74 | color: 75 | } 76 | 77 | 78 | /* Heads */ 79 | 80 | span.ipa { font-style: auto; font-family: sans-serif; margin-right: 7px; } 81 | .right { text-align: right; } 82 | .awesome { font-size: 1.2em; width: 50%; margin: 1cm auto; } 83 | .speech { font-style: italic; font-size: 1.4em; font-family: "Linux Libertine O",serif; } 84 | .entry { 85 | font-size: x-large; 86 | font-weight: bold; 87 | margin-right: 13px; 88 | } 89 | .glow { 90 | text-shadow: hsl(260, 70%, 70%) 0px 0px 42px; 91 | background: none; 92 | text-decoration: underline; 93 | } 94 | 95 | 96 | /* Manual */ 97 | 98 | .manual { 99 | text-indent: 7px; 100 | } 101 | 102 | 103 | /* Downloads */ 104 | .downloads { 105 | margin: auto; 106 | border-spacing: 7pt; 107 | } 108 | .downloads a { 109 | display: block; 110 | padding: 12pt; 111 | border-radius: 3px; 112 | -moz-border-radius: 3px; 113 | 114 | color: #c80; 115 | text-shadow: #b34d4d 1px 1px 0px, #644 -1px -1px 1px, 116 | #c90 2px 2px 0px, #c90 3px 3px 0px; 117 | box-shadow: #94b -1px -1px 0px inset, #c95 -2px -2px 1px inset, 118 | #94b 1px 1px 0px inset, #950 2px 2px 1px inset, 119 | #606 0px 0px 50px inset; 120 | -webkit-transition: 0.3s ease-out box-shadow; 121 | -moz-transition: 0.3s ease-out box-shadow; 122 | transition: 0.3s ease-out box-shadow; 123 | 124 | letter-spacing: 2.5pt; 125 | font-family: "Ubuntu Mono", monospace; 126 | font-size: 14pt; 127 | font-style: italic; 128 | font-weight: bold; 129 | text-decoration: none; 130 | } 131 | .downloads a:hover { 132 | box-shadow: #94b -1px -1px 0px inset, #c99 -2px -2px 1px inset, 133 | #94b 1px 1px 0px inset, #953 2px 2px 1px inset, 134 | #606 0px 0px 20px inset; /* Main one */ 135 | } 136 | .scout { 137 | background-color: hsl(280, 50%, 45%); /* #9020d0; */ 138 | } 139 | .scoutmin { 140 | background-color: hsl(280, 70%, 50%); /* #9020d0; */ 141 | } 142 | .camp { 143 | background-color: hsl(260, 70%, 60%); 144 | } 145 | .zipball { 146 | background-color: #e46787; 147 | } 148 | .browse { 149 | background-color: #7dc4e8; /* or hsl(110, 75%, 60%) ? */ 150 | } 151 | .nodejs { 152 | background-color: #bea3f5; 153 | } 154 | 155 | /* Comments */ 156 | div.idea { 157 | border: 1px solid #503; 158 | -webkit-box-shadow: 0.7pt 0.7pt 4px #503 inset; 159 | -moz-box-shadow: 0.7pt 0.7pt 4px #503 inset; 160 | border-radius: 3px; 161 | } 162 | 163 | table.comment tr { 164 | border-radius: 5px 7px; 165 | } 166 | table.comment tr:nth-child(2n+1) { background-color: rgba(200,0,200, 0.2); } 167 | table.comment tr:nth-child(2n) { background-color: rgba(200,0,200, 0.1); } 168 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile: Publish your website, start/stop your server. 2 | # Copyright © 2011-2015 Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | # Code covered by the LGPL license. 4 | 5 | # The name of your main server file. 6 | SERVER = app.js 7 | 8 | # The folder where your precious website is. 9 | WEB = web 10 | 11 | # The folder where your minified, production-ready website should be published. 12 | # Warning: `make` and `make clean` will delete this folder. 13 | PUBLISH = publish 14 | 15 | # The JS minifier. Change the priority order to your convenience. 16 | # It must accept some JS in stdin, and produce the result on stdout. 17 | # Note: you must create `google-closure.sh` yourself if you want it. 18 | JSMIN = uglifyjs jsmin google-closure.sh js-minifier 19 | 20 | # The suffix to use for minified files. 21 | MIN = min 22 | 23 | # To make your server secure, generate SSL certificates (e.g. `make https`), 24 | # then start it with something like `SECURE=secure make start`. 25 | ifdef SECURE 26 | PORT ?= 443 27 | SECURE = secure 28 | else 29 | PORT ?= 80 30 | SECURE = insecure 31 | endif 32 | DEBUG ?= 0 33 | 34 | # The output of console.log statements goes in this file when you `make`. 35 | # Note: when you `make debug`, the output appears on the console. 36 | LOG = node.log 37 | 38 | # You can define custom rules and settings in such a file. 39 | -include local.mk 40 | 41 | # Default behavior for `make`. 42 | all: publish stop start 43 | 44 | # Publish your website. 45 | publish: clean copy minify 46 | 47 | # Try your unpublished website. 48 | debug: stop startweb 49 | 50 | # Delete generated files and logs. 51 | clean: 52 | @echo "clean" 53 | @rm -rf $(LOG) $(PUBLISH) 54 | 55 | # Simply copy all website files over to your publishing folder. 56 | copy: 57 | @echo "copy" 58 | @cp -rf $(WEB) $(PUBLISH) 59 | 60 | # Minify everything we can inside your publishing folder. 61 | minify: 62 | @echo "minify" 63 | @for ajsmin in $(JSMIN); do \ 64 | if which $$ajsmin > /dev/null; then chosenjsmin=$$ajsmin; break; fi; \ 65 | done; \ 66 | if which $$chosenjsmin > /dev/null ; then \ 67 | for file in `find $(PUBLISH) -name '*\.js'`; do \ 68 | $$chosenjsmin < "$${file}" > "$${file}$(MIN)"; \ 69 | mv "$${file}$(MIN)" "$${file}"; \ 70 | done; \ 71 | else \ 72 | echo ' `sudo make jsmin` or install uglifyjs for minification.'; \ 73 | fi 74 | 75 | # Stop any previously-started Camp server. 76 | stop: 77 | @echo "stop" 78 | @for pid in `ps aux | grep -v make | grep node | grep $(SERVER) | awk '{print $$2}'` ; do \ 79 | kill -9 $$pid 2> /dev/null ; \ 80 | if [ "$$?" -ne "0" ] ; then \ 81 | sudo kill -9 $$pid 2> /dev/null ; \ 82 | fi \ 83 | done; \ 84 | 85 | # Start a Camp server with your published website (for production). 86 | start: 87 | @echo "start" 88 | @if [ `id -u` -ne "0" -a $(PORT) -lt 1024 ] ; \ 89 | then \ 90 | sudo node $(SERVER) $(PORT) $(SECURE) $(DEBUG) >> $(LOG) ; \ 91 | else \ 92 | node $(SERVER) $(PORT) $(SECURE) $(DEBUG) >> $(LOG) ; \ 93 | fi 94 | 95 | # Start a Camp server with your unpublished website (for development). 96 | startweb: 97 | @echo "start web" 98 | @if [ `id -u` -ne "0" -a $(PORT) -lt 1024 ] ; \ 99 | then \ 100 | sudo node $(SERVER) $(PORT) $(SECURE) $(DEBUG) >> $(LOG) ; \ 101 | else \ 102 | node $(SERVER) $(PORT) $(SECURE) $(DEBUG) >> $(LOG) ; \ 103 | fi 104 | 105 | # Run the ScoutCamp tests. 106 | test: 107 | node test/test-api.js 108 | 109 | # Update a ScoutCamp fork. 110 | # Warning: overwrites this Makefile, you may want to create a local.mk! 111 | update: 112 | @git clone https://github.com/espadrine/sc 113 | @cp sc/web/js/scout.js ./$(WEB)/js/scout.js 114 | @cp sc/lib/* ./lib/ 115 | @cp sc/Makefile . 116 | @rm -rf sc/ 117 | 118 | # Install Doug Crockford's `jsmin` in `/usr/bin/jsmin`. 119 | jsmin: 120 | @if [ `id -u` = "0" ] ; \ 121 | then wget "http://crockford.com/javascript/jsmin.c" && gcc -o /usr/bin/jsmin jsmin.c ; \ 122 | rm -rf jsmin.c ; \ 123 | else echo ' `sudo make jsmin`'; fi 124 | 125 | # Generate self-signed HTTPS credentials. 126 | https: https.crt 127 | 128 | # Delete HTTPS credentials. 129 | rmhttps: 130 | @echo "delete https credentials" 131 | @rm -rf https.key https.csr https.crt 132 | 133 | # Generate an SSL certificate secret key. Never share this! 134 | https.key: 135 | @openssl genrsa -out https.key 4096 136 | @chmod 400 https.key # read by owner 137 | 138 | # Generate a CSR (Certificate Signing Request) for someone to sign your 139 | # SSL certificate. 140 | https.csr: https.key 141 | @openssl req -new -sha256 -key https.key -out https.csr 142 | @chmod 400 https.csr # read by owner 143 | 144 | # Create a self-signed SSL certificate. 145 | # Warning: web users will be shown a useless security warning. 146 | https.crt: https.key https.csr 147 | @openssl x509 -req -days 365 -in https.csr -signkey https.key -out https.crt 148 | @chmod 444 https.crt # read by all 149 | 150 | # Download Scout's JS dependencies into your website's js/ folder. 151 | scout-update: 152 | @curl https://raw.github.com/jquery/sizzle/master/sizzle.js > $(WEB)/js/sizzle.js 2> /dev/null 153 | @curl https://raw.github.com/douglascrockford/JSON-js/master/json2.js > $(WEB)/js/json2.js 2> /dev/null 154 | @curl https://raw.github.com/remy/polyfills/master/EventSource.js > $(WEB)/js/EventSource.js 2> /dev/null 155 | 156 | # Build the `scout.js` client script. 157 | scout-build: 158 | @for ajsmin in $(JSMIN); do \ 159 | if which $$ajsmin > /dev/null; then chosenjsmin=$$ajsmin; break; fi; \ 160 | done; \ 161 | cat $(WEB)/js/sizzle.js $(WEB)/js/json2.js $(WEB)/js/EventSource.js $(WEB)/js/additions.js | $$ajsmin > $(WEB)/js/scout.js 162 | @cp $(WEB)/js/scout.js . 163 | 164 | # This is a self-documenting Makefile. 165 | help: 166 | @cat Makefile | less 167 | 168 | wtf: help 169 | 170 | ?: wtf 171 | 172 | coffee: 173 | @echo "\n ) (\n ( ) )\n _..,-(--,.._\n .-;'-.,____,.-';\n (( | |\n \`-; ;\n \\ /\n .-''\`-.____.-'''-.\n ( '------' )\n \`--..________..--'\n"; 174 | 175 | me a: 176 | @cd . 177 | 178 | sandwich: 179 | @if [ `id -u` = "0" ] ; then echo "OKAY." ; else echo "What? Make it yourself." ; fi 180 | 181 | .PHONY: all publish debug clean copy minify stop start startweb test update jsmin https scout-update scout-build help wtf ? coffee me a sandwich 182 | 183 | -------------------------------------------------------------------------------- /web/js/EventSource.js: -------------------------------------------------------------------------------- 1 | ;(function (global) { 2 | 3 | if ("EventSource" in global) return; 4 | 5 | var reTrim = /^(\s|\u00A0)+|(\s|\u00A0)+$/g; 6 | 7 | var EventSource = function (url) { 8 | var eventsource = this, 9 | interval = 500, // polling interval 10 | lastEventId = null, 11 | cache = ''; 12 | 13 | if (!url || typeof url != 'string') { 14 | throw new SyntaxError('Not enough arguments'); 15 | } 16 | 17 | this.URL = url; 18 | this.readyState = this.CONNECTING; 19 | this._pollTimer = null; 20 | this._xhr = null; 21 | 22 | function pollAgain(interval) { 23 | eventsource._pollTimer = setTimeout(function () { 24 | poll.call(eventsource); 25 | }, interval); 26 | } 27 | 28 | function poll() { 29 | try { // force hiding of the error message... insane? 30 | if (eventsource.readyState == eventsource.CLOSED) return; 31 | 32 | // NOTE: IE7 and upwards support 33 | var xhr = new XMLHttpRequest(); 34 | xhr.open('GET', eventsource.URL, true); 35 | xhr.setRequestHeader('Accept', 'text/event-stream'); 36 | xhr.setRequestHeader('Cache-Control', 'no-cache'); 37 | // we must make use of this on the server side if we're working with Android - because they don't trigger 38 | // readychange until the server connection is closed 39 | xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest'); 40 | 41 | if (lastEventId != null) xhr.setRequestHeader('Last-Event-ID', lastEventId); 42 | cache = ''; 43 | 44 | xhr.timeout = 50000; 45 | xhr.onreadystatechange = function () { 46 | if (this.readyState == 3 || (this.readyState == 4 && this.status == 200)) { 47 | // on success 48 | if (eventsource.readyState == eventsource.CONNECTING) { 49 | eventsource.readyState = eventsource.OPEN; 50 | eventsource.dispatchEvent('open', { type: 'open' }); 51 | } 52 | 53 | var responseText = ''; 54 | try { 55 | responseText = this.responseText || ''; 56 | } catch (e) {} 57 | 58 | // process this.responseText 59 | var parts = responseText.substr(cache.length).split("\n"), 60 | eventType = 'message', 61 | data = [], 62 | i = 0, 63 | line = ''; 64 | 65 | cache = responseText; 66 | 67 | // TODO handle 'event' (for buffer name), retry 68 | for (; i < parts.length; i++) { 69 | line = parts[i].replace(reTrim, ''); 70 | if (line.indexOf('event') == 0) { 71 | eventType = line.replace(/event:?\s*/, ''); 72 | } else if (line.indexOf('retry') == 0) { 73 | retry = parseInt(line.replace(/retry:?\s*/, '')); 74 | if(!isNaN(retry)) { interval = retry; } 75 | } else if (line.indexOf('data') == 0) { 76 | data.push(line.replace(/data:?\s*/, '')); 77 | } else if (line.indexOf('id:') == 0) { 78 | lastEventId = line.replace(/id:?\s*/, ''); 79 | } else if (line.indexOf('id') == 0) { // this resets the id 80 | lastEventId = null; 81 | } else if (line == '') { 82 | if (data.length) { 83 | var event = new MessageEvent(data.join('\n'), eventsource.url, lastEventId); 84 | eventsource.dispatchEvent(eventType, event); 85 | data = []; 86 | eventType = 'message'; 87 | } 88 | } 89 | } 90 | 91 | if (this.readyState == 4) pollAgain(interval); 92 | // don't need to poll again, because we're long-loading 93 | } else if (eventsource.readyState !== eventsource.CLOSED) { 94 | if (this.readyState == 4) { // and some other status 95 | // dispatch error 96 | eventsource.readyState = eventsource.CONNECTING; 97 | eventsource.dispatchEvent('error', { type: 'error' }); 98 | pollAgain(interval); 99 | } else if (this.readyState == 0) { // likely aborted 100 | pollAgain(interval); 101 | } else { 102 | } 103 | } 104 | }; 105 | 106 | xhr.send(); 107 | 108 | setTimeout(function () { 109 | if (true || xhr.readyState == 3) xhr.abort(); 110 | }, xhr.timeout); 111 | 112 | eventsource._xhr = xhr; 113 | 114 | } catch (e) { // in an attempt to silence the errors 115 | eventsource.dispatchEvent('error', { type: 'error', data: e.message }); // ??? 116 | } 117 | }; 118 | 119 | poll(); // init now 120 | }; 121 | 122 | EventSource.prototype = { 123 | close: function () { 124 | // closes the connection - disabling the polling 125 | this.readyState = this.CLOSED; 126 | clearInterval(this._pollTimer); 127 | this._xhr.abort(); 128 | }, 129 | CONNECTING: 0, 130 | OPEN: 1, 131 | CLOSED: 2, 132 | dispatchEvent: function (type, event) { 133 | var handlers = this['_' + type + 'Handlers']; 134 | if (handlers) { 135 | for (var i = 0; i < handlers.length; i++) { 136 | handlers[i].call(this, event); 137 | } 138 | } 139 | 140 | if (this['on' + type]) { 141 | this['on' + type].call(this, event); 142 | } 143 | }, 144 | addEventListener: function (type, handler) { 145 | if (!this['_' + type + 'Handlers']) { 146 | this['_' + type + 'Handlers'] = []; 147 | } 148 | 149 | this['_' + type + 'Handlers'].push(handler); 150 | }, 151 | removeEventListener: function (type, handler) { 152 | var handlers = this['_' + type + 'Handlers']; 153 | if (!handlers) { 154 | return; 155 | } 156 | for (var i = handlers.length - 1; i >= 0; --i) { 157 | if (handlers[i] === handler) { 158 | handlers.splice(i, 1); 159 | break; 160 | } 161 | } 162 | }, 163 | onerror: null, 164 | onmessage: null, 165 | onopen: null, 166 | readyState: 0, 167 | URL: '' 168 | }; 169 | 170 | var MessageEvent = function (data, origin, lastEventId) { 171 | this.data = data; 172 | this.origin = origin; 173 | this.lastEventId = lastEventId || ''; 174 | }; 175 | 176 | MessageEvent.prototype = { 177 | data: null, 178 | type: 'message', 179 | lastEventId: '', 180 | origin: '' 181 | }; 182 | 183 | if ('module' in global) module.exports = EventSource; 184 | global.EventSource = EventSource; 185 | 186 | })(this); 187 | -------------------------------------------------------------------------------- /web/js/additions.js: -------------------------------------------------------------------------------- 1 | /* Scout is an ajax library. 2 | * Copyright © 2010-2013 Thaddee Tyl. All rights reserved. 3 | * Produced under the MIT license. 4 | * 5 | * Requires Sizzle.js 6 | * Copyright 2010, The Dojo Foundation 7 | * Released under the MIT, BSD, and GPL licenses. 8 | * 9 | * Requires Json2.js by Douglas Crockford. 10 | * 11 | * Requires EventSource.js by Remy Sharp 12 | * under the MIT license . 13 | */ 14 | 15 | var Scout = function(){}; 16 | Scout = (function Scoutmaker () { 17 | 18 | /* xhr is in a closure. */ 19 | var xhr; 20 | if (window.XMLHttpRequest) { 21 | xhr = new XMLHttpRequest(); 22 | if (xhr.overrideMimeType) { 23 | xhr.overrideMimeType('text/xml'); /* Old Mozilla browsers. */ 24 | } 25 | } else { /* Betting on IE, I know no other implementation. */ 26 | try { 27 | xhr = new ActiveXObject("Msxml2.XMLHTTP"); 28 | } catch (e) { 29 | xhr = new ActiveXObject("Microsoft.XMLHTTP"); 30 | } 31 | } 32 | 33 | 34 | /* "On" property (beginning with the default parameters). */ 35 | 36 | var params = { 37 | method: 'POST', 38 | resp: function (resp, xhr) {}, 39 | error: function (status, xhr) {}, 40 | partial: function (raw, resp, xhr) {} 41 | }; 42 | 43 | /* Convert object literal to xhr-sendable. */ 44 | var toxhrsend = function (data) { 45 | var str = '', start = true; 46 | var jsondata = ''; 47 | for (var key in data) { 48 | if (typeof (jsondata = JSON.stringify(data[key])) === 'string') { 49 | str += (start? '': '&'); 50 | str += encodeURIComponent(key) + '=' + encodeURIComponent(jsondata); 51 | if (start) { start = false; } 52 | } 53 | } 54 | return str; 55 | }; 56 | 57 | var sendxhr = function (target, params) { 58 | if (params.action) { params.url = '/$' + params.action; } 59 | /* XHR stuff now. */ 60 | if (params.url) { 61 | /* We have somewhere to go to. */ 62 | xhr.onreadystatechange = function () { 63 | switch (xhr.readyState) { 64 | case 3: 65 | if (params.partial === undefined) { 66 | var raw = xhr.responseText; 67 | var resp; 68 | try { 69 | resp = JSON.parse(raw); 70 | } catch (e) {} 71 | params.partial.apply(target, [raw, resp, xhr]); 72 | } 73 | break; 74 | case 4: 75 | if (xhr.status === 200) { 76 | var resp = JSON.parse(xhr.responseText); 77 | params.resp.apply(target, [resp, xhr]); 78 | } else { 79 | params.error.apply(target, [xhr.status, xhr]); 80 | } 81 | break; 82 | } 83 | }; 84 | 85 | // foolproof: POST requests with nothing to send are 86 | // converted to GET requests. 87 | if (params.method === 'POST' 88 | && (params.data === {} || params.data === undefined)) { 89 | params.method = 'GET'; 90 | } 91 | xhr.open(params.method, 92 | params.url + (params.method === 'POST'? '': 93 | '?' + toxhrsend(params.data)), 94 | true, 95 | params.user, 96 | params.password); 97 | 98 | if (params.method === 'POST' && params.data !== {}) { 99 | xhr.setRequestHeader('Content-Type', 100 | 'application/x-www-form-urlencoded'); 101 | xhr.send(toxhrsend(params.data)); 102 | } else { 103 | xhr.send(null); 104 | } 105 | } 106 | }; 107 | 108 | var onprop = function (eventName, before) { 109 | /* Defaults. */ 110 | before = before || function (params, e, xhr) {}; 111 | 112 | /* Event Listener callback. */ 113 | var listenerfunc = function (e) { 114 | /* IE wrapper. */ 115 | if (!e && window.event) { e = event; } 116 | var target = e.target || e.srcElement || undefined; 117 | 118 | /* We must not change page unless otherwise stated. */ 119 | if (eventName === 'submit') { 120 | if (e.preventDefault) { e.preventDefault(); } 121 | else { e.returnValue = false; } 122 | } 123 | /*window.event.cancelBubble = true; 124 | if (e.stopPropagation) e.stopPropagation();*/ 125 | 126 | /* User action before xhr send. */ 127 | before.apply(target, [params, e, xhr]); 128 | 129 | sendxhr(target, params); 130 | }; 131 | 132 | if (document.addEventListener) { 133 | this.addEventListener(eventName, listenerfunc, false); 134 | } else { /* Hoping only IE lacks addEventListener. */ 135 | this.attachEvent('on' + eventName, listenerfunc); 136 | } 137 | }; 138 | 139 | /* End of "on" property. */ 140 | 141 | 142 | var ret = function (id) { 143 | /* Get the corresponding html element. */ 144 | var domelt = document.querySelector(id); 145 | if (!domelt) { 146 | return { on: function () {} }; 147 | } 148 | 149 | /* Now that we have the elt and onprop, assign it. */ 150 | domelt.on = onprop; 151 | return domelt; 152 | }; 153 | ret.send = function (before) { 154 | /* Fool-safe XHR creation if the current XHR object is in use. */ 155 | if (xhr.readyState === 1) { return Scoutmaker().send(before); } 156 | 157 | before = before || function (params, xhr) {}; 158 | 159 | return function () { 160 | before.apply(undefined, [params, xhr]); 161 | sendxhr(undefined, params); 162 | }; 163 | }; 164 | ret.maker = Scoutmaker; 165 | 166 | /* Wrapper for EventSource. */ 167 | ret.eventSource = function (channel) { 168 | if (channel[0] !== '/') { channel = '/$' + channel; } 169 | var es = new EventSource(channel); 170 | es.onrecv = function (cb) { 171 | es.onmessage = function (event) { 172 | cb(JSON.parse(event.data)); 173 | }; 174 | }; 175 | return es; 176 | }; 177 | 178 | /* Wrapper for socket.io – if downloaded. */ 179 | if (window.io) { 180 | ret.socket = function (namespace) { 181 | if (namespace === undefined) { 182 | namespace = '/'; // Default namespace. 183 | } 184 | return io.connect(namespace, { 185 | resource: '$socket.io' 186 | }); 187 | }; 188 | } 189 | 190 | /* If WebSockets are available, have them ready. */ 191 | if (window.WebSocket) { 192 | var wsSend = function wsSendJSON (json) { 193 | // Bound by the socket. 194 | this.send(JSON.stringify(json)); 195 | }; 196 | ret.webSocket = function newWebSocket (channel) { 197 | var socket = new WebSocket( 198 | // Trick: use the end of either http: or https:. 199 | 'ws' + window.location.protocol.slice(4) + '//' + 200 | window.location.host + 201 | '/$websocket:' + channel); 202 | socket.sendjson = wsSend.bind(socket); 203 | return socket; 204 | }; 205 | } 206 | 207 | 208 | return ret; 209 | })(); 210 | -------------------------------------------------------------------------------- /lib/License: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | 167 | -------------------------------------------------------------------------------- /test/test-api.js: -------------------------------------------------------------------------------- 1 | var camp = require('../lib/camp'); 2 | var fleau = require('fleau'); 3 | var http = require('http'); 4 | var mime = require('../lib/mime.json'); 5 | var Test = require('./test'); 6 | var t = new Test(); 7 | var WebSocket = require('ws'); 8 | 9 | var get = function (path, callback) { 10 | http.get('http://localhost:' + portNumber + path, callback); 11 | }; 12 | var post = function (path, postData, contentType, callback) { 13 | var options = { 14 | hostname: 'localhost', 15 | port: portNumber, 16 | path: path, 17 | method: 'POST' 18 | }; 19 | var req = http.request(options, callback); 20 | if (contentType) req.setHeader('Content-Type', contentType); 21 | if (postData) req.write(postData); 22 | req.end(); 23 | }; 24 | 25 | var launchTests = function () { 26 | t.seq([ 27 | function t0 (next) { 28 | get('', function (res) { 29 | t.eq(res.httpVersion, '1.1', "Server must be HTTP 1.1."); 30 | t.eq(res.headers['transfer-encoding'], 'chunked', 31 | "Connection should be chunked by default."); 32 | res.on('data', function (content) { 33 | t.eq('' + content, '404', 34 | "Did not receive content of index.html."); 35 | next(); 36 | }); 37 | }); 38 | }, 39 | 40 | function t1 (next) { 41 | // Using a streamed route. 42 | // Create a stream out of the following string. 43 | var template = '{{= text in plain}}\n{{for comment in comments{{\n- {{= comment in plain}}}} }}'; 44 | 45 | server.route( /^\/blog$/, function (query, match, end) { 46 | end ({ 47 | text: 'My, what a silly blog.', 48 | comments: ['first comment!', 'second comment…'] 49 | }, { 50 | string: template, // A stream. 51 | reader: fleau 52 | }); 53 | }); 54 | 55 | // Test that now. 56 | get('/blog', function (res) { 57 | var content = ''; 58 | res.on('data', function (chunk) { 59 | content += '' + chunk; 60 | }); 61 | res.on('end', function () { 62 | t.eq(content, 'My, what a silly blog.\n\n- first comment!\n- second comment…', 63 | "Routing a streamed template should work."); 64 | next(); 65 | }); 66 | }); 67 | }, 68 | 69 | function t2 (next) { 70 | // Using `sc.path` with/out named non-slash placeholders (i.e. ':foo'). 71 | server.path('foo', function (req, res) { 72 | res.end('foo'); 73 | }); 74 | 75 | server.path('foo/:bar', function (req, res) { 76 | res.end('bar=' + req.data.bar); 77 | }); 78 | 79 | server.path('foo/:bar/baz', function (req, res) { 80 | res.end('baz'); 81 | }); 82 | 83 | get('/foo', function (res) { 84 | res.on('data', function (body) { 85 | t.eq(String(body), 'foo', 86 | 'Basic sc.path should work.'); 87 | 88 | get('/foo/quux', function (res) { 89 | res.on('data', function (body) { 90 | t.eq(String(body), 'bar=quux', 91 | 'Named sc.path placeholder should work.'); 92 | 93 | get('/foo/quux/baz', function (res) { 94 | res.on('data', function (body) { 95 | t.eq(String(body), 'baz', 96 | 'Named sc.path placeholder should not pre-empt sub-paths.'); 97 | next(); 98 | }); 99 | }); 100 | }); 101 | }); 102 | }); 103 | }); // Mmmh… I love spaghetti! 104 | }, 105 | 106 | function t3 (next) { 107 | var data = { message: '☃' }; 108 | 109 | server.path('json', function (req, res) { 110 | res.statusCode = 418; // I'm a teapot (see RFC 2324). 111 | res.json(data, null, 2); 112 | }); 113 | 114 | get('/json', function (res) { 115 | t.eq(res.statusCode, 418, 116 | 'Setting `res.statusCode` before `res.json(data)` should work.'); 117 | t.eq(res.headers['content-type'], mime.json, 118 | 'Served content type should always be "' + mime.json + '".'); 119 | var body = ''; 120 | res.on('data', function (chunk) { 121 | body += String(chunk); 122 | }); 123 | res.on('end', function () { 124 | t.eq(String(body).trim(), JSON.stringify(data, null, 2), 125 | '`res.json(data, null, 2)` should return human-readable JSON.'); 126 | next(); 127 | }); 128 | }); 129 | }, 130 | 131 | function t4 (next) { 132 | var data = { id: '☃' }; 133 | var things = {}; 134 | 135 | server.get('things/:thing', function (req, res) { 136 | var thing = things[req.query.thing]; 137 | if (!thing) { 138 | res.statusCode = 404; 139 | res.end('Could not find the thing :('); 140 | return; 141 | } 142 | res.json(thing); 143 | }); 144 | 145 | server.post('things/:thing', function (req, res) { 146 | t.eq(req.headers['content-type'], mime.json, 147 | 'Request content type should be "' + mime.json + '".'); 148 | var json = ''; 149 | req.on('data', function (chunk) { 150 | json += String(chunk); 151 | }); 152 | req.on('end', function () { 153 | var thing = JSON.parse(json); 154 | t.eq(data.id, thing.id, 155 | 'Handling JSON post data should work.'); 156 | things[req.query.thing] = thing; 157 | res.statusCode = 201; // Created. 158 | res.end('Created the thing! :)'); 159 | }); 160 | }); 161 | 162 | post('/things/snowman', JSON.stringify(data), mime.json, function (res) { 163 | t.eq(res.statusCode, 201, 164 | 'Response status should be 201, not ' + res.statusCode + '.'); 165 | get('/things/snowman', function (res) { 166 | var json = ''; 167 | res.on('data', function (chunk) { 168 | json += String(chunk); 169 | }); 170 | res.on('end', function () { 171 | t.eq(data.id, JSON.parse(json).id, 172 | 'Should receive the original data back.'); 173 | next(); 174 | }); 175 | }); 176 | }); 177 | }, 178 | 179 | function t5 (next) { 180 | var name = 'Robert\'); DROP TABLE Students;--'; 181 | var data = encodeURI('Name="' + name + '"'); 182 | var urlencoded = 'application\/x-www-form-urlencoded'; 183 | 184 | server.path('/Students.php', function (req, res) { 185 | t.eq(req.query.Name, name, 'POST data should be processed.'); 186 | if (server.saveRequestChunks) { 187 | t.eq(req.savedChunks && req.savedChunks.toString(), data, 188 | 'Processed chunks should be saved in the request when required.'); 189 | } else { 190 | t.eq(req.savedChunks, undefined, 191 | 'Processed chunks should not be saved when it isn\'t required.'); 192 | } 193 | 194 | res.statusCode = 500; // Internal Server Error (not really) 195 | res.end('mysql> Query OK, 1337 rows affected (0.03 sec)\n'); 196 | }); 197 | 198 | server.saveRequestChunks = false; 199 | post('/Students.php', data, urlencoded, function (res) { 200 | t.eq(res.statusCode, 500, 201 | 'Response status should be 500, not ' + res.statusCode + '.'); 202 | server.saveRequestChunks = true; 203 | post('/Students.php', data, urlencoded, function (res) { 204 | t.eq(res.statusCode, 500, 205 | 'Response status should be 500, not ' + res.statusCode + '.'); 206 | next(); 207 | }); 208 | }); 209 | }, 210 | 211 | function t6 (next) { 212 | server.path('/redirection', function (req, res) { 213 | res.redirect('/other/path'); 214 | }); 215 | get('/redirection', function(res) { 216 | t.eq(res.statusCode, 303, 'Redirection should be a 303'); 217 | t.eq(res.headers['location'], '/other/path', 218 | 'Redirection should send the right location header'); 219 | next(); 220 | }); 221 | }, 222 | 223 | function t7 (next) { 224 | server.ws('/chat', function (socket) { 225 | socket.on('message', function (data) { 226 | var replaced = String(data).replace(/hunter2/g, '*******'); 227 | socket.send(replaced); 228 | }); 229 | }); 230 | 231 | var socket = new WebSocket('ws://localhost:' + portNumber + '/chat'); 232 | socket.on('open', function () { 233 | socket.send('you can go hunter2 my hunter2-ing hunter2'); 234 | }); 235 | socket.on('message', function (data) { 236 | t.eq(String(data), 'you can go ******* my *******-ing *******', 237 | 'No matter how many times you type hunter2, WebSocket chat should show *******'); 238 | next(); 239 | }); 240 | }, 241 | ], function end () { 242 | t.tldr(); 243 | t.exit(); 244 | }); 245 | }; 246 | 247 | // FIXME: is there a good way to make a server get a port for testing? 248 | var server; 249 | var portNumber = 8000; 250 | var startServer = function () { 251 | server = camp.start({port:portNumber, documentRoot:'./test/web'}); 252 | server.on('listening', launchTests); 253 | }; 254 | var serverStartDomain = require('domain').create(); 255 | serverStartDomain.on('error', function (err) { 256 | if (err.code === 'EADDRINUSE') { 257 | portNumber++; 258 | serverStartDomain.run(startServer); 259 | } else { 260 | throw err; 261 | } 262 | }); 263 | serverStartDomain.run(startServer); 264 | 265 | -------------------------------------------------------------------------------- /web/js/json2.js: -------------------------------------------------------------------------------- 1 | /* 2 | json2.js 3 | 2013-05-26 4 | 5 | Public Domain. 6 | 7 | NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK. 8 | 9 | See http://www.JSON.org/js.html 10 | 11 | 12 | This code should be minified before deployment. 13 | See http://javascript.crockford.com/jsmin.html 14 | 15 | USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO 16 | NOT CONTROL. 17 | 18 | 19 | This file creates a global JSON object containing two methods: stringify 20 | and parse. 21 | 22 | JSON.stringify(value, replacer, space) 23 | value any JavaScript value, usually an object or array. 24 | 25 | replacer an optional parameter that determines how object 26 | values are stringified for objects. It can be a 27 | function or an array of strings. 28 | 29 | space an optional parameter that specifies the indentation 30 | of nested structures. If it is omitted, the text will 31 | be packed without extra whitespace. If it is a number, 32 | it will specify the number of spaces to indent at each 33 | level. If it is a string (such as '\t' or ' '), 34 | it contains the characters used to indent at each level. 35 | 36 | This method produces a JSON text from a JavaScript value. 37 | 38 | When an object value is found, if the object contains a toJSON 39 | method, its toJSON method will be called and the result will be 40 | stringified. A toJSON method does not serialize: it returns the 41 | value represented by the name/value pair that should be serialized, 42 | or undefined if nothing should be serialized. The toJSON method 43 | will be passed the key associated with the value, and this will be 44 | bound to the value 45 | 46 | For example, this would serialize Dates as ISO strings. 47 | 48 | Date.prototype.toJSON = function (key) { 49 | function f(n) { 50 | // Format integers to have at least two digits. 51 | return n < 10 ? '0' + n : n; 52 | } 53 | 54 | return this.getUTCFullYear() + '-' + 55 | f(this.getUTCMonth() + 1) + '-' + 56 | f(this.getUTCDate()) + 'T' + 57 | f(this.getUTCHours()) + ':' + 58 | f(this.getUTCMinutes()) + ':' + 59 | f(this.getUTCSeconds()) + 'Z'; 60 | }; 61 | 62 | You can provide an optional replacer method. It will be passed the 63 | key and value of each member, with this bound to the containing 64 | object. The value that is returned from your method will be 65 | serialized. If your method returns undefined, then the member will 66 | be excluded from the serialization. 67 | 68 | If the replacer parameter is an array of strings, then it will be 69 | used to select the members to be serialized. It filters the results 70 | such that only members with keys listed in the replacer array are 71 | stringified. 72 | 73 | Values that do not have JSON representations, such as undefined or 74 | functions, will not be serialized. Such values in objects will be 75 | dropped; in arrays they will be replaced with null. You can use 76 | a replacer function to replace those with JSON values. 77 | JSON.stringify(undefined) returns undefined. 78 | 79 | The optional space parameter produces a stringification of the 80 | value that is filled with line breaks and indentation to make it 81 | easier to read. 82 | 83 | If the space parameter is a non-empty string, then that string will 84 | be used for indentation. If the space parameter is a number, then 85 | the indentation will be that many spaces. 86 | 87 | Example: 88 | 89 | text = JSON.stringify(['e', {pluribus: 'unum'}]); 90 | // text is '["e",{"pluribus":"unum"}]' 91 | 92 | 93 | text = JSON.stringify(['e', {pluribus: 'unum'}], null, '\t'); 94 | // text is '[\n\t"e",\n\t{\n\t\t"pluribus": "unum"\n\t}\n]' 95 | 96 | text = JSON.stringify([new Date()], function (key, value) { 97 | return this[key] instanceof Date ? 98 | 'Date(' + this[key] + ')' : value; 99 | }); 100 | // text is '["Date(---current time---)"]' 101 | 102 | 103 | JSON.parse(text, reviver) 104 | This method parses a JSON text to produce an object or array. 105 | It can throw a SyntaxError exception. 106 | 107 | The optional reviver parameter is a function that can filter and 108 | transform the results. It receives each of the keys and values, 109 | and its return value is used instead of the original value. 110 | If it returns what it received, then the structure is not modified. 111 | If it returns undefined then the member is deleted. 112 | 113 | Example: 114 | 115 | // Parse the text. Values that look like ISO date strings will 116 | // be converted to Date objects. 117 | 118 | myData = JSON.parse(text, function (key, value) { 119 | var a; 120 | if (typeof value === 'string') { 121 | a = 122 | /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2}(?:\.\d*)?)Z$/.exec(value); 123 | if (a) { 124 | return new Date(Date.UTC(+a[1], +a[2] - 1, +a[3], +a[4], 125 | +a[5], +a[6])); 126 | } 127 | } 128 | return value; 129 | }); 130 | 131 | myData = JSON.parse('["Date(09/09/2001)"]', function (key, value) { 132 | var d; 133 | if (typeof value === 'string' && 134 | value.slice(0, 5) === 'Date(' && 135 | value.slice(-1) === ')') { 136 | d = new Date(value.slice(5, -1)); 137 | if (d) { 138 | return d; 139 | } 140 | } 141 | return value; 142 | }); 143 | 144 | 145 | This is a reference implementation. You are free to copy, modify, or 146 | redistribute. 147 | */ 148 | 149 | /*jslint evil: true, regexp: true */ 150 | 151 | /*members "", "\b", "\t", "\n", "\f", "\r", "\"", JSON, "\\", apply, 152 | call, charCodeAt, getUTCDate, getUTCFullYear, getUTCHours, 153 | getUTCMinutes, getUTCMonth, getUTCSeconds, hasOwnProperty, join, 154 | lastIndex, length, parse, prototype, push, replace, slice, stringify, 155 | test, toJSON, toString, valueOf 156 | */ 157 | 158 | 159 | // Create a JSON object only if one does not already exist. We create the 160 | // methods in a closure to avoid creating global variables. 161 | 162 | if (typeof JSON !== 'object') { 163 | JSON = {}; 164 | } 165 | 166 | (function () { 167 | 'use strict'; 168 | 169 | function f(n) { 170 | // Format integers to have at least two digits. 171 | return n < 10 ? '0' + n : n; 172 | } 173 | 174 | if (typeof Date.prototype.toJSON !== 'function') { 175 | 176 | Date.prototype.toJSON = function () { 177 | 178 | return isFinite(this.valueOf()) 179 | ? this.getUTCFullYear() + '-' + 180 | f(this.getUTCMonth() + 1) + '-' + 181 | f(this.getUTCDate()) + 'T' + 182 | f(this.getUTCHours()) + ':' + 183 | f(this.getUTCMinutes()) + ':' + 184 | f(this.getUTCSeconds()) + 'Z' 185 | : null; 186 | }; 187 | 188 | String.prototype.toJSON = 189 | Number.prototype.toJSON = 190 | Boolean.prototype.toJSON = function () { 191 | return this.valueOf(); 192 | }; 193 | } 194 | 195 | var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 196 | escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g, 197 | gap, 198 | indent, 199 | meta = { // table of character substitutions 200 | '\b': '\\b', 201 | '\t': '\\t', 202 | '\n': '\\n', 203 | '\f': '\\f', 204 | '\r': '\\r', 205 | '"' : '\\"', 206 | '\\': '\\\\' 207 | }, 208 | rep; 209 | 210 | 211 | function quote(string) { 212 | 213 | // If the string contains no control characters, no quote characters, and no 214 | // backslash characters, then we can safely slap some quotes around it. 215 | // Otherwise we must also replace the offending characters with safe escape 216 | // sequences. 217 | 218 | escapable.lastIndex = 0; 219 | return escapable.test(string) ? '"' + string.replace(escapable, function (a) { 220 | var c = meta[a]; 221 | return typeof c === 'string' 222 | ? c 223 | : '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 224 | }) + '"' : '"' + string + '"'; 225 | } 226 | 227 | 228 | function str(key, holder) { 229 | 230 | // Produce a string from holder[key]. 231 | 232 | var i, // The loop counter. 233 | k, // The member key. 234 | v, // The member value. 235 | length, 236 | mind = gap, 237 | partial, 238 | value = holder[key]; 239 | 240 | // If the value has a toJSON method, call it to obtain a replacement value. 241 | 242 | if (value && typeof value === 'object' && 243 | typeof value.toJSON === 'function') { 244 | value = value.toJSON(key); 245 | } 246 | 247 | // If we were called with a replacer function, then call the replacer to 248 | // obtain a replacement value. 249 | 250 | if (typeof rep === 'function') { 251 | value = rep.call(holder, key, value); 252 | } 253 | 254 | // What happens next depends on the value's type. 255 | 256 | switch (typeof value) { 257 | case 'string': 258 | return quote(value); 259 | 260 | case 'number': 261 | 262 | // JSON numbers must be finite. Encode non-finite numbers as null. 263 | 264 | return isFinite(value) ? String(value) : 'null'; 265 | 266 | case 'boolean': 267 | case 'null': 268 | 269 | // If the value is a boolean or null, convert it to a string. Note: 270 | // typeof null does not produce 'null'. The case is included here in 271 | // the remote chance that this gets fixed someday. 272 | 273 | return String(value); 274 | 275 | // If the type is 'object', we might be dealing with an object or an array or 276 | // null. 277 | 278 | case 'object': 279 | 280 | // Due to a specification blunder in ECMAScript, typeof null is 'object', 281 | // so watch out for that case. 282 | 283 | if (!value) { 284 | return 'null'; 285 | } 286 | 287 | // Make an array to hold the partial results of stringifying this object value. 288 | 289 | gap += indent; 290 | partial = []; 291 | 292 | // Is the value an array? 293 | 294 | if (Object.prototype.toString.apply(value) === '[object Array]') { 295 | 296 | // The value is an array. Stringify every element. Use null as a placeholder 297 | // for non-JSON values. 298 | 299 | length = value.length; 300 | for (i = 0; i < length; i += 1) { 301 | partial[i] = str(i, value) || 'null'; 302 | } 303 | 304 | // Join all of the elements together, separated with commas, and wrap them in 305 | // brackets. 306 | 307 | v = partial.length === 0 308 | ? '[]' 309 | : gap 310 | ? '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' 311 | : '[' + partial.join(',') + ']'; 312 | gap = mind; 313 | return v; 314 | } 315 | 316 | // If the replacer is an array, use it to select the members to be stringified. 317 | 318 | if (rep && typeof rep === 'object') { 319 | length = rep.length; 320 | for (i = 0; i < length; i += 1) { 321 | if (typeof rep[i] === 'string') { 322 | k = rep[i]; 323 | v = str(k, value); 324 | if (v) { 325 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 326 | } 327 | } 328 | } 329 | } else { 330 | 331 | // Otherwise, iterate through all of the keys in the object. 332 | 333 | for (k in value) { 334 | if (Object.prototype.hasOwnProperty.call(value, k)) { 335 | v = str(k, value); 336 | if (v) { 337 | partial.push(quote(k) + (gap ? ': ' : ':') + v); 338 | } 339 | } 340 | } 341 | } 342 | 343 | // Join all of the member texts together, separated with commas, 344 | // and wrap them in braces. 345 | 346 | v = partial.length === 0 347 | ? '{}' 348 | : gap 349 | ? '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' 350 | : '{' + partial.join(',') + '}'; 351 | gap = mind; 352 | return v; 353 | } 354 | } 355 | 356 | // If the JSON object does not yet have a stringify method, give it one. 357 | 358 | if (typeof JSON.stringify !== 'function') { 359 | JSON.stringify = function (value, replacer, space) { 360 | 361 | // The stringify method takes a value and an optional replacer, and an optional 362 | // space parameter, and returns a JSON text. The replacer can be a function 363 | // that can replace values, or an array of strings that will select the keys. 364 | // A default replacer method can be provided. Use of the space parameter can 365 | // produce text that is more easily readable. 366 | 367 | var i; 368 | gap = ''; 369 | indent = ''; 370 | 371 | // If the space parameter is a number, make an indent string containing that 372 | // many spaces. 373 | 374 | if (typeof space === 'number') { 375 | for (i = 0; i < space; i += 1) { 376 | indent += ' '; 377 | } 378 | 379 | // If the space parameter is a string, it will be used as the indent string. 380 | 381 | } else if (typeof space === 'string') { 382 | indent = space; 383 | } 384 | 385 | // If there is a replacer, it must be a function or an array. 386 | // Otherwise, throw an error. 387 | 388 | rep = replacer; 389 | if (replacer && typeof replacer !== 'function' && 390 | (typeof replacer !== 'object' || 391 | typeof replacer.length !== 'number')) { 392 | throw new Error('JSON.stringify'); 393 | } 394 | 395 | // Make a fake root object containing our value under the key of ''. 396 | // Return the result of stringifying the value. 397 | 398 | return str('', {'': value}); 399 | }; 400 | } 401 | 402 | 403 | // If the JSON object does not yet have a parse method, give it one. 404 | 405 | if (typeof JSON.parse !== 'function') { 406 | JSON.parse = function (text, reviver) { 407 | 408 | // The parse method takes a text and an optional reviver function, and returns 409 | // a JavaScript value if the text is a valid JSON text. 410 | 411 | var j; 412 | 413 | function walk(holder, key) { 414 | 415 | // The walk method is used to recursively walk the resulting structure so 416 | // that modifications can be made. 417 | 418 | var k, v, value = holder[key]; 419 | if (value && typeof value === 'object') { 420 | for (k in value) { 421 | if (Object.prototype.hasOwnProperty.call(value, k)) { 422 | v = walk(value, k); 423 | if (v !== undefined) { 424 | value[k] = v; 425 | } else { 426 | delete value[k]; 427 | } 428 | } 429 | } 430 | } 431 | return reviver.call(holder, key, value); 432 | } 433 | 434 | 435 | // Parsing happens in four stages. In the first stage, we replace certain 436 | // Unicode characters with escape sequences. JavaScript handles many characters 437 | // incorrectly, either silently deleting them, or treating them as line endings. 438 | 439 | text = String(text); 440 | cx.lastIndex = 0; 441 | if (cx.test(text)) { 442 | text = text.replace(cx, function (a) { 443 | return '\\u' + 444 | ('0000' + a.charCodeAt(0).toString(16)).slice(-4); 445 | }); 446 | } 447 | 448 | // In the second stage, we run the text against regular expressions that look 449 | // for non-JSON patterns. We are especially concerned with '()' and 'new' 450 | // because they can cause invocation, and '=' because it can cause mutation. 451 | // But just to be safe, we want to reject all unexpected forms. 452 | 453 | // We split the second stage into 4 regexp operations in order to work around 454 | // crippling inefficiencies in IE's and Safari's regexp engines. First we 455 | // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we 456 | // replace all simple value tokens with ']' characters. Third, we delete all 457 | // open brackets that follow a colon or comma or that begin the text. Finally, 458 | // we look to see that the remaining characters are only whitespace or ']' or 459 | // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. 460 | 461 | if (/^[\],:{}\s]*$/ 462 | .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') 463 | .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') 464 | .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { 465 | 466 | // In the third stage we use the eval function to compile the text into a 467 | // JavaScript structure. The '{' operator is subject to a syntactic ambiguity 468 | // in JavaScript: it can begin a block or an object literal. We wrap the text 469 | // in parens to eliminate the ambiguity. 470 | 471 | j = eval('(' + text + ')'); 472 | 473 | // In the optional fourth stage, we recursively walk the new structure, passing 474 | // each name/value pair to a reviver function for possible transformation. 475 | 476 | return typeof reviver === 'function' 477 | ? walk({'': j}, '') 478 | : j; 479 | } 480 | 481 | // If the text is not JSON parseable, then a SyntaxError is thrown. 482 | 483 | throw new SyntaxError('JSON.parse'); 484 | }; 485 | } 486 | }()); 487 | -------------------------------------------------------------------------------- /doc/Readme.md: -------------------------------------------------------------------------------- 1 | SCOUT CAMP 2 | ========== 3 | 4 | 5 | Dealing with server systems can be alleviated by systems which allow clear 6 | distinction between: 7 | 8 | * serving pages; and 9 | * powering applications. 10 | 11 | 12 | Camp.js 13 | ------- 14 | 15 | We start the web server. 16 | 17 | ```js 18 | var camp = require('camp').start(); 19 | ``` 20 | 21 | The `start()` function has the following properties: 22 | 23 | - `documentRoot`: the path to the directory containing the static files you 24 | serve (and the template files, potentially). If your website is made of HTML 25 | pages, this is where they are located. Defaults to `./web`. 26 | - `templateReader`: the default template engine used. See below. 27 | - `passphrase`, `key`, `cert`, `ca`: in the case of a secure website (using 28 | HTTPS), those are fields you may specify to indicate where to find information 29 | about the website's security. Defaults include "https.key", "https.crt", and, 30 | as the CA (Certificate Authority, a list of certificates) an empty list. 31 | - `setuid`: once the server has made the connection, set the user id to 32 | something else. This is particularly useful if you don't want the server to 33 | run as the almighty root user. However, executing this requires to be root (so 34 | you will need to use `sudo` or the like to run the server). 35 | - `saveRequestChunks`: some requests will have their data processed by Camp 36 | before reaching your handlers, in order to fill the Augmented Request's 37 | `req.data` dictionary, but this means that you won't receive any of the 38 | processed chunks by doing `req.on('data',function(chunk){})` in your handler. 39 | If you need to access these raw chunks (e.g. to pipe complete requests to a 40 | different server), you'll find them in `req.chunks` when `saveRequestChunks` 41 | is set to `true`. 42 | 43 | The result of `require('camp')` can also be useful, for instance, to log 44 | warnings from the server. The logging system uses 45 | [multilog](https://www.npmjs.org/package/multilog). 46 | 47 | ```js 48 | var Camp = require('camp'); 49 | Camp.log.unpipe('warn', 'stderr'); 50 | // There are three streams: warn, error, and all. 51 | // warn and error are individually piped to stderr by default. 52 | ``` 53 | 54 | ### Ajax 55 | 56 | The Camp.js engine targets ease of use of both serving plain html files and ajax 57 | calls. By default, when given a request, it looks for files in the `./web/` 58 | directory. However, it also has the concept of Ajax actions. 59 | 60 | ```js 61 | camp.ajax.on('getinfo', function(json, end, ask) { 62 | console.log(json); 63 | end(json); // Send that back to the client. 64 | }); 65 | ``` 66 | 67 | An action maps a string to the path request `/$`. When a client asks 68 | for this resource, sending in information stored in the "json" parameter, 69 | Camp.js will send it back the object literal that the callback function gives. 70 | 71 | In the example given, it merely sends back whatever information the client 72 | gives, which is a very contrived example. 73 | 74 | The purpose of this distinction between normally served html pages and ajax 75 | actions is to treat servers more like applications. You first serve the 76 | graphical interface, in html and css, and then, you let the user interact with 77 | the server's data seemlessly through ajax calls. 78 | 79 | Note that the `json` parameter given is a single object containing all 80 | parameters from the following sources: 81 | 82 | - the query string from GET requests 83 | - POST requests with enctype application/x-www-form-urlencoded 84 | - POST requests with enctype multipart/form-data. This one uses the same API as 85 | [formidable](https://github.com/felixge/node-formidable) for file objects. 86 | 87 | You also get an [Ask](#the-ask-class) object, see below. 88 | 89 | Before downloading POST Ajax data, you can hook a function up using the 90 | following code: 91 | 92 | ```js 93 | camp.ajaxReq.on('getinfo', function(ask) { … }); 94 | ``` 95 | 96 | That can be useful to give information about the progress of an upload, for 97 | instance, using `ask.form.on('progress', function(bytesReceived, bytesExpected) {})`. 98 | 99 | ### EventSource 100 | 101 | Let's build a path named `/path`. When we receive a call on `/talk`, we send 102 | the data it gives us to the EventSource path. 103 | 104 | ```js 105 | // This is actually a full-fledged chat. 106 | var chat = camp.eventSource ( '/all' ); 107 | camp.post('/talk', function(req, res) { chat.send(req.data); res.end(); }); 108 | ``` 109 | 110 | This EventSource object we get has two methods: 111 | 112 | - The `send` method takes a JSON object and emits the `message` event to the 113 | client. It is meant to be used with `es.onrecv`. 114 | - The `emit` method takes an event name and a textual message and emits this 115 | event with that message to the client. It is meant to be used with 116 | `es.on(event, callback)`. 117 | 118 | ### WebSocket 119 | 120 | We also include the raw duplex communication system provided by the WebSocket 121 | protocol. 122 | 123 | ```js 124 | camp.ws('/path', function(socket)); 125 | ``` 126 | 127 | Every time a WebSocket connection is initiated (say, by a Web browser), the 128 | function is run. The `socket` is an instance of [ws.WebSocket] 129 | (https://github.com/einaros/ws/blob/master/doc/ws.md#class-wswebsocket). 130 | Usually, you only need to know about `socket.on('message', function(data))`, 131 | and `socket.send(data)`. 132 | 133 | This function returns an instance of a [WebSocket server] 134 | (https://github.com/einaros/ws/blob/master/doc/ws.md#class-wsserver) 135 | for that path. 136 | Most notably, it has a `wsServer.clients` list of opened sockets on a path. 137 | 138 | A map from paths to WebSocket servers is available at: 139 | 140 | ```js 141 | camp.wsChannels[path]; 142 | ``` 143 | 144 | For the purpose of broadcasting (ie, sending messages to every connected socket 145 | on the path), we provide the following function. 146 | 147 | ```js 148 | camp.wsBroadcast('/path', function recv(req, res)) 149 | ``` 150 | 151 | The `recv` function is run once every time a client sends data. 152 | 153 | - Its `req` parameter provides `req.data` (the data that a client sent), and 154 | `req.flags` (`req.flags.binary` is true if binary data is received; 155 | `req.flags.masked` if the data was masked). 156 | - Its `res` parameter provides `res.send(data)`, which sends the same data to each socket on the path. 157 | 158 | Client-side, obviously, your browser needs to have a 159 | [WebSocket API](http://caniuse.com/#feat=websockets). 160 | The client-side code may look like this. 161 | 162 | ```js 163 | // `socket` is a genuine WebSocket instance. 164 | var socket = new WebSocket('/path'); 165 | socket.send(JSON.stringify({ some: "data" })); 166 | ``` 167 | 168 | ### Socket.io 169 | 170 | Be warned before you read on: the Socket.io interface is deprecated. 171 | Use the WebSocket interface provided above instead. 172 | Also, do not use *both* the socket.io interface and the WebSocket interface. 173 | That seems to be asking for trouble. 174 | 175 | We also include the duplex communication system that socket.io provides. When 176 | you start the server, by default, socket.io is already launched. You can use its 177 | APIs as documented at from the `camp.io` object. 178 | 179 | ```js 180 | camp.io.sockets.on('connection', function (socket) { … }); 181 | ``` 182 | 183 | On the client-side, `Scout.js` also provides shortcuts, through its 184 | `Scout.socket(namespace)` function. Calling `Scout.socket()` returns the 185 | documented Socket.io object that you can use according to their API. 186 | 187 | ```js 188 | var io = Scout.socket(); 189 | io.emit('event name', {data: 'to send'}); 190 | io.on('event name', function (jsonObject) { … }); 191 | ``` 192 | 193 | ### Handlers 194 | 195 | If you want a bit of code to be executed on every request, or if you want to 196 | manually manage requests at a low level without all that fluff described above, 197 | you can add handlers to the server. 198 | 199 | Each request goes through each handler you provided in the order you provided 200 | them. Unless a handler calls `next()`, the request gets caught by that handler: 201 | 202 | 1. None of the handlers after that one get called, 203 | 2. None of the subsequent layers of Camp (such as WebSocket, EventSource, 204 | Route…) get called. 205 | 206 | Otherwise, all the handlers get called, and the request will get caught by one 207 | of the subsequent layers of Camp. 208 | 209 | ```js 210 | var addOSSHeader = function(req, res, next) { 211 | ask.res.setHeader('X-Open-Source', 'https://github.com/espadrine/sc/'); 212 | next(); 213 | }; 214 | camp.handle(addOSSHeader); 215 | // There's no reason to remove that amazing handler, but if that was what 216 | // floated your boat, here is how you would do that: 217 | camp.removeHandler(addOSSHeader); 218 | ``` 219 | 220 | 221 | Templates 222 | --------- 223 | 224 | An associated possibility, very much linked to the normal use of Camp.js, is to 225 | handle templates. Those are server-side preprocessed files. 226 | 227 | ### Basic Usage 228 | 229 | Mostly, you first decide where to put your template file. Let's say we have 230 | such a file at `/first/post.html` (from the root of the web/ or publish/ 231 | directory). 232 | 233 | ```js 234 | var posts = ['This is the f1rst p0st!']; 235 | 236 | camp.path( 'first/post.html', function(req, res) { 237 | res.template({ 238 | text: posts[0], 239 | comments: ['first comment!', 'second comment…'] 240 | }); 241 | }); 242 | ``` 243 | 244 | `req` is an Augmented Request, and `res` an Augmented Response. 245 | Therefore, if the request is `/first/post.html?key=value`, then `req.data.key` 246 | will be "value". 247 | 248 | `res.template(scope, templates)` responds to the request with a list of 249 | templates (produced with `Camp.template()` or `camp.template()`), a single 250 | template, or no template: 251 | in the latter case, the URI's path will be treated as a template file on disk 252 | under `documentRoot`. This is the case here with "first/post.html". 253 | 254 | The file `/web/first/post.html` might look like this: 255 | 256 | ```html 257 | 258 |

{{= text in html}}

259 |
    260 | {{for comment in comments {{ 261 |
  • {{= comment in html}}
  • 262 | }}}} 263 |
264 | ``` 265 | 266 | Because it will be preprocessed server-side, the browser will actually receive 267 | the following file: 268 | 269 | ```html 270 | 271 |

This is the f1rst p0st!

272 |
    273 |
  • first comment!
  • 274 |
  • second comment...
  • 275 |
276 | ``` 277 | 278 | If you need to specify a different template, you can do so: 279 | 280 | ```js 281 | var postsTemplate = Camp.template( './templates/posts.html' ); 282 | camp.path('posts', function(req, res) { 283 | res.template({comments: comments}, postsTemplate); 284 | }); 285 | ``` 286 | 287 | `Camp.template(paths, options)` takes an Array of String paths to templating 288 | files (or a single path to a templating file), and the following options: 289 | - reader: the template reader function in use, defaulting to 290 | `camp.templateReader`, which defaults to 291 | [Fleau](https://github.com/espadrine/fleau). 292 | - asString: boolean; use the string as a template, not as a file path. 293 | - callback: function taking a function(scope) → readableStream. 294 | If you don't want the template creation to be synchronous, use this. 295 | We return nothing from the function if `callback` is set. 296 | 297 | This function returns a function(scope) → readableStream, unless `callback` is 298 | set. 299 | 300 | So this is how to be explicit about the template. On the opposite extreme, you 301 | can be extra implicit: the URL path will them be used as the template path on 302 | disk, and `req.data` will be used as the template's scope. 303 | 304 | ```js 305 | // Supports ?mobile=true 306 | camp.path('blog.html'); 307 | ``` 308 | 309 | 310 | ## Fall through 311 | 312 | ```js 313 | camp.notFound( 'blog/*', function(req, res) { 314 | res.file('/templates/404.html'); 315 | }); 316 | ``` 317 | 318 | The `camp.notFound()` function works in exactly the same way as the 319 | `camp.path()` function, with two important differences: 320 | 321 | 1. It only gets used when nothing else matches the path, including paths and 322 | static files on disk under `documentRoot`, 323 | 2. It responds with a 404 (Not Found) status code. 324 | 325 | 326 | 327 | Camp In Depth 328 | ------------- 329 | 330 | In Camp.js, there is a lot more than meets the eye. Up until now, we have only 331 | discussed the default behaviour of ScoutCamp. For most uses, this is actually 332 | more than enough. Sometimes, however, you need to dig a little deeper. 333 | 334 | ### The Camp Object 335 | 336 | `Camp.start` is the simple way to launch the server in a single line. You may 337 | not know, however, that it returns an `http.Server` (or an `https.Server`) 338 | subclass instance. As a result, you can use all node.js' HTTP and HTTPS 339 | methods. 340 | 341 | You may provide the `start` function with a JSON object defining the server's 342 | settings. It defaults to this: 343 | 344 | ```js 345 | { 346 | port: 80, // The port to listen to. 347 | hostname: '::', // The hostname to use as a server 348 | security: { 349 | secure: true, 350 | key: 'https.key', // Either the name of a file on disk, 351 | cert: 'https.crt', // or the content as a String. 352 | ca: ['https.ca'] 353 | } 354 | } 355 | ``` 356 | 357 | If you provide the relevant HTTPS files and set the `secure` option to true, the 358 | server will be secure. 359 | 360 | `Camp.createServer()` creates a Camp instance directly, and 361 | `Camp.createSecureServer(settings)` creates an HTTPS Camp instance. The latter 362 | takes the same parameters as `https.Server`. 363 | 364 | `Camp.Camp` and `Camp.SecureCamp` are the class constructors. 365 | 366 | 367 | ### The stack 368 | 369 | Camp is stack-based. When we receive a request, it goes through all the layers 370 | of the stack until it hits the bottom. It should never hit the bottom: each 371 | layer can either pass it on to the next, or end the request (by sending a 372 | response). 373 | 374 | The default stack is defined this way: 375 | 376 | ```js 377 | campInstance.stack = [wsLayer, ajaxLayer, eventSourceLayer, pathLayer 378 | routeLayer, staticLayer, notfoundLayer]; 379 | ``` 380 | 381 | Each element of the stack `function(req, res, next){}` takes two parameters: 382 | 383 | - augmented [IncomingMessage][] (`req`) and [ServerResponse][] (`res`) 384 | (more on that below), 385 | - a `next` function, which the layer may call if it will not send an HTTP 386 | response itself. The layer that does catch the request and responds fully to 387 | it will not call `next()`, the others will call `next()`. 388 | 389 | [IncomingMessage]: https://nodejs.org/api/http.html#http_class_http_incomingmessage 390 | [ServerResponse]: https://nodejs.org/api/http.html#http_class_http_serverresponse 391 | 392 | You can add layers to the stack with `handle()`, which is described way above. 393 | 394 | ```js 395 | camp.handle(function(ask, next) { 396 | ask.res.setHeader('X-Open-Source', 'https://github.com/espadrine/sc/'); 397 | next(); 398 | }); 399 | ``` 400 | 401 | By default, it inserts it before the `wsLayer`, but after other inserted 402 | handlers. Its insertion point is at `camp.stackInsertion` (an integer). 403 | 404 | ### Ask and Augmented Request 405 | 406 | The **Ask class** is a way to provide a lot of useful elements associated with 407 | a request. It contains the following fields: 408 | 409 | - server: the Camp instance, 410 | - req: the [http.IncomingMessage](http://nodejs.org/api/http.html#http_http_incomingmessage) object. 411 | - res: the [http.ServerResponse](http://nodejs.org/api/http.html#http_class_http_serverresponse) object. 412 | - uri: the URI. 413 | - path: the pathname associated with the request. 414 | - query: the query taken from the URI. 415 | - cookies: using the [cookies](https://github.com/pillarjs/cookies) library. 416 | - form: a `formidable.IncomingForm` object as specified by 417 | the [formidable](https://github.com/felixge/node-formidable) 418 | library API. Noteworthy are `form.uploadDir` (where the files are uploaded, 419 | this property is settable), 420 | `form.path` (where the uploaded file resides), 421 | and `form.on('progress', function(bytesReceived, bytesExpected) {})`. 422 | - username, password: in the case of a Basic Authentication HTTP request, parses 423 | the contents of the request and places the username and password as strings in 424 | those fields. 425 | 426 | An `Ask` instance is provided as an extra parameter to 427 | `camp.route(pattern, function(query, path, end, ask))` 428 | (see the start of section "Diving In"), 429 | and as a parameter in each function of the server's stack 430 | `function(ask, next)` 431 | (see the start of section "The stack"). 432 | 433 | An **Augmented Request** is an [IncomingMessage][] which has several additional 434 | fields which you can also find in `Ask`: `server`, `uri`, `form`, `path`, 435 | `data` (which is the same as `query`), `username`, `password`, `cookies`. 436 | 437 | It also contains form information for `multipart/form-data` requests in the 438 | following fields: 439 | - form: a `formidable.IncomingForm` object as specified by 440 | the [formidable](https://github.com/felixge/node-formidable) 441 | library API. Noteworthy are `form.uploadDir` (where the files are uploaded) 442 | and `form.on('progress', function(bytesReceived, bytesExpected) {})`. 443 | - fields: a map from the field name (eg, `fieldname` for 444 | ``) to the corresponding form values. 445 | - files: a map from the field name (eg, `fieldname` for 446 | ``) to a list of files, each with properties: 447 | - path: the location on disk where the the file resides 448 | - name: the name of the file, as asserted by the uploader. 449 | 450 | An **Augmented Response** is a [ServerResponse][] which also has: 451 | 452 | - `template(scope, templates)`: responds to the request with a list of templates 453 | (produced with `Camp.template()` or `camp.template()`), a single template, or 454 | no template (in which case, the URI's path will be treated as a template file 455 | on disk under `documentRoot`). The `scope` is a JS object used to fill in the 456 | template. 457 | - `file(path)`: responds to the request with the contents of the file at `path`, 458 | on disk under `documentRoot`. 459 | - `json(data, replacer, space)`: responds to the request with stringified JSON 460 | data. Arguments are passed to `JSON.stringify()`, so you can use either 461 | `res.json({a: 42})` (minified) or `res.json({a: 42}, null, 2)` 462 | (human-readable). 463 | - `compressed()`: returns a writable stream. All data sent to that stream gets 464 | compressed and sent as a response. 465 | - `redirect(path)`: responds to the request with a 303 redirection to a path 466 | or URL. 467 | 468 | Note: `file(path)` leverages browser caching by comparing `If-Modified-Since` 469 | request headers against actual file timestamps, and saves time and bandwidth by 470 | replying "304 Not Modified" with no content to requests where the browser 471 | already knows the latest version of a file. However, this header is limited to 472 | second-level precision by specification, so any file changes happening within 473 | the same second, or within a 2-second window in the case of leap seconds, cause 474 | a small risk of browsers fetching and caching a stale version of the file in 475 | between these changes. Such a cached version would remain stale until the next 476 | file change and subsequent browser request updating the cache. 477 | 478 | Additionally, you can set the mime type of the response with 479 | `req.mime('png')`, for instance. 480 | 481 | ### Default layers 482 | 483 | The default layers provided are generated from what we call units, which are 484 | exported as shown below. Each unit is a function that takes a server instance 485 | and returns a layer (`function(ask, next){}`). 486 | 487 | - `Camp.ajaxUnit` (seen previously) 488 | - `Camp.socketUnit` (idem) 489 | - `Camp.wsUnit` (idem) 490 | - `Camp.eventSourceUnit` (idem) 491 | - `Camp.pathUnit` (idem) 492 | - `Camp.routeUnit` (idem) 493 | - `Camp.staticUnit` (idem, relies on `camp.documentRoot` which specifies the 494 | location of the root of your static web files. The default is "./web". 495 | - `Camp.notfoundUnit` (idem) 496 | 497 | Scout.js 498 | -------- 499 | 500 | ### XHR 501 | 502 | Browsers' built-in Ajax libraries are usually poor. They are not cross-browser 503 | (because of Internet Explorer) and they can quickly become a hassle. Scout.js 504 | is a javascript library to remove that hassle. 505 | 506 | With Scout.js, one can easily target a specific element in the page which 507 | must trigger an XHR(XML Http Request) when a specific event is fired. This is 508 | what you do, most of the time, anyway. Otherwise, it is also easy to attach an 509 | XHR upon a "setTimeout", and so on. 510 | 511 | ```js 512 | Scout ( '#id-of-element' ).on ( 'click', function (params, evt, xhr) { 513 | params.action = 'getinfo'; 514 | var sent = this.parentNode.textContent; 515 | params.data = { ready: true, data: sent }; 516 | params.resp = function ( resp, xhr ) { 517 | if (resp.data === sent) { 518 | console.log ('Got exactly what we sent.'); 519 | } 520 | }; 521 | }); 522 | 523 | // or... 524 | 525 | setTimeout ( Scout.send ( function ( params, xhr ) { … } ), 1000 ); 526 | ``` 527 | 528 | One thing that can bite is the fact that each Scout object only has one XHR 529 | object inside. If you do two Ajax roundtrips at the same time, with the same 530 | Scout object, one will cancel the other. 531 | 532 | This behavior is very easy to spot. On the Web Inspector of your navigator, in 533 | the "Network" tab, if a `$action` POST request is red (or cancelled), it means 534 | that it was killed by another XHR call. 535 | 536 | The cure is to create another Scout object through the 537 | `var newScout = Scout.maker()` call. 538 | 539 | ### Server-Sent Events 540 | 541 | All modern browsers support a mechanism for receiving a continuous, 542 | event-driven flow of information from the server. This technology is called 543 | *Server-Sent Events*. 544 | 545 | The bad news about it is that it is a hassle to set up server-side. The good 546 | news is that you are using ScoutCamp, which makes it a breeze. Additionally, 547 | ScoutCamp makes it work even in IE7. 548 | 549 | ```js 550 | var es = Scout.eventSource('/path'); 551 | 552 | es.on('eventName', function (data) { 553 | // `data` is a string. 554 | }); 555 | 556 | es.onrecv(function (json) { 557 | // `json` is a JSON object. 558 | }); 559 | ``` 560 | - - - 561 | 562 | Thaddee Tyl, author of ScoutCamp. 563 | -------------------------------------------------------------------------------- /scout.js: -------------------------------------------------------------------------------- 1 | (function(window){var i,support,cachedruns,Expr,getText,isXML,compile,outermostContext,sortInput,setDocument,document,docElem,documentIsHTML,rbuggyQSA,rbuggyMatches,matches,contains,expando="sizzle"+-new Date,preferredDoc=window.document,dirruns=0,done=0,classCache=createCache(),tokenCache=createCache(),compilerCache=createCache(),hasDuplicate=false,sortOrder=function(a,b){if(a===b){hasDuplicate=true;return 0}return 0},strundefined=typeof undefined,MAX_NEGATIVE=1<<31,hasOwn={}.hasOwnProperty,arr=[],pop=arr.pop,push_native=arr.push,push=arr.push,slice=arr.slice,indexOf=arr.indexOf||function(elem){var i=0,len=this.length;for(;i+~]|"+whitespace+")"+whitespace+"*"),rsibling=new RegExp(whitespace+"*[+~]"),rattributeQuotes=new RegExp("="+whitespace+"*([^\\]'\"]*)"+whitespace+"*\\]","g"),rpseudo=new RegExp(pseudos),ridentifier=new RegExp("^"+identifier+"$"),matchExpr={ID:new RegExp("^#("+characterEncoding+")"),CLASS:new RegExp("^\\.("+characterEncoding+")"),TAG:new RegExp("^("+characterEncoding.replace("w","w*")+")"),ATTR:new RegExp("^"+attributes),PSEUDO:new RegExp("^"+pseudos),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+whitespace+"*(even|odd|(([+-]|)(\\d*)n|)"+whitespace+"*(?:([+-]|)"+whitespace+"*(\\d+)|))"+whitespace+"*\\)|)","i"),bool:new RegExp("^(?:"+booleans+")$","i"),needsContext:new RegExp("^"+whitespace+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+whitespace+"*((?:-\\d)?\\d*)"+whitespace+"*\\)|)(?=[^-]|$)","i")},rnative=/^[^{]+\{\s*\[native \w/,rquickExpr=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,rinputs=/^(?:input|select|textarea|button)$/i,rheader=/^h\d$/i,rescape=/'|\\/g,runescape=new RegExp("\\\\([\\da-f]{1,6}"+whitespace+"?|("+whitespace+")|.)","ig"),funescape=function(_,escaped,escapedWhitespace){var high="0x"+escaped-65536;return high!==high||escapedWhitespace?escaped:high<0?String.fromCharCode(high+65536):String.fromCharCode(high>>10|55296,high&1023|56320)};try{push.apply(arr=slice.call(preferredDoc.childNodes),preferredDoc.childNodes);arr[preferredDoc.childNodes.length].nodeType}catch(e){push={apply:arr.length?function(target,els){push_native.apply(target,slice.call(els))}:function(target,els){var j=target.length,i=0;while(target[j++]=els[i++]){}target.length=j-1}}}function Sizzle(selector,context,results,seed){var match,elem,m,nodeType,i,groups,old,nid,newContext,newSelector;if((context?context.ownerDocument||context:preferredDoc)!==document){setDocument(context)}context=context||document;results=results||[];if(!selector||typeof selector!=="string"){return results}if((nodeType=context.nodeType)!==1&&nodeType!==9){return[]}if(documentIsHTML&&!seed){if(match=rquickExpr.exec(selector)){if(m=match[1]){if(nodeType===9){elem=context.getElementById(m);if(elem&&elem.parentNode){if(elem.id===m){results.push(elem);return results}}else{return results}}else{if(context.ownerDocument&&(elem=context.ownerDocument.getElementById(m))&&contains(context,elem)&&elem.id===m){results.push(elem);return results}}}else if(match[2]){push.apply(results,context.getElementsByTagName(selector));return results}else if((m=match[3])&&support.getElementsByClassName&&context.getElementsByClassName){push.apply(results,context.getElementsByClassName(m));return results}}if(support.qsa&&(!rbuggyQSA||!rbuggyQSA.test(selector))){nid=old=expando;newContext=context;newSelector=nodeType===9&&selector;if(nodeType===1&&context.nodeName.toLowerCase()!=="object"){groups=tokenize(selector);if(old=context.getAttribute("id")){nid=old.replace(rescape,"\\$&")}else{context.setAttribute("id",nid)}nid="[id='"+nid+"'] ";i=groups.length;while(i--){groups[i]=nid+toSelector(groups[i])}newContext=rsibling.test(selector)&&context.parentNode||context;newSelector=groups.join(",")}if(newSelector){try{push.apply(results,newContext.querySelectorAll(newSelector));return results}catch(qsaError){}finally{if(!old){context.removeAttribute("id")}}}}}return select(selector.replace(rtrim,"$1"),context,results,seed)}function createCache(){var keys=[];function cache(key,value){if(keys.push(key+=" ")>Expr.cacheLength){delete cache[keys.shift()]}return cache[key]=value}return cache}function markFunction(fn){fn[expando]=true;return fn}function assert(fn){var div=document.createElement("div");try{return!!fn(div)}catch(e){return false}finally{if(div.parentNode){div.parentNode.removeChild(div)}div=null}}function addHandle(attrs,handler){var arr=attrs.split("|"),i=attrs.length;while(i--){Expr.attrHandle[arr[i]]=handler}}function siblingCheck(a,b){var cur=b&&a,diff=cur&&a.nodeType===1&&b.nodeType===1&&(~b.sourceIndex||MAX_NEGATIVE)-(~a.sourceIndex||MAX_NEGATIVE);if(diff){return diff}if(cur){while(cur=cur.nextSibling){if(cur===b){return-1}}}return a?1:-1}function createInputPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type===type}}function createButtonPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return(name==="input"||name==="button")&&elem.type===type}}function createPositionalPseudo(fn){return markFunction(function(argument){argument=+argument;return markFunction(function(seed,matches){var j,matchIndexes=fn([],seed.length,argument),i=matchIndexes.length;while(i--){if(seed[j=matchIndexes[i]]){seed[j]=!(matches[j]=seed[j])}}})})}isXML=Sizzle.isXML=function(elem){var documentElement=elem&&(elem.ownerDocument||elem).documentElement;return documentElement?documentElement.nodeName!=="HTML":false};support=Sizzle.support={};setDocument=Sizzle.setDocument=function(node){var doc=node?node.ownerDocument||node:preferredDoc,parent=doc.defaultView;if(doc===document||doc.nodeType!==9||!doc.documentElement){return document}document=doc;docElem=doc.documentElement;documentIsHTML=!isXML(doc);if(parent&&parent.attachEvent&&parent!==parent.top){parent.attachEvent("onbeforeunload",function(){setDocument()})}support.attributes=assert(function(div){div.className="i";return!div.getAttribute("className")});support.getElementsByTagName=assert(function(div){div.appendChild(doc.createComment(""));return!div.getElementsByTagName("*").length});support.getElementsByClassName=assert(function(div){div.innerHTML="
";div.firstChild.className="i";return div.getElementsByClassName("i").length===2});support.getById=assert(function(div){docElem.appendChild(div).id=expando;return!doc.getElementsByName||!doc.getElementsByName(expando).length});if(support.getById){Expr.find["ID"]=function(id,context){if(typeof context.getElementById!==strundefined&&documentIsHTML){var m=context.getElementById(id);return m&&m.parentNode?[m]:[]}};Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){return elem.getAttribute("id")===attrId}}}else{delete Expr.find["ID"];Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){var node=typeof elem.getAttributeNode!==strundefined&&elem.getAttributeNode("id");return node&&node.value===attrId}}}Expr.find["TAG"]=support.getElementsByTagName?function(tag,context){if(typeof context.getElementsByTagName!==strundefined){return context.getElementsByTagName(tag)}}:function(tag,context){var elem,tmp=[],i=0,results=context.getElementsByTagName(tag);if(tag==="*"){while(elem=results[i++]){if(elem.nodeType===1){tmp.push(elem)}}return tmp}return results};Expr.find["CLASS"]=support.getElementsByClassName&&function(className,context){if(typeof context.getElementsByClassName!==strundefined&&documentIsHTML){return context.getElementsByClassName(className)}};rbuggyMatches=[];rbuggyQSA=[];if(support.qsa=rnative.test(doc.querySelectorAll)){assert(function(div){div.innerHTML="";if(!div.querySelectorAll("[selected]").length){rbuggyQSA.push("\\["+whitespace+"*(?:value|"+booleans+")")}if(!div.querySelectorAll(":checked").length){rbuggyQSA.push(":checked")}});assert(function(div){var input=doc.createElement("input");input.setAttribute("type","hidden");div.appendChild(input).setAttribute("t","");if(div.querySelectorAll("[t^='']").length){rbuggyQSA.push("[*^$]="+whitespace+"*(?:''|\"\")")}if(!div.querySelectorAll(":enabled").length){rbuggyQSA.push(":enabled",":disabled")}div.querySelectorAll("*,:x");rbuggyQSA.push(",.*:")})}if(support.matchesSelector=rnative.test(matches=docElem.webkitMatchesSelector||docElem.mozMatchesSelector||docElem.oMatchesSelector||docElem.msMatchesSelector)){assert(function(div){support.disconnectedMatch=matches.call(div,"div");matches.call(div,"[s!='']:x");rbuggyMatches.push("!=",pseudos)})}rbuggyQSA=rbuggyQSA.length&&new RegExp(rbuggyQSA.join("|"));rbuggyMatches=rbuggyMatches.length&&new RegExp(rbuggyMatches.join("|"));contains=rnative.test(docElem.contains)||docElem.compareDocumentPosition?function(a,b){var adown=a.nodeType===9?a.documentElement:a,bup=b&&b.parentNode;return a===bup||!!(bup&&bup.nodeType===1&&(adown.contains?adown.contains(bup):a.compareDocumentPosition&&a.compareDocumentPosition(bup)&16))}:function(a,b){if(b){while(b=b.parentNode){if(b===a){return true}}}return false};sortOrder=docElem.compareDocumentPosition?function(a,b){if(a===b){hasDuplicate=true;return 0}var compare=b.compareDocumentPosition&&a.compareDocumentPosition&&a.compareDocumentPosition(b);if(compare){if(compare&1||!support.sortDetached&&b.compareDocumentPosition(a)===compare){if(a===doc||contains(preferredDoc,a)){return-1}if(b===doc||contains(preferredDoc,b)){return 1}return sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0}return compare&4?-1:1}return a.compareDocumentPosition?-1:1}:function(a,b){var cur,i=0,aup=a.parentNode,bup=b.parentNode,ap=[a],bp=[b];if(a===b){hasDuplicate=true;return 0}else if(!aup||!bup){return a===doc?-1:b===doc?1:aup?-1:bup?1:sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0}else if(aup===bup){return siblingCheck(a,b)}cur=a;while(cur=cur.parentNode){ap.unshift(cur)}cur=b;while(cur=cur.parentNode){bp.unshift(cur)}while(ap[i]===bp[i]){i++}return i?siblingCheck(ap[i],bp[i]):ap[i]===preferredDoc?-1:bp[i]===preferredDoc?1:0};return doc};Sizzle.matches=function(expr,elements){return Sizzle(expr,null,null,elements)};Sizzle.matchesSelector=function(elem,expr){if((elem.ownerDocument||elem)!==document){setDocument(elem)}expr=expr.replace(rattributeQuotes,"='$1']");if(support.matchesSelector&&documentIsHTML&&(!rbuggyMatches||!rbuggyMatches.test(expr))&&(!rbuggyQSA||!rbuggyQSA.test(expr))){try{var ret=matches.call(elem,expr);if(ret||support.disconnectedMatch||elem.document&&elem.document.nodeType!==11){return ret}}catch(e){}}return Sizzle(expr,document,null,[elem]).length>0};Sizzle.contains=function(context,elem){if((context.ownerDocument||context)!==document){setDocument(context)}return contains(context,elem)};Sizzle.attr=function(elem,name){if((elem.ownerDocument||elem)!==document){setDocument(elem)}var fn=Expr.attrHandle[name.toLowerCase()],val=fn&&hasOwn.call(Expr.attrHandle,name.toLowerCase())?fn(elem,name,!documentIsHTML):undefined;return val===undefined?support.attributes||!documentIsHTML?elem.getAttribute(name):(val=elem.getAttributeNode(name))&&val.specified?val.value:null:val};Sizzle.error=function(msg){throw new Error("Syntax error, unrecognized expression: "+msg)};Sizzle.uniqueSort=function(results){var elem,duplicates=[],j=0,i=0;hasDuplicate=!support.detectDuplicates;sortInput=!support.sortStable&&results.slice(0);results.sort(sortOrder);if(hasDuplicate){while(elem=results[i++]){if(elem===results[i]){j=duplicates.push(i)}}while(j--){results.splice(duplicates[j],1)}}return results};getText=Sizzle.getText=function(elem){var node,ret="",i=0,nodeType=elem.nodeType;if(!nodeType){for(;node=elem[i];i++){ret+=getText(node)}}else if(nodeType===1||nodeType===9||nodeType===11){if(typeof elem.textContent==="string"){return elem.textContent}else{for(elem=elem.firstChild;elem;elem=elem.nextSibling){ret+=getText(elem)}}}else if(nodeType===3||nodeType===4){return elem.nodeValue}return ret};Expr=Sizzle.selectors={cacheLength:50,createPseudo:markFunction,match:matchExpr,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:true}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:true},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(match){match[1]=match[1].replace(runescape,funescape);match[3]=(match[4]||match[5]||"").replace(runescape,funescape);if(match[2]==="~="){match[3]=" "+match[3]+" "}return match.slice(0,4)},CHILD:function(match){match[1]=match[1].toLowerCase();if(match[1].slice(0,3)==="nth"){if(!match[3]){Sizzle.error(match[0])}match[4]=+(match[4]?match[5]+(match[6]||1):2*(match[3]==="even"||match[3]==="odd"));match[5]=+(match[7]+match[8]||match[3]==="odd")}else if(match[3]){Sizzle.error(match[0])}return match},PSEUDO:function(match){var excess,unquoted=!match[5]&&match[2];if(matchExpr["CHILD"].test(match[0])){return null}if(match[3]&&match[4]!==undefined){match[2]=match[4]}else if(unquoted&&rpseudo.test(unquoted)&&(excess=tokenize(unquoted,true))&&(excess=unquoted.indexOf(")",unquoted.length-excess)-unquoted.length)){match[0]=match[0].slice(0,excess);match[2]=unquoted.slice(0,excess)}return match.slice(0,3)}},filter:{TAG:function(nodeNameSelector){var nodeName=nodeNameSelector.replace(runescape,funescape).toLowerCase();return nodeNameSelector==="*"?function(){return true}:function(elem){return elem.nodeName&&elem.nodeName.toLowerCase()===nodeName}},CLASS:function(className){var pattern=classCache[className+" "];return pattern||(pattern=new RegExp("(^|"+whitespace+")"+className+"("+whitespace+"|$)"))&&classCache(className,function(elem){return pattern.test(typeof elem.className==="string"&&elem.className||typeof elem.getAttribute!==strundefined&&elem.getAttribute("class")||"")})},ATTR:function(name,operator,check){return function(elem){var result=Sizzle.attr(elem,name);if(result==null){return operator==="!="}if(!operator){return true}result+="";return operator==="="?result===check:operator==="!="?result!==check:operator==="^="?check&&result.indexOf(check)===0:operator==="*="?check&&result.indexOf(check)>-1:operator==="$="?check&&result.slice(-check.length)===check:operator==="~="?(" "+result+" ").indexOf(check)>-1:operator==="|="?result===check||result.slice(0,check.length+1)===check+"-":false}},CHILD:function(type,what,argument,first,last){var simple=type.slice(0,3)!=="nth",forward=type.slice(-4)!=="last",ofType=what==="of-type";return first===1&&last===0?function(elem){return!!elem.parentNode}:function(elem,context,xml){var cache,outerCache,node,diff,nodeIndex,start,dir=simple!==forward?"nextSibling":"previousSibling",parent=elem.parentNode,name=ofType&&elem.nodeName.toLowerCase(),useCache=!xml&&!ofType;if(parent){if(simple){while(dir){node=elem;while(node=node[dir]){if(ofType?node.nodeName.toLowerCase()===name:node.nodeType===1){return false}}start=dir=type==="only"&&!start&&"nextSibling"}return true}start=[forward?parent.firstChild:parent.lastChild];if(forward&&useCache){outerCache=parent[expando]||(parent[expando]={});cache=outerCache[type]||[];nodeIndex=cache[0]===dirruns&&cache[1];diff=cache[0]===dirruns&&cache[2];node=nodeIndex&&parent.childNodes[nodeIndex];while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if(node.nodeType===1&&++diff&&node===elem){outerCache[type]=[dirruns,nodeIndex,diff];break}}}else if(useCache&&(cache=(elem[expando]||(elem[expando]={}))[type])&&cache[0]===dirruns){diff=cache[1]}else{while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if((ofType?node.nodeName.toLowerCase()===name:node.nodeType===1)&&++diff){if(useCache){(node[expando]||(node[expando]={}))[type]=[dirruns,diff]}if(node===elem){break}}}}diff-=last;return diff===first||diff%first===0&&diff/first>=0}}},PSEUDO:function(pseudo,argument){var args,fn=Expr.pseudos[pseudo]||Expr.setFilters[pseudo.toLowerCase()]||Sizzle.error("unsupported pseudo: "+pseudo);if(fn[expando]){return fn(argument)}if(fn.length>1){args=[pseudo,pseudo,"",argument];return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase())?markFunction(function(seed,matches){var idx,matched=fn(seed,argument),i=matched.length;while(i--){idx=indexOf.call(seed,matched[i]);seed[idx]=!(matches[idx]=matched[i])}}):function(elem){return fn(elem,0,args)}}return fn}},pseudos:{not:markFunction(function(selector){var input=[],results=[],matcher=compile(selector.replace(rtrim,"$1"));return matcher[expando]?markFunction(function(seed,matches,context,xml){var elem,unmatched=matcher(seed,null,xml,[]),i=seed.length;while(i--){if(elem=unmatched[i]){seed[i]=!(matches[i]=elem)}}}):function(elem,context,xml){input[0]=elem;matcher(input,null,xml,results);return!results.pop()}}),has:markFunction(function(selector){return function(elem){return Sizzle(selector,elem).length>0}}),contains:markFunction(function(text){return function(elem){return(elem.textContent||elem.innerText||getText(elem)).indexOf(text)>-1}}),lang:markFunction(function(lang){if(!ridentifier.test(lang||"")){Sizzle.error("unsupported lang: "+lang)}lang=lang.replace(runescape,funescape).toLowerCase();return function(elem){var elemLang;do{if(elemLang=documentIsHTML?elem.lang:elem.getAttribute("xml:lang")||elem.getAttribute("lang")){elemLang=elemLang.toLowerCase();return elemLang===lang||elemLang.indexOf(lang+"-")===0}}while((elem=elem.parentNode)&&elem.nodeType===1);return false}}),target:function(elem){var hash=window.location&&window.location.hash;return hash&&hash.slice(1)===elem.id},root:function(elem){return elem===docElem},focus:function(elem){return elem===document.activeElement&&(!document.hasFocus||document.hasFocus())&&!!(elem.type||elem.href||~elem.tabIndex)},enabled:function(elem){return elem.disabled===false},disabled:function(elem){return elem.disabled===true},checked:function(elem){var nodeName=elem.nodeName.toLowerCase();return nodeName==="input"&&!!elem.checked||nodeName==="option"&&!!elem.selected},selected:function(elem){if(elem.parentNode){elem.parentNode.selectedIndex}return elem.selected===true},empty:function(elem){for(elem=elem.firstChild;elem;elem=elem.nextSibling){if(elem.nodeName>"@"||elem.nodeType===3||elem.nodeType===4){return false}}return true},parent:function(elem){return!Expr.pseudos["empty"](elem)},header:function(elem){return rheader.test(elem.nodeName)},input:function(elem){return rinputs.test(elem.nodeName)},button:function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type==="button"||name==="button"},text:function(elem){var attr;return elem.nodeName.toLowerCase()==="input"&&elem.type==="text"&&((attr=elem.getAttribute("type"))==null||attr.toLowerCase()===elem.type)},first:createPositionalPseudo(function(){return[0]}),last:createPositionalPseudo(function(matchIndexes,length){return[length-1]}),eq:createPositionalPseudo(function(matchIndexes,length,argument){return[argument<0?argument+length:argument]}),even:createPositionalPseudo(function(matchIndexes,length){var i=0;for(;i=0;){matchIndexes.push(i)}return matchIndexes}),gt:createPositionalPseudo(function(matchIndexes,length,argument){var i=argument<0?argument+length:argument;for(;++i1?function(elem,context,xml){var i=matchers.length;while(i--){if(!matchers[i](elem,context,xml)){return false}}return true}:matchers[0]}function condense(unmatched,map,filter,context,xml){var elem,newUnmatched=[],i=0,len=unmatched.length,mapped=map!=null;for(;i-1){seed[temp]=!(results[temp]=elem)}}}}else{matcherOut=condense(matcherOut===results?matcherOut.splice(preexisting,matcherOut.length):matcherOut);if(postFinder){postFinder(null,results,matcherOut,xml)}else{push.apply(results,matcherOut)}}})}function matcherFromTokens(tokens){var checkContext,matcher,j,len=tokens.length,leadingRelative=Expr.relative[tokens[0].type],implicitRelative=leadingRelative||Expr.relative[" "],i=leadingRelative?1:0,matchContext=addCombinator(function(elem){return elem===checkContext},implicitRelative,true),matchAnyContext=addCombinator(function(elem){return indexOf.call(checkContext,elem)>-1},implicitRelative,true),matchers=[function(elem,context,xml){return!leadingRelative&&(xml||context!==outermostContext)||((checkContext=context).nodeType?matchContext(elem,context,xml):matchAnyContext(elem,context,xml))}];for(;i1&&elementMatcher(matchers),i>1&&toSelector(tokens.slice(0,i-1).concat({value:tokens[i-2].type===" "?"*":""})).replace(rtrim,"$1"),matcher,i0,byElement=elementMatchers.length>0,superMatcher=function(seed,context,xml,results,expandContext){var elem,j,matcher,setMatched=[],matchedCount=0,i="0",unmatched=seed&&[],outermost=expandContext!=null,contextBackup=outermostContext,elems=seed||byElement&&Expr.find["TAG"]("*",expandContext&&context.parentNode||context),dirrunsUnique=dirruns+=contextBackup==null?1:Math.random()||.1,len=elems.length;if(outermost){outermostContext=context!==document&&context;cachedruns=matcherCachedRuns}for(;i!==len&&(elem=elems[i])!=null;i++){if(byElement&&elem){j=0;while(matcher=elementMatchers[j++]){if(matcher(elem,context,xml)){results.push(elem);break}}if(outermost){dirruns=dirrunsUnique;cachedruns=++matcherCachedRuns}}if(bySet){if(elem=!matcher&&elem){matchedCount--}if(seed){unmatched.push(elem)}}}matchedCount+=i;if(bySet&&i!==matchedCount){j=0;while(matcher=setMatchers[j++]){matcher(unmatched,setMatched,context,xml)}if(seed){if(matchedCount>0){while(i--){if(!(unmatched[i]||setMatched[i])){setMatched[i]=pop.call(results)}}}setMatched=condense(setMatched)}push.apply(results,setMatched);if(outermost&&!seed&&setMatched.length>0&&matchedCount+setMatchers.length>1){Sizzle.uniqueSort(results)}}if(outermost){dirruns=dirrunsUnique;outermostContext=contextBackup}return unmatched};return bySet?markFunction(superMatcher):superMatcher}compile=Sizzle.compile=function(selector,group){var i,setMatchers=[],elementMatchers=[],cached=compilerCache[selector+" "];if(!cached){if(!group){group=tokenize(selector)}i=group.length;while(i--){cached=matcherFromTokens(group[i]);if(cached[expando]){setMatchers.push(cached)}else{elementMatchers.push(cached)}}cached=compilerCache(selector,matcherFromGroupMatchers(elementMatchers,setMatchers))}return cached};function multipleContexts(selector,contexts,results){var i=0,len=contexts.length;for(;i2&&(token=tokens[0]).type==="ID"&&support.getById&&context.nodeType===9&&documentIsHTML&&Expr.relative[tokens[1].type]){context=(Expr.find["ID"](token.matches[0].replace(runescape,funescape),context)||[])[0];if(!context){return results}selector=selector.slice(tokens.shift().value.length)}i=matchExpr["needsContext"].test(selector)?0:tokens.length;while(i--){token=tokens[i];if(Expr.relative[type=token.type]){break}if(find=Expr.find[type]){if(seed=find(token.matches[0].replace(runescape,funescape),rsibling.test(tokens[0].type)&&context.parentNode||context)){tokens.splice(i,1);selector=seed.length&&toSelector(tokens);if(!selector){push.apply(results,seed);return results}break}}}}}compile(selector,match)(seed,context,!documentIsHTML,results,rsibling.test(selector));return results}support.sortStable=expando.split("").sort(sortOrder).join("")===expando;support.detectDuplicates=hasDuplicate;setDocument();support.sortDetached=assert(function(div1){return div1.compareDocumentPosition(document.createElement("div"))&1});if(!assert(function(div){div.innerHTML="";return div.firstChild.getAttribute("href")==="#"})){addHandle("type|href|height|width",function(elem,name,isXML){if(!isXML){return elem.getAttribute(name,name.toLowerCase()==="type"?1:2)}})}if(!support.attributes||!assert(function(div){div.innerHTML="";div.firstChild.setAttribute("value","");return div.firstChild.getAttribute("value")===""})){addHandle("value",function(elem,name,isXML){if(!isXML&&elem.nodeName.toLowerCase()==="input"){return elem.defaultValue}})}if(!assert(function(div){return div.getAttribute("disabled")==null})){addHandle(booleans,function(elem,name,isXML){var val;if(!isXML){return(val=elem.getAttributeNode(name))&&val.specified?val.value:elem[name]===true?name.toLowerCase():null}})}if(typeof define==="function"&&define.amd){define(function(){return Sizzle})}else{window.Sizzle=Sizzle}})(window);if(typeof JSON!=="object"){JSON={}}(function(){"use strict";function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i=0;--i){if(handlers[i]===handler){handlers.splice(i,1);break}}},onerror:null,onmessage:null,onopen:null,readyState:0,URL:""};var MessageEvent=function(data,origin,lastEventId){this.data=data;this.origin=origin;this.lastEventId=lastEventId||""};MessageEvent.prototype={data:null,type:"message",lastEventId:"",origin:""};if("module"in global)module.exports=EventSource;global.EventSource=EventSource})(this);var Scout=function(){};Scout=function Scoutmaker(){var xhr;if(window.XMLHttpRequest){xhr=new XMLHttpRequest;if(xhr.overrideMimeType){xhr.overrideMimeType("text/xml")}}else{try{xhr=new ActiveXObject("Msxml2.XMLHTTP")}catch(e){xhr=new ActiveXObject("Microsoft.XMLHTTP")}}var params={method:"POST",resp:function(resp,xhr){},error:function(status,xhr){},partial:function(raw,resp,xhr){}};var toxhrsend=function(data){var str="",start=true;var jsondata="";for(var key in data){if(typeof(jsondata=JSON.stringify(data[key]))==="string"){str+=start?"":"&";str+=encodeURIComponent(key)+"="+encodeURIComponent(jsondata);if(start){start=false}}}return str};var sendxhr=function(target,params){if(params.action){params.url="/$"+params.action}if(params.url){xhr.onreadystatechange=function(){switch(xhr.readyState){case 3:if(params.partial===undefined){var raw=xhr.responseText;var resp;try{resp=JSON.parse(raw)}catch(e){}params.partial.apply(target,[raw,resp,xhr])}break;case 4:if(xhr.status===200){var resp=JSON.parse(xhr.responseText);params.resp.apply(target,[resp,xhr])}else{params.error.apply(target,[xhr.status,xhr])}break}};if(params.method==="POST"&&(params.data==={}||params.data===undefined)){params.method="GET"}xhr.open(params.method,params.url+(params.method==="POST"?"":"?"+toxhrsend(params.data)),true,params.user,params.password);if(params.method==="POST"&¶ms.data!=={}){xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");xhr.send(toxhrsend(params.data))}else{xhr.send(null)}}};var onprop=function(eventName,before){before=before||function(params,e,xhr){};var listenerfunc=function(e){if(!e&&window.event){e=event}var target=e.target||e.srcElement||undefined;if(eventName==="submit"){if(e.preventDefault){e.preventDefault()}else{e.returnValue=false}}before.apply(target,[params,e,xhr]);sendxhr(target,params)};if(document.addEventListener){this.addEventListener(eventName,listenerfunc,false)}else{this.attachEvent("on"+eventName,listenerfunc)}};var ret=function(id){var domelt=document.querySelector(id);if(!domelt){return{on:function(){}}}domelt.on=onprop;return domelt};ret.send=function(before){if(xhr.readyState===1){return Scoutmaker().send(before)}before=before||function(params,xhr){};return function(){before.apply(undefined,[params,xhr]);sendxhr(undefined,params)}};ret.maker=Scoutmaker;ret.eventSource=function(channel){var es=new EventSource("/$"+channel);es.onrecv=function(cb){es.onmessage=function(event){cb(JSON.parse(event.data))}};return es};if(window.io){ret.socket=function(namespace){if(namespace===undefined){namespace="/"}return io.connect(namespace,{resource:"$socket.io"})}}if(window.WebSocket){var wsSend=function wsSendJSON(json){this.send(JSON.stringify(json))};ret.webSocket=function newWebSocket(channel){var socket=new WebSocket("ws"+window.location.protocol.slice(4)+"//"+window.location.host+"/$websocket:"+channel);socket.sendjson=wsSend.bind(socket);return socket}}return ret}(); -------------------------------------------------------------------------------- /web/js/scout.js: -------------------------------------------------------------------------------- 1 | (function(window){var i,support,cachedruns,Expr,getText,isXML,compile,outermostContext,sortInput,setDocument,document,docElem,documentIsHTML,rbuggyQSA,rbuggyMatches,matches,contains,expando="sizzle"+-new Date,preferredDoc=window.document,dirruns=0,done=0,classCache=createCache(),tokenCache=createCache(),compilerCache=createCache(),hasDuplicate=false,sortOrder=function(a,b){if(a===b){hasDuplicate=true;return 0}return 0},strundefined=typeof undefined,MAX_NEGATIVE=1<<31,hasOwn={}.hasOwnProperty,arr=[],pop=arr.pop,push_native=arr.push,push=arr.push,slice=arr.slice,indexOf=arr.indexOf||function(elem){var i=0,len=this.length;for(;i+~]|"+whitespace+")"+whitespace+"*"),rsibling=new RegExp(whitespace+"*[+~]"),rattributeQuotes=new RegExp("="+whitespace+"*([^\\]'\"]*)"+whitespace+"*\\]","g"),rpseudo=new RegExp(pseudos),ridentifier=new RegExp("^"+identifier+"$"),matchExpr={ID:new RegExp("^#("+characterEncoding+")"),CLASS:new RegExp("^\\.("+characterEncoding+")"),TAG:new RegExp("^("+characterEncoding.replace("w","w*")+")"),ATTR:new RegExp("^"+attributes),PSEUDO:new RegExp("^"+pseudos),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+whitespace+"*(even|odd|(([+-]|)(\\d*)n|)"+whitespace+"*(?:([+-]|)"+whitespace+"*(\\d+)|))"+whitespace+"*\\)|)","i"),bool:new RegExp("^(?:"+booleans+")$","i"),needsContext:new RegExp("^"+whitespace+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+whitespace+"*((?:-\\d)?\\d*)"+whitespace+"*\\)|)(?=[^-]|$)","i")},rnative=/^[^{]+\{\s*\[native \w/,rquickExpr=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,rinputs=/^(?:input|select|textarea|button)$/i,rheader=/^h\d$/i,rescape=/'|\\/g,runescape=new RegExp("\\\\([\\da-f]{1,6}"+whitespace+"?|("+whitespace+")|.)","ig"),funescape=function(_,escaped,escapedWhitespace){var high="0x"+escaped-65536;return high!==high||escapedWhitespace?escaped:high<0?String.fromCharCode(high+65536):String.fromCharCode(high>>10|55296,high&1023|56320)};try{push.apply(arr=slice.call(preferredDoc.childNodes),preferredDoc.childNodes);arr[preferredDoc.childNodes.length].nodeType}catch(e){push={apply:arr.length?function(target,els){push_native.apply(target,slice.call(els))}:function(target,els){var j=target.length,i=0;while(target[j++]=els[i++]){}target.length=j-1}}}function Sizzle(selector,context,results,seed){var match,elem,m,nodeType,i,groups,old,nid,newContext,newSelector;if((context?context.ownerDocument||context:preferredDoc)!==document){setDocument(context)}context=context||document;results=results||[];if(!selector||typeof selector!=="string"){return results}if((nodeType=context.nodeType)!==1&&nodeType!==9){return[]}if(documentIsHTML&&!seed){if(match=rquickExpr.exec(selector)){if(m=match[1]){if(nodeType===9){elem=context.getElementById(m);if(elem&&elem.parentNode){if(elem.id===m){results.push(elem);return results}}else{return results}}else{if(context.ownerDocument&&(elem=context.ownerDocument.getElementById(m))&&contains(context,elem)&&elem.id===m){results.push(elem);return results}}}else if(match[2]){push.apply(results,context.getElementsByTagName(selector));return results}else if((m=match[3])&&support.getElementsByClassName&&context.getElementsByClassName){push.apply(results,context.getElementsByClassName(m));return results}}if(support.qsa&&(!rbuggyQSA||!rbuggyQSA.test(selector))){nid=old=expando;newContext=context;newSelector=nodeType===9&&selector;if(nodeType===1&&context.nodeName.toLowerCase()!=="object"){groups=tokenize(selector);if(old=context.getAttribute("id")){nid=old.replace(rescape,"\\$&")}else{context.setAttribute("id",nid)}nid="[id='"+nid+"'] ";i=groups.length;while(i--){groups[i]=nid+toSelector(groups[i])}newContext=rsibling.test(selector)&&context.parentNode||context;newSelector=groups.join(",")}if(newSelector){try{push.apply(results,newContext.querySelectorAll(newSelector));return results}catch(qsaError){}finally{if(!old){context.removeAttribute("id")}}}}}return select(selector.replace(rtrim,"$1"),context,results,seed)}function createCache(){var keys=[];function cache(key,value){if(keys.push(key+=" ")>Expr.cacheLength){delete cache[keys.shift()]}return cache[key]=value}return cache}function markFunction(fn){fn[expando]=true;return fn}function assert(fn){var div=document.createElement("div");try{return!!fn(div)}catch(e){return false}finally{if(div.parentNode){div.parentNode.removeChild(div)}div=null}}function addHandle(attrs,handler){var arr=attrs.split("|"),i=attrs.length;while(i--){Expr.attrHandle[arr[i]]=handler}}function siblingCheck(a,b){var cur=b&&a,diff=cur&&a.nodeType===1&&b.nodeType===1&&(~b.sourceIndex||MAX_NEGATIVE)-(~a.sourceIndex||MAX_NEGATIVE);if(diff){return diff}if(cur){while(cur=cur.nextSibling){if(cur===b){return-1}}}return a?1:-1}function createInputPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type===type}}function createButtonPseudo(type){return function(elem){var name=elem.nodeName.toLowerCase();return(name==="input"||name==="button")&&elem.type===type}}function createPositionalPseudo(fn){return markFunction(function(argument){argument=+argument;return markFunction(function(seed,matches){var j,matchIndexes=fn([],seed.length,argument),i=matchIndexes.length;while(i--){if(seed[j=matchIndexes[i]]){seed[j]=!(matches[j]=seed[j])}}})})}isXML=Sizzle.isXML=function(elem){var documentElement=elem&&(elem.ownerDocument||elem).documentElement;return documentElement?documentElement.nodeName!=="HTML":false};support=Sizzle.support={};setDocument=Sizzle.setDocument=function(node){var doc=node?node.ownerDocument||node:preferredDoc,parent=doc.defaultView;if(doc===document||doc.nodeType!==9||!doc.documentElement){return document}document=doc;docElem=doc.documentElement;documentIsHTML=!isXML(doc);if(parent&&parent.attachEvent&&parent!==parent.top){parent.attachEvent("onbeforeunload",function(){setDocument()})}support.attributes=assert(function(div){div.className="i";return!div.getAttribute("className")});support.getElementsByTagName=assert(function(div){div.appendChild(doc.createComment(""));return!div.getElementsByTagName("*").length});support.getElementsByClassName=assert(function(div){div.innerHTML="
";div.firstChild.className="i";return div.getElementsByClassName("i").length===2});support.getById=assert(function(div){docElem.appendChild(div).id=expando;return!doc.getElementsByName||!doc.getElementsByName(expando).length});if(support.getById){Expr.find["ID"]=function(id,context){if(typeof context.getElementById!==strundefined&&documentIsHTML){var m=context.getElementById(id);return m&&m.parentNode?[m]:[]}};Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){return elem.getAttribute("id")===attrId}}}else{delete Expr.find["ID"];Expr.filter["ID"]=function(id){var attrId=id.replace(runescape,funescape);return function(elem){var node=typeof elem.getAttributeNode!==strundefined&&elem.getAttributeNode("id");return node&&node.value===attrId}}}Expr.find["TAG"]=support.getElementsByTagName?function(tag,context){if(typeof context.getElementsByTagName!==strundefined){return context.getElementsByTagName(tag)}}:function(tag,context){var elem,tmp=[],i=0,results=context.getElementsByTagName(tag);if(tag==="*"){while(elem=results[i++]){if(elem.nodeType===1){tmp.push(elem)}}return tmp}return results};Expr.find["CLASS"]=support.getElementsByClassName&&function(className,context){if(typeof context.getElementsByClassName!==strundefined&&documentIsHTML){return context.getElementsByClassName(className)}};rbuggyMatches=[];rbuggyQSA=[];if(support.qsa=rnative.test(doc.querySelectorAll)){assert(function(div){div.innerHTML="";if(!div.querySelectorAll("[selected]").length){rbuggyQSA.push("\\["+whitespace+"*(?:value|"+booleans+")")}if(!div.querySelectorAll(":checked").length){rbuggyQSA.push(":checked")}});assert(function(div){var input=doc.createElement("input");input.setAttribute("type","hidden");div.appendChild(input).setAttribute("t","");if(div.querySelectorAll("[t^='']").length){rbuggyQSA.push("[*^$]="+whitespace+"*(?:''|\"\")")}if(!div.querySelectorAll(":enabled").length){rbuggyQSA.push(":enabled",":disabled")}div.querySelectorAll("*,:x");rbuggyQSA.push(",.*:")})}if(support.matchesSelector=rnative.test(matches=docElem.webkitMatchesSelector||docElem.mozMatchesSelector||docElem.oMatchesSelector||docElem.msMatchesSelector)){assert(function(div){support.disconnectedMatch=matches.call(div,"div");matches.call(div,"[s!='']:x");rbuggyMatches.push("!=",pseudos)})}rbuggyQSA=rbuggyQSA.length&&new RegExp(rbuggyQSA.join("|"));rbuggyMatches=rbuggyMatches.length&&new RegExp(rbuggyMatches.join("|"));contains=rnative.test(docElem.contains)||docElem.compareDocumentPosition?function(a,b){var adown=a.nodeType===9?a.documentElement:a,bup=b&&b.parentNode;return a===bup||!!(bup&&bup.nodeType===1&&(adown.contains?adown.contains(bup):a.compareDocumentPosition&&a.compareDocumentPosition(bup)&16))}:function(a,b){if(b){while(b=b.parentNode){if(b===a){return true}}}return false};sortOrder=docElem.compareDocumentPosition?function(a,b){if(a===b){hasDuplicate=true;return 0}var compare=b.compareDocumentPosition&&a.compareDocumentPosition&&a.compareDocumentPosition(b);if(compare){if(compare&1||!support.sortDetached&&b.compareDocumentPosition(a)===compare){if(a===doc||contains(preferredDoc,a)){return-1}if(b===doc||contains(preferredDoc,b)){return 1}return sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0}return compare&4?-1:1}return a.compareDocumentPosition?-1:1}:function(a,b){var cur,i=0,aup=a.parentNode,bup=b.parentNode,ap=[a],bp=[b];if(a===b){hasDuplicate=true;return 0}else if(!aup||!bup){return a===doc?-1:b===doc?1:aup?-1:bup?1:sortInput?indexOf.call(sortInput,a)-indexOf.call(sortInput,b):0}else if(aup===bup){return siblingCheck(a,b)}cur=a;while(cur=cur.parentNode){ap.unshift(cur)}cur=b;while(cur=cur.parentNode){bp.unshift(cur)}while(ap[i]===bp[i]){i++}return i?siblingCheck(ap[i],bp[i]):ap[i]===preferredDoc?-1:bp[i]===preferredDoc?1:0};return doc};Sizzle.matches=function(expr,elements){return Sizzle(expr,null,null,elements)};Sizzle.matchesSelector=function(elem,expr){if((elem.ownerDocument||elem)!==document){setDocument(elem)}expr=expr.replace(rattributeQuotes,"='$1']");if(support.matchesSelector&&documentIsHTML&&(!rbuggyMatches||!rbuggyMatches.test(expr))&&(!rbuggyQSA||!rbuggyQSA.test(expr))){try{var ret=matches.call(elem,expr);if(ret||support.disconnectedMatch||elem.document&&elem.document.nodeType!==11){return ret}}catch(e){}}return Sizzle(expr,document,null,[elem]).length>0};Sizzle.contains=function(context,elem){if((context.ownerDocument||context)!==document){setDocument(context)}return contains(context,elem)};Sizzle.attr=function(elem,name){if((elem.ownerDocument||elem)!==document){setDocument(elem)}var fn=Expr.attrHandle[name.toLowerCase()],val=fn&&hasOwn.call(Expr.attrHandle,name.toLowerCase())?fn(elem,name,!documentIsHTML):undefined;return val===undefined?support.attributes||!documentIsHTML?elem.getAttribute(name):(val=elem.getAttributeNode(name))&&val.specified?val.value:null:val};Sizzle.error=function(msg){throw new Error("Syntax error, unrecognized expression: "+msg)};Sizzle.uniqueSort=function(results){var elem,duplicates=[],j=0,i=0;hasDuplicate=!support.detectDuplicates;sortInput=!support.sortStable&&results.slice(0);results.sort(sortOrder);if(hasDuplicate){while(elem=results[i++]){if(elem===results[i]){j=duplicates.push(i)}}while(j--){results.splice(duplicates[j],1)}}return results};getText=Sizzle.getText=function(elem){var node,ret="",i=0,nodeType=elem.nodeType;if(!nodeType){for(;node=elem[i];i++){ret+=getText(node)}}else if(nodeType===1||nodeType===9||nodeType===11){if(typeof elem.textContent==="string"){return elem.textContent}else{for(elem=elem.firstChild;elem;elem=elem.nextSibling){ret+=getText(elem)}}}else if(nodeType===3||nodeType===4){return elem.nodeValue}return ret};Expr=Sizzle.selectors={cacheLength:50,createPseudo:markFunction,match:matchExpr,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:true}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:true},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(match){match[1]=match[1].replace(runescape,funescape);match[3]=(match[4]||match[5]||"").replace(runescape,funescape);if(match[2]==="~="){match[3]=" "+match[3]+" "}return match.slice(0,4)},CHILD:function(match){match[1]=match[1].toLowerCase();if(match[1].slice(0,3)==="nth"){if(!match[3]){Sizzle.error(match[0])}match[4]=+(match[4]?match[5]+(match[6]||1):2*(match[3]==="even"||match[3]==="odd"));match[5]=+(match[7]+match[8]||match[3]==="odd")}else if(match[3]){Sizzle.error(match[0])}return match},PSEUDO:function(match){var excess,unquoted=!match[5]&&match[2];if(matchExpr["CHILD"].test(match[0])){return null}if(match[3]&&match[4]!==undefined){match[2]=match[4]}else if(unquoted&&rpseudo.test(unquoted)&&(excess=tokenize(unquoted,true))&&(excess=unquoted.indexOf(")",unquoted.length-excess)-unquoted.length)){match[0]=match[0].slice(0,excess);match[2]=unquoted.slice(0,excess)}return match.slice(0,3)}},filter:{TAG:function(nodeNameSelector){var nodeName=nodeNameSelector.replace(runescape,funescape).toLowerCase();return nodeNameSelector==="*"?function(){return true}:function(elem){return elem.nodeName&&elem.nodeName.toLowerCase()===nodeName}},CLASS:function(className){var pattern=classCache[className+" "];return pattern||(pattern=new RegExp("(^|"+whitespace+")"+className+"("+whitespace+"|$)"))&&classCache(className,function(elem){return pattern.test(typeof elem.className==="string"&&elem.className||typeof elem.getAttribute!==strundefined&&elem.getAttribute("class")||"")})},ATTR:function(name,operator,check){return function(elem){var result=Sizzle.attr(elem,name);if(result==null){return operator==="!="}if(!operator){return true}result+="";return operator==="="?result===check:operator==="!="?result!==check:operator==="^="?check&&result.indexOf(check)===0:operator==="*="?check&&result.indexOf(check)>-1:operator==="$="?check&&result.slice(-check.length)===check:operator==="~="?(" "+result+" ").indexOf(check)>-1:operator==="|="?result===check||result.slice(0,check.length+1)===check+"-":false}},CHILD:function(type,what,argument,first,last){var simple=type.slice(0,3)!=="nth",forward=type.slice(-4)!=="last",ofType=what==="of-type";return first===1&&last===0?function(elem){return!!elem.parentNode}:function(elem,context,xml){var cache,outerCache,node,diff,nodeIndex,start,dir=simple!==forward?"nextSibling":"previousSibling",parent=elem.parentNode,name=ofType&&elem.nodeName.toLowerCase(),useCache=!xml&&!ofType;if(parent){if(simple){while(dir){node=elem;while(node=node[dir]){if(ofType?node.nodeName.toLowerCase()===name:node.nodeType===1){return false}}start=dir=type==="only"&&!start&&"nextSibling"}return true}start=[forward?parent.firstChild:parent.lastChild];if(forward&&useCache){outerCache=parent[expando]||(parent[expando]={});cache=outerCache[type]||[];nodeIndex=cache[0]===dirruns&&cache[1];diff=cache[0]===dirruns&&cache[2];node=nodeIndex&&parent.childNodes[nodeIndex];while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if(node.nodeType===1&&++diff&&node===elem){outerCache[type]=[dirruns,nodeIndex,diff];break}}}else if(useCache&&(cache=(elem[expando]||(elem[expando]={}))[type])&&cache[0]===dirruns){diff=cache[1]}else{while(node=++nodeIndex&&node&&node[dir]||(diff=nodeIndex=0)||start.pop()){if((ofType?node.nodeName.toLowerCase()===name:node.nodeType===1)&&++diff){if(useCache){(node[expando]||(node[expando]={}))[type]=[dirruns,diff]}if(node===elem){break}}}}diff-=last;return diff===first||diff%first===0&&diff/first>=0}}},PSEUDO:function(pseudo,argument){var args,fn=Expr.pseudos[pseudo]||Expr.setFilters[pseudo.toLowerCase()]||Sizzle.error("unsupported pseudo: "+pseudo);if(fn[expando]){return fn(argument)}if(fn.length>1){args=[pseudo,pseudo,"",argument];return Expr.setFilters.hasOwnProperty(pseudo.toLowerCase())?markFunction(function(seed,matches){var idx,matched=fn(seed,argument),i=matched.length;while(i--){idx=indexOf.call(seed,matched[i]);seed[idx]=!(matches[idx]=matched[i])}}):function(elem){return fn(elem,0,args)}}return fn}},pseudos:{not:markFunction(function(selector){var input=[],results=[],matcher=compile(selector.replace(rtrim,"$1"));return matcher[expando]?markFunction(function(seed,matches,context,xml){var elem,unmatched=matcher(seed,null,xml,[]),i=seed.length;while(i--){if(elem=unmatched[i]){seed[i]=!(matches[i]=elem)}}}):function(elem,context,xml){input[0]=elem;matcher(input,null,xml,results);return!results.pop()}}),has:markFunction(function(selector){return function(elem){return Sizzle(selector,elem).length>0}}),contains:markFunction(function(text){return function(elem){return(elem.textContent||elem.innerText||getText(elem)).indexOf(text)>-1}}),lang:markFunction(function(lang){if(!ridentifier.test(lang||"")){Sizzle.error("unsupported lang: "+lang)}lang=lang.replace(runescape,funescape).toLowerCase();return function(elem){var elemLang;do{if(elemLang=documentIsHTML?elem.lang:elem.getAttribute("xml:lang")||elem.getAttribute("lang")){elemLang=elemLang.toLowerCase();return elemLang===lang||elemLang.indexOf(lang+"-")===0}}while((elem=elem.parentNode)&&elem.nodeType===1);return false}}),target:function(elem){var hash=window.location&&window.location.hash;return hash&&hash.slice(1)===elem.id},root:function(elem){return elem===docElem},focus:function(elem){return elem===document.activeElement&&(!document.hasFocus||document.hasFocus())&&!!(elem.type||elem.href||~elem.tabIndex)},enabled:function(elem){return elem.disabled===false},disabled:function(elem){return elem.disabled===true},checked:function(elem){var nodeName=elem.nodeName.toLowerCase();return nodeName==="input"&&!!elem.checked||nodeName==="option"&&!!elem.selected},selected:function(elem){if(elem.parentNode){elem.parentNode.selectedIndex}return elem.selected===true},empty:function(elem){for(elem=elem.firstChild;elem;elem=elem.nextSibling){if(elem.nodeName>"@"||elem.nodeType===3||elem.nodeType===4){return false}}return true},parent:function(elem){return!Expr.pseudos["empty"](elem)},header:function(elem){return rheader.test(elem.nodeName)},input:function(elem){return rinputs.test(elem.nodeName)},button:function(elem){var name=elem.nodeName.toLowerCase();return name==="input"&&elem.type==="button"||name==="button"},text:function(elem){var attr;return elem.nodeName.toLowerCase()==="input"&&elem.type==="text"&&((attr=elem.getAttribute("type"))==null||attr.toLowerCase()===elem.type)},first:createPositionalPseudo(function(){return[0]}),last:createPositionalPseudo(function(matchIndexes,length){return[length-1]}),eq:createPositionalPseudo(function(matchIndexes,length,argument){return[argument<0?argument+length:argument]}),even:createPositionalPseudo(function(matchIndexes,length){var i=0;for(;i=0;){matchIndexes.push(i)}return matchIndexes}),gt:createPositionalPseudo(function(matchIndexes,length,argument){var i=argument<0?argument+length:argument;for(;++i1?function(elem,context,xml){var i=matchers.length;while(i--){if(!matchers[i](elem,context,xml)){return false}}return true}:matchers[0]}function condense(unmatched,map,filter,context,xml){var elem,newUnmatched=[],i=0,len=unmatched.length,mapped=map!=null;for(;i-1){seed[temp]=!(results[temp]=elem)}}}}else{matcherOut=condense(matcherOut===results?matcherOut.splice(preexisting,matcherOut.length):matcherOut);if(postFinder){postFinder(null,results,matcherOut,xml)}else{push.apply(results,matcherOut)}}})}function matcherFromTokens(tokens){var checkContext,matcher,j,len=tokens.length,leadingRelative=Expr.relative[tokens[0].type],implicitRelative=leadingRelative||Expr.relative[" "],i=leadingRelative?1:0,matchContext=addCombinator(function(elem){return elem===checkContext},implicitRelative,true),matchAnyContext=addCombinator(function(elem){return indexOf.call(checkContext,elem)>-1},implicitRelative,true),matchers=[function(elem,context,xml){return!leadingRelative&&(xml||context!==outermostContext)||((checkContext=context).nodeType?matchContext(elem,context,xml):matchAnyContext(elem,context,xml))}];for(;i1&&elementMatcher(matchers),i>1&&toSelector(tokens.slice(0,i-1).concat({value:tokens[i-2].type===" "?"*":""})).replace(rtrim,"$1"),matcher,i0,byElement=elementMatchers.length>0,superMatcher=function(seed,context,xml,results,expandContext){var elem,j,matcher,setMatched=[],matchedCount=0,i="0",unmatched=seed&&[],outermost=expandContext!=null,contextBackup=outermostContext,elems=seed||byElement&&Expr.find["TAG"]("*",expandContext&&context.parentNode||context),dirrunsUnique=dirruns+=contextBackup==null?1:Math.random()||.1,len=elems.length;if(outermost){outermostContext=context!==document&&context;cachedruns=matcherCachedRuns}for(;i!==len&&(elem=elems[i])!=null;i++){if(byElement&&elem){j=0;while(matcher=elementMatchers[j++]){if(matcher(elem,context,xml)){results.push(elem);break}}if(outermost){dirruns=dirrunsUnique;cachedruns=++matcherCachedRuns}}if(bySet){if(elem=!matcher&&elem){matchedCount--}if(seed){unmatched.push(elem)}}}matchedCount+=i;if(bySet&&i!==matchedCount){j=0;while(matcher=setMatchers[j++]){matcher(unmatched,setMatched,context,xml)}if(seed){if(matchedCount>0){while(i--){if(!(unmatched[i]||setMatched[i])){setMatched[i]=pop.call(results)}}}setMatched=condense(setMatched)}push.apply(results,setMatched);if(outermost&&!seed&&setMatched.length>0&&matchedCount+setMatchers.length>1){Sizzle.uniqueSort(results)}}if(outermost){dirruns=dirrunsUnique;outermostContext=contextBackup}return unmatched};return bySet?markFunction(superMatcher):superMatcher}compile=Sizzle.compile=function(selector,group){var i,setMatchers=[],elementMatchers=[],cached=compilerCache[selector+" "];if(!cached){if(!group){group=tokenize(selector)}i=group.length;while(i--){cached=matcherFromTokens(group[i]);if(cached[expando]){setMatchers.push(cached)}else{elementMatchers.push(cached)}}cached=compilerCache(selector,matcherFromGroupMatchers(elementMatchers,setMatchers))}return cached};function multipleContexts(selector,contexts,results){var i=0,len=contexts.length;for(;i2&&(token=tokens[0]).type==="ID"&&support.getById&&context.nodeType===9&&documentIsHTML&&Expr.relative[tokens[1].type]){context=(Expr.find["ID"](token.matches[0].replace(runescape,funescape),context)||[])[0];if(!context){return results}selector=selector.slice(tokens.shift().value.length)}i=matchExpr["needsContext"].test(selector)?0:tokens.length;while(i--){token=tokens[i];if(Expr.relative[type=token.type]){break}if(find=Expr.find[type]){if(seed=find(token.matches[0].replace(runescape,funescape),rsibling.test(tokens[0].type)&&context.parentNode||context)){tokens.splice(i,1);selector=seed.length&&toSelector(tokens);if(!selector){push.apply(results,seed);return results}break}}}}}compile(selector,match)(seed,context,!documentIsHTML,results,rsibling.test(selector));return results}support.sortStable=expando.split("").sort(sortOrder).join("")===expando;support.detectDuplicates=hasDuplicate;setDocument();support.sortDetached=assert(function(div1){return div1.compareDocumentPosition(document.createElement("div"))&1});if(!assert(function(div){div.innerHTML="";return div.firstChild.getAttribute("href")==="#"})){addHandle("type|href|height|width",function(elem,name,isXML){if(!isXML){return elem.getAttribute(name,name.toLowerCase()==="type"?1:2)}})}if(!support.attributes||!assert(function(div){div.innerHTML="";div.firstChild.setAttribute("value","");return div.firstChild.getAttribute("value")===""})){addHandle("value",function(elem,name,isXML){if(!isXML&&elem.nodeName.toLowerCase()==="input"){return elem.defaultValue}})}if(!assert(function(div){return div.getAttribute("disabled")==null})){addHandle(booleans,function(elem,name,isXML){var val;if(!isXML){return(val=elem.getAttributeNode(name))&&val.specified?val.value:elem[name]===true?name.toLowerCase():null}})}if(typeof define==="function"&&define.amd){define(function(){return Sizzle})}else{window.Sizzle=Sizzle}})(window);if(typeof JSON!=="object"){JSON={}}(function(){"use strict";function f(n){return n<10?"0"+n:n}if(typeof Date.prototype.toJSON!=="function"){Date.prototype.toJSON=function(){return isFinite(this.valueOf())?this.getUTCFullYear()+"-"+f(this.getUTCMonth()+1)+"-"+f(this.getUTCDate())+"T"+f(this.getUTCHours())+":"+f(this.getUTCMinutes())+":"+f(this.getUTCSeconds())+"Z":null};String.prototype.toJSON=Number.prototype.toJSON=Boolean.prototype.toJSON=function(){return this.valueOf()}}var cx=/[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,escapable=/[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,gap,indent,meta={"\b":"\\b"," ":"\\t","\n":"\\n","\f":"\\f","\r":"\\r",'"':'\\"',"\\":"\\\\"},rep;function quote(string){escapable.lastIndex=0;return escapable.test(string)?'"'+string.replace(escapable,function(a){var c=meta[a];return typeof c==="string"?c:"\\u"+("0000"+a.charCodeAt(0).toString(16)).slice(-4)})+'"':'"'+string+'"'}function str(key,holder){var i,k,v,length,mind=gap,partial,value=holder[key];if(value&&typeof value==="object"&&typeof value.toJSON==="function"){value=value.toJSON(key)}if(typeof rep==="function"){value=rep.call(holder,key,value)}switch(typeof value){case"string":return quote(value);case"number":return isFinite(value)?String(value):"null";case"boolean":case"null":return String(value);case"object":if(!value){return"null"}gap+=indent;partial=[];if(Object.prototype.toString.apply(value)==="[object Array]"){length=value.length;for(i=0;i=0;--i){if(handlers[i]===handler){handlers.splice(i,1);break}}},onerror:null,onmessage:null,onopen:null,readyState:0,URL:""};var MessageEvent=function(data,origin,lastEventId){this.data=data;this.origin=origin;this.lastEventId=lastEventId||""};MessageEvent.prototype={data:null,type:"message",lastEventId:"",origin:""};if("module"in global)module.exports=EventSource;global.EventSource=EventSource})(this);var Scout=function(){};Scout=function Scoutmaker(){var xhr;if(window.XMLHttpRequest){xhr=new XMLHttpRequest;if(xhr.overrideMimeType){xhr.overrideMimeType("text/xml")}}else{try{xhr=new ActiveXObject("Msxml2.XMLHTTP")}catch(e){xhr=new ActiveXObject("Microsoft.XMLHTTP")}}var params={method:"POST",resp:function(resp,xhr){},error:function(status,xhr){},partial:function(raw,resp,xhr){}};var toxhrsend=function(data){var str="",start=true;var jsondata="";for(var key in data){if(typeof(jsondata=JSON.stringify(data[key]))==="string"){str+=start?"":"&";str+=encodeURIComponent(key)+"="+encodeURIComponent(jsondata);if(start){start=false}}}return str};var sendxhr=function(target,params){if(params.action){params.url="/$"+params.action}if(params.url){xhr.onreadystatechange=function(){switch(xhr.readyState){case 3:if(params.partial===undefined){var raw=xhr.responseText;var resp;try{resp=JSON.parse(raw)}catch(e){}params.partial.apply(target,[raw,resp,xhr])}break;case 4:if(xhr.status===200){var resp=JSON.parse(xhr.responseText);params.resp.apply(target,[resp,xhr])}else{params.error.apply(target,[xhr.status,xhr])}break}};if(params.method==="POST"&&(params.data==={}||params.data===undefined)){params.method="GET"}xhr.open(params.method,params.url+(params.method==="POST"?"":"?"+toxhrsend(params.data)),true,params.user,params.password);if(params.method==="POST"&¶ms.data!=={}){xhr.setRequestHeader("Content-Type","application/x-www-form-urlencoded");xhr.send(toxhrsend(params.data))}else{xhr.send(null)}}};var onprop=function(eventName,before){before=before||function(params,e,xhr){};var listenerfunc=function(e){if(!e&&window.event){e=event}var target=e.target||e.srcElement||undefined;if(eventName==="submit"){if(e.preventDefault){e.preventDefault()}else{e.returnValue=false}}before.apply(target,[params,e,xhr]);sendxhr(target,params)};if(document.addEventListener){this.addEventListener(eventName,listenerfunc,false)}else{this.attachEvent("on"+eventName,listenerfunc)}};var ret=function(id){var domelt=document.querySelector(id);if(!domelt){return{on:function(){}}}domelt.on=onprop;return domelt};ret.send=function(before){if(xhr.readyState===1){return Scoutmaker().send(before)}before=before||function(params,xhr){};return function(){before.apply(undefined,[params,xhr]);sendxhr(undefined,params)}};ret.maker=Scoutmaker;ret.eventSource=function(channel){var es=new EventSource("/$"+channel);es.onrecv=function(cb){es.onmessage=function(event){cb(JSON.parse(event.data))}};return es};if(window.io){ret.socket=function(namespace){if(namespace===undefined){namespace="/"}return io.connect(namespace,{resource:"$socket.io"})}}if(window.WebSocket){var wsSend=function wsSendJSON(json){this.send(JSON.stringify(json))};ret.webSocket=function newWebSocket(channel){var socket=new WebSocket("ws"+window.location.protocol.slice(4)+"//"+window.location.host+"/$websocket:"+channel);socket.sendjson=wsSend.bind(socket);return socket}}return ret}(); -------------------------------------------------------------------------------- /lib/camp.js: -------------------------------------------------------------------------------- 1 | // Server-side Ajax handler that wraps around node.js. 2 | // Copyright © Thaddee Tyl, Jan Keromnes. All rights reserved. 3 | // Code covered by the LGPL license. 4 | 5 | "use strict"; 6 | 7 | var templateReader = require('fleau'); 8 | var formidable = require('formidable'); 9 | var WebSocket = require('ws'); 10 | var Cookies = require('cookies'); 11 | 12 | var EventEmitter = require ('events').EventEmitter; 13 | var inherits = require('util').inherits; 14 | var http = require('http'); 15 | var https = require('https'); 16 | var spdy = require('spdy'); 17 | var log = require('multilog'); 18 | var p = require('path'); 19 | var fs = require('fs'); 20 | var url = require('url'); 21 | var zlib = require('zlib'); 22 | var stream = require('stream'); 23 | var querystring = require('querystring'); 24 | 25 | // Logs. 26 | log.pipe('warn', 'all'); 27 | log.pipe('error', 'all'); 28 | log.pipe('error', 'stderr'); 29 | log.pipe('warn', 'stderr'); 30 | 31 | 32 | 33 | 34 | var mime = require('./mime.json'); 35 | var binaries = [ 36 | 'pdf', 'ps', 'odt', 'ods', 'odp', 'xls', 'doc', 'ppt', 'dvi', 'ttf', 37 | 'swf', 'rar', 'zip', 'tar', 'gz', 'ogg', 'mp3', 'mpeg', 'wav', 'wma', 38 | 'gif', 'jpg', 'jpeg', 'png', 'svg', 'tiff', 'ico', 'mp4', 'ogv', 'mov', 39 | 'webm', 'wmv' 40 | ]; 41 | var serverStartTime = new Date(); 42 | var serverStartTimeGMTString = serverStartTime.toGMTString(); 43 | 44 | 45 | // Augment IncomingMessage and ServerResponse. 46 | function augmentReqRes(req, res, server) { 47 | req.server = server; 48 | req.uri = url.parse(req.url, true); 49 | // The form is used for multipart data. 50 | req.form = undefined; 51 | try { 52 | req.path = decodeURIComponent(req.uri.pathname); 53 | } catch(e) { // Using `escape` should not kill the server. 54 | req.path = unescape(req.uri.pathname); 55 | } 56 | req.query = req.uri.query; 57 | req.data = req.query; 58 | // Check (and add fields) for basic authentication. 59 | req.username = undefined; 60 | req.password = undefined; 61 | if (req.headers.authorization) { 62 | var authorization = req.headers.authorization; 63 | var authComponent = authorization.split(/\s+/); 64 | if (authComponent[0] === 'Basic') { 65 | // Username / password. 66 | if (typeof authComponent[1] === 'string' || authComponent[1] instanceof String) { 67 | var up = Buffer.from(authComponent[1], 'base64').toString().split(':'); 68 | req.username = up[0]; 69 | req.password = up[1]; 70 | } 71 | } 72 | } 73 | // Cookies 74 | req.cookies = new Cookies(req, res); 75 | // Templates 76 | res.template = function(scope, templates) { 77 | // If there is no template. 78 | if (templates === undefined) { 79 | // We assume that the template is for a file on disk under documentRoot. 80 | var realpath = protectedPath(req.server.documentRoot, req.path); 81 | templates = [server.template(realpath)]; 82 | } 83 | // If there is only one template. 84 | if (!(templates instanceof Array)) { templates = [templates]; } 85 | 86 | if (templates.length > 0) { 87 | res.mime(p.extname(templates[0].paths[0]).slice(1)); 88 | } 89 | var templateStreams = templates.map(function(template) { 90 | if (typeof template === 'string') { 91 | return streamFromString(template); 92 | } else if (typeof template === 'function') { 93 | return template(scope); 94 | } else { 95 | log('Template ' + template + ' does not have a valid type', 'warn'); 96 | return streamFromString(''); 97 | } 98 | }); 99 | var template = concatStreams(templateStreams); 100 | template.on('error', function(err) { 101 | log(err.stack, 'error'); 102 | res.end('Not Found\n'); 103 | }); 104 | template.pipe(res.compressed()); 105 | }; 106 | // Sending a file 107 | res.file = function(path) { 108 | respondWithFile(req, res, path, function ifNoFile() { 109 | log('The file "' + path + '", which was meant to be sent back, ' + 110 | 'was not found', 'error'); 111 | res.statusCode = 404; 112 | res.end('Not Found\n'); 113 | }); 114 | }; 115 | // Sending JSON data 116 | res.json = function (data, replacer, space) { 117 | res.setHeader('Content-Type', mime.json); 118 | var json = JSON.stringify(data, replacer, space) + (space ? '\n' : ''); 119 | res.compressed().end(json); 120 | }; 121 | res.compressed = function() { 122 | return getCompressedStream(req, res) || res; 123 | }; 124 | res.redirect = function(path) { 125 | res.setHeader('Location', path); 126 | res.statusCode = 303; 127 | res.end(); 128 | }; 129 | } 130 | 131 | // Set a content type based on a file extension. 132 | http.ServerResponse.prototype.mime = 133 | function mimeFromExt(ext) { 134 | this.setHeader('Content-Type', mime[ext] || 'text/plain'); 135 | } 136 | 137 | // Ask is a model of the client's request / response environment. 138 | // It will be slowly deprecated. 139 | function Ask(server, req, res) { 140 | this.server = server; 141 | this.req = req; 142 | this.res = res; 143 | this.uri = req.uri; 144 | this.form = req.form; 145 | this.path = req.path; 146 | this.query = req.query; 147 | this.username = req.username; 148 | this.password = req.password; 149 | this.cookies = req.cookies; 150 | } 151 | 152 | // Set the mime type of the response. 153 | Ask.prototype.mime = function (type) { 154 | this.res.setHeader('Content-Type', type); 155 | }; 156 | 157 | function addToQuery(ask, obj) { 158 | for (var item in obj) { 159 | ask.query[item] = obj[item]; 160 | } 161 | } 162 | 163 | function jsonFromQuery(query, obj) { 164 | obj = obj || Object.create(null); 165 | // First attempt to decode the query as JSON 166 | // (ie, 'foo="bar"&baz={"something":"else"}'). 167 | try { 168 | var items = query.split('&'); 169 | for (var i = 0; i < items.length; i++) { 170 | // Each element of key=value is then again split along `=`. 171 | var elems = items[i].split('='); 172 | obj[decodeURIComponent(elems[0])] = 173 | JSON.parse(decodeURIComponent(elems[1])); 174 | } 175 | } catch(e) { 176 | // Couldn't parse as JSON. 177 | try { 178 | var newobj = querystring.parse(query); 179 | } catch(e) { 180 | log('Error while parsing query ', JSON.stringify(query) + '\n' 181 | + '(' + e.toString() + ')\n' 182 | , 'error'); 183 | } 184 | var keys = Object.keys(newobj); 185 | for (var i = 0; i < keys.length; i++) { 186 | obj[keys[i]] = newobj[keys[i]]; 187 | } 188 | } 189 | } 190 | 191 | // We'll need to parse the query as a literal. 192 | // Ask objects already have ask.query set after the URL query part. 193 | // This function updates ask.query with: 194 | // - application/x-www-form-urlencoded 195 | // - multipart/form-data 196 | function getQueries(req, end) { 197 | var urlencoded = /^application\/x-www-form-urlencoded/; 198 | var multipart = /^multipart\/form-data/; 199 | var contentType = req.headers['content-type'] || ''; 200 | 201 | if (multipart.test(contentType)) { 202 | // Multipart data. 203 | req.form = req.form || new formidable.IncomingForm(); 204 | req.form.multiples = true; 205 | req.form.parse(req, function(err, fields, files) { 206 | req.fields = fields; 207 | req.files = files; 208 | // Ensure that files are arrays. 209 | for (var key in req.files) { 210 | if (!(req.files[key] instanceof Array)) { 211 | req.files[key] = [req.files[key]]; 212 | } 213 | } 214 | if (err == null) { 215 | addToQuery(req, req.fields); 216 | addToQuery(req, req.files); 217 | } 218 | end(err); 219 | }); 220 | 221 | } else if (urlencoded.test(contentType)) { 222 | // URL encoded data. 223 | var chunks; 224 | var gotrequest = function (chunk) { 225 | if (chunk !== undefined) { 226 | if (chunks === undefined) { 227 | chunks = chunk; 228 | } else { 229 | chunks = Buffer.concat([chunks, chunk]); 230 | } 231 | } 232 | }; 233 | req.on('data', gotrequest); 234 | req.on('end', function(err) { 235 | if (req.server.saveRequestChunks) { 236 | req.savedChunks = chunks; 237 | } 238 | var strquery = chunks? chunks.toString(): ''; 239 | jsonFromQuery(strquery, req.query); 240 | end(err); 241 | }); 242 | } else { 243 | // URL query parameters. 244 | var search = req.uri.search || ''; 245 | jsonFromQuery(search.slice(1), req.query); 246 | end(); 247 | } 248 | } 249 | 250 | // Return a writable response stream, using compression when possible. 251 | function getCompressedStream(req, res) { 252 | var encoding = req.headers['accept-encoding'] || ''; 253 | var stream; 254 | var contentEncodingHeader = res.getHeader('Content-Encoding'); 255 | if ((contentEncodingHeader === 'gzip') || /\bgzip\b/.test(encoding)) { 256 | if (!contentEncodingHeader) { 257 | res.setHeader('Content-Encoding', 'gzip'); 258 | } 259 | stream = zlib.createGzip(); 260 | stream.pipe(res); 261 | } else if ((contentEncodingHeader === 'deflate') || 262 | /\bdeflate\b/.test(encoding)) { 263 | if (!contentEncodingHeader) { 264 | res.setHeader('Content-Encoding', 'deflate'); 265 | } 266 | stream = zlib.createDeflate(); 267 | stream.pipe(res); 268 | } 269 | return stream; 270 | } 271 | 272 | // Concatenate an array of streams into a single stream. 273 | function concatStreams(array) { 274 | var concat = new stream.PassThrough(); 275 | 276 | function pipe(i) { 277 | if (i < array.length - 1) { 278 | array[i].pipe(concat, {end: false}); 279 | array[i].on('end', function () { pipe(i + 1) }); 280 | } else { 281 | array[i].pipe(concat); 282 | } 283 | } 284 | if (array.length > 0) { 285 | pipe(0); 286 | } 287 | 288 | return concat; 289 | } 290 | 291 | // paths: paths to templating file (String), or to an Array of templating files. 292 | // options: 293 | // - reader: template reader function. 294 | // - asString: use the string as a template, not as a file path. 295 | // - callback: function taking a function(scope) → readableStream. 296 | // If you don't want the template creation to be synchronous, use this. 297 | // We return nothing from the function if `callback` is set. 298 | // Returns a function(scope) → readableStream, unless `callback` is set. 299 | function template(paths, options) { 300 | options = options || {}; 301 | var callback = options.callback; 302 | var reader = options.reader || this.templateReader; 303 | 304 | // Deal with a list of paths in the general case. 305 | if (!(paths instanceof Array)) { paths = [paths]; } 306 | 307 | var input = ''; 308 | if (!callback) { 309 | for (var i = 0, pathLen = paths.length; i < pathLen; i++) { 310 | if (options.asString) { 311 | input += paths[i]; 312 | } else { 313 | input += '' + fs.readFileSync(paths[i]); 314 | } 315 | } 316 | var result = reader.create(input); 317 | result.paths = paths; 318 | return result; 319 | 320 | } else { 321 | // We have a callback. 322 | var pathLen = paths.length; 323 | var pathCounter = 0; 324 | paths.forEach(function(path, i) { 325 | if (options.asString) { 326 | var getInput = function(cb) { cb(path); }; 327 | } else { 328 | var getInput = function(cb) { 329 | fs.readFile(path, function(err, string) { 330 | if (err != null) { 331 | log('Error reading template file:\n' + err, 'error'); 332 | return cb(''); 333 | } 334 | return cb('' + string); 335 | }); 336 | }; 337 | } 338 | getInput(function(fileInput) { 339 | pathCounter++; 340 | input += fileInput; 341 | 342 | if (pathCounter >= pathLen) { 343 | var result = reader.create(input); 344 | result.paths = paths; 345 | callback(result); 346 | } 347 | }); 348 | }); 349 | } 350 | } 351 | 352 | 353 | 354 | 355 | // Camp class is classy. 356 | // 357 | // Camp has a router function that returns the stack of functions to call, one 358 | // after the other, in order to process the request. 359 | 360 | function augmentServer(server, opts) { 361 | server.templateReader = opts.templateReader || templateReader; 362 | server.documentRoot = opts.documentRoot || p.join(process.cwd(), 'web'); 363 | server.saveRequestChunks = !!opts.saveRequestChunks; 364 | server.template = template; 365 | server.stack = []; 366 | server.staticMaxAge = opts.staticMaxAge 367 | server.stackInsertion = 0; 368 | defaultRoute.forEach(function(mkfn) { server.handle(mkfn(server)); }); 369 | server.stackInsertion = 0; 370 | server.on('request', function(req, res) { listener(server, req, res) }); 371 | } 372 | 373 | function Camp(opts) { 374 | http.Server.call(this); 375 | augmentServer(this, opts); 376 | } 377 | inherits(Camp, http.Server); 378 | 379 | function SecureCamp(opts) { 380 | https.Server.call(this, opts); 381 | augmentServer(this, opts); 382 | } 383 | inherits(SecureCamp, https.Server); 384 | 385 | function SpdyCamp(opts) { 386 | spdy.Server.call(this, opts); 387 | augmentServer(this, opts); 388 | } 389 | inherits(SpdyCamp, spdy.Server); 390 | 391 | Camp.prototype.handle = SecureCamp.prototype.handle = SpdyCamp.prototype.handle = 392 | function handle(fn) { 393 | this.stack.splice(this.stackInsertion, 0, fn); 394 | this.stackInsertion++; 395 | }; 396 | 397 | Camp.prototype.removeHandler = SecureCamp.prototype.removeHandler = SpdyCamp.prototype.removeHandler = 398 | function removeHandler(fn) { 399 | var index = this.stack.indexOf(fn); 400 | if (index < 0) { return; } 401 | if (index < this.stackInsertion) { 402 | this.stackInsertion--; 403 | } 404 | this.stack.splice(index, 1); 405 | }; 406 | 407 | // Default request listener. 408 | 409 | function listener(server, req, res) { 410 | augmentReqRes(req, res, server); 411 | var ask = new Ask(server, req, res); 412 | req.ask = ask; // Legacy. 413 | bubble(ask, 0); 414 | } 415 | 416 | // The bubble goes through each layer of the stack until it reaches the surface. 417 | // The surface is a Server Error, btw. 418 | function bubble(ask, layer) { 419 | ask.server.stack[layer](ask.req, ask.res, function next() { 420 | if (ask.server.stack.length > layer + 1) bubble(ask, layer + 1); 421 | else { 422 | ask.res.statusCode = 500; 423 | ask.res.end('Internal Server Error\n'); 424 | } 425 | }); 426 | } 427 | 428 | 429 | // On-demand loading of socket.io. 430 | Camp.prototype.socketIo = SecureCamp.prototype.socketIo = SpdyCamp.prototype.socketIo 431 | = null; 432 | var socketIoProperty = { 433 | get: function() { 434 | if (this.socketIo === null) { 435 | this.socketIo = require('socket.io')(this, {path: '/$socket.io'}); 436 | // Add socketUnit only once. 437 | this.stack.unshift(socketUnit(this)); 438 | } 439 | return this.socketIo; 440 | }, 441 | }; 442 | Object.defineProperty(Camp.prototype, 'io', socketIoProperty); 443 | Object.defineProperty(SecureCamp.prototype, 'io', socketIoProperty); 444 | Object.defineProperty(SpdyCamp.prototype, 'io', socketIoProperty); 445 | 446 | 447 | 448 | // Generic unit. Deprecated. 449 | function genericUnit (server) { 450 | var processors = []; 451 | server.handler = function (f) { processors.push(f); }; 452 | return function genericLayer (req, res, next) { 453 | for (var i = 0; i < processors.length; i++) { 454 | var keep = processors[i](req.ask); 455 | if (keep) { return; } // Don't call next, nor the rest. 456 | } 457 | next(); // We never catch that request. 458 | }; 459 | } 460 | 461 | // Socket.io unit. 462 | function socketUnit (server) { 463 | var io = server.io; 464 | // Client-side: 465 | 466 | return function socketLayer (req, res, next) { 467 | // Socket.io doesn't care about anything but /$socket.io now. 468 | if (req.path.slice(1, 11) !== '$socket.io') next(); 469 | }; 470 | } 471 | 472 | // WebSocket unit. 473 | function wsUnit (server) { 474 | var chanPool = server.wsChannels = {}; 475 | // Main WebSocket API: 476 | // ws(channel :: String, conListener :: function(socket)) 477 | server.ws = function ws (channel, conListener) { 478 | if (channel[0] !== '/') { 479 | channel = '/$websocket:' + channel; // Deprecated API. 480 | } 481 | if (chanPool[channel] !== undefined) { 482 | chanPool[channel].close(); 483 | } 484 | chanPool[channel] = new WebSocket.Server({ 485 | server: server, 486 | path: channel, 487 | }); 488 | chanPool[channel].on('connection', conListener); 489 | return chanPool[channel]; 490 | }; 491 | // WebSocket broadcast API. 492 | // webBroadcast(channel :: String, recvListener :: function(data, end)) 493 | server.wsBroadcast = function wsBroadcast (channel, recvListener) { 494 | if (channel[0] === '/') { 495 | return server.ws(channel, function (socket) { 496 | socket.on('message', function wsBroadcastRecv (data, flags) { 497 | recvListener({data: data, flags: flags}, { 498 | send: function wsBroadcastSend (dataBack) { 499 | chanPool[channel].clients.forEach(function (s) { 500 | s.send(dataBack); 501 | }); 502 | }, 503 | }); 504 | }); 505 | }); 506 | } else { // Deprecated API 507 | return server.ws(channel, function (socket) { 508 | socket.on('message', function wsBroadcastRecv (data, flags) { 509 | recvListener(data, function wsBroadcastSend (dataBack) { 510 | chanPool[channel].clients.forEach(function (s) { s.send(dataBack); }); 511 | }); 512 | }); 513 | }); 514 | } 515 | }; 516 | 517 | return function wsLayer (req, res, next) { 518 | // This doesn't actually get run, since ws overrides it at the root. 519 | if (chanPool[req.path] === undefined) return next(); 520 | }; 521 | } 522 | 523 | // Ajax unit. 524 | function ajaxUnit (server) { 525 | var ajax = server.ajax = new EventEmitter(); 526 | // Register events to be fired before loading the ajax data. 527 | var ajaxReq = server.ajaxReq = new EventEmitter(); 528 | 529 | return function ajaxLayer (req, res, next) { 530 | if (req.path[1] !== '$') { return next(); } 531 | var action = req.path.slice(2); 532 | 533 | if (ajax.listeners(action).length <= 0) { return next(); } 534 | 535 | res.setHeader('Content-Type', mime.json); 536 | 537 | ajaxReq.emit(action, req.ask); 538 | // Get all data requests. 539 | getQueries(req, function(err) { 540 | if (err == null) { 541 | ajax.emit(action, req.query, function ajaxEnd(data) { 542 | res.compressed().end(JSON.stringify(data || {})); 543 | }, req.ask); 544 | } else { 545 | log('While parsing', req.url + ':\n' 546 | + err 547 | , 'error'); 548 | return next(); 549 | } 550 | }); 551 | }; 552 | } 553 | 554 | 555 | // EventSource unit. 556 | // 557 | // Note: great inspiration was taken from Remy Sharp's code. 558 | function eventSourceUnit (server) { 559 | var sources = {}; 560 | 561 | function Source () { 562 | this.conn = []; 563 | this.history = []; 564 | this.lastMsgId = 0; 565 | } 566 | 567 | Source.prototype.removeConn = function(res) { 568 | var i = this.conn.indexOf(res); 569 | if (i !== -1) { 570 | this.conn.splice(i, 1); 571 | } 572 | }; 573 | 574 | Source.prototype.sendSSE = function (res, id, event, message) { 575 | var data = ''; 576 | if (event !== null) { 577 | data += 'event:' + event + '\n'; 578 | } 579 | 580 | // Blank id resets the id counter. 581 | if (id !== null) { 582 | data += 'id:' + id + '\n'; 583 | } else { 584 | data += 'id\n'; 585 | } 586 | 587 | if (message) { 588 | data += 'data:' + message.split('\n').join('\ndata:') + '\n'; 589 | } 590 | data += '\n'; 591 | 592 | res.write(data); 593 | 594 | if (res.hasOwnProperty('xhr')) { 595 | clearTimeout(res.xhr); 596 | var self = this; 597 | res.xhr = setTimeout(function () { 598 | res.end(); 599 | self.removeConn(res); 600 | }, 250); 601 | } 602 | }; 603 | 604 | Source.prototype.emit = function (event, msg) { 605 | this.lastMsgId++; 606 | this.history.push({ 607 | id: this.lastMsgId, 608 | event: event, 609 | msg: msg 610 | }); 611 | 612 | for (var i = 0; i < this.conn.length; i++) { 613 | this.sendSSE(this.conn[i], this.lastMsgId, event, msg); 614 | } 615 | } 616 | 617 | Source.prototype.send = function (msg) { 618 | this.emit(null, JSON.stringify(msg)); 619 | } 620 | 621 | function eventSource (channel) { 622 | if (channel[0] !== '/') { channel = '/$' + channel; } 623 | return sources[channel] = new Source(); 624 | } 625 | 626 | server.eventSource = eventSource; 627 | 628 | 629 | return function eventSourceLayer (req, res, next) { 630 | if (sources[req.path] === undefined) return next(); 631 | var source = sources[req.path]; 632 | if (!source || req.headers.accept !== 'text/event-stream') 633 | return next(); // Don't bother if the client cannot handle it. 634 | 635 | // Remy Sharp's Polyfill support. 636 | if (req.headers['x-requested-with'] == 'XMLHttpRequest') { 637 | res.xhr = null; 638 | } 639 | 640 | res.setHeader('Content-Type', 'text/event-stream'); 641 | res.setHeader('Cache-Control', 'no-cache'); 642 | 643 | if (req.headers['last-event-id']) { 644 | var id = parseInt(req.headers['last-event-id']); 645 | for (var i = 0; i < source.history.length; i++) 646 | if (source.history[i].id > id) 647 | source.sendSSE(res, source.history[i].id, 648 | source.history[i].event, source.history[i].msg); 649 | } else res.write('id\n\n'); // Reset id. 650 | 651 | source.conn.push(res); 652 | 653 | // Every 15s, send a comment (avoids proxy dropping HTTP connection). 654 | var to = setInterval(function () {res.write(':\n');}, 15000); 655 | 656 | // This can only end in blood. 657 | req.on('close', function () { 658 | source.removeConn(res); 659 | clearInterval(to); 660 | }); 661 | }; 662 | } 663 | 664 | function protectedPath(documentRoot, path) { 665 | return p.join(documentRoot, p.join('/', path)); 666 | } 667 | 668 | function setHeadersForCacheLength(res, cacheLengthSeconds) { 669 | const now = new Date(); 670 | const cacheControl = `max-age=${cacheLengthSeconds}, s-maxage=${cacheLengthSeconds}`; 671 | const expires = new Date(now.getTime() + cacheLengthSeconds * 1000).toGMTString(); 672 | 673 | res.setHeader('Cache-Control', cacheControl); 674 | res.setHeader('Expires', expires); 675 | } 676 | 677 | // End the response `res` with file at path `path`. 678 | // If the file does not exist, we call `ifNoFile()`. 679 | function respondWithFile(req, res, path, ifNoFile) { 680 | ifNoFile = ifNoFile || function() {}; 681 | // We use `documentRoot` as the root wherein we seek files. 682 | var realpath = protectedPath(req.server.documentRoot, path); 683 | fs.stat(realpath, function(err, stats) { 684 | if (err) return ifNoFile(); 685 | 686 | if (stats.isDirectory()) { 687 | realpath = p.join(realpath, 'index.html'); 688 | } 689 | res.mime(p.extname(realpath).slice(1)); 690 | 691 | if (req.server.staticMaxAge) { 692 | setHeadersForCacheLength(res, req.server.staticMaxAge); 693 | } 694 | 695 | // Cache management (compare timestamps at second-level precision). 696 | var since = req.headers['if-modified-since']; 697 | if (since && (Math.floor(serverStartTime / 1000) <= Math.floor(new Date(since) / 1000))) { 698 | res.statusCode = 304; // not modified. 699 | res.end(); 700 | return; 701 | } 702 | res.setHeader('Last-Modified', serverStartTimeGMTString); 703 | 704 | // Connect the output of the file to the network! 705 | var raw = fs.createReadStream(realpath); 706 | raw.on('error', function(err) { 707 | log(err.stack, 'error'); 708 | res.statusCode = 404; 709 | res.end('Not Found\n'); 710 | }); 711 | raw.pipe(res.compressed()); 712 | }); 713 | } 714 | 715 | // Static unit. 716 | function staticUnit (server) { 717 | return function staticLayer (req, res, next) { 718 | respondWithFile(req, res, req.path, next); 719 | }; 720 | } 721 | 722 | function escapeRegex(string) { 723 | return string.replace(/[-\/\\^$+?.()|[\]{}]/g, '\\$&'); 724 | } 725 | 726 | // Convert a String to a RegExp, escaping everything. 727 | // Make * stand for anything and :foo be a named non-slash placeholder. 728 | // ** is a star, :: is a colon. 729 | function regexFromString(string) { 730 | var r = /::|\*\*|:[a-zA-Z_]+|\*/g; 731 | var match; 732 | var regexString = '^'; 733 | var previousIndex = 0; 734 | var regexKeys = []; 735 | var starKey = 0; 736 | while ((match = r.exec(string)) !== null) { 737 | var matched = match[0]; 738 | var index = match.index; 739 | regexString += escapeRegex(string.slice(previousIndex, index)); 740 | previousIndex = index; 741 | if (matched === '**') { 742 | regexString += '\\*\\*'; 743 | previousIndex += 2; 744 | } else if (matched === '::') { 745 | regexString += '::'; 746 | previousIndex += 2; 747 | } else if (matched === '*') { 748 | regexString += '(.*)'; 749 | regexKeys.push(starKey); 750 | previousIndex += 1; 751 | starKey++; 752 | } else { // :foo 753 | regexString += '([^/]+)'; 754 | regexKeys.push(matched.slice(1)); 755 | previousIndex += matched.length; 756 | } 757 | } 758 | regexString += escapeRegex(string.slice(previousIndex)) + '$'; 759 | var regex = new RegExp(regexString); 760 | regex.keys = regexKeys; 761 | return regex; 762 | } 763 | 764 | function Path(matcher) { 765 | this.matcher = matcher; 766 | if (matcher instanceof RegExp) { 767 | this.match = function(path) { 768 | return path.match(matcher); 769 | }; 770 | } else if (typeof matcher === 'string') { 771 | if (matcher[0] !== '/' && matcher[0] !== '*') { 772 | matcher = '/' + matcher; 773 | } 774 | if (/[:*]/.test(matcher)) { 775 | matcher = regexFromString(matcher); 776 | } 777 | 778 | // Takes a String path. If the matcher doesn't match it, return null; 779 | // otherwise, return an object mapping keys (from the matcher) to the String 780 | // corresponding to it in the path. Matchers like :foo have key 'foo', 781 | // matchers like * have an integer key starting from 0. 782 | this.match = function(path) { 783 | var matched; 784 | if ((typeof matcher === 'string') && (path === matcher)) { 785 | return {}; 786 | } else if ((matcher instanceof RegExp) 787 | && (matched = matcher.exec(path))) { 788 | var data = {}; 789 | for (var i = 0; i < matcher.keys.length; i++) { 790 | var key = matcher.keys[i]; 791 | var value = matched[i + 1]; 792 | data[key] = value; 793 | } 794 | return data; 795 | } else { return null; } 796 | }; 797 | } 798 | } 799 | 800 | // Path-like unit. 801 | // 802 | // The optional `httpMethods` array allows specifying which HTTP methods should 803 | // have their own specific helpers like `server.get()`, `server.post()`, etc. 804 | // Warning: Calling `pathLikeUnit` several times with the same HTTP methods will 805 | // overwrite any corresponding `server[method]` helpers! 806 | function pathLikeUnit(serverMethod, statusCode, httpMethods) { 807 | httpMethods = httpMethods || []; 808 | return function pathLikeUnit(server) { 809 | var callbacks = []; 810 | 811 | var addCallback = function (path, callback) { 812 | callbacks.push({ 813 | path: new Path(path), 814 | methods: null, 815 | callback: callback 816 | }); 817 | }; 818 | server[serverMethod] = addCallback; 819 | 820 | // HTTP method-specific helpers like server.get(), server.post(), etc. 821 | httpMethods.forEach(function (method) { 822 | server[method.toLowerCase()] = function (path, callback) { 823 | callbacks.push({ 824 | path: new Path(path), 825 | methods: [method], 826 | callback: callback 827 | }); 828 | }; 829 | }); 830 | 831 | return function pathLayer (req, res, next) { 832 | var pathLen = callbacks.length; 833 | var matched = null; 834 | var cbindex = -1; 835 | for (var i = 0; i < pathLen; i++) { 836 | matched = callbacks[i].path.match(req.path); 837 | if (matched == null) { 838 | continue; 839 | } 840 | var methods = callbacks[i].methods; 841 | if (methods != null && methods.indexOf(req.method) === -1) { 842 | continue; 843 | } 844 | cbindex = i; break; 845 | } 846 | if (cbindex >= 0) { 847 | getQueries(req, function(err) { 848 | if (err != null) { 849 | log('While getting queries for ' + req.url + ' in pathUnit:\n' 850 | + err.stack, 'error'); 851 | } 852 | for (var key in matched) { 853 | req.data[key] = matched[key]; 854 | } 855 | res.statusCode = statusCode; 856 | var cb = callbacks[cbindex] && callbacks[cbindex].callback; 857 | if (cb != null) { 858 | cb(req, res); 859 | } else { 860 | res.template(req.data); 861 | } 862 | }); 863 | } else { next(); } 864 | }; 865 | }; 866 | } 867 | var pathUnit = pathLikeUnit('path', 200, http.METHODS); 868 | var notFoundUnit = pathLikeUnit('notFound', 404); 869 | 870 | // Template unit. 871 | function routeUnit (server) { 872 | var regexes = []; 873 | var callbacks = []; 874 | 875 | function route (paths, literalCall) { 876 | regexes.push(RegExp(paths)); 877 | callbacks.push(literalCall); 878 | } 879 | 880 | server.route = route; 881 | 882 | return function routeLayer (req, res, next) { 883 | var matched = null; 884 | var cbindex = -1; 885 | for (var i = 0; i < regexes.length; i++) { 886 | matched = req.path.match (regexes[i]); 887 | if (matched !== null) { cbindex = i; break; } 888 | } 889 | if (cbindex >= 0) { 890 | catchpath(req, res, matched, callbacks[cbindex], server.templateReader); 891 | } else { 892 | next(); 893 | } 894 | }; 895 | } 896 | 897 | // Not Fount unit — in fact, mostly a copy&paste of the route unit. 898 | function notfoundUnit (server) { 899 | var regexes = []; 900 | var callbacks = []; 901 | 902 | function notfound (paths, literalCall) { 903 | regexes.push(RegExp(paths)); 904 | callbacks.push(literalCall); 905 | } 906 | 907 | server.notfound = notfound; 908 | 909 | return function notfoundLayer (req, res) { 910 | res.statusCode = 404; 911 | var matched = null; 912 | var cbindex = -1; 913 | for (var i = 0; i < regexes.length; i++) { 914 | matched = req.path.match (regexes[i]); 915 | if (matched !== null) { cbindex = i; break; } 916 | } 917 | if (cbindex >= 0) { 918 | catchpath(req, res, matched, callbacks[cbindex], server.templateReader); 919 | } else { 920 | res.end('Not Found\n'); 921 | } 922 | }; 923 | } 924 | 925 | // Route *and* not found units — see what I did there? 926 | 927 | function catchpath (req, res, pathmatch, callback, templateReader) { 928 | getQueries(req, function gotQueries(err) { 929 | if (err != null) { 930 | log('While getting queries for ' + req.uri + ':\n' 931 | + err 932 | , 'error'); 933 | } else { 934 | // params: template parameters (JSON-serializable). 935 | callback(req.query, pathmatch, function end (params, options) { 936 | options = options || {}; 937 | var templates = options.template || pathmatch[0]; 938 | var reader = options.reader || templateReader; 939 | if (Object(options.string) instanceof String) { 940 | templates = [streamFromString(options.string)]; 941 | } else if (Object(params) instanceof String) { 942 | templates = [streamFromString(params)]; 943 | } else if (!Array.isArray(templates)) { 944 | templates = [templates]; 945 | } 946 | if (!res.getHeader('Content-Type') // Allow overriding. 947 | && (Object(templates[0]) instanceof String)) { 948 | res.mime(p.extname(templates[0]).slice(1)); 949 | } 950 | for (var i = 0; i < templates.length; i++) { 951 | if (Object(templates[i]) instanceof String) { 952 | // `templates[i]` is a string path for a file. 953 | var templatePath = p.join(req.server.documentRoot, templates[i]); 954 | templates[i] = fs.createReadStream(templatePath); 955 | } 956 | } 957 | 958 | var template = concatStreams(templates); 959 | 960 | template.on('error', function(err) { 961 | log(err.stack, 'error'); 962 | res.end('Not Found\n'); 963 | }); 964 | 965 | if (params === null || reader === null) { 966 | // No data was given. Same behaviour as static. 967 | template.pipe(res); 968 | } else { 969 | reader(template, res, params, function errorcb(err) { 970 | if (err) { 971 | log(err.stack, 'error'); 972 | res.end('Not Found\n'); 973 | } 974 | }); 975 | } 976 | }, req.ask); 977 | } 978 | }); 979 | } 980 | 981 | // The default routing function: 982 | // 983 | // - if the request is of the form /$socket.io, it runs the socket.io unit. 984 | // (By default, that is not in. Using `server.io` loads the library.) 985 | // - if the request is of the form /$websocket:, it runs the websocket unit. 986 | // - if the request is of the form /$..., it runs the ajax / eventSource unit. 987 | // - if the request is a registered template, it runs the template unit. 988 | // - if the request isn't a registered route, it runs the static unit. 989 | // - else, it runs the notfound unit. 990 | 991 | var defaultRoute = [genericUnit, wsUnit, ajaxUnit, eventSourceUnit, 992 | pathUnit, routeUnit, staticUnit, 993 | notFoundUnit, notfoundUnit]; 994 | 995 | function streamFromString(string) { 996 | var sstream = new stream.Readable(); 997 | sstream._read = function() { sstream.push(string); sstream.push(null); }; 998 | return sstream; 999 | } 1000 | 1001 | 1002 | 1003 | 1004 | 1005 | 1006 | // Internal start function. 1007 | // 1008 | 1009 | function createServer () { return new Camp(); } 1010 | 1011 | function createSecureServer (opts) { return new SecureCamp(opts); } 1012 | 1013 | function createSpdyServer (opts) { return new SpdyCamp(opts); } 1014 | 1015 | var KEY_HEADER = /^-+BEGIN \w* ?PRIVATE KEY-+/; 1016 | var CERT_HEADER = /^-+BEGIN CERTIFICATE-+/; 1017 | function createServerWithSettings (settings) { 1018 | var server; 1019 | settings.hostname = settings.hostname || '::'; 1020 | 1021 | // Are we running https? 1022 | if (settings.secure) { // Yep 1023 | var key, cert; 1024 | if (KEY_HEADER.test(settings.key)) { 1025 | key = settings.key; 1026 | } else { 1027 | key = fs.readFileSync(settings.key); 1028 | } 1029 | if (CERT_HEADER.test(settings.cert)) { 1030 | cert = settings.cert; 1031 | } else { 1032 | cert = fs.readFileSync(settings.cert); 1033 | } 1034 | settings.key = key; 1035 | settings.cert = cert; 1036 | settings.ca = settings.ca.map(function(file) { 1037 | try { 1038 | var ca; 1039 | if (CERT_HEADER.test(file)) { 1040 | ca = file; 1041 | } else { 1042 | ca = fs.readFileSync(file); 1043 | } 1044 | return ca; 1045 | } catch (e) { log('CA file not found: ' + file, 'error'); } 1046 | }); 1047 | if (settings.spdy === false) { 1048 | server = new SecureCamp(settings); 1049 | } else { 1050 | server = new SpdyCamp(settings); 1051 | } 1052 | } else { // Nope 1053 | server = new Camp(settings); 1054 | } 1055 | if (settings.setuid) { 1056 | server.on('listening', function switchuid() { 1057 | process.setuid(settings.setuid); 1058 | }); 1059 | } 1060 | 1061 | server.listenAsConfigured = function() { 1062 | return this.listen(settings.port, settings.hostname); 1063 | } 1064 | 1065 | return server; 1066 | } 1067 | 1068 | 1069 | // Each camp instance creates an HTTP / HTTPS server automatically. 1070 | // 1071 | function create (settings) { 1072 | 1073 | settings = settings || {}; 1074 | 1075 | // Populate security values with the corresponding files. 1076 | if (settings.secure) { 1077 | settings.passphrase = settings.passphrase || '1234'; 1078 | settings.key = settings.key || 'https.key'; 1079 | settings.cert = settings.cert || 'https.crt'; 1080 | settings.ca = settings.ca || []; 1081 | } 1082 | 1083 | settings.port = settings.port || (settings.secure ? 443 : 80); 1084 | 1085 | return createServerWithSettings(settings); 1086 | }; 1087 | 1088 | function start (settings) { 1089 | return create(settings).listenAsConfigured(); 1090 | } 1091 | 1092 | 1093 | exports.create = create; 1094 | exports.start = start; 1095 | exports.createServer = createServer; 1096 | exports.createSecureServer = createSecureServer; 1097 | exports.Camp = Camp; 1098 | exports.SecureCamp = SecureCamp; 1099 | exports.SpdyCamp = SpdyCamp; 1100 | 1101 | exports.genericUnit = genericUnit; 1102 | exports.socketUnit = socketUnit; 1103 | exports.wsUnit = wsUnit; 1104 | exports.ajaxUnit = ajaxUnit; 1105 | exports.eventSourceUnit = eventSourceUnit; 1106 | exports.pathUnit = pathUnit; 1107 | exports.routeUnit = routeUnit; 1108 | exports.staticUnit = staticUnit; 1109 | exports.notfoundUnit = notfoundUnit; 1110 | 1111 | exports.template = template; 1112 | exports.templateReader = templateReader; 1113 | exports.augmentReqRes = augmentReqRes; 1114 | exports.mime = mime; 1115 | exports.binaries = binaries; 1116 | exports.log = log; 1117 | --------------------------------------------------------------------------------