From b04d262fe428a80ed3ed448575b5db7b6970afeb Mon Sep 17 00:00:00 2001 From: psychobunny Date: Mon, 22 Apr 2013 16:51:32 +0000 Subject: [PATCH] init, just some testing --- .gitignore | 8 + README.md | 6 + RedisDB.js | 59 + app.js | 26 + config.json | 0 node_modules/.bin/express | 1 + node_modules/connect/.npmignore | 12 + node_modules/connect/.travis.yml | 5 + node_modules/connect/LICENSE | 24 + node_modules/connect/index.js | 4 + node_modules/connect/lib/cache.js | 81 + node_modules/connect/lib/connect.js | 92 + node_modules/connect/lib/index.js | 50 + .../connect/lib/middleware/basicAuth.js | 103 + .../connect/lib/middleware/bodyParser.js | 61 + .../connect/lib/middleware/compress.js | 152 + .../connect/lib/middleware/cookieParser.js | 62 + .../connect/lib/middleware/cookieSession.js | 117 + node_modules/connect/lib/middleware/csrf.js | 73 + .../connect/lib/middleware/directory.js | 229 + .../connect/lib/middleware/errorHandler.js | 86 + .../connect/lib/middleware/favicon.js | 80 + node_modules/connect/lib/middleware/json.js | 86 + node_modules/connect/lib/middleware/limit.js | 78 + node_modules/connect/lib/middleware/logger.js | 339 + .../connect/lib/middleware/methodOverride.js | 40 + .../connect/lib/middleware/multipart.js | 133 + node_modules/connect/lib/middleware/query.js | 46 + .../connect/lib/middleware/responseTime.js | 32 + .../connect/lib/middleware/session.js | 356 + .../connect/lib/middleware/session/cookie.js | 140 + .../connect/lib/middleware/session/memory.js | 129 + .../connect/lib/middleware/session/session.js | 116 + .../connect/lib/middleware/session/store.js | 84 + node_modules/connect/lib/middleware/static.js | 95 + .../connect/lib/middleware/staticCache.js | 231 + .../connect/lib/middleware/timeout.js | 55 + .../connect/lib/middleware/urlencoded.js | 78 + node_modules/connect/lib/middleware/vhost.js | 40 + node_modules/connect/lib/patch.js | 79 + node_modules/connect/lib/proto.js | 230 + .../connect/lib/public/directory.html | 81 + node_modules/connect/lib/public/error.html | 14 + node_modules/connect/lib/public/favicon.ico | Bin 0 -> 1406 bytes .../connect/lib/public/icons/page.png | Bin 0 -> 635 bytes .../connect/lib/public/icons/page_add.png | Bin 0 -> 739 bytes .../connect/lib/public/icons/page_attach.png | Bin 0 -> 794 bytes .../connect/lib/public/icons/page_code.png | Bin 0 -> 818 bytes .../connect/lib/public/icons/page_copy.png | Bin 0 -> 663 bytes .../connect/lib/public/icons/page_delete.png | Bin 0 -> 740 bytes .../connect/lib/public/icons/page_edit.png | Bin 0 -> 807 bytes .../connect/lib/public/icons/page_error.png | Bin 0 -> 793 bytes .../connect/lib/public/icons/page_excel.png | Bin 0 -> 817 bytes .../connect/lib/public/icons/page_find.png | Bin 0 -> 879 bytes .../connect/lib/public/icons/page_gear.png | Bin 0 -> 833 bytes .../connect/lib/public/icons/page_go.png | Bin 0 -> 779 bytes .../connect/lib/public/icons/page_green.png | Bin 0 -> 621 bytes .../connect/lib/public/icons/page_key.png | Bin 0 -> 801 bytes .../lib/public/icons/page_lightning.png | Bin 0 -> 839 bytes .../connect/lib/public/icons/page_link.png | Bin 0 -> 830 bytes .../lib/public/icons/page_paintbrush.png | Bin 0 -> 813 bytes .../connect/lib/public/icons/page_paste.png | Bin 0 -> 703 bytes .../connect/lib/public/icons/page_red.png | Bin 0 -> 641 bytes .../connect/lib/public/icons/page_refresh.png | Bin 0 -> 858 bytes .../connect/lib/public/icons/page_save.png | Bin 0 -> 774 bytes .../connect/lib/public/icons/page_white.png | Bin 0 -> 294 bytes .../lib/public/icons/page_white_acrobat.png | Bin 0 -> 591 bytes .../public/icons/page_white_actionscript.png | Bin 0 -> 664 bytes .../lib/public/icons/page_white_add.png | Bin 0 -> 512 bytes .../connect/lib/public/icons/page_white_c.png | Bin 0 -> 587 bytes .../lib/public/icons/page_white_camera.png | Bin 0 -> 656 bytes .../lib/public/icons/page_white_cd.png | Bin 0 -> 666 bytes .../lib/public/icons/page_white_code.png | Bin 0 -> 603 bytes .../lib/public/icons/page_white_code_red.png | Bin 0 -> 587 bytes .../public/icons/page_white_coldfusion.png | Bin 0 -> 592 bytes .../public/icons/page_white_compressed.png | Bin 0 -> 724 bytes .../lib/public/icons/page_white_copy.png | Bin 0 -> 309 bytes .../lib/public/icons/page_white_cplusplus.png | Bin 0 -> 621 bytes .../lib/public/icons/page_white_csharp.png | Bin 0 -> 700 bytes .../lib/public/icons/page_white_cup.png | Bin 0 -> 639 bytes .../lib/public/icons/page_white_database.png | Bin 0 -> 579 bytes .../lib/public/icons/page_white_delete.png | Bin 0 -> 536 bytes .../lib/public/icons/page_white_dvd.png | Bin 0 -> 638 bytes .../lib/public/icons/page_white_edit.png | Bin 0 -> 618 bytes .../lib/public/icons/page_white_error.png | Bin 0 -> 623 bytes .../lib/public/icons/page_white_excel.png | Bin 0 -> 663 bytes .../lib/public/icons/page_white_find.png | Bin 0 -> 676 bytes .../lib/public/icons/page_white_flash.png | Bin 0 -> 582 bytes .../lib/public/icons/page_white_freehand.png | Bin 0 -> 639 bytes .../lib/public/icons/page_white_gear.png | Bin 0 -> 402 bytes .../lib/public/icons/page_white_get.png | Bin 0 -> 516 bytes .../lib/public/icons/page_white_go.png | Bin 0 -> 612 bytes .../connect/lib/public/icons/page_white_h.png | Bin 0 -> 603 bytes .../public/icons/page_white_horizontal.png | Bin 0 -> 296 bytes .../lib/public/icons/page_white_key.png | Bin 0 -> 616 bytes .../lib/public/icons/page_white_lightning.png | Bin 0 -> 669 bytes .../lib/public/icons/page_white_link.png | Bin 0 -> 614 bytes .../lib/public/icons/page_white_magnify.png | Bin 0 -> 554 bytes .../lib/public/icons/page_white_medal.png | Bin 0 -> 706 bytes .../lib/public/icons/page_white_office.png | Bin 0 -> 779 bytes .../lib/public/icons/page_white_paint.png | Bin 0 -> 688 bytes .../public/icons/page_white_paintbrush.png | Bin 0 -> 618 bytes .../lib/public/icons/page_white_paste.png | Bin 0 -> 620 bytes .../lib/public/icons/page_white_php.png | Bin 0 -> 538 bytes .../lib/public/icons/page_white_picture.png | Bin 0 -> 650 bytes .../public/icons/page_white_powerpoint.png | Bin 0 -> 588 bytes .../lib/public/icons/page_white_put.png | Bin 0 -> 523 bytes .../lib/public/icons/page_white_ruby.png | Bin 0 -> 626 bytes .../lib/public/icons/page_white_stack.png | Bin 0 -> 317 bytes .../lib/public/icons/page_white_star.png | Bin 0 -> 565 bytes .../lib/public/icons/page_white_swoosh.png | Bin 0 -> 634 bytes .../lib/public/icons/page_white_text.png | Bin 0 -> 342 bytes .../public/icons/page_white_text_width.png | Bin 0 -> 315 bytes .../lib/public/icons/page_white_tux.png | Bin 0 -> 668 bytes .../lib/public/icons/page_white_vector.png | Bin 0 -> 644 bytes .../public/icons/page_white_visualstudio.png | Bin 0 -> 702 bytes .../lib/public/icons/page_white_width.png | Bin 0 -> 309 bytes .../lib/public/icons/page_white_word.png | Bin 0 -> 651 bytes .../lib/public/icons/page_white_world.png | Bin 0 -> 734 bytes .../lib/public/icons/page_white_wrench.png | Bin 0 -> 613 bytes .../lib/public/icons/page_white_zip.png | Bin 0 -> 386 bytes .../connect/lib/public/icons/page_word.png | Bin 0 -> 777 bytes .../connect/lib/public/icons/page_world.png | Bin 0 -> 903 bytes node_modules/connect/lib/public/style.css | 141 + node_modules/connect/lib/utils.js | 404 ++ node_modules/connect/package.json | 53 + node_modules/connect/test.js | 40 + node_modules/express/.npmignore | 9 + node_modules/express/.travis.yml | 4 + node_modules/express/History.md | 1115 +++ node_modules/express/LICENSE | 22 + node_modules/express/Makefile | 33 + node_modules/express/Readme.md | 180 + node_modules/express/bin/express | 422 ++ node_modules/express/client.js | 25 + node_modules/express/index.js | 4 + node_modules/express/lib/application.js | 531 ++ node_modules/express/lib/express.js | 92 + node_modules/express/lib/middleware.js | 33 + node_modules/express/lib/request.js | 526 ++ node_modules/express/lib/response.js | 756 ++ node_modules/express/lib/router/index.js | 273 + node_modules/express/lib/router/route.js | 72 + node_modules/express/lib/utils.js | 313 + node_modules/express/lib/view.js | 76 + node_modules/express/package.json | 87 + node_modules/express/test.js | 20 + node_modules/redis/.npmignore | 1 + node_modules/redis/README.md | 707 ++ node_modules/redis/benches/buffer_bench.js | 89 + node_modules/redis/benches/hiredis_parser.js | 38 + node_modules/redis/benches/re_sub_test.js | 14 + node_modules/redis/benches/reconnect_test.js | 29 + node_modules/redis/benches/stress/codec.js | 16 + .../redis/benches/stress/pubsub/pub.js | 38 + node_modules/redis/benches/stress/pubsub/run | 10 + .../redis/benches/stress/pubsub/server.js | 23 + .../redis/benches/stress/rpushblpop/pub.js | 49 + .../redis/benches/stress/rpushblpop/run | 6 + .../redis/benches/stress/rpushblpop/server.js | 30 + node_modules/redis/benches/stress/speed/00 | 13 + node_modules/redis/benches/stress/speed/plot | 13 + .../redis/benches/stress/speed/size-rate.png | Bin 0 -> 6672 bytes .../redis/benches/stress/speed/speed.js | 84 + node_modules/redis/benches/sub_quit_test.js | 18 + node_modules/redis/changelog.md | 240 + node_modules/redis/diff_multi_bench_output.js | 90 + node_modules/redis/examples/auth.js | 5 + .../redis/examples/backpressure_drain.js | 33 + node_modules/redis/examples/eval.js | 14 + node_modules/redis/examples/extend.js | 24 + node_modules/redis/examples/file.js | 32 + node_modules/redis/examples/mget.js | 5 + node_modules/redis/examples/monitor.js | 10 + node_modules/redis/examples/multi.js | 46 + node_modules/redis/examples/multi2.js | 29 + node_modules/redis/examples/psubscribe.js | 33 + node_modules/redis/examples/pub_sub.js | 41 + node_modules/redis/examples/simple.js | 24 + node_modules/redis/examples/sort.js | 17 + node_modules/redis/examples/subqueries.js | 15 + node_modules/redis/examples/subquery.js | 19 + node_modules/redis/examples/unix_socket.js | 29 + node_modules/redis/examples/web_server.js | 31 + node_modules/redis/generate_commands.js | 39 + node_modules/redis/index.js | 1144 +++ node_modules/redis/lib/commands.js | 149 + node_modules/redis/lib/parser/hiredis.js | 46 + node_modules/redis/lib/parser/javascript.js | 301 + node_modules/redis/lib/queue.js | 59 + node_modules/redis/lib/to_array.js | 12 + node_modules/redis/lib/util.js | 11 + node_modules/redis/multi_bench.js | 222 + node_modules/redis/package.json | 40 + node_modules/redis/test.js | 1955 ++++++ node_modules/socket.io/.npmignore | 3 + node_modules/socket.io/.travis.yml | 6 + node_modules/socket.io/History.md | 315 + node_modules/socket.io/LICENSE | 22 + node_modules/socket.io/Makefile | 31 + node_modules/socket.io/Readme.md | 364 + node_modules/socket.io/TODO | 1 + .../socket.io/benchmarks/decode.bench.js | 64 + .../socket.io/benchmarks/encode.bench.js | 90 + node_modules/socket.io/benchmarks/runner.js | 55 + node_modules/socket.io/index.js | 8 + node_modules/socket.io/lib/logger.js | 97 + node_modules/socket.io/lib/manager.js | 1027 +++ node_modules/socket.io/lib/namespace.js | 355 + node_modules/socket.io/lib/parser.js | 249 + node_modules/socket.io/lib/socket.io.js | 143 + node_modules/socket.io/lib/socket.js | 369 + node_modules/socket.io/lib/static.js | 395 ++ node_modules/socket.io/lib/store.js | 98 + node_modules/socket.io/lib/stores/memory.js | 143 + node_modules/socket.io/lib/stores/redis.js | 269 + node_modules/socket.io/lib/transport.js | 534 ++ .../socket.io/lib/transports/flashsocket.js | 129 + .../socket.io/lib/transports/htmlfile.js | 82 + .../socket.io/lib/transports/http-polling.js | 147 + node_modules/socket.io/lib/transports/http.js | 121 + .../socket.io/lib/transports/index.js | 12 + .../socket.io/lib/transports/jsonp-polling.js | 97 + .../socket.io/lib/transports/websocket.js | 36 + .../lib/transports/websocket/default.js | 362 + .../lib/transports/websocket/hybi-07-12.js | 622 ++ .../lib/transports/websocket/hybi-16.js | 622 ++ .../lib/transports/websocket/index.js | 11 + .../socket.io/lib/transports/xhr-polling.js | 69 + node_modules/socket.io/lib/util.js | 50 + node_modules/socket.io/package.json | 72 + package.json | 22 + public/oldindex.html | 40 + public/socket.io/WebSocketMain.swf | Bin 0 -> 175830 bytes public/socket.io/WebSocketMainInsecure.swf | Bin 0 -> 175953 bytes public/socket.io/socket.io.js | 3871 +++++++++++ public/socket.io/socket.io.min.js | 2 + public/templates/footer.tpl | 3 + public/templates/header.tpl | 141 + public/templates/home.tpl | 1 + public/templates/register.tpl | 57 + public/test.html | 58 + public/vendor/ajaxify/ajaxify.js | 70 + .../bootstrap/css/bootstrap-responsive.css | 1109 +++ .../css/bootstrap-responsive.min.css | 9 + public/vendor/bootstrap/css/bootstrap.css | 6158 +++++++++++++++++ public/vendor/bootstrap/css/bootstrap.min.css | 9 + .../img/glyphicons-halflings-white.png | Bin 0 -> 8777 bytes .../bootstrap/img/glyphicons-halflings.png | Bin 0 -> 12799 bytes public/vendor/bootstrap/js/bootstrap.js | 2276 ++++++ public/vendor/bootstrap/js/bootstrap.min.js | 6 + src/auth.js | 0 src/templates.js | 23 + src/user.js | 67 + src/webserver.js | 52 + src/websockets.js | 42 + 256 files changed, 36023 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 RedisDB.js create mode 100644 app.js create mode 100644 config.json create mode 120000 node_modules/.bin/express create mode 100644 node_modules/connect/.npmignore create mode 100644 node_modules/connect/.travis.yml create mode 100644 node_modules/connect/LICENSE create mode 100644 node_modules/connect/index.js create mode 100644 node_modules/connect/lib/cache.js create mode 100644 node_modules/connect/lib/connect.js create mode 100644 node_modules/connect/lib/index.js create mode 100644 node_modules/connect/lib/middleware/basicAuth.js create mode 100644 node_modules/connect/lib/middleware/bodyParser.js create mode 100644 node_modules/connect/lib/middleware/compress.js create mode 100644 node_modules/connect/lib/middleware/cookieParser.js create mode 100644 node_modules/connect/lib/middleware/cookieSession.js create mode 100644 node_modules/connect/lib/middleware/csrf.js create mode 100644 node_modules/connect/lib/middleware/directory.js create mode 100644 node_modules/connect/lib/middleware/errorHandler.js create mode 100644 node_modules/connect/lib/middleware/favicon.js create mode 100644 node_modules/connect/lib/middleware/json.js create mode 100644 node_modules/connect/lib/middleware/limit.js create mode 100644 node_modules/connect/lib/middleware/logger.js create mode 100644 node_modules/connect/lib/middleware/methodOverride.js create mode 100644 node_modules/connect/lib/middleware/multipart.js create mode 100644 node_modules/connect/lib/middleware/query.js create mode 100644 node_modules/connect/lib/middleware/responseTime.js create mode 100644 node_modules/connect/lib/middleware/session.js create mode 100644 node_modules/connect/lib/middleware/session/cookie.js create mode 100644 node_modules/connect/lib/middleware/session/memory.js create mode 100644 node_modules/connect/lib/middleware/session/session.js create mode 100644 node_modules/connect/lib/middleware/session/store.js create mode 100644 node_modules/connect/lib/middleware/static.js create mode 100644 node_modules/connect/lib/middleware/staticCache.js create mode 100644 node_modules/connect/lib/middleware/timeout.js create mode 100644 node_modules/connect/lib/middleware/urlencoded.js create mode 100644 node_modules/connect/lib/middleware/vhost.js create mode 100644 node_modules/connect/lib/patch.js create mode 100644 node_modules/connect/lib/proto.js create mode 100644 node_modules/connect/lib/public/directory.html create mode 100644 node_modules/connect/lib/public/error.html create mode 100644 node_modules/connect/lib/public/favicon.ico create mode 100644 node_modules/connect/lib/public/icons/page.png create mode 100644 node_modules/connect/lib/public/icons/page_add.png create mode 100644 node_modules/connect/lib/public/icons/page_attach.png create mode 100644 node_modules/connect/lib/public/icons/page_code.png create mode 100644 node_modules/connect/lib/public/icons/page_copy.png create mode 100644 node_modules/connect/lib/public/icons/page_delete.png create mode 100644 node_modules/connect/lib/public/icons/page_edit.png create mode 100644 node_modules/connect/lib/public/icons/page_error.png create mode 100644 node_modules/connect/lib/public/icons/page_excel.png create mode 100644 node_modules/connect/lib/public/icons/page_find.png create mode 100644 node_modules/connect/lib/public/icons/page_gear.png create mode 100644 node_modules/connect/lib/public/icons/page_go.png create mode 100644 node_modules/connect/lib/public/icons/page_green.png create mode 100644 node_modules/connect/lib/public/icons/page_key.png create mode 100644 node_modules/connect/lib/public/icons/page_lightning.png create mode 100644 node_modules/connect/lib/public/icons/page_link.png create mode 100644 node_modules/connect/lib/public/icons/page_paintbrush.png create mode 100644 node_modules/connect/lib/public/icons/page_paste.png create mode 100644 node_modules/connect/lib/public/icons/page_red.png create mode 100644 node_modules/connect/lib/public/icons/page_refresh.png create mode 100644 node_modules/connect/lib/public/icons/page_save.png create mode 100644 node_modules/connect/lib/public/icons/page_white.png create mode 100644 node_modules/connect/lib/public/icons/page_white_acrobat.png create mode 100644 node_modules/connect/lib/public/icons/page_white_actionscript.png create mode 100644 node_modules/connect/lib/public/icons/page_white_add.png create mode 100644 node_modules/connect/lib/public/icons/page_white_c.png create mode 100644 node_modules/connect/lib/public/icons/page_white_camera.png create mode 100644 node_modules/connect/lib/public/icons/page_white_cd.png create mode 100644 node_modules/connect/lib/public/icons/page_white_code.png create mode 100644 node_modules/connect/lib/public/icons/page_white_code_red.png create mode 100644 node_modules/connect/lib/public/icons/page_white_coldfusion.png create mode 100644 node_modules/connect/lib/public/icons/page_white_compressed.png create mode 100644 node_modules/connect/lib/public/icons/page_white_copy.png create mode 100644 node_modules/connect/lib/public/icons/page_white_cplusplus.png create mode 100644 node_modules/connect/lib/public/icons/page_white_csharp.png create mode 100644 node_modules/connect/lib/public/icons/page_white_cup.png create mode 100644 node_modules/connect/lib/public/icons/page_white_database.png create mode 100644 node_modules/connect/lib/public/icons/page_white_delete.png create mode 100644 node_modules/connect/lib/public/icons/page_white_dvd.png create mode 100644 node_modules/connect/lib/public/icons/page_white_edit.png create mode 100644 node_modules/connect/lib/public/icons/page_white_error.png create mode 100644 node_modules/connect/lib/public/icons/page_white_excel.png create mode 100644 node_modules/connect/lib/public/icons/page_white_find.png create mode 100644 node_modules/connect/lib/public/icons/page_white_flash.png create mode 100644 node_modules/connect/lib/public/icons/page_white_freehand.png create mode 100644 node_modules/connect/lib/public/icons/page_white_gear.png create mode 100644 node_modules/connect/lib/public/icons/page_white_get.png create mode 100644 node_modules/connect/lib/public/icons/page_white_go.png create mode 100644 node_modules/connect/lib/public/icons/page_white_h.png create mode 100644 node_modules/connect/lib/public/icons/page_white_horizontal.png create mode 100644 node_modules/connect/lib/public/icons/page_white_key.png create mode 100644 node_modules/connect/lib/public/icons/page_white_lightning.png create mode 100644 node_modules/connect/lib/public/icons/page_white_link.png create mode 100644 node_modules/connect/lib/public/icons/page_white_magnify.png create mode 100644 node_modules/connect/lib/public/icons/page_white_medal.png create mode 100644 node_modules/connect/lib/public/icons/page_white_office.png create mode 100644 node_modules/connect/lib/public/icons/page_white_paint.png create mode 100644 node_modules/connect/lib/public/icons/page_white_paintbrush.png create mode 100644 node_modules/connect/lib/public/icons/page_white_paste.png create mode 100644 node_modules/connect/lib/public/icons/page_white_php.png create mode 100644 node_modules/connect/lib/public/icons/page_white_picture.png create mode 100644 node_modules/connect/lib/public/icons/page_white_powerpoint.png create mode 100644 node_modules/connect/lib/public/icons/page_white_put.png create mode 100644 node_modules/connect/lib/public/icons/page_white_ruby.png create mode 100644 node_modules/connect/lib/public/icons/page_white_stack.png create mode 100644 node_modules/connect/lib/public/icons/page_white_star.png create mode 100644 node_modules/connect/lib/public/icons/page_white_swoosh.png create mode 100644 node_modules/connect/lib/public/icons/page_white_text.png create mode 100644 node_modules/connect/lib/public/icons/page_white_text_width.png create mode 100644 node_modules/connect/lib/public/icons/page_white_tux.png create mode 100644 node_modules/connect/lib/public/icons/page_white_vector.png create mode 100644 node_modules/connect/lib/public/icons/page_white_visualstudio.png create mode 100644 node_modules/connect/lib/public/icons/page_white_width.png create mode 100644 node_modules/connect/lib/public/icons/page_white_word.png create mode 100644 node_modules/connect/lib/public/icons/page_white_world.png create mode 100644 node_modules/connect/lib/public/icons/page_white_wrench.png create mode 100644 node_modules/connect/lib/public/icons/page_white_zip.png create mode 100644 node_modules/connect/lib/public/icons/page_word.png create mode 100644 node_modules/connect/lib/public/icons/page_world.png create mode 100644 node_modules/connect/lib/public/style.css create mode 100644 node_modules/connect/lib/utils.js create mode 100644 node_modules/connect/package.json create mode 100644 node_modules/connect/test.js create mode 100644 node_modules/express/.npmignore create mode 100644 node_modules/express/.travis.yml create mode 100644 node_modules/express/History.md create mode 100644 node_modules/express/LICENSE create mode 100644 node_modules/express/Makefile create mode 100644 node_modules/express/Readme.md create mode 100644 node_modules/express/bin/express create mode 100644 node_modules/express/client.js create mode 100644 node_modules/express/index.js create mode 100644 node_modules/express/lib/application.js create mode 100644 node_modules/express/lib/express.js create mode 100644 node_modules/express/lib/middleware.js create mode 100644 node_modules/express/lib/request.js create mode 100644 node_modules/express/lib/response.js create mode 100644 node_modules/express/lib/router/index.js create mode 100644 node_modules/express/lib/router/route.js create mode 100644 node_modules/express/lib/utils.js create mode 100644 node_modules/express/lib/view.js create mode 100644 node_modules/express/package.json create mode 100644 node_modules/express/test.js create mode 100644 node_modules/redis/.npmignore create mode 100644 node_modules/redis/README.md create mode 100644 node_modules/redis/benches/buffer_bench.js create mode 100644 node_modules/redis/benches/hiredis_parser.js create mode 100644 node_modules/redis/benches/re_sub_test.js create mode 100644 node_modules/redis/benches/reconnect_test.js create mode 100644 node_modules/redis/benches/stress/codec.js create mode 100644 node_modules/redis/benches/stress/pubsub/pub.js create mode 100644 node_modules/redis/benches/stress/pubsub/run create mode 100644 node_modules/redis/benches/stress/pubsub/server.js create mode 100644 node_modules/redis/benches/stress/rpushblpop/pub.js create mode 100644 node_modules/redis/benches/stress/rpushblpop/run create mode 100644 node_modules/redis/benches/stress/rpushblpop/server.js create mode 100644 node_modules/redis/benches/stress/speed/00 create mode 100644 node_modules/redis/benches/stress/speed/plot create mode 100644 node_modules/redis/benches/stress/speed/size-rate.png create mode 100644 node_modules/redis/benches/stress/speed/speed.js create mode 100644 node_modules/redis/benches/sub_quit_test.js create mode 100644 node_modules/redis/changelog.md create mode 100644 node_modules/redis/diff_multi_bench_output.js create mode 100644 node_modules/redis/examples/auth.js create mode 100644 node_modules/redis/examples/backpressure_drain.js create mode 100644 node_modules/redis/examples/eval.js create mode 100644 node_modules/redis/examples/extend.js create mode 100644 node_modules/redis/examples/file.js create mode 100644 node_modules/redis/examples/mget.js create mode 100644 node_modules/redis/examples/monitor.js create mode 100644 node_modules/redis/examples/multi.js create mode 100644 node_modules/redis/examples/multi2.js create mode 100644 node_modules/redis/examples/psubscribe.js create mode 100644 node_modules/redis/examples/pub_sub.js create mode 100644 node_modules/redis/examples/simple.js create mode 100644 node_modules/redis/examples/sort.js create mode 100644 node_modules/redis/examples/subqueries.js create mode 100644 node_modules/redis/examples/subquery.js create mode 100644 node_modules/redis/examples/unix_socket.js create mode 100644 node_modules/redis/examples/web_server.js create mode 100644 node_modules/redis/generate_commands.js create mode 100644 node_modules/redis/index.js create mode 100644 node_modules/redis/lib/commands.js create mode 100644 node_modules/redis/lib/parser/hiredis.js create mode 100644 node_modules/redis/lib/parser/javascript.js create mode 100644 node_modules/redis/lib/queue.js create mode 100644 node_modules/redis/lib/to_array.js create mode 100644 node_modules/redis/lib/util.js create mode 100644 node_modules/redis/multi_bench.js create mode 100644 node_modules/redis/package.json create mode 100644 node_modules/redis/test.js create mode 100644 node_modules/socket.io/.npmignore create mode 100644 node_modules/socket.io/.travis.yml create mode 100644 node_modules/socket.io/History.md create mode 100644 node_modules/socket.io/LICENSE create mode 100644 node_modules/socket.io/Makefile create mode 100644 node_modules/socket.io/Readme.md create mode 100644 node_modules/socket.io/TODO create mode 100644 node_modules/socket.io/benchmarks/decode.bench.js create mode 100644 node_modules/socket.io/benchmarks/encode.bench.js create mode 100644 node_modules/socket.io/benchmarks/runner.js create mode 100644 node_modules/socket.io/index.js create mode 100644 node_modules/socket.io/lib/logger.js create mode 100644 node_modules/socket.io/lib/manager.js create mode 100644 node_modules/socket.io/lib/namespace.js create mode 100644 node_modules/socket.io/lib/parser.js create mode 100644 node_modules/socket.io/lib/socket.io.js create mode 100644 node_modules/socket.io/lib/socket.js create mode 100644 node_modules/socket.io/lib/static.js create mode 100644 node_modules/socket.io/lib/store.js create mode 100644 node_modules/socket.io/lib/stores/memory.js create mode 100644 node_modules/socket.io/lib/stores/redis.js create mode 100644 node_modules/socket.io/lib/transport.js create mode 100644 node_modules/socket.io/lib/transports/flashsocket.js create mode 100644 node_modules/socket.io/lib/transports/htmlfile.js create mode 100644 node_modules/socket.io/lib/transports/http-polling.js create mode 100644 node_modules/socket.io/lib/transports/http.js create mode 100644 node_modules/socket.io/lib/transports/index.js create mode 100644 node_modules/socket.io/lib/transports/jsonp-polling.js create mode 100644 node_modules/socket.io/lib/transports/websocket.js create mode 100644 node_modules/socket.io/lib/transports/websocket/default.js create mode 100644 node_modules/socket.io/lib/transports/websocket/hybi-07-12.js create mode 100644 node_modules/socket.io/lib/transports/websocket/hybi-16.js create mode 100644 node_modules/socket.io/lib/transports/websocket/index.js create mode 100644 node_modules/socket.io/lib/transports/xhr-polling.js create mode 100644 node_modules/socket.io/lib/util.js create mode 100644 node_modules/socket.io/package.json create mode 100644 package.json create mode 100644 public/oldindex.html create mode 100644 public/socket.io/WebSocketMain.swf create mode 100644 public/socket.io/WebSocketMainInsecure.swf create mode 100644 public/socket.io/socket.io.js create mode 100644 public/socket.io/socket.io.min.js create mode 100644 public/templates/footer.tpl create mode 100644 public/templates/header.tpl create mode 100644 public/templates/home.tpl create mode 100644 public/templates/register.tpl create mode 100644 public/test.html create mode 100644 public/vendor/ajaxify/ajaxify.js create mode 100644 public/vendor/bootstrap/css/bootstrap-responsive.css create mode 100644 public/vendor/bootstrap/css/bootstrap-responsive.min.css create mode 100644 public/vendor/bootstrap/css/bootstrap.css create mode 100644 public/vendor/bootstrap/css/bootstrap.min.css create mode 100644 public/vendor/bootstrap/img/glyphicons-halflings-white.png create mode 100644 public/vendor/bootstrap/img/glyphicons-halflings.png create mode 100644 public/vendor/bootstrap/js/bootstrap.js create mode 100644 public/vendor/bootstrap/js/bootstrap.min.js create mode 100644 src/auth.js create mode 100644 src/templates.js create mode 100644 src/user.js create mode 100644 src/webserver.js create mode 100644 src/websockets.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..db2bf00d5a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +################# +## npm +################# + +npm-debug.log +node_modules/ +!/node_modules/ +!src/node_modules/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000000..e0411f30d6 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +Installation + +1. npm install + +2. sudo node app + diff --git a/RedisDB.js b/RedisDB.js new file mode 100644 index 0000000000..100e6a70c6 --- /dev/null +++ b/RedisDB.js @@ -0,0 +1,59 @@ +(function(RedisDB) { + var PRODUCTION = false, + ERROR_LOGS = true, + + redis = require('redis'), + db = redis.createClient(); + + // todo (holy cow): append,auth,bgrewriteaof,bgsave,bitcount,bitop,blpop,brpop,brpoplpush,client kill,client list,client getname,client setname,config get,config set,config resetstat,dbsize,debug object,debug segfault,decrby,del,discard,dump,echo,eval,evalsha,exec,exists,expire,expireat,flushall,flushdb,getbit,getrange,getset,hdel,hexists,hget,hgetall,hincrby,hincrbyfloat,hkeys,hlen,hmget,hmset,hset,hsetnx,hvals,incrby,incrbyfloat,info,keys,lastsave,lindex,linsert,llen,lpop,lpush,lpushx,lrange,lrem,lset,ltrim,mget,migrate,monitor,move,mset,msetnx,multi,object,persist,pexpire,pexpireat,ping,psetex,psubscribe,pttl,publish,punsubscribe,quit,randomkey,rename,renamenx,restore,rpop,rpoplpush,rpush,rpushx,sadd,save,scard,script exists,script flush,script kill,script load,sdiff,sdiffstore,select,setbit,setex,setnx,setrange,shutdown,sinter,sinterstore,sismember,slaveof,slowlog,smembers,smove,sort,spop,srandmember,srem,strlen,subscribe,sunion,sunionstore,sync,time,ttl,type,unsubscribe,unwatch,watch,zadd,zcard,zcount,zincrby,zinterstore,zrange,zrangebyscore,zrank,zrem,zremrangebyrank,zremrangebyscore,zrevrange,zrevrangebyscore,zrevrank,zscore,zunionstore + // done :^) get, set, incr, decr + + function return_handler(error, data, callback, error_handler) { + if (error !== null) { + if (error_handler !== null) { + error_handler(error); + } else if (PRODUCTION === false) { + throw new Exception('RedisDB Error: ' + error); + } else if (ERROR_LOGS === true) { + console.log('RedisDB Error: ' + error); + } + } else { + callback(data); + } + } + + RedisDB.set = function(key, value) { + db.set(key, value); + }; + + RedisDB.get = function(key, callback, error_handler) { + db.get(key, function(error, data) { + return_handler(error, data, callback, error_handler); + }); + }; + + // Atomic Operations + RedisDB.incr = function(key, callback, error_handler) { + db.incr(key, function(error, data) { + if (callback) { + return_handler(error, data, callback, error_handler); + } + }); + }; + + RedisDB.decr = function(key) { + db.decr(key); + }; + + // Lists + RedisDB.lpush = function(key, item) { + db.lpush(key, item); + } + + RedisDB.lrange = function(key, start, end, callback, error_handler) { + db.lrange(key, start, end, function(error, data) { + return_handler(error, data, callback, error_handler); + }); + } + +}(exports)); \ No newline at end of file diff --git a/app.js b/app.js new file mode 100644 index 0000000000..447b0ff314 --- /dev/null +++ b/app.js @@ -0,0 +1,26 @@ +var modules = { + user: require('./src/user.js'), + templates: require('./src/templates.js'), + webserver: require('./src/webserver.js'), + websockets: require('./src/websockets.js') + } + + DEVELOPMENT = true; + + +global.configuration = {}; +global.modules = modules; + + + + +(function(config) { + config['ROOT_DIRECTORY'] = __dirname; + + modules.templates.init(); + modules.webserver.init(); + modules.websockets.init(); + + + +}(global.configuration)); \ No newline at end of file diff --git a/config.json b/config.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/node_modules/.bin/express b/node_modules/.bin/express new file mode 120000 index 0000000000..b741d99c66 --- /dev/null +++ b/node_modules/.bin/express @@ -0,0 +1 @@ +../express/bin/express \ No newline at end of file diff --git a/node_modules/connect/.npmignore b/node_modules/connect/.npmignore new file mode 100644 index 0000000000..9046dde51c --- /dev/null +++ b/node_modules/connect/.npmignore @@ -0,0 +1,12 @@ +*.markdown +*.md +.git* +Makefile +benchmarks/ +docs/ +examples/ +install.sh +support/ +test/ +.DS_Store +coverage.html diff --git a/node_modules/connect/.travis.yml b/node_modules/connect/.travis.yml new file mode 100644 index 0000000000..c46b989508 --- /dev/null +++ b/node_modules/connect/.travis.yml @@ -0,0 +1,5 @@ +language: node_js +node_js: + - "0.6" + - "0.8" + - "0.10" \ No newline at end of file diff --git a/node_modules/connect/LICENSE b/node_modules/connect/LICENSE new file mode 100644 index 0000000000..0c5d22d96d --- /dev/null +++ b/node_modules/connect/LICENSE @@ -0,0 +1,24 @@ +(The MIT License) + +Copyright (c) 2010 Sencha Inc. +Copyright (c) 2011 LearnBoost +Copyright (c) 2011 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/node_modules/connect/index.js b/node_modules/connect/index.js new file mode 100644 index 0000000000..23240eedaa --- /dev/null +++ b/node_modules/connect/index.js @@ -0,0 +1,4 @@ + +module.exports = process.env.CONNECT_COV + ? require('./lib-cov/connect') + : require('./lib/connect'); \ No newline at end of file diff --git a/node_modules/connect/lib/cache.js b/node_modules/connect/lib/cache.js new file mode 100644 index 0000000000..052fcdb3d5 --- /dev/null +++ b/node_modules/connect/lib/cache.js @@ -0,0 +1,81 @@ + +/*! + * Connect - Cache + * Copyright(c) 2011 Sencha Inc. + * MIT Licensed + */ + +/** + * Expose `Cache`. + */ + +module.exports = Cache; + +/** + * LRU cache store. + * + * @param {Number} limit + * @api private + */ + +function Cache(limit) { + this.store = {}; + this.keys = []; + this.limit = limit; +} + +/** + * Touch `key`, promoting the object. + * + * @param {String} key + * @param {Number} i + * @api private + */ + +Cache.prototype.touch = function(key, i){ + this.keys.splice(i,1); + this.keys.push(key); +}; + +/** + * Remove `key`. + * + * @param {String} key + * @api private + */ + +Cache.prototype.remove = function(key){ + delete this.store[key]; +}; + +/** + * Get the object stored for `key`. + * + * @param {String} key + * @return {Array} + * @api private + */ + +Cache.prototype.get = function(key){ + return this.store[key]; +}; + +/** + * Add a cache `key`. + * + * @param {String} key + * @return {Array} + * @api private + */ + +Cache.prototype.add = function(key){ + // initialize store + var len = this.keys.push(key); + + // limit reached, invalidate LRU + if (len > this.limit) this.remove(this.keys.shift()); + + var arr = this.store[key] = []; + arr.createdAt = new Date; + return arr; +}; diff --git a/node_modules/connect/lib/connect.js b/node_modules/connect/lib/connect.js new file mode 100644 index 0000000000..c4f81c03a2 --- /dev/null +++ b/node_modules/connect/lib/connect.js @@ -0,0 +1,92 @@ +/*! + * Connect + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var EventEmitter = require('events').EventEmitter + , proto = require('./proto') + , utils = require('./utils') + , path = require('path') + , basename = path.basename + , fs = require('fs'); + +// node patches + +require('./patch'); + +// expose createServer() as the module + +exports = module.exports = createServer; + +/** + * Framework version. + */ + +exports.version = '2.7.6'; + +/** + * Expose mime module. + */ + +exports.mime = require('./middleware/static').mime; + +/** + * Expose the prototype. + */ + +exports.proto = proto; + +/** + * Auto-load middleware getters. + */ + +exports.middleware = {}; + +/** + * Expose utilities. + */ + +exports.utils = utils; + +/** + * Create a new connect server. + * + * @return {Function} + * @api public + */ + +function createServer() { + function app(req, res, next){ app.handle(req, res, next); } + utils.merge(app, proto); + utils.merge(app, EventEmitter.prototype); + app.route = '/'; + app.stack = []; + for (var i = 0; i < arguments.length; ++i) { + app.use(arguments[i]); + } + return app; +}; + +/** + * Support old `.createServer()` method. + */ + +createServer.createServer = createServer; + +/** + * Auto-load bundled middleware with getters. + */ + +fs.readdirSync(__dirname + '/middleware').forEach(function(filename){ + if (!/\.js$/.test(filename)) return; + var name = basename(filename, '.js'); + function load(){ return require('./middleware/' + name); } + exports.middleware.__defineGetter__(name, load); + exports.__defineGetter__(name, load); +}); diff --git a/node_modules/connect/lib/index.js b/node_modules/connect/lib/index.js new file mode 100644 index 0000000000..2618ddca41 --- /dev/null +++ b/node_modules/connect/lib/index.js @@ -0,0 +1,50 @@ + +/** + * Connect is a middleware framework for node, + * shipping with over 18 bundled middleware and a rich selection of + * 3rd-party middleware. + * + * var app = connect() + * .use(connect.logger('dev')) + * .use(connect.static('public')) + * .use(function(req, res){ + * res.end('hello world\n'); + * }) + * .listen(3000); + * + * Installation: + * + * $ npm install connect + * + * Middleware: + * + * - [logger](logger.html) request logger with custom format support + * - [csrf](csrf.html) Cross-site request forgery protection + * - [compress](compress.html) Gzip compression middleware + * - [basicAuth](basicAuth.html) basic http authentication + * - [bodyParser](bodyParser.html) extensible request body parser + * - [json](json.html) application/json parser + * - [urlencoded](urlencoded.html) application/x-www-form-urlencoded parser + * - [multipart](multipart.html) multipart/form-data parser + * - [timeout](timeout.html) request timeouts + * - [cookieParser](cookieParser.html) cookie parser + * - [session](session.html) session management support with bundled MemoryStore + * - [cookieSession](cookieSession.html) cookie-based session support + * - [methodOverride](methodOverride.html) faux HTTP method support + * - [responseTime](responseTime.html) calculates response-time and exposes via X-Response-Time + * - [staticCache](staticCache.html) memory cache layer for the static() middleware + * - [static](static.html) streaming static file server supporting `Range` and more + * - [directory](directory.html) directory listing middleware + * - [vhost](vhost.html) virtual host sub-domain mapping middleware + * - [favicon](favicon.html) efficient favicon server (with default icon) + * - [limit](limit.html) limit the bytesize of request bodies + * - [query](query.html) automatic querystring parser, populating `req.query` + * - [errorHandler](errorHandler.html) flexible error handler + * + * Links: + * + * - list of [3rd-party](https://github.com/senchalabs/connect/wiki) middleware + * - GitHub [repository](http://github.com/senchalabs/connect) + * - [test documentation](https://github.com/senchalabs/connect/blob/gh-pages/tests.md) + * + */ \ No newline at end of file diff --git a/node_modules/connect/lib/middleware/basicAuth.js b/node_modules/connect/lib/middleware/basicAuth.js new file mode 100644 index 0000000000..bc7ec97aa7 --- /dev/null +++ b/node_modules/connect/lib/middleware/basicAuth.js @@ -0,0 +1,103 @@ + +/*! + * Connect - basicAuth + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('../utils') + , unauthorized = utils.unauthorized; + +/** + * Basic Auth: + * + * Enfore basic authentication by providing a `callback(user, pass)`, + * which must return `true` in order to gain access. Alternatively an async + * method is provided as well, invoking `callback(user, pass, callback)`. Populates + * `req.user`. The final alternative is simply passing username / password + * strings. + * + * Simple username and password + * + * connect(connect.basicAuth('username', 'password')); + * + * Callback verification + * + * connect() + * .use(connect.basicAuth(function(user, pass){ + * return 'tj' == user & 'wahoo' == pass; + * })) + * + * Async callback verification, accepting `fn(err, user)`. + * + * connect() + * .use(connect.basicAuth(function(user, pass, fn){ + * User.authenticate({ user: user, pass: pass }, fn); + * })) + * + * @param {Function|String} callback or username + * @param {String} realm + * @api public + */ + +module.exports = function basicAuth(callback, realm) { + var username, password; + + // user / pass strings + if ('string' == typeof callback) { + username = callback; + password = realm; + if ('string' != typeof password) throw new Error('password argument required'); + realm = arguments[2]; + callback = function(user, pass){ + return user == username && pass == password; + } + } + + realm = realm || 'Authorization Required'; + + return function(req, res, next) { + var authorization = req.headers.authorization; + + if (req.user) return next(); + if (!authorization) return unauthorized(res, realm); + + var parts = authorization.split(' '); + + if (parts.length !== 2) return next(utils.error(400)); + + var scheme = parts[0] + , credentials = new Buffer(parts[1], 'base64').toString() + , index = credentials.indexOf(':'); + + if ('Basic' != scheme || index < 0) return next(utils.error(400)); + + var user = credentials.slice(0, index) + , pass = credentials.slice(index + 1); + + // async + if (callback.length >= 3) { + var pause = utils.pause(req); + callback(user, pass, function(err, user){ + if (err || !user) return unauthorized(res, realm); + req.user = req.remoteUser = user; + next(); + pause.resume(); + }); + // sync + } else { + if (callback(user, pass)) { + req.user = req.remoteUser = user; + next(); + } else { + unauthorized(res, realm); + } + } + } +}; + diff --git a/node_modules/connect/lib/middleware/bodyParser.js b/node_modules/connect/lib/middleware/bodyParser.js new file mode 100644 index 0000000000..9f692cdcaa --- /dev/null +++ b/node_modules/connect/lib/middleware/bodyParser.js @@ -0,0 +1,61 @@ + +/*! + * Connect - bodyParser + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var multipart = require('./multipart') + , urlencoded = require('./urlencoded') + , json = require('./json'); + +/** + * Body parser: + * + * Parse request bodies, supports _application/json_, + * _application/x-www-form-urlencoded_, and _multipart/form-data_. + * + * This is equivalent to: + * + * app.use(connect.json()); + * app.use(connect.urlencoded()); + * app.use(connect.multipart()); + * + * Examples: + * + * connect() + * .use(connect.bodyParser()) + * .use(function(req, res) { + * res.end('viewing user ' + req.body.user.name); + * }); + * + * $ curl -d 'user[name]=tj' http://local/ + * $ curl -d '{"user":{"name":"tj"}}' -H "Content-Type: application/json" http://local/ + * + * View [json](json.html), [urlencoded](urlencoded.html), and [multipart](multipart.html) for more info. + * + * @param {Object} options + * @return {Function} + * @api public + */ + +exports = module.exports = function bodyParser(options){ + var _urlencoded = urlencoded(options) + , _multipart = multipart(options) + , _json = json(options); + + return function bodyParser(req, res, next) { + _json(req, res, function(err){ + if (err) return next(err); + _urlencoded(req, res, function(err){ + if (err) return next(err); + _multipart(req, res, next); + }); + }); + } +}; \ No newline at end of file diff --git a/node_modules/connect/lib/middleware/compress.js b/node_modules/connect/lib/middleware/compress.js new file mode 100644 index 0000000000..e71ea8c827 --- /dev/null +++ b/node_modules/connect/lib/middleware/compress.js @@ -0,0 +1,152 @@ +/*! + * Connect - compress + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var zlib = require('zlib'); + +/** + * Supported content-encoding methods. + */ + +exports.methods = { + gzip: zlib.createGzip + , deflate: zlib.createDeflate +}; + +/** + * Default filter function. + */ + +exports.filter = function(req, res){ + return /json|text|javascript/.test(res.getHeader('Content-Type')); +}; + +/** + * Compress: + * + * Compress response data with gzip/deflate. + * + * Filter: + * + * A `filter` callback function may be passed to + * replace the default logic of: + * + * exports.filter = function(req, res){ + * return /json|text|javascript/.test(res.getHeader('Content-Type')); + * }; + * + * Options: + * + * All remaining options are passed to the gzip/deflate + * creation functions. Consult node's docs for additional details. + * + * - `chunkSize` (default: 16*1024) + * - `windowBits` + * - `level`: 0-9 where 0 is no compression, and 9 is slow but best compression + * - `memLevel`: 1-9 low is slower but uses less memory, high is fast but uses more + * - `strategy`: compression strategy + * + * @param {Object} options + * @return {Function} + * @api public + */ + +module.exports = function compress(options) { + options = options || {}; + var names = Object.keys(exports.methods) + , filter = options.filter || exports.filter; + + return function compress(req, res, next){ + var accept = req.headers['accept-encoding'] + , vary = res.getHeader('Vary') + , write = res.write + , end = res.end + , stream + , method; + + // vary + if (!vary) { + res.setHeader('Vary', 'Accept-Encoding'); + } else if (!~vary.indexOf('Accept-Encoding')) { + res.setHeader('Vary', vary + ', Accept-Encoding'); + } + + // proxy + + res.write = function(chunk, encoding){ + if (!this.headerSent) this._implicitHeader(); + return stream + ? stream.write(new Buffer(chunk, encoding)) + : write.call(res, chunk, encoding); + }; + + res.end = function(chunk, encoding){ + if (chunk) this.write(chunk, encoding); + return stream + ? stream.end() + : end.call(res); + }; + + res.on('header', function(){ + var encoding = res.getHeader('Content-Encoding') || 'identity'; + + // already encoded + if ('identity' != encoding) return; + + // default request filter + if (!filter(req, res)) return; + + // SHOULD use identity + if (!accept) return; + + // head + if ('HEAD' == req.method) return; + + // default to gzip + if ('*' == accept.trim()) method = 'gzip'; + + // compression method + if (!method) { + for (var i = 0, len = names.length; i < len; ++i) { + if (~accept.indexOf(names[i])) { + method = names[i]; + break; + } + } + } + + // compression method + if (!method) return; + + // compression stream + stream = exports.methods[method](options); + + // header fields + res.setHeader('Content-Encoding', method); + res.removeHeader('Content-Length'); + + // compression + + stream.on('data', function(chunk){ + write.call(res, chunk); + }); + + stream.on('end', function(){ + end.call(res); + }); + + stream.on('drain', function() { + res.emit('drain'); + }); + }); + + next(); + }; +}; diff --git a/node_modules/connect/lib/middleware/cookieParser.js b/node_modules/connect/lib/middleware/cookieParser.js new file mode 100644 index 0000000000..5da23f2580 --- /dev/null +++ b/node_modules/connect/lib/middleware/cookieParser.js @@ -0,0 +1,62 @@ + +/*! + * Connect - cookieParser + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('./../utils') + , cookie = require('cookie'); + +/** + * Cookie parser: + * + * Parse _Cookie_ header and populate `req.cookies` + * with an object keyed by the cookie names. Optionally + * you may enabled signed cookie support by passing + * a `secret` string, which assigns `req.secret` so + * it may be used by other middleware. + * + * Examples: + * + * connect() + * .use(connect.cookieParser('optional secret string')) + * .use(function(req, res, next){ + * res.end(JSON.stringify(req.cookies)); + * }) + * + * @param {String} secret + * @return {Function} + * @api public + */ + +module.exports = function cookieParser(secret){ + return function cookieParser(req, res, next) { + if (req.cookies) return next(); + var cookies = req.headers.cookie; + + req.secret = secret; + req.cookies = {}; + req.signedCookies = {}; + + if (cookies) { + try { + req.cookies = cookie.parse(cookies); + if (secret) { + req.signedCookies = utils.parseSignedCookies(req.cookies, secret); + req.signedCookies = utils.parseJSONCookies(req.signedCookies); + } + req.cookies = utils.parseJSONCookies(req.cookies); + } catch (err) { + err.status = 400; + return next(err); + } + } + next(); + }; +}; diff --git a/node_modules/connect/lib/middleware/cookieSession.js b/node_modules/connect/lib/middleware/cookieSession.js new file mode 100644 index 0000000000..402fd55f4b --- /dev/null +++ b/node_modules/connect/lib/middleware/cookieSession.js @@ -0,0 +1,117 @@ + +/*! + * Connect - cookieSession + * Copyright(c) 2011 Sencha Inc. + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('./../utils') + , Cookie = require('./session/cookie') + , debug = require('debug')('connect:cookieSession') + , signature = require('cookie-signature') + , crc32 = require('buffer-crc32'); + +/** + * Cookie Session: + * + * Cookie session middleware. + * + * var app = connect(); + * app.use(connect.cookieParser()); + * app.use(connect.cookieSession({ secret: 'tobo!', cookie: { maxAge: 60 * 60 * 1000 }})); + * + * Options: + * + * - `key` cookie name defaulting to `connect.sess` + * - `secret` prevents cookie tampering + * - `cookie` session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: null }` + * - `proxy` trust the reverse proxy when setting secure cookies (via "x-forwarded-proto") + * + * Clearing sessions: + * + * To clear the session simply set its value to `null`, + * `cookieSession()` will then respond with a 1970 Set-Cookie. + * + * req.session = null; + * + * @param {Object} options + * @return {Function} + * @api public + */ + +module.exports = function cookieSession(options){ + // TODO: utilize Session/Cookie to unify API + options = options || {}; + var key = options.key || 'connect.sess' + , trustProxy = options.proxy; + + return function cookieSession(req, res, next) { + + // req.secret is for backwards compatibility + var secret = options.secret || req.secret; + if (!secret) throw new Error('`secret` option required for cookie sessions'); + + // default session + req.session = {}; + var cookie = req.session.cookie = new Cookie(options.cookie); + + // pathname mismatch + if (0 != req.originalUrl.indexOf(cookie.path)) return next(); + + // cookieParser secret + if (!options.secret && req.secret) { + req.session = req.signedCookies[key] || {}; + req.session.cookie = cookie; + } else { + // TODO: refactor + var rawCookie = req.cookies[key]; + if (rawCookie) { + var unsigned = utils.parseSignedCookie(rawCookie, secret); + if (unsigned) { + var originalHash = crc32.signed(unsigned); + req.session = utils.parseJSONCookie(unsigned) || {}; + req.session.cookie = cookie; + } + } + } + + res.on('header', function(){ + // removed + if (!req.session) { + debug('clear session'); + cookie.expires = new Date(0); + res.setHeader('Set-Cookie', cookie.serialize(key, '')); + return; + } + + delete req.session.cookie; + + // check security + var proto = (req.headers['x-forwarded-proto'] || '').toLowerCase() + , tls = req.connection.encrypted || (trustProxy && 'https' == proto) + , secured = cookie.secure && tls; + + // only send secure cookies via https + if (cookie.secure && !secured) return debug('not secured'); + + // serialize + debug('serializing %j', req.session); + var val = 'j:' + JSON.stringify(req.session); + + // compare hashes, no need to set-cookie if unchanged + if (originalHash == crc32.signed(val)) return debug('unmodified session'); + + // set-cookie + val = 's:' + signature.sign(val, secret); + val = cookie.serialize(key, val); + debug('set-cookie %j', cookie); + res.setHeader('Set-Cookie', val); + }); + + next(); + }; +}; diff --git a/node_modules/connect/lib/middleware/csrf.js b/node_modules/connect/lib/middleware/csrf.js new file mode 100644 index 0000000000..e3c353ea76 --- /dev/null +++ b/node_modules/connect/lib/middleware/csrf.js @@ -0,0 +1,73 @@ +/*! + * Connect - csrf + * Copyright(c) 2011 Sencha Inc. + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('../utils'); + +/** + * Anti CSRF: + * + * CRSF protection middleware. + * + * By default this middleware generates a token named "_csrf" + * which should be added to requests which mutate + * state, within a hidden form field, query-string etc. This + * token is validated against the visitor's `req.session._csrf` + * property. + * + * The default `value` function checks `req.body` generated + * by the `bodyParser()` middleware, `req.query` generated + * by `query()`, and the "X-CSRF-Token" header field. + * + * This middleware requires session support, thus should be added + * somewhere _below_ `session()` and `cookieParser()`. + * + * Options: + * + * - `value` a function accepting the request, returning the token + * + * @param {Object} options + * @api public + */ + +module.exports = function csrf(options) { + options = options || {}; + var value = options.value || defaultValue; + + return function(req, res, next){ + // generate CSRF token + var token = req.session._csrf || (req.session._csrf = utils.uid(24)); + + // ignore these methods + if ('GET' == req.method || 'HEAD' == req.method || 'OPTIONS' == req.method) return next(); + + // determine value + var val = value(req); + + // check + if (val != token) return next(utils.error(403)); + + next(); + } +}; + +/** + * Default value function, checking the `req.body` + * and `req.query` for the CSRF token. + * + * @param {IncomingMessage} req + * @return {String} + * @api private + */ + +function defaultValue(req) { + return (req.body && req.body._csrf) + || (req.query && req.query._csrf) + || (req.headers['x-csrf-token']); +} diff --git a/node_modules/connect/lib/middleware/directory.js b/node_modules/connect/lib/middleware/directory.js new file mode 100644 index 0000000000..1c925a7da3 --- /dev/null +++ b/node_modules/connect/lib/middleware/directory.js @@ -0,0 +1,229 @@ + +/*! + * Connect - directory + * Copyright(c) 2011 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +// TODO: icon / style for directories +// TODO: arrow key navigation +// TODO: make icons extensible + +/** + * Module dependencies. + */ + +var fs = require('fs') + , parse = require('url').parse + , utils = require('../utils') + , path = require('path') + , normalize = path.normalize + , extname = path.extname + , join = path.join; + +/*! + * Icon cache. + */ + +var cache = {}; + +/** + * Directory: + * + * Serve directory listings with the given `root` path. + * + * Options: + * + * - `hidden` display hidden (dot) files. Defaults to false. + * - `icons` display icons. Defaults to false. + * - `filter` Apply this filter function to files. Defaults to false. + * + * @param {String} root + * @param {Object} options + * @return {Function} + * @api public + */ + +exports = module.exports = function directory(root, options){ + options = options || {}; + + // root required + if (!root) throw new Error('directory() root path required'); + var hidden = options.hidden + , icons = options.icons + , filter = options.filter + , root = normalize(root); + + return function directory(req, res, next) { + if ('GET' != req.method && 'HEAD' != req.method) return next(); + + var accept = req.headers.accept || 'text/plain' + , url = parse(req.url) + , dir = decodeURIComponent(url.pathname) + , path = normalize(join(root, dir)) + , originalUrl = parse(req.originalUrl) + , originalDir = decodeURIComponent(originalUrl.pathname) + , showUp = path != root && path != root + '/'; + + // null byte(s), bad request + if (~path.indexOf('\0')) return next(utils.error(400)); + + // malicious path, forbidden + if (0 != path.indexOf(root)) return next(utils.error(403)); + + // check if we have a directory + fs.stat(path, function(err, stat){ + if (err) return 'ENOENT' == err.code + ? next() + : next(err); + + if (!stat.isDirectory()) return next(); + + // fetch files + fs.readdir(path, function(err, files){ + if (err) return next(err); + if (!hidden) files = removeHidden(files); + if (filter) files = files.filter(filter); + files.sort(); + + // content-negotiation + for (var key in exports) { + if (~accept.indexOf(key) || ~accept.indexOf('*/*')) { + exports[key](req, res, files, next, originalDir, showUp, icons); + return; + } + } + + // not acceptable + next(utils.error(406)); + }); + }); + }; +}; + +/** + * Respond with text/html. + */ + +exports.html = function(req, res, files, next, dir, showUp, icons){ + fs.readFile(__dirname + '/../public/directory.html', 'utf8', function(err, str){ + if (err) return next(err); + fs.readFile(__dirname + '/../public/style.css', 'utf8', function(err, style){ + if (err) return next(err); + if (showUp) files.unshift('..'); + str = str + .replace('{style}', style) + .replace('{files}', html(files, dir, icons)) + .replace('{directory}', dir) + .replace('{linked-path}', htmlPath(dir)); + res.setHeader('Content-Type', 'text/html'); + res.setHeader('Content-Length', str.length); + res.end(str); + }); + }); +}; + +/** + * Respond with application/json. + */ + +exports.json = function(req, res, files){ + files = JSON.stringify(files); + res.setHeader('Content-Type', 'application/json'); + res.setHeader('Content-Length', files.length); + res.end(files); +}; + +/** + * Respond with text/plain. + */ + +exports.plain = function(req, res, files){ + files = files.join('\n') + '\n'; + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Length', files.length); + res.end(files); +}; + +/** + * Map html `dir`, returning a linked path. + */ + +function htmlPath(dir) { + var curr = []; + return dir.split('/').map(function(part){ + curr.push(part); + return '' + part + ''; + }).join(' / '); +} + +/** + * Map html `files`, returning an html unordered list. + */ + +function html(files, dir, useIcons) { + return ''; +} + +/** + * Load and cache the given `icon`. + * + * @param {String} icon + * @return {String} + * @api private + */ + +function load(icon) { + if (cache[icon]) return cache[icon]; + return cache[icon] = fs.readFileSync(__dirname + '/../public/icons/' + icon, 'base64'); +} + +/** + * Filter "hidden" `files`, aka files + * beginning with a `.`. + * + * @param {Array} files + * @return {Array} + * @api private + */ + +function removeHidden(files) { + return files.filter(function(file){ + return '.' != file[0]; + }); +} + +/** + * Icon map. + */ + +var icons = { + '.js': 'page_white_code_red.png' + , '.c': 'page_white_c.png' + , '.h': 'page_white_h.png' + , '.cc': 'page_white_cplusplus.png' + , '.php': 'page_white_php.png' + , '.rb': 'page_white_ruby.png' + , '.cpp': 'page_white_cplusplus.png' + , '.swf': 'page_white_flash.png' + , '.pdf': 'page_white_acrobat.png' + , 'default': 'page_white.png' +}; diff --git a/node_modules/connect/lib/middleware/errorHandler.js b/node_modules/connect/lib/middleware/errorHandler.js new file mode 100644 index 0000000000..4a84edca01 --- /dev/null +++ b/node_modules/connect/lib/middleware/errorHandler.js @@ -0,0 +1,86 @@ +/*! + * Connect - errorHandler + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('../utils') + , fs = require('fs'); + +// environment + +var env = process.env.NODE_ENV || 'development'; + +/** + * Error handler: + * + * Development error handler, providing stack traces + * and error message responses for requests accepting text, html, + * or json. + * + * Text: + * + * By default, and when _text/plain_ is accepted a simple stack trace + * or error message will be returned. + * + * JSON: + * + * When _application/json_ is accepted, connect will respond with + * an object in the form of `{ "error": error }`. + * + * HTML: + * + * When accepted connect will output a nice html stack trace. + * + * @return {Function} + * @api public + */ + +exports = module.exports = function errorHandler(){ + return function errorHandler(err, req, res, next){ + if (err.status) res.statusCode = err.status; + if (res.statusCode < 400) res.statusCode = 500; + if ('test' != env) console.error(err.stack); + var accept = req.headers.accept || ''; + // html + if (~accept.indexOf('html')) { + fs.readFile(__dirname + '/../public/style.css', 'utf8', function(e, style){ + fs.readFile(__dirname + '/../public/error.html', 'utf8', function(e, html){ + var stack = (err.stack || '') + .split('\n').slice(1) + .map(function(v){ return '
  • ' + v + '
  • '; }).join(''); + html = html + .replace('{style}', style) + .replace('{stack}', stack) + .replace('{title}', exports.title) + .replace('{statusCode}', res.statusCode) + .replace(/\{error\}/g, utils.escape(err.toString())); + res.setHeader('Content-Type', 'text/html; charset=utf-8'); + res.end(html); + }); + }); + // json + } else if (~accept.indexOf('json')) { + var error = { message: err.message, stack: err.stack }; + for (var prop in err) error[prop] = err[prop]; + var json = JSON.stringify({ error: error }); + res.setHeader('Content-Type', 'application/json'); + res.end(json); + // plain text + } else { + res.writeHead(res.statusCode, { 'Content-Type': 'text/plain' }); + res.end(err.stack); + } + }; +}; + +/** + * Template title, framework authors may override this value. + */ + +exports.title = 'Connect'; diff --git a/node_modules/connect/lib/middleware/favicon.js b/node_modules/connect/lib/middleware/favicon.js new file mode 100644 index 0000000000..ef543544ce --- /dev/null +++ b/node_modules/connect/lib/middleware/favicon.js @@ -0,0 +1,80 @@ +/*! + * Connect - favicon + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var fs = require('fs') + , utils = require('../utils'); + +/** + * Favicon: + * + * By default serves the connect favicon, or the favicon + * located by the given `path`. + * + * Options: + * + * - `maxAge` cache-control max-age directive, defaulting to 1 day + * + * Examples: + * + * Serve default favicon: + * + * connect() + * .use(connect.favicon()) + * + * Serve favicon before logging for brevity: + * + * connect() + * .use(connect.favicon()) + * .use(connect.logger('dev')) + * + * Serve custom favicon: + * + * connect() + * .use(connect.favicon('public/favicon.ico')) + * + * @param {String} path + * @param {Object} options + * @return {Function} + * @api public + */ + +module.exports = function favicon(path, options){ + var options = options || {} + , path = path || __dirname + '/../public/favicon.ico' + , maxAge = options.maxAge || 86400000 + , icon; // favicon cache + + return function favicon(req, res, next){ + if ('/favicon.ico' == req.url) { + if (icon) { + res.writeHead(200, icon.headers); + res.end(icon.body); + } else { + fs.readFile(path, function(err, buf){ + if (err) return next(err); + icon = { + headers: { + 'Content-Type': 'image/x-icon' + , 'Content-Length': buf.length + , 'ETag': '"' + utils.md5(buf) + '"' + , 'Cache-Control': 'public, max-age=' + (maxAge / 1000) + }, + body: buf + }; + res.writeHead(200, icon.headers); + res.end(icon.body); + }); + } + } else { + next(); + } + }; +}; diff --git a/node_modules/connect/lib/middleware/json.js b/node_modules/connect/lib/middleware/json.js new file mode 100644 index 0000000000..17e591838a --- /dev/null +++ b/node_modules/connect/lib/middleware/json.js @@ -0,0 +1,86 @@ + +/*! + * Connect - json + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('../utils') + , _limit = require('./limit'); + +/** + * noop middleware. + */ + +function noop(req, res, next) { + next(); +} + +/** + * JSON: + * + * Parse JSON request bodies, providing the + * parsed object as `req.body`. + * + * Options: + * + * - `strict` when `false` anything `JSON.parse()` accepts will be parsed + * - `reviver` used as the second "reviver" argument for JSON.parse + * - `limit` byte limit disabled by default + * + * @param {Object} options + * @return {Function} + * @api public + */ + +exports = module.exports = function(options){ + var options = options || {} + , strict = options.strict !== false; + + var limit = options.limit + ? _limit(options.limit) + : noop; + + return function json(req, res, next) { + if (req._body) return next(); + req.body = req.body || {}; + + if (!utils.hasBody(req)) return next(); + + // check Content-Type + if ('application/json' != utils.mime(req)) return next(); + + // flag as parsed + req._body = true; + + // parse + limit(req, res, function(err){ + if (err) return next(err); + var buf = ''; + req.setEncoding('utf8'); + req.on('data', function(chunk){ buf += chunk }); + req.on('end', function(){ + var first = buf.trim()[0]; + + if (0 == buf.length) { + return next(utils.error(400, 'invalid json, empty body')); + } + + if (strict && '{' != first && '[' != first) return next(utils.error(400, 'invalid json')); + try { + req.body = JSON.parse(buf, options.reviver); + } catch (err){ + err.body = buf; + err.status = 400; + return next(err); + } + next(); + }); + }); + }; +}; diff --git a/node_modules/connect/lib/middleware/limit.js b/node_modules/connect/lib/middleware/limit.js new file mode 100644 index 0000000000..09bd1c47ce --- /dev/null +++ b/node_modules/connect/lib/middleware/limit.js @@ -0,0 +1,78 @@ + +/*! + * Connect - limit + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('../utils'), + brokenPause = utils.brokenPause; + +/** + * Limit: + * + * Limit request bodies to the given size in `bytes`. + * + * A string representation of the bytesize may also be passed, + * for example "5mb", "200kb", "1gb", etc. + * + * connect() + * .use(connect.limit('5.5mb')) + * .use(handleImageUpload) + * + * @param {Number|String} bytes + * @return {Function} + * @api public + */ + +module.exports = function limit(bytes){ + if ('string' == typeof bytes) bytes = utils.parseBytes(bytes); + if ('number' != typeof bytes) throw new Error('limit() bytes required'); + return function limit(req, res, next){ + var received = 0 + , len = req.headers['content-length'] + ? parseInt(req.headers['content-length'], 10) + : null; + + // self-awareness + if (req._limit) return next(); + req._limit = true; + + // limit by content-length + if (len && len > bytes) return next(utils.error(413)); + + // limit + if (brokenPause) { + listen(); + } else { + req.on('newListener', function handler(event) { + if (event !== 'data') return; + + req.removeListener('newListener', handler); + // Start listening at the end of the current loop + // otherwise the request will be consumed too early. + // Sideaffect is `limit` will miss the first chunk, + // but that's not a big deal. + // Unfortunately, the tests don't have large enough + // request bodies to test this. + process.nextTick(listen); + }); + }; + + next(); + + function listen() { + req.on('data', function(chunk) { + received += Buffer.isBuffer(chunk) + ? chunk.length : + Buffer.byteLength(chunk); + + if (received > bytes) req.destroy(); + }); + }; + }; +}; \ No newline at end of file diff --git a/node_modules/connect/lib/middleware/logger.js b/node_modules/connect/lib/middleware/logger.js new file mode 100644 index 0000000000..de72244991 --- /dev/null +++ b/node_modules/connect/lib/middleware/logger.js @@ -0,0 +1,339 @@ +/*! + * Connect - logger + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var bytes = require('bytes'); + +/*! + * Log buffer. + */ + +var buf = []; + +/*! + * Default log buffer duration. + */ + +var defaultBufferDuration = 1000; + +/** + * Logger: + * + * Log requests with the given `options` or a `format` string. + * + * Options: + * + * - `format` Format string, see below for tokens + * - `stream` Output stream, defaults to _stdout_ + * - `buffer` Buffer duration, defaults to 1000ms when _true_ + * - `immediate` Write log line on request instead of response (for response times) + * + * Tokens: + * + * - `:req[header]` ex: `:req[Accept]` + * - `:res[header]` ex: `:res[Content-Length]` + * - `:http-version` + * - `:response-time` + * - `:remote-addr` + * - `:date` + * - `:method` + * - `:url` + * - `:referrer` + * - `:user-agent` + * - `:status` + * + * Formats: + * + * Pre-defined formats that ship with connect: + * + * - `default` ':remote-addr - - [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"' + * - `short` ':remote-addr - :method :url HTTP/:http-version :status :res[content-length] - :response-time ms' + * - `tiny` ':method :url :status :res[content-length] - :response-time ms' + * - `dev` concise output colored by response status for development use + * + * Examples: + * + * connect.logger() // default + * connect.logger('short') + * connect.logger('tiny') + * connect.logger({ immediate: true, format: 'dev' }) + * connect.logger(':method :url - :referrer') + * connect.logger(':req[content-type] -> :res[content-type]') + * connect.logger(function(tokens, req, res){ return 'some format string' }) + * + * Defining Tokens: + * + * To define a token, simply invoke `connect.logger.token()` with the + * name and a callback function. The value returned is then available + * as ":type" in this case. + * + * connect.logger.token('type', function(req, res){ return req.headers['content-type']; }) + * + * Defining Formats: + * + * All default formats are defined this way, however it's public API as well: + * + * connect.logger.format('name', 'string or function') + * + * @param {String|Function|Object} format or options + * @return {Function} + * @api public + */ + +exports = module.exports = function logger(options) { + if ('object' == typeof options) { + options = options || {}; + } else if (options) { + options = { format: options }; + } else { + options = {}; + } + + // output on request instead of response + var immediate = options.immediate; + + // format name + var fmt = exports[options.format] || options.format || exports.default; + + // compile format + if ('function' != typeof fmt) fmt = compile(fmt); + + // options + var stream = options.stream || process.stdout + , buffer = options.buffer; + + // buffering support + if (buffer) { + var realStream = stream + , interval = 'number' == typeof buffer + ? buffer + : defaultBufferDuration; + + // flush interval + setInterval(function(){ + if (buf.length) { + realStream.write(buf.join('')); + buf.length = 0; + } + }, interval); + + // swap the stream + stream = { + write: function(str){ + buf.push(str); + } + }; + } + + return function logger(req, res, next) { + req._startTime = new Date; + + // immediate + if (immediate) { + var line = fmt(exports, req, res); + if (null == line) return; + stream.write(line + '\n'); + // proxy end to output logging + } else { + var end = res.end; + res.end = function(chunk, encoding){ + res.end = end; + res.end(chunk, encoding); + var line = fmt(exports, req, res); + if (null == line) return; + stream.write(line + '\n'); + }; + } + + + next(); + }; +}; + +/** + * Compile `fmt` into a function. + * + * @param {String} fmt + * @return {Function} + * @api private + */ + +function compile(fmt) { + fmt = fmt.replace(/"/g, '\\"'); + var js = ' return "' + fmt.replace(/:([-\w]{2,})(?:\[([^\]]+)\])?/g, function(_, name, arg){ + return '"\n + (tokens["' + name + '"](req, res, "' + arg + '") || "-") + "'; + }) + '";' + return new Function('tokens, req, res', js); +}; + +/** + * Define a token function with the given `name`, + * and callback `fn(req, res)`. + * + * @param {String} name + * @param {Function} fn + * @return {Object} exports for chaining + * @api public + */ + +exports.token = function(name, fn) { + exports[name] = fn; + return this; +}; + +/** + * Define a `fmt` with the given `name`. + * + * @param {String} name + * @param {String|Function} fmt + * @return {Object} exports for chaining + * @api public + */ + +exports.format = function(name, str){ + exports[name] = str; + return this; +}; + +/** + * Default format. + */ + +exports.format('default', ':remote-addr - - [:date] ":method :url HTTP/:http-version" :status :res[content-length] ":referrer" ":user-agent"'); + +/** + * Short format. + */ + +exports.format('short', ':remote-addr - :method :url HTTP/:http-version :status :res[content-length] - :response-time ms'); + +/** + * Tiny format. + */ + +exports.format('tiny', ':method :url :status :res[content-length] - :response-time ms'); + +/** + * dev (colored) + */ + +exports.format('dev', function(tokens, req, res){ + var status = res.statusCode + , len = parseInt(res.getHeader('Content-Length'), 10) + , color = 32; + + if (status >= 500) color = 31 + else if (status >= 400) color = 33 + else if (status >= 300) color = 36; + + len = isNaN(len) + ? '' + : len = ' - ' + bytes(len); + + return '\033[90m' + req.method + + ' ' + req.originalUrl + ' ' + + '\033[' + color + 'm' + res.statusCode + + ' \033[90m' + + (new Date - req._startTime) + + 'ms' + len + + '\033[0m'; +}); + +/** + * request url + */ + +exports.token('url', function(req){ + return req.originalUrl || req.url; +}); + +/** + * request method + */ + +exports.token('method', function(req){ + return req.method; +}); + +/** + * response time in milliseconds + */ + +exports.token('response-time', function(req){ + return new Date - req._startTime; +}); + +/** + * UTC date + */ + +exports.token('date', function(){ + return new Date().toUTCString(); +}); + +/** + * response status code + */ + +exports.token('status', function(req, res){ + return res.statusCode; +}); + +/** + * normalized referrer + */ + +exports.token('referrer', function(req){ + return req.headers['referer'] || req.headers['referrer']; +}); + +/** + * remote address + */ + +exports.token('remote-addr', function(req){ + if (req.ip) return req.ip; + var sock = req.socket; + if (sock.socket) return sock.socket.remoteAddress; + return sock.remoteAddress; +}); + +/** + * HTTP version + */ + +exports.token('http-version', function(req){ + return req.httpVersionMajor + '.' + req.httpVersionMinor; +}); + +/** + * UA string + */ + +exports.token('user-agent', function(req){ + return req.headers['user-agent']; +}); + +/** + * request header + */ + +exports.token('req', function(req, res, field){ + return req.headers[field.toLowerCase()]; +}); + +/** + * response header + */ + +exports.token('res', function(req, res, field){ + return (res._headers || {})[field.toLowerCase()]; +}); + diff --git a/node_modules/connect/lib/middleware/methodOverride.js b/node_modules/connect/lib/middleware/methodOverride.js new file mode 100644 index 0000000000..aaf4014f26 --- /dev/null +++ b/node_modules/connect/lib/middleware/methodOverride.js @@ -0,0 +1,40 @@ + +/*! + * Connect - methodOverride + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Method Override: + * + * Provides faux HTTP method support. + * + * Pass an optional `key` to use when checking for + * a method override, othewise defaults to _\_method_. + * The original method is available via `req.originalMethod`. + * + * @param {String} key + * @return {Function} + * @api public + */ + +module.exports = function methodOverride(key){ + key = key || "_method"; + return function methodOverride(req, res, next) { + req.originalMethod = req.originalMethod || req.method; + + // req.body + if (req.body && key in req.body) { + req.method = req.body[key].toUpperCase(); + delete req.body[key]; + // check X-HTTP-Method-Override + } else if (req.headers['x-http-method-override']) { + req.method = req.headers['x-http-method-override'].toUpperCase(); + } + + next(); + }; +}; + diff --git a/node_modules/connect/lib/middleware/multipart.js b/node_modules/connect/lib/middleware/multipart.js new file mode 100644 index 0000000000..7b26fae807 --- /dev/null +++ b/node_modules/connect/lib/middleware/multipart.js @@ -0,0 +1,133 @@ +/*! + * Connect - multipart + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var formidable = require('formidable') + , _limit = require('./limit') + , utils = require('../utils') + , qs = require('qs'); + +/** + * noop middleware. + */ + +function noop(req, res, next) { + next(); +} + +/** + * Multipart: + * + * Parse multipart/form-data request bodies, + * providing the parsed object as `req.body` + * and `req.files`. + * + * Configuration: + * + * The options passed are merged with [formidable](https://github.com/felixge/node-formidable)'s + * `IncomingForm` object, allowing you to configure the upload directory, + * size limits, etc. For example if you wish to change the upload dir do the following. + * + * app.use(connect.multipart({ uploadDir: path })); + * + * Options: + * + * - `limit` byte limit defaulting to none + * - `defer` defers processing and exposes the Formidable form object as `req.form`. + * `next()` is called without waiting for the form's "end" event. + * This option is useful if you need to bind to the "progress" event, for example. + * + * @param {Object} options + * @return {Function} + * @api public + */ + +exports = module.exports = function(options){ + options = options || {}; + + var limit = options.limit + ? _limit(options.limit) + : noop; + + return function multipart(req, res, next) { + if (req._body) return next(); + req.body = req.body || {}; + req.files = req.files || {}; + + if (!utils.hasBody(req)) return next(); + + // ignore GET + if ('GET' == req.method || 'HEAD' == req.method) return next(); + + // check Content-Type + if ('multipart/form-data' != utils.mime(req)) return next(); + + // flag as parsed + req._body = true; + + // parse + limit(req, res, function(err){ + if (err) return next(err); + + var form = new formidable.IncomingForm + , data = {} + , files = {} + , done; + + Object.keys(options).forEach(function(key){ + form[key] = options[key]; + }); + + function ondata(name, val, data){ + if (Array.isArray(data[name])) { + data[name].push(val); + } else if (data[name]) { + data[name] = [data[name], val]; + } else { + data[name] = val; + } + } + + form.on('field', function(name, val){ + ondata(name, val, data); + }); + + form.on('file', function(name, val){ + ondata(name, val, files); + }); + + form.on('error', function(err){ + if (!options.defer) { + err.status = 400; + next(err); + } + done = true; + }); + + form.on('end', function(){ + if (done) return; + try { + req.body = qs.parse(data); + req.files = qs.parse(files); + if (!options.defer) next(); + } catch (err) { + form.emit('error', err); + } + }); + + form.parse(req); + + if (options.defer) { + req.form = form; + next(); + } + }); + } +}; diff --git a/node_modules/connect/lib/middleware/query.js b/node_modules/connect/lib/middleware/query.js new file mode 100644 index 0000000000..93fc5d347b --- /dev/null +++ b/node_modules/connect/lib/middleware/query.js @@ -0,0 +1,46 @@ +/*! + * Connect - query + * Copyright(c) 2011 TJ Holowaychuk + * Copyright(c) 2011 Sencha Inc. + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var qs = require('qs') + , parse = require('../utils').parseUrl; + +/** + * Query: + * + * Automatically parse the query-string when available, + * populating the `req.query` object. + * + * Examples: + * + * connect() + * .use(connect.query()) + * .use(function(req, res){ + * res.end(JSON.stringify(req.query)); + * }); + * + * The `options` passed are provided to qs.parse function. + * + * @param {Object} options + * @return {Function} + * @api public + */ + +module.exports = function query(options){ + return function query(req, res, next){ + if (!req.query) { + req.query = ~req.url.indexOf('?') + ? qs.parse(parse(req).query, options) + : {}; + } + + next(); + }; +}; diff --git a/node_modules/connect/lib/middleware/responseTime.js b/node_modules/connect/lib/middleware/responseTime.js new file mode 100644 index 0000000000..62abc04948 --- /dev/null +++ b/node_modules/connect/lib/middleware/responseTime.js @@ -0,0 +1,32 @@ + +/*! + * Connect - responseTime + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Reponse time: + * + * Adds the `X-Response-Time` header displaying the response + * duration in milliseconds. + * + * @return {Function} + * @api public + */ + +module.exports = function responseTime(){ + return function(req, res, next){ + var start = new Date; + + if (res._responseTime) return next(); + res._responseTime = true; + + res.on('header', function(){ + var duration = new Date - start; + res.setHeader('X-Response-Time', duration + 'ms'); + }); + + next(); + }; +}; diff --git a/node_modules/connect/lib/middleware/session.js b/node_modules/connect/lib/middleware/session.js new file mode 100644 index 0000000000..9be6c8b6e8 --- /dev/null +++ b/node_modules/connect/lib/middleware/session.js @@ -0,0 +1,356 @@ + +/*! + * Connect - session + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var Session = require('./session/session') + , debug = require('debug')('connect:session') + , MemoryStore = require('./session/memory') + , signature = require('cookie-signature') + , Cookie = require('./session/cookie') + , Store = require('./session/store') + , utils = require('./../utils') + , parse = utils.parseUrl + , crc32 = require('buffer-crc32'); + +// environment + +var env = process.env.NODE_ENV; + +/** + * Expose the middleware. + */ + +exports = module.exports = session; + +/** + * Expose constructors. + */ + +exports.Store = Store; +exports.Cookie = Cookie; +exports.Session = Session; +exports.MemoryStore = MemoryStore; + +/** + * Warning message for `MemoryStore` usage in production. + */ + +var warning = 'Warning: connection.session() MemoryStore is not\n' + + 'designed for a production environment, as it will leak\n' + + 'memory, and will not scale past a single process.'; + +/** + * Session: + * + * Setup session store with the given `options`. + * + * Session data is _not_ saved in the cookie itself, however + * cookies are used, so we must use the [cookieParser()](cookieParser.html) + * middleware _before_ `session()`. + * + * Examples: + * + * connect() + * .use(connect.cookieParser()) + * .use(connect.session({ secret: 'keyboard cat', key: 'sid', cookie: { secure: true }})) + * + * Options: + * + * - `key` cookie name defaulting to `connect.sid` + * - `store` session store instance + * - `secret` session cookie is signed with this secret to prevent tampering + * - `cookie` session cookie settings, defaulting to `{ path: '/', httpOnly: true, maxAge: null }` + * - `proxy` trust the reverse proxy when setting secure cookies (via "x-forwarded-proto") + * + * Cookie option: + * + * By default `cookie.maxAge` is `null`, meaning no "expires" parameter is set + * so the cookie becomes a browser-session cookie. When the user closes the + * browser the cookie (and session) will be removed. + * + * ## req.session + * + * To store or access session data, simply use the request property `req.session`, + * which is (generally) serialized as JSON by the store, so nested objects + * are typically fine. For example below is a user-specific view counter: + * + * connect() + * .use(connect.favicon()) + * .use(connect.cookieParser()) + * .use(connect.session({ secret: 'keyboard cat', cookie: { maxAge: 60000 }})) + * .use(function(req, res, next){ + * var sess = req.session; + * if (sess.views) { + * res.setHeader('Content-Type', 'text/html'); + * res.write('

    views: ' + sess.views + '

    '); + * res.write('

    expires in: ' + (sess.cookie.maxAge / 1000) + 's

    '); + * res.end(); + * sess.views++; + * } else { + * sess.views = 1; + * res.end('welcome to the session demo. refresh!'); + * } + * } + * )).listen(3000); + * + * ## Session#regenerate() + * + * To regenerate the session simply invoke the method, once complete + * a new SID and `Session` instance will be initialized at `req.session`. + * + * req.session.regenerate(function(err){ + * // will have a new session here + * }); + * + * ## Session#destroy() + * + * Destroys the session, removing `req.session`, will be re-generated next request. + * + * req.session.destroy(function(err){ + * // cannot access session here + * }); + * + * ## Session#reload() + * + * Reloads the session data. + * + * req.session.reload(function(err){ + * // session updated + * }); + * + * ## Session#save() + * + * Save the session. + * + * req.session.save(function(err){ + * // session saved + * }); + * + * ## Session#touch() + * + * Updates the `.maxAge` property. Typically this is + * not necessary to call, as the session middleware does this for you. + * + * ## Session#cookie + * + * Each session has a unique cookie object accompany it. This allows + * you to alter the session cookie per visitor. For example we can + * set `req.session.cookie.expires` to `false` to enable the cookie + * to remain for only the duration of the user-agent. + * + * ## Session#maxAge + * + * Alternatively `req.session.cookie.maxAge` will return the time + * remaining in milliseconds, which we may also re-assign a new value + * to adjust the `.expires` property appropriately. The following + * are essentially equivalent + * + * var hour = 3600000; + * req.session.cookie.expires = new Date(Date.now() + hour); + * req.session.cookie.maxAge = hour; + * + * For example when `maxAge` is set to `60000` (one minute), and 30 seconds + * has elapsed it will return `30000` until the current request has completed, + * at which time `req.session.touch()` is called to reset `req.session.maxAge` + * to its original value. + * + * req.session.cookie.maxAge; + * // => 30000 + * + * Session Store Implementation: + * + * Every session store _must_ implement the following methods + * + * - `.get(sid, callback)` + * - `.set(sid, session, callback)` + * - `.destroy(sid, callback)` + * + * Recommended methods include, but are not limited to: + * + * - `.length(callback)` + * - `.clear(callback)` + * + * For an example implementation view the [connect-redis](http://github.com/visionmedia/connect-redis) repo. + * + * @param {Object} options + * @return {Function} + * @api public + */ + +function session(options){ + var options = options || {} + , key = options.key || 'connect.sid' + , store = options.store || new MemoryStore + , cookie = options.cookie || {} + , trustProxy = options.proxy + , storeReady = true; + + // notify user that this store is not + // meant for a production environment + if ('production' == env && store instanceof MemoryStore) { + console.warn(warning); + } + + // generates the new session + store.generate = function(req){ + req.sessionID = utils.uid(24); + req.session = new Session(req); + req.session.cookie = new Cookie(cookie); + }; + + store.on('disconnect', function(){ storeReady = false; }); + store.on('connect', function(){ storeReady = true; }); + + return function session(req, res, next) { + // self-awareness + if (req.session) return next(); + + // Handle connection as if there is no session if + // the store has temporarily disconnected etc + if (!storeReady) return debug('store is disconnected'), next(); + + // pathname mismatch + if (0 != req.originalUrl.indexOf(cookie.path || '/')) return next(); + + // backwards compatibility for signed cookies + // req.secret is passed from the cookie parser middleware + var secret = options.secret || req.secret; + + // ensure secret is available or bail + if (!secret) throw new Error('`secret` option required for sessions'); + + // parse url + var originalHash + , originalId; + + // expose store + req.sessionStore = store; + + // grab the session cookie value and check the signature + var rawCookie = req.cookies[key]; + + // get signedCookies for backwards compat with signed cookies + var unsignedCookie = req.signedCookies[key]; + + if (!unsignedCookie && rawCookie) { + unsignedCookie = utils.parseSignedCookie(rawCookie, secret); + } + + // set-cookie + res.on('header', function(){ + if (!req.session) return; + var cookie = req.session.cookie + , proto = (req.headers['x-forwarded-proto'] || '').split(',')[0].toLowerCase().trim() + , tls = req.connection.encrypted || (trustProxy && 'https' == proto) + , secured = cookie.secure && tls + , isNew = unsignedCookie != req.sessionID; + + // only send secure cookies via https + if (cookie.secure && !secured) return debug('not secured'); + + // long expires, handle expiry server-side + if (!isNew && cookie.hasLongExpires) return debug('already set cookie'); + + // browser-session length cookie + if (null == cookie.expires) { + if (!isNew) return debug('already set browser-session cookie'); + // compare hashes and ids + } else if (originalHash == hash(req.session) && originalId == req.session.id) { + return debug('unmodified session'); + } + + var val = 's:' + signature.sign(req.sessionID, secret); + val = cookie.serialize(key, val); + debug('set-cookie %s', val); + res.setHeader('Set-Cookie', val); + }); + + // proxy end() to commit the session + var end = res.end; + res.end = function(data, encoding){ + res.end = end; + if (!req.session) return res.end(data, encoding); + debug('saving'); + req.session.resetMaxAge(); + req.session.save(function(err){ + if (err) console.error(err.stack); + debug('saved'); + res.end(data, encoding); + }); + }; + + // generate the session + function generate() { + store.generate(req); + } + + // get the sessionID from the cookie + req.sessionID = unsignedCookie; + + // generate a session if the browser doesn't send a sessionID + if (!req.sessionID) { + debug('no SID sent, generating session'); + generate(); + next(); + return; + } + + // generate the session object + var pause = utils.pause(req); + debug('fetching %s', req.sessionID); + store.get(req.sessionID, function(err, sess){ + // proxy to resume() events + var _next = next; + next = function(err){ + _next(err); + pause.resume(); + }; + + // error handling + if (err) { + debug('error %j', err); + if ('ENOENT' == err.code) { + generate(); + next(); + } else { + next(err); + } + // no session + } else if (!sess) { + debug('no session found'); + generate(); + next(); + // populate req.session + } else { + debug('session found'); + store.createSession(req, sess); + originalId = req.sessionID; + originalHash = hash(sess); + next(); + } + }); + }; +}; + +/** + * Hash the given `sess` object omitting changes + * to `.cookie`. + * + * @param {Object} sess + * @return {String} + * @api private + */ + +function hash(sess) { + return crc32.signed(JSON.stringify(sess, function(key, val){ + if ('cookie' != key) return val; + })); +} diff --git a/node_modules/connect/lib/middleware/session/cookie.js b/node_modules/connect/lib/middleware/session/cookie.js new file mode 100644 index 0000000000..cdce2a5e67 --- /dev/null +++ b/node_modules/connect/lib/middleware/session/cookie.js @@ -0,0 +1,140 @@ + +/*! + * Connect - session - Cookie + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('../../utils') + , cookie = require('cookie'); + +/** + * Initialize a new `Cookie` with the given `options`. + * + * @param {IncomingMessage} req + * @param {Object} options + * @api private + */ + +var Cookie = module.exports = function Cookie(options) { + this.path = '/'; + this.maxAge = null; + this.httpOnly = true; + if (options) utils.merge(this, options); + this.originalMaxAge = undefined == this.originalMaxAge + ? this.maxAge + : this.originalMaxAge; +}; + +/*! + * Prototype. + */ + +Cookie.prototype = { + + /** + * Set expires `date`. + * + * @param {Date} date + * @api public + */ + + set expires(date) { + this._expires = date; + this.originalMaxAge = this.maxAge; + }, + + /** + * Get expires `date`. + * + * @return {Date} + * @api public + */ + + get expires() { + return this._expires; + }, + + /** + * Set expires via max-age in `ms`. + * + * @param {Number} ms + * @api public + */ + + set maxAge(ms) { + this.expires = 'number' == typeof ms + ? new Date(Date.now() + ms) + : ms; + }, + + /** + * Get expires max-age in `ms`. + * + * @return {Number} + * @api public + */ + + get maxAge() { + return this.expires instanceof Date + ? this.expires.valueOf() - Date.now() + : this.expires; + }, + + /** + * Return cookie data object. + * + * @return {Object} + * @api private + */ + + get data() { + return { + originalMaxAge: this.originalMaxAge + , expires: this._expires + , secure: this.secure + , httpOnly: this.httpOnly + , domain: this.domain + , path: this.path + } + }, + + /** + * Check if the cookie has a reasonably large max-age. + * + * @return {Boolean} + * @api private + */ + + get hasLongExpires() { + var week = 604800000; + return this.maxAge > (4 * week); + }, + + /** + * Return a serialized cookie string. + * + * @return {String} + * @api public + */ + + serialize: function(name, val){ + return cookie.serialize(name, val, this.data); + }, + + /** + * Return JSON representation of this cookie. + * + * @return {Object} + * @api private + */ + + toJSON: function(){ + return this.data; + } +}; diff --git a/node_modules/connect/lib/middleware/session/memory.js b/node_modules/connect/lib/middleware/session/memory.js new file mode 100644 index 0000000000..fb93939280 --- /dev/null +++ b/node_modules/connect/lib/middleware/session/memory.js @@ -0,0 +1,129 @@ + +/*! + * Connect - session - MemoryStore + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var Store = require('./store'); + +/** + * Initialize a new `MemoryStore`. + * + * @api public + */ + +var MemoryStore = module.exports = function MemoryStore() { + this.sessions = {}; +}; + +/** + * Inherit from `Store.prototype`. + */ + +MemoryStore.prototype.__proto__ = Store.prototype; + +/** + * Attempt to fetch session by the given `sid`. + * + * @param {String} sid + * @param {Function} fn + * @api public + */ + +MemoryStore.prototype.get = function(sid, fn){ + var self = this; + process.nextTick(function(){ + var expires + , sess = self.sessions[sid]; + if (sess) { + sess = JSON.parse(sess); + expires = 'string' == typeof sess.cookie.expires + ? new Date(sess.cookie.expires) + : sess.cookie.expires; + if (!expires || new Date < expires) { + fn(null, sess); + } else { + self.destroy(sid, fn); + } + } else { + fn(); + } + }); +}; + +/** + * Commit the given `sess` object associated with the given `sid`. + * + * @param {String} sid + * @param {Session} sess + * @param {Function} fn + * @api public + */ + +MemoryStore.prototype.set = function(sid, sess, fn){ + var self = this; + process.nextTick(function(){ + self.sessions[sid] = JSON.stringify(sess); + fn && fn(); + }); +}; + +/** + * Destroy the session associated with the given `sid`. + * + * @param {String} sid + * @api public + */ + +MemoryStore.prototype.destroy = function(sid, fn){ + var self = this; + process.nextTick(function(){ + delete self.sessions[sid]; + fn && fn(); + }); +}; + +/** + * Invoke the given callback `fn` with all active sessions. + * + * @param {Function} fn + * @api public + */ + +MemoryStore.prototype.all = function(fn){ + var arr = [] + , keys = Object.keys(this.sessions); + for (var i = 0, len = keys.length; i < len; ++i) { + arr.push(this.sessions[keys[i]]); + } + fn(null, arr); +}; + +/** + * Clear all sessions. + * + * @param {Function} fn + * @api public + */ + +MemoryStore.prototype.clear = function(fn){ + this.sessions = {}; + fn && fn(); +}; + +/** + * Fetch number of sessions. + * + * @param {Function} fn + * @api public + */ + +MemoryStore.prototype.length = function(fn){ + fn(null, Object.keys(this.sessions).length); +}; diff --git a/node_modules/connect/lib/middleware/session/session.js b/node_modules/connect/lib/middleware/session/session.js new file mode 100644 index 0000000000..0dd4b40072 --- /dev/null +++ b/node_modules/connect/lib/middleware/session/session.js @@ -0,0 +1,116 @@ + +/*! + * Connect - session - Session + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('../../utils'); + +/** + * Create a new `Session` with the given request and `data`. + * + * @param {IncomingRequest} req + * @param {Object} data + * @api private + */ + +var Session = module.exports = function Session(req, data) { + Object.defineProperty(this, 'req', { value: req }); + Object.defineProperty(this, 'id', { value: req.sessionID }); + if ('object' == typeof data) utils.merge(this, data); +}; + +/** + * Update reset `.cookie.maxAge` to prevent + * the cookie from expiring when the + * session is still active. + * + * @return {Session} for chaining + * @api public + */ + +Session.prototype.touch = function(){ + return this.resetMaxAge(); +}; + +/** + * Reset `.maxAge` to `.originalMaxAge`. + * + * @return {Session} for chaining + * @api public + */ + +Session.prototype.resetMaxAge = function(){ + this.cookie.maxAge = this.cookie.originalMaxAge; + return this; +}; + +/** + * Save the session data with optional callback `fn(err)`. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +Session.prototype.save = function(fn){ + this.req.sessionStore.set(this.id, this, fn || function(){}); + return this; +}; + +/** + * Re-loads the session data _without_ altering + * the maxAge properties. Invokes the callback `fn(err)`, + * after which time if no exception has occurred the + * `req.session` property will be a new `Session` object, + * although representing the same session. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +Session.prototype.reload = function(fn){ + var req = this.req + , store = this.req.sessionStore; + store.get(this.id, function(err, sess){ + if (err) return fn(err); + if (!sess) return fn(new Error('failed to load session')); + store.createSession(req, sess); + fn(); + }); + return this; +}; + +/** + * Destroy `this` session. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +Session.prototype.destroy = function(fn){ + delete this.req.session; + this.req.sessionStore.destroy(this.id, fn); + return this; +}; + +/** + * Regenerate this request's session. + * + * @param {Function} fn + * @return {Session} for chaining + * @api public + */ + +Session.prototype.regenerate = function(fn){ + this.req.sessionStore.regenerate(this.req, fn); + return this; +}; diff --git a/node_modules/connect/lib/middleware/session/store.js b/node_modules/connect/lib/middleware/session/store.js new file mode 100644 index 0000000000..54294cbdf7 --- /dev/null +++ b/node_modules/connect/lib/middleware/session/store.js @@ -0,0 +1,84 @@ + +/*! + * Connect - session - Store + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var EventEmitter = require('events').EventEmitter + , Session = require('./session') + , Cookie = require('./cookie'); + +/** + * Initialize abstract `Store`. + * + * @api private + */ + +var Store = module.exports = function Store(options){}; + +/** + * Inherit from `EventEmitter.prototype`. + */ + +Store.prototype.__proto__ = EventEmitter.prototype; + +/** + * Re-generate the given requests's session. + * + * @param {IncomingRequest} req + * @return {Function} fn + * @api public + */ + +Store.prototype.regenerate = function(req, fn){ + var self = this; + this.destroy(req.sessionID, function(err){ + self.generate(req); + fn(err); + }); +}; + +/** + * Load a `Session` instance via the given `sid` + * and invoke the callback `fn(err, sess)`. + * + * @param {String} sid + * @param {Function} fn + * @api public + */ + +Store.prototype.load = function(sid, fn){ + var self = this; + this.get(sid, function(err, sess){ + if (err) return fn(err); + if (!sess) return fn(); + var req = { sessionID: sid, sessionStore: self }; + sess = self.createSession(req, sess); + fn(null, sess); + }); +}; + +/** + * Create session from JSON `sess` data. + * + * @param {IncomingRequest} req + * @param {Object} sess + * @return {Session} + * @api private + */ + +Store.prototype.createSession = function(req, sess){ + var expires = sess.cookie.expires + , orig = sess.cookie.originalMaxAge; + sess.cookie = new Cookie(sess.cookie); + if ('string' == typeof expires) sess.cookie.expires = new Date(expires); + sess.cookie.originalMaxAge = orig; + req.session = new Session(req, sess); + return req.session; +}; diff --git a/node_modules/connect/lib/middleware/static.js b/node_modules/connect/lib/middleware/static.js new file mode 100644 index 0000000000..f69b58e8a1 --- /dev/null +++ b/node_modules/connect/lib/middleware/static.js @@ -0,0 +1,95 @@ +/*! + * Connect - static + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var send = require('send') + , utils = require('../utils') + , parse = utils.parseUrl + , url = require('url'); + +/** + * Static: + * + * Static file server with the given `root` path. + * + * Examples: + * + * var oneDay = 86400000; + * + * connect() + * .use(connect.static(__dirname + '/public')) + * + * connect() + * .use(connect.static(__dirname + '/public', { maxAge: oneDay })) + * + * Options: + * + * - `maxAge` Browser cache maxAge in milliseconds. defaults to 0 + * - `hidden` Allow transfer of hidden files. defaults to false + * - `redirect` Redirect to trailing "/" when the pathname is a dir. defaults to true + * - `index` Default file name, defaults to 'index.html' + * + * @param {String} root + * @param {Object} options + * @return {Function} + * @api public + */ + +exports = module.exports = function static(root, options){ + options = options || {}; + + // root required + if (!root) throw new Error('static() root path required'); + + // default redirect + var redirect = false !== options.redirect; + + return function static(req, res, next) { + if ('GET' != req.method && 'HEAD' != req.method) return next(); + var path = parse(req).pathname; + var pause = utils.pause(req); + + function resume() { + next(); + pause.resume(); + } + + function directory() { + if (!redirect) return resume(); + var pathname = url.parse(req.originalUrl).pathname; + res.statusCode = 301; + res.setHeader('Location', pathname + '/'); + res.end('Redirecting to ' + utils.escape(pathname) + '/'); + } + + function error(err) { + if (404 == err.status) return resume(); + next(err); + } + + send(req, path) + .maxage(options.maxAge || 0) + .root(root) + .index(options.index || 'index.html') + .hidden(options.hidden) + .on('error', error) + .on('directory', directory) + .pipe(res); + }; +}; + +/** + * Expose mime module. + * + * If you wish to extend the mime table use this + * reference to the "mime" module in the npm registry. + */ + +exports.mime = send.mime; diff --git a/node_modules/connect/lib/middleware/staticCache.js b/node_modules/connect/lib/middleware/staticCache.js new file mode 100644 index 0000000000..7354a8ffdb --- /dev/null +++ b/node_modules/connect/lib/middleware/staticCache.js @@ -0,0 +1,231 @@ + +/*! + * Connect - staticCache + * Copyright(c) 2011 Sencha Inc. + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('../utils') + , Cache = require('../cache') + , fresh = require('fresh'); + +/** + * Static cache: + * + * Enables a memory cache layer on top of + * the `static()` middleware, serving popular + * static files. + * + * By default a maximum of 128 objects are + * held in cache, with a max of 256k each, + * totalling ~32mb. + * + * A Least-Recently-Used (LRU) cache algo + * is implemented through the `Cache` object, + * simply rotating cache objects as they are + * hit. This means that increasingly popular + * objects maintain their positions while + * others get shoved out of the stack and + * garbage collected. + * + * Benchmarks: + * + * static(): 2700 rps + * node-static: 5300 rps + * static() + staticCache(): 7500 rps + * + * Options: + * + * - `maxObjects` max cache objects [128] + * - `maxLength` max cache object length 256kb + * + * @param {Object} options + * @return {Function} + * @api public + */ + +module.exports = function staticCache(options){ + var options = options || {} + , cache = new Cache(options.maxObjects || 128) + , maxlen = options.maxLength || 1024 * 256; + + console.warn('connect.staticCache() is deprecated and will be removed in 3.0'); + console.warn('use varnish or similar reverse proxy caches.'); + + return function staticCache(req, res, next){ + var key = cacheKey(req) + , ranges = req.headers.range + , hasCookies = req.headers.cookie + , hit = cache.get(key); + + // cache static + // TODO: change from staticCache() -> cache() + // and make this work for any request + req.on('static', function(stream){ + var headers = res._headers + , cc = utils.parseCacheControl(headers['cache-control'] || '') + , contentLength = headers['content-length'] + , hit; + + // dont cache set-cookie responses + if (headers['set-cookie']) return hasCookies = true; + + // dont cache when cookies are present + if (hasCookies) return; + + // ignore larger files + if (!contentLength || contentLength > maxlen) return; + + // don't cache partial files + if (headers['content-range']) return; + + // dont cache items we shouldn't be + // TODO: real support for must-revalidate / no-cache + if ( cc['no-cache'] + || cc['no-store'] + || cc['private'] + || cc['must-revalidate']) return; + + // if already in cache then validate + if (hit = cache.get(key)){ + if (headers.etag == hit[0].etag) { + hit[0].date = new Date; + return; + } else { + cache.remove(key); + } + } + + // validation notifiactions don't contain a steam + if (null == stream) return; + + // add the cache object + var arr = []; + + // store the chunks + stream.on('data', function(chunk){ + arr.push(chunk); + }); + + // flag it as complete + stream.on('end', function(){ + var cacheEntry = cache.add(key); + delete headers['x-cache']; // Clean up (TODO: others) + cacheEntry.push(200); + cacheEntry.push(headers); + cacheEntry.push.apply(cacheEntry, arr); + }); + }); + + if (req.method == 'GET' || req.method == 'HEAD') { + if (ranges) { + next(); + } else if (!hasCookies && hit && !mustRevalidate(req, hit)) { + res.setHeader('X-Cache', 'HIT'); + respondFromCache(req, res, hit); + } else { + res.setHeader('X-Cache', 'MISS'); + next(); + } + } else { + next(); + } + } +}; + +/** + * Respond with the provided cached value. + * TODO: Assume 200 code, that's iffy. + * + * @param {Object} req + * @param {Object} res + * @param {Object} cacheEntry + * @return {String} + * @api private + */ + +function respondFromCache(req, res, cacheEntry) { + var status = cacheEntry[0] + , headers = utils.merge({}, cacheEntry[1]) + , content = cacheEntry.slice(2); + + headers.age = (new Date - new Date(headers.date)) / 1000 || 0; + + switch (req.method) { + case 'HEAD': + res.writeHead(status, headers); + res.end(); + break; + case 'GET': + if (utils.conditionalGET(req) && fresh(req.headers, headers)) { + headers['content-length'] = 0; + res.writeHead(304, headers); + res.end(); + } else { + res.writeHead(status, headers); + + function write() { + while (content.length) { + if (false === res.write(content.shift())) { + res.once('drain', write); + return; + } + } + res.end(); + } + + write(); + } + break; + default: + // This should never happen. + res.writeHead(500, ''); + res.end(); + } +} + +/** + * Determine whether or not a cached value must be revalidated. + * + * @param {Object} req + * @param {Object} cacheEntry + * @return {String} + * @api private + */ + +function mustRevalidate(req, cacheEntry) { + var cacheHeaders = cacheEntry[1] + , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '') + , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '') + , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0; + + if ( cacheCC['no-cache'] + || cacheCC['must-revalidate'] + || cacheCC['proxy-revalidate']) return true; + + if (reqCC['no-cache']) return true; + + if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge; + + if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge; + + return false; +} + +/** + * The key to use in the cache. For now, this is the URL path and query. + * + * 'http://example.com?key=value' -> '/?key=value' + * + * @param {Object} req + * @return {String} + * @api private + */ + +function cacheKey(req) { + return utils.parseUrl(req).path; +} diff --git a/node_modules/connect/lib/middleware/timeout.js b/node_modules/connect/lib/middleware/timeout.js new file mode 100644 index 0000000000..dba4654d30 --- /dev/null +++ b/node_modules/connect/lib/middleware/timeout.js @@ -0,0 +1,55 @@ +/*! + * Connect - timeout + * Ported from https://github.com/LearnBoost/connect-timeout + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var debug = require('debug')('connect:timeout'); + +/** + * Timeout: + * + * Times out the request in `ms`, defaulting to `5000`. The + * method `req.clearTimeout()` is added to revert this behaviour + * programmatically within your application's middleware, routes, etc. + * + * The timeout error is passed to `next()` so that you may customize + * the response behaviour. This error has the `.timeout` property as + * well as `.status == 408`. + * + * @param {Number} ms + * @return {Function} + * @api public + */ + +module.exports = function timeout(ms) { + ms = ms || 5000; + + return function(req, res, next) { + var id = setTimeout(function(){ + req.emit('timeout', ms); + }, ms); + + req.on('timeout', function(){ + if (res.headerSent) return debug('response started, cannot timeout'); + var err = new Error('Response timeout'); + err.timeout = ms; + err.status = 503; + next(err); + }); + + req.clearTimeout = function(){ + clearTimeout(id); + }; + + res.on('header', function(){ + clearTimeout(id); + }); + + next(); + }; +}; diff --git a/node_modules/connect/lib/middleware/urlencoded.js b/node_modules/connect/lib/middleware/urlencoded.js new file mode 100644 index 0000000000..cceafc0c89 --- /dev/null +++ b/node_modules/connect/lib/middleware/urlencoded.js @@ -0,0 +1,78 @@ + +/*! + * Connect - urlencoded + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var utils = require('../utils') + , _limit = require('./limit') + , qs = require('qs'); + +/** + * noop middleware. + */ + +function noop(req, res, next) { + next(); +} + +/** + * Urlencoded: + * + * Parse x-ww-form-urlencoded request bodies, + * providing the parsed object as `req.body`. + * + * Options: + * + * - `limit` byte limit disabled by default + * + * @param {Object} options + * @return {Function} + * @api public + */ + +exports = module.exports = function(options){ + options = options || {}; + + var limit = options.limit + ? _limit(options.limit) + : noop; + + return function urlencoded(req, res, next) { + if (req._body) return next(); + req.body = req.body || {}; + + if (!utils.hasBody(req)) return next(); + + // check Content-Type + if ('application/x-www-form-urlencoded' != utils.mime(req)) return next(); + + // flag as parsed + req._body = true; + + // parse + limit(req, res, function(err){ + if (err) return next(err); + var buf = ''; + req.setEncoding('utf8'); + req.on('data', function(chunk){ buf += chunk }); + req.on('end', function(){ + try { + req.body = buf.length + ? qs.parse(buf, options) + : {}; + next(); + } catch (err){ + err.body = buf; + next(err); + } + }); + }); + } +}; diff --git a/node_modules/connect/lib/middleware/vhost.js b/node_modules/connect/lib/middleware/vhost.js new file mode 100644 index 0000000000..abbb0500a7 --- /dev/null +++ b/node_modules/connect/lib/middleware/vhost.js @@ -0,0 +1,40 @@ + +/*! + * Connect - vhost + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Vhost: + * + * Setup vhost for the given `hostname` and `server`. + * + * connect() + * .use(connect.vhost('foo.com', fooApp)) + * .use(connect.vhost('bar.com', barApp)) + * .use(connect.vhost('*.com', mainApp)) + * + * The `server` may be a Connect server or + * a regular Node `http.Server`. + * + * @param {String} hostname + * @param {Server} server + * @return {Function} + * @api public + */ + +module.exports = function vhost(hostname, server){ + if (!hostname) throw new Error('vhost hostname required'); + if (!server) throw new Error('vhost server required'); + var regexp = new RegExp('^' + hostname.replace(/[^*\w]/g, '\\$&').replace(/[*]/g, '(?:.*?)') + '$', 'i'); + if (server.onvhost) server.onvhost(hostname); + return function vhost(req, res, next){ + if (!req.headers.host) return next(); + var host = req.headers.host.split(':')[0]; + if (!regexp.test(host)) return next(); + if ('function' == typeof server) return server(req, res, next); + server.emit('request', req, res); + }; +}; diff --git a/node_modules/connect/lib/patch.js b/node_modules/connect/lib/patch.js new file mode 100644 index 0000000000..7cf001255d --- /dev/null +++ b/node_modules/connect/lib/patch.js @@ -0,0 +1,79 @@ + +/*! + * Connect + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var http = require('http') + , res = http.ServerResponse.prototype + , setHeader = res.setHeader + , _renderHeaders = res._renderHeaders + , writeHead = res.writeHead; + +// apply only once + +if (!res._hasConnectPatch) { + + /** + * Provide a public "header sent" flag + * until node does. + * + * @return {Boolean} + * @api public + */ + + res.__defineGetter__('headerSent', function(){ + return this._header; + }); + + /** + * Set header `field` to `val`, special-casing + * the `Set-Cookie` field for multiple support. + * + * @param {String} field + * @param {String} val + * @api public + */ + + res.setHeader = function(field, val){ + var key = field.toLowerCase() + , prev; + + // special-case Set-Cookie + if (this._headers && 'set-cookie' == key) { + if (prev = this.getHeader(field)) { + val = Array.isArray(prev) + ? prev.concat(val) + : [prev, val]; + } + // charset + } else if ('content-type' == key && this.charset) { + val += '; charset=' + this.charset; + } + + return setHeader.call(this, field, val); + }; + + /** + * Proxy to emit "header" event. + */ + + res._renderHeaders = function(){ + if (!this._emittedHeader) this.emit('header'); + this._emittedHeader = true; + return _renderHeaders.call(this); + }; + + res.writeHead = function(){ + if (!this._emittedHeader) this.emit('header'); + this._emittedHeader = true; + return writeHead.apply(this, arguments); + }; + + res._hasConnectPatch = true; +} diff --git a/node_modules/connect/lib/proto.js b/node_modules/connect/lib/proto.js new file mode 100644 index 0000000000..b304cf72ff --- /dev/null +++ b/node_modules/connect/lib/proto.js @@ -0,0 +1,230 @@ + +/*! + * Connect - HTTPServer + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var http = require('http') + , utils = require('./utils') + , debug = require('debug')('connect:dispatcher'); + +// prototype + +var app = module.exports = {}; + +// environment + +var env = process.env.NODE_ENV || 'development'; + +/** + * Utilize the given middleware `handle` to the given `route`, + * defaulting to _/_. This "route" is the mount-point for the + * middleware, when given a value other than _/_ the middleware + * is only effective when that segment is present in the request's + * pathname. + * + * For example if we were to mount a function at _/admin_, it would + * be invoked on _/admin_, and _/admin/settings_, however it would + * not be invoked for _/_, or _/posts_. + * + * Examples: + * + * var app = connect(); + * app.use(connect.favicon()); + * app.use(connect.logger()); + * app.use(connect.static(__dirname + '/public')); + * + * If we wanted to prefix static files with _/public_, we could + * "mount" the `static()` middleware: + * + * app.use('/public', connect.static(__dirname + '/public')); + * + * This api is chainable, so the following is valid: + * + * connect() + * .use(connect.favicon()) + * .use(connect.logger()) + * .use(connect.static(__dirname + '/public')) + * .listen(3000); + * + * @param {String|Function|Server} route, callback or server + * @param {Function|Server} callback or server + * @return {Server} for chaining + * @api public + */ + +app.use = function(route, fn){ + // default route to '/' + if ('string' != typeof route) { + fn = route; + route = '/'; + } + + // wrap sub-apps + if ('function' == typeof fn.handle) { + var server = fn; + fn.route = route; + fn = function(req, res, next){ + server.handle(req, res, next); + }; + } + + // wrap vanilla http.Servers + if (fn instanceof http.Server) { + fn = fn.listeners('request')[0]; + } + + // strip trailing slash + if ('/' == route[route.length - 1]) { + route = route.slice(0, -1); + } + + // add the middleware + debug('use %s %s', route || '/', fn.name || 'anonymous'); + this.stack.push({ route: route, handle: fn }); + + return this; +}; + +/** + * Handle server requests, punting them down + * the middleware stack. + * + * @api private + */ + +app.handle = function(req, res, out) { + var stack = this.stack + , fqdn = ~req.url.indexOf('://') + , removed = '' + , slashAdded = false + , index = 0; + + function next(err) { + var layer, path, status, c; + + if (slashAdded) { + req.url = req.url.substr(1); + slashAdded = false; + } + + req.url = removed + req.url; + req.originalUrl = req.originalUrl || req.url; + removed = ''; + + // next callback + layer = stack[index++]; + + // all done + if (!layer || res.headerSent) { + // delegate to parent + if (out) return out(err); + + // unhandled error + if (err) { + // default to 500 + if (res.statusCode < 400) res.statusCode = 500; + debug('default %s', res.statusCode); + + // respect err.status + if (err.status) res.statusCode = err.status; + + // production gets a basic error message + var msg = 'production' == env + ? http.STATUS_CODES[res.statusCode] + : err.stack || err.toString(); + + // log to stderr in a non-test env + if ('test' != env) console.error(err.stack || err.toString()); + if (res.headerSent) return req.socket.destroy(); + res.setHeader('Content-Type', 'text/plain'); + res.setHeader('Content-Length', Buffer.byteLength(msg)); + if ('HEAD' == req.method) return res.end(); + res.end(msg); + } else { + debug('default 404'); + res.statusCode = 404; + res.setHeader('Content-Type', 'text/plain'); + if ('HEAD' == req.method) return res.end(); + res.end('Cannot ' + req.method + ' ' + utils.escape(req.originalUrl)); + } + return; + } + + try { + path = utils.parseUrl(req).pathname; + if (undefined == path) path = '/'; + + // skip this layer if the route doesn't match. + if (0 != path.toLowerCase().indexOf(layer.route.toLowerCase())) return next(err); + + c = path[layer.route.length]; + if (c && '/' != c && '.' != c) return next(err); + + // Call the layer handler + // Trim off the part of the url that matches the route + removed = layer.route; + req.url = req.url.substr(removed.length); + + // Ensure leading slash + if (!fqdn && '/' != req.url[0]) { + req.url = '/' + req.url; + slashAdded = true; + } + + debug('%s', layer.handle.name || 'anonymous'); + var arity = layer.handle.length; + if (err) { + if (arity === 4) { + layer.handle(err, req, res, next); + } else { + next(err); + } + } else if (arity < 4) { + layer.handle(req, res, next); + } else { + next(); + } + } catch (e) { + next(e); + } + } + next(); +}; + +/** + * Listen for connections. + * + * This method takes the same arguments + * as node's `http.Server#listen()`. + * + * HTTP and HTTPS: + * + * If you run your application both as HTTP + * and HTTPS you may wrap them individually, + * since your Connect "server" is really just + * a JavaScript `Function`. + * + * var connect = require('connect') + * , http = require('http') + * , https = require('https'); + * + * var app = connect(); + * + * http.createServer(app).listen(80); + * https.createServer(options, app).listen(443); + * + * @return {http.Server} + * @api public + */ + +app.listen = function(){ + var server = http.createServer(this); + return server.listen.apply(server, arguments); +}; diff --git a/node_modules/connect/lib/public/directory.html b/node_modules/connect/lib/public/directory.html new file mode 100644 index 0000000000..2d63704214 --- /dev/null +++ b/node_modules/connect/lib/public/directory.html @@ -0,0 +1,81 @@ + + + + + listing directory {directory} + + + + + +
    +

    {linked-path}

    + {files} +
    + + \ No newline at end of file diff --git a/node_modules/connect/lib/public/error.html b/node_modules/connect/lib/public/error.html new file mode 100644 index 0000000000..a6d3fafd87 --- /dev/null +++ b/node_modules/connect/lib/public/error.html @@ -0,0 +1,14 @@ + + + + {error} + + + +
    +

    {title}

    +

    {statusCode} {error}

    + +
    + + diff --git a/node_modules/connect/lib/public/favicon.ico b/node_modules/connect/lib/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..895fc96a76b68b4924f1c51d022e1b82fa0f461f GIT binary patch literal 1406 zcmZQzU<5(|0R}M0U}azs1F|%L7$l?s#Ec9aKoZP=&}i&OouUjIY8@C}uZw4x5z5N2 zvEG^C^vXtt_xtJ?p3O32c(KTx;lsgZhW%5M85Sf}k-mf`L80)|7ga~M{a znlL=>m1Q`#uoPkbC~GtXMnhnDh5$DU1D6mx+;2QAKt3ZQFH}H~1y~00GcqzVg9QXw z_<%(a7y$V|wJgF=E>MgE#Aid|14}IyCM5MhEnxFF;pTzOK(>#80puP=gnOXwAd33` mpMe2}f66m3eB@_fcnmb^7!b1nO#opK8zcsj1F30)+jEP);68^d)m`eN0o>(5%D`Q(1;j>g@G;xlf`0VBQ`PFY?6)!N&f?*K}$p; zB!U=NBn{eB8${1}&-2_L*HuZp@ZP1@clS@cHp)4iM1ewzw59vko7eMM{e9z|%NNdX z0V;`?KKSzTCvTm5bc{L^CIKLUxc2X{i{ISz$8Sgf{q)1nXTP{`{s?9mQ$4&hPiKC- zY8q7(Y1Xu5iCf33=O4Vy(+|zQ?rW#gkKB0f%}?+6{G*qT22|DQB-73`YzA{N4W^=s zq0kQYcbtFfz zLz)H<&|z(Y4kBG67=JY6c|L1R-#TR>fC$3^Y%QEnYO1xHsf)+GU`3F<{J0kR(;pbF3)zyg$H+idfnl-wl5Wkh!vUH z4Z32YP=l_}1rZd1W_D&^$A($A+&a0e&P?xx0!ctY2}*<#p+qPVN*B(YzvAWXa*%bzq z7Fz41LKILT(GWohi9|LgIzSZBhb*Zf6R6O}WYQ4GOi&71s9lmll0x6;8&ILOl$j(c z0Z1T(6Tg09{?wd{moFHNN6PS?$|e>1MxSJ(0Z7o2)J-Zv|>acY@f`(Y@g7GwsEj5NLQo+q|HsxQ5}XSX_d@*^A9ZT9=A{W~j+$GyI1 zc4oqTHx@1FlRjw4XWyPN5i2~l_F3@aBk!0yu^aoRDvXy}8@HCjUVQUsuSH4$T5|r< zzZOn^?Wfa6y|Q($Hx4{ws+)wX6-HP4zo!S?4KJ@7PG@G3G{CjXs(p*kIrj6rHs7_y z+=<-=Q62s9FuWa^X~WKgJIAAZJR&XBB002ovPDHLkV1jCMPILeO literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_attach.png b/node_modules/connect/lib/public/icons/page_attach.png new file mode 100644 index 0000000000000000000000000000000000000000..89ee2da0753040d1ba0a3487473a715a8fe89322 GIT binary patch literal 794 zcmV+#1LgdQP)i_t#ewV_0K6;=bl;e_Jt7$~$sQ)q$+ia<4Ec+jeaGt9oWH@O|2`W6&O0t!k{B9sUvLWxkCaPsd9W(`fa z;j-|^ZI^2XnzhgZWYRW-kP&J>DWPo`%;JaBX}or79k=+Jo@h%4Eo72tqev+cB?PjP zO<|ByL#>Tehyq$jR74O$B9WDW1`tK`LzYyL3A9iAcRxLkJ`I)n}v%Od-3H>j$OTBtk>(k-9o?8PqI=0 zB&f-+KOXVnjyKJlf4iHOtnuiE_4+ZVJ$dHjU<^o^YCjQ-wt^!;rPpBv(@pFO{9rdw98 z_s@3+yta93oyfL>7AD5}r=|`zS3Gm$_|(iSl8XBd9k%=91J0j2=ivT5cJ18ZmDjh{ z$-RMd{jQ#X79#Sc literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_code.png b/node_modules/connect/lib/public/icons/page_code.png new file mode 100644 index 0000000000000000000000000000000000000000..f7ea90419d950f9e69d977a1f5847456d96a5f0b GIT binary patch literal 818 zcmV-21I_%2P)@LCln44|RX7Ti z0HI3&7jPq){odH{?_{%nYVq_;n_c4WbUpvU(&Cvnj!vq|kVC-vpF6vp^;;e0mm6HW z+WPzA`AZ|;pPp$&dNjzrc??4rt`k%Q1l*u-BPD0MQ}Fbm8jnsyezNt7+u{23>t7Em zJtETY?ja9KrVs^!LJ$xEMF3-bAZO;-IQJavE60KA7fO$VY_%N)R6s>g5mW>fL4&aR z*EVgKKTBXm!=L?S0?xM zYqL@C$|EDF2q*3zWW7;PDZ}SK*IE8;i!3U62=qn80C&*I1Le7WwNP5EcX;_oh2dJn zf#HgBe4@r$GcjHjmj2vAfT%(YN?}kK=(*+1*DkNNc1H5R++vfBMhACi<5uFUU+N4+ z<&U*CPmWi}REa7C6-t>2im1CWv5Jkefxa6>)dEj-CAW wWa{_}BJ!}~75?MkfaCnj>Dn=~vkLS70Pk`;z)@TQj{pDw07*qoM6N<$f@imYHUIzs literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_copy.png b/node_modules/connect/lib/public/icons/page_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..195dc6d6c365d298e466026b37c1959d96119ea7 GIT binary patch literal 663 zcmV;I0%-k-P)^@R5;6Z z(>-WZK@^7J_sq=QY_e{46@P+~LNG}sRzZsxQHvCsN*h5ir6^j7pq-$xu$N#V1gx}9 zClV7;5)7zih-s3DB)G=7|99>ji@So7-P24n=VQ(@GctDX!^_@$bj%oviY6e4Dh;od zooe%Wvs8LEKQ&&bL&@bwi=STIAI@!-gB2jC5+?y?VR~VkrNxam-`6*8&po|RZ5LpS zNKdJ%c4bTX`XjKsnecf%W>1%6WT?pKNdLLq{=(f(Col?P1+oq@R>)W(n=x!|*BIIh z6DJGw_w`)u6yN|vAhMteYK5#b%r5^v+VCFl1IGssaclZZMS{vs-LJ2$)n7DAr6==K z<29#%AXsBsDoO}SBaXR#_Ap!JKx)(1)3O2pj0_dYWz5By*X74fRT01$Fk%P_RzOMDtV?GU{nsYq#K8iy zb6qzLYDj`_f5$BwC*WE(t0m#xYJ*=jC2|HQYHh=pf#QG7oowi`h!L!{DB$8|qY{~X zu8@sU1tWq;n$XThR0%;45mdqXM892|{CJ@0DS*}>?ami06Q_^tvM~Y3K(_-`#m!8f z8f!QIrH4y#61;0Ym0cCoLl8{IPombPHtnn7%SbTdI&G-d>ZQo!_wBMF9nzX!g8HVY xYTJPGciz9XMh3w2fmZ(7v{)r*QZD48?mrio{~IaoqP z|1Ep}yDQG09bP~E^Dk?@JiKQJ z6-pO(3~IOP)IYisL6D6;oAEd;E%zR}{U$rMRNuD6nQV7nesKS>)yLo7JuDCrD>Abi zbj3uW23?^GA}9jQ{M^8v?ejL?HaT7AX5WPZNkBmfN`w-jL?{tT7ykZt$%Yln?p_m~ z-?>&d(LD(jAd}h=LPltPQbO$*Wbyl@G-_k5jXbb#qffHY03>M1jfEqoPJQ6Mr=Byp=^jfzePZV1 zLjCmNi31hdIJHa%e;5g=1(`u3BRzfeExY%=VCu{loOr{`%2hUR*x>tL^W_TTaj);0 zpPR6CUD1+0>4TQ6zVfH3TQ;%l6#(_%yspK@3gcmG#Q4!WCPyLU93nMKk7E2pcA=l45({2jNho>sdF*A~bA zxX?-cp~y_z_kFf+yqu3m#QiB}03?Z&9vvR5TNgj<)($Vm)xq5G>|o2sFMag&6aNF+ WAT1?sQBYt20000iHtsh1EzPArg^Q zIZrOk#rNsfjaSbMAL;<4h;Z=jvu8dzyz8N&Nb7=z03ZUw?9z%8KQEa6yM5=kUnka& z3?FJk2}L7q>na=T#;<7U*P91xfF`;`6%pVgWgRy0?1ZryL@%z52=-!fGXWGEn4M351L4<+7eDgwo|moqXT+s1&Kmn>-uQQ8mL7XY)w5Zk*(g+<3Y3tmkR!bL zOUKaUtj_pX26sH+=Iorwu}MGd`_%O-_sS}8VpG#fJA)Fcs#ezwtZf?q?Ac70mDv`rVs{$od?VPKeqf<-kUjNtS6ecB*mq<&M97K^6IVsDO zt2$Ru!b+>2S<}_H>$RcInusU_8PMNdf(W{sNlJ3FkrwMJPeBPO#d}Y^a{9TH(#{Y) l0D?dWAV4eUJX#h`!2gmISk&ZKd4B)^002ovPDHLkV1g&sd|Lnj literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_error.png b/node_modules/connect/lib/public/icons/page_error.png new file mode 100644 index 0000000000000000000000000000000000000000..f07f449a44ff2761bfc7b752db3d08d0e1238b02 GIT binary patch literal 793 zcmV+!1LpjRP)lHwhvrAu0-@MQwt}+5~MQTtu}C0%;W( z1<{R?aHBz*g;pk%AyQVBR_Zu5m~;ES_vxI-O!vIF|H*|T{l`n#garr?$RMk>)?Y48 z(ZF2yTneKb};DNWF+jK)IF`6_IfJ{i|F3o%Q+l&4_HGBD|ACE8na_6>L z=s{^>-C(a7J$6=8A_%h5W!1K6dcL!D?XX+Ndk)oei?UundDpX_E&1Y&`)3P8#Ny0s z2Ag7_&ZPhyGj%)g&S6V2LNun1;iBQm#Fwlfv zgyESZR$X}2P;=RW!2zid1r$hBL{K7>2qi*f7>pT1=RdT3@-anEoH{ z={KFOO;Dh#bV*jaN>}M>RZqQd`S=6O9C3KpI~I>l%QFYfo;jqQYe5fcn`)+)zMm6P z4X&L(>gnN0!%J4^rhX->?$S5bY<=GEU%jc!KLL8sww-Eg;h z`H-yBHa)yfojYT}&G*GFc$<(Yja_q=lZvj66DC^O5%$B)|Z(CeD=n`|eM04SP; z>-=-l+xdJjA~vR6^xB#o{ehf~tSM`iwaQv$O<8NIHA}W_WOw*~ XD^gE}t;YAo00000NkvXXu0mjfl6ZQ> literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_excel.png b/node_modules/connect/lib/public/icons/page_excel.png new file mode 100644 index 0000000000000000000000000000000000000000..eb6158eb5ca9c4b64c81e70e0fd894dbc8e2bed9 GIT binary patch literal 817 zcmV-11J3-3P)hdKqhFO_H1|Vn1E?(=|cjAh_&P}y{{^`u< zcYD@PK?$)4i~7o9*6F_$F$4lR(d4AAvrez^(88Hk+)+B7E)M3jc=Ewl4$S^`_qwSF zA%qz@=c2EOsz@0qB1IqsqJU)HaG&+}%`-OM8YqW{K85hqj@4&V9vz!Cm_n0-W#f;# zeEpJde%vdgRn5?(+PY=W*z~|lT2-mtown({ll8&3S5+lWz5K}LTRW{k{eJn3Qz!SU zQ`@qI_n2;K?RG~pYJ9=dj-RWgG;P&wEuoAxL~Q)<>x0b=dED^Outj&xQ^rA;u3pw| zca_ClTh_d9cxXg_U!lLRl0`xU@$=UXO|_dRdtXfKwPawmnf(LC7u}-U>8k6}3u|{8 zs9LHr>MIJZGD^r9h|q2yF24Wuh+PM^yMN9GP1$khlDkdyCY}D{kg}jEf-(kW5jBY> z0rB29ZhG-r=i_R{;+1k0?A-sBM;AP6(k1i9ZuixR4?MqXOvphQgCYj~RnXKKL~J9Q zIDNz~XMfO{ZhUg&BTtMyXJqDVqc<x643?SvKHx004lTvR=0a5$dUw z>Xb470000$S;ka1sfH4I-R8njUol7M4dtApOahDylptpYswf1hD#CwFoz6oEA>(SIECU&IQ%a}GXdnC!9$70`0uH1B00000NkvXXu0mjf!8w72 literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_find.png b/node_modules/connect/lib/public/icons/page_find.png new file mode 100644 index 0000000000000000000000000000000000000000..2f193889f7ea091c292acdd684c595dcb206b5c4 GIT binary patch literal 879 zcmV-#1CacQP)@+1&aazfGU7ezSm^v zpACwO+tu0su66!(dT=`e05DeeCnCFJW(8|RKtKa{4LGONnx2V85A4m%PEQ?MEtR-esdM$pB-`H542D0)N2zSC6Imf)4L8?>%ZrW+H>xCKi$unm zvGZq-*Q%Aahx;C*=l+K%-?>XB)6TB$-L$r*`RUvlA`xP1NG2?)ge8@TQ4EN|Jks0u zcDg;oFC#-#R`YbWB`D?Q`1#y7l$LXhjSLf8AvQuB84}i#j0^!#g{VE#(K7h@5pFHy zSenl=@XBEdxp`h2Ji>CR%=qXJ7!e|?paKet-~;#ok#jETyeB(5&Bkhp;!+;51~G=) zH?L7xmDUu_h+a$+xuWom;AWW!mS$%%+436Rjc@}y?l1134kgD0AOf$OmjOR zstUlshZk$ZC!bAyIg{Y29z#&@3SJ;6D4+_eFume9^#TmMccC5u0J!ZCTnO6m$lnD| z5JeFHf`Xs~1vP>RLKI1GKDY<~pjr2&bi(fX;6Nj-ss@Ds0CcoO0H{JsEQkm{q03skAA);_bv3q{k31qwVo&s-q`Z?_e+j^w(WL? zl+uETs5+~xBU2};OqEE9ETLGwsMGe1%iTRNue)9}|0~E4B*@5#oRXZ9oRXZ9TqRep zPrGZuoOON4n@=uPbyP1y4G=+HktC6l(gZoFD>@_lXDrN?wo+zozGt3P=Qh+3L7+}q z2!WK7geXLnO3Vw;o12Skp%_E#+N#9;DDWP?Q-VS3B$v~Ha)dDzWn0zG~O(^_1!n0HYp-( z+;wPIdoFgQlpYV!10V>5@a)1LyGBMvoa}miyp(bxbMTM-FYNyx;V@TfYddyT00000 LNkvXXu0mjf90!wr literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_go.png b/node_modules/connect/lib/public/icons/page_go.png new file mode 100644 index 0000000000000000000000000000000000000000..80fe1ed0cc75fbb67e9398ae686641f8fb287238 GIT binary patch literal 779 zcmV+m1N8ifP)JNR2Ufr z!Apo%Wf%b9=l#x^8AolrB&K9H?Pg_|78WA8(M3toqE%3B#7*srcF`i*xhupr27%Nr ziguah1+mN)U5GNmyEfQ3-e%_i&-vb-Co#(>FJ+EhQEwjRYVQ(&UYy{U@%vbY||>@4x=B^vIqabI?L* z;-S&DS^V3-ni4^fl|HMkOEbgX)(390>A}|VIypb5Xee4g;7ck zwKh^A3Mi1Mh@eC$5lV!}Fw%sP623U`g3pd_Hr5sunLvTskx4}&Gm%Q6L}(l4x}jCe z*81q1_4-O*ffi~_nMslo?EQ8t*&Ec(pzEw$vc}pKn_Qp0>D7Jr>ATNC3w@9f|Y+U&+)#!t7l&wKp+nP{PQsb+fb=Yf!Fu&5j8vpRj{FT>jD z>d>$sx;A&+`n$HcF}&sYKSyR;=(=9tvvOj@hUG;~4qTYk^_@E=?$*^_pVh_bGnOt~ z;pEw)j{SK$XVc;qy181rT655gW9NG{(yeablViIL>cDI_ux8m>Pp{tY$J0lgo4#ax za?j0EA3s0S!f>{~ykN9h_RhM&g3K(E`q(dE(Rd49+%xMeR9{qlWnmd{s#(SQ>PmFtSQqUjAtB;_Vvt6}AS_5YgM`Uqu`yva+H8^=4U$e4gHb}u zAQ2N{V3A%pO|?Pv?tb6z=jC}SiRa$G^v3q?*6XcYz$p|cq{uLj@#~Fi`J(>5{@&&N zy%T^+;>8cXx%|o77anP?&W1?1A(>-T49z9pyeCl@7YI+Si zKti7=B~``}TImz(G{0PnlQA3P#MAd}sorMjkP!50B7$nAkU^%#nl{Q9lW0@}9fE-> zN(q7tRuiC_T1r|BBtVBTlQ2+70$Rf;eF`Z;lx46Cpu-rEgb)EBKq(b^W8l<^We(`D z43?0=01z<3G6+UUv6`CsWCk6^93!#+<;ws7007{zS3k2k9-zZKFO~(k`>s0y006+1 zgF_jyIhsL-`FMf~JL~C=cV75(CrJ|q;MVO961G=O zm9d)YpJg5g(4i_HKL75eSE}mq$Y}r}hyVdcV~p>6a}oXr80q`oj%+s700000NkvXX Hu0mjfPs|!l literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_key.png b/node_modules/connect/lib/public/icons/page_key.png new file mode 100644 index 0000000000000000000000000000000000000000..d6626cb09eb11a298b90a8a27b0d8eab41f49a82 GIT binary patch literal 801 zcmV++1K#|JP)$lC4gU2-`f*>nhR-;k6IP7e>YO!0^w)WK%3$w02v-#>5Ep64PCP| zJihT#O|N+nT7XR2h7dAB?UEAOhJF^mol1i`QtQB`HSY}RE7=r! z)zaVIHr5?>v2Gz&fdYw&2ug$!p+txby(aWZ7(4QT)l2`jX7eMQ{>)lG6ev(fWKxmH zOr%mM5$6B%u~qGtCf40#`mbGj3s!n+^%wnJ&#rl>g<4Z)lB5J6f!?|AP275)Zswr* z%T}4~{;_(?waU!#?JabbF3Cy-kf0{R{z}6$e=5yMQKt3BPcl2>zoTPMqMwF;3!_n|>sT?~bK_-2O_m+o>GJ6h zt=+g$4n7y%1qVJI7*5Yw(hqM=JusY{d}*?U(Oj*gT655eZ>Ksn(qrd7v3}DX1}C>` z+X+8@+4-pVq_fxG zlU}~Ye!0+%>J+pPk+0wV{GM$QaYM?5ux)w2z59=S&H2+K?;gH$bZGzL&g5>G ft+noNiyiPkP9r@8gT|RZ00000NkvXXu0mjfuqTIu literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_lightning.png b/node_modules/connect/lib/public/icons/page_lightning.png new file mode 100644 index 0000000000000000000000000000000000000000..7e568703d6432c530224e443771a04fc1e2e59c6 GIT binary patch literal 839 zcmV-N1GxN&P)73{`^;G#xwAtHz%LU)4b zqTRU=ve1RNa3QT=ZM7C`iJ~QFQ*9t7<~pX$d^7W%^M8FFCkcQj-~0ZRTBCB(J0^iD z-~e!d9LR`f3#|=(>$bPvx_D-~2jC%pJ=n_e_OK zeJ_2b-KdDDh@@UlzBSMC;EPygH_MwjWBnPGQegihBV73D?-x9PlHL9A=(Vg=8^d<4 z<9r=UkxuIm)*CO=9e###7PztDxUv}e?$0)rQicmYhV`pQ%S!g@;K(?TVfhM#E?bM| z=B0gfb6h@a8bf5FVT-SV~6}?X}9lK@@Yynoty&1zdZP@?RfODsl=2XzzU% zS8gIN43How+9%bK2S@Xbc`O>`z5`%^;pXGy8^4f>9^3!Sp@|O&)m;dOa3q6d;4P-l zca|=H_{G&m?D_+&-}r{u-J$5T=(X4R&)q|O^gN8cgv;s#@5sEPT5_Z)oFo9Ac>l+I zc4ng5zHpps|9)<_Rw>5bKzE(M1j)dFWI_%OH$BJSz0?T+02W0)_a>#vFqb!*d|5wB zzBUN|M&ty51O@=i?kiDrjQ{{}e|^rU?OS|RdxxP1p5mAw36cX72#`R6UsoeCQFI~! z0ATITp!vfeYyQ?Dr=^5BAshfEa0nB~JG?nUa2Aur006MC*<9`)86SPS(W^`H2n+xi ztOWohsFfVfVWrI7PSKW}BmkyPoj(-|J?ES|BGd-}fIxr{00@ANnO*ZR`#)pee4I5T Rmm>fG002ovPDHLkV1i|Hc$okI literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_link.png b/node_modules/connect/lib/public/icons/page_link.png new file mode 100644 index 0000000000000000000000000000000000000000..312eab0914ab59271384686255d1be913a6b3add GIT binary patch literal 830 zcmV-E1Ht@>P)VWgGzD=Y79#JI$lhEn`|2MpRa?Bt#-nSD~P0P(mbVe{KrOBoKnSsk>m|ML{6l zBosu@om4j#WzNLRAk+{k1JRvL(MfE&vvbb->v>W{*z*1_uMP}0cIRX*?mz+wk%*#O z%0D-+$B*g1nRkvI+_3E8Pr1NC6@5M&4vWaLCnNlr;lNlr4i91z&)eBGqL{L{GNu;Fof}GS9{gM5BJuH;2QWk8yuOZdB3pGR#s8bd~ zAmt<>3Q=YH$t5YJ5;7@+8Uh6=ktBgY6#6Pa%2F?h910?U8cLT43KAj$Z1*==ra&gILO{WkHfs(--F=bly9l~${z@AT>V$oat!YAD@M zBE0v_F{`g#^wOSP-u~!wvlmXdd*uqFqoZ0^{&nEMDU+=!>({S0wrQhFmoB}Yq1)~0 z^{A)L8Xjzdr(W4_exYO6u3a{4*kIeXZMJUR>Q=ksjW_p!rAwDKYUs90>6Q|C>56o@ zbrSq^Xk7Gq#>dAsn@un`Hz$?w$;Ss`%jV7L%9ShgHFV~C)6M>B`Tp%|nqPc&G*A3| z)Qe+}vT4-x^t1~XE@(6wR;^lPWMo9n*~E!cy~)YRsT2{`?fqeIw-e7N@mOA%UcCIq z_kH(_EK7|>pM*1Wt2^DaDAp|cvp*@(ZZDKpYkKC^?97(`0sb&XTXy7N#sB~S07*qo IM6N<$f;YmCWB>pF literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_paintbrush.png b/node_modules/connect/lib/public/icons/page_paintbrush.png new file mode 100644 index 0000000000000000000000000000000000000000..246a2f0b426faa0c7f5ba009e32b1deaf88d1288 GIT binary patch literal 813 zcmV+|1JeA7P)otxGRZMDZ!_a~nK|b_-`n%VosaL{KDuPV10`(1LIen8kX2Xff$3BE zah#djvFGJ&eE^89Pk*-O^+&d>FC~^GjRYVQ(uuPJyS|-v?9lxA-+tM5>1Qu*n+Ir1 z6KhA>X4$XDH6?-|E5oe1E?pQ5-M;2xw_ex!x}I2+b=}mPFW$U%^;o(Zg*LP!K^1kP%8ynsD^= z1y^6xD1#GLjO{VLdh@0GKY7;d$+NGukV)GRLPn^=q=dF%B#XaJrNP`0E6=}e&Gj3d zKJbQre*WXt!60_DnIzgMQc6S#fvjXxsE1v7;T;njHkdy2miIqAS(nX~o%cO+q+b#h z5tIleLWvL=dQE8OC#{%y*Tnku&K`Tuub&_ELI0t_ea{@3f>Jv&sYqld(%}3_GY3Dm z;O{3*Y?v^A`a|D;^qrM=ykI)U6QHd%WhO~VF!SGjGn0GOZrc3mGZudNl9{Q#X5&-F zuGwVReFLBjE5jr!!^-5*L%!I%PkYH#Hs5rMrEBl^)9)9XTD;xjHFxVZMc3~Dw6#k$ z(-S}RE$bgMHv6Z`mS5|u$$78sp4G-8b@lVkl`HtEv+MGn!F&bKcHPi$$oP_;=BrPf z$(~b3&p3CsuQxhoV$%jIR;`lB-s7FDX)xCTXuJ7ZyIQk96uIR=HBt%-P?N*bp`)EF zq14c}QM+O70NTOa@V~_)&GMZ$^cQDlkyOCa(H3Mf+6xhCuZh`VSN{cQBl5Ys9{cp( rh`2H3A^=GuC6HjQ*7|*0>;m{7QlnX3z3MSD00000NkvXXu0mjfR5FYo literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_paste.png b/node_modules/connect/lib/public/icons/page_paste.png new file mode 100644 index 0000000000000000000000000000000000000000..968f073fdddc1cc0f0800b1ac4001cd9a55f053d GIT binary patch literal 703 zcmV;w0zmzVP)AVs!l4K}n~L(tL`6d4Up4iSWnZ3Qg~4n+_J zDGk-qQdogO5JUtO-d5pRp7Nd7_r1^a|M&Zq%mn9Oe((|e0sw%Ur!K7T1pojj=U#f? zQM`qbQrM^DPkwa?DK_be^~z<~RgSMIa<`xP_4P7gg2jCwJ{9^k!fsU=#Ti|%I3p;>90Qd+7|~0h&mIklA#nb>ATL2+v$&u)OBgB z;nsHb)I&QRKeX40H~~cIZxCd}5C} z=79lXoXK%6YlyLtsV$~bSm?Upq|DJh#{|*a7XMm`4QJWZ>s6nL2R1|&J z0VPEwJ9?!n`o5PKAjc->P1Gi8BY*%!5&FVp=#)$mMJYul1Jton}gujiUf??eOy!x&!tsjxy;=Q3_DdcXx=a^OBhW0N~`A@4xB0a*%F? l+@c^sQA%W+?pa#c`9H5UNfS6T{e=Jk002ovPDHLkV1grvM=byV literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_red.png b/node_modules/connect/lib/public/icons/page_red.png new file mode 100644 index 0000000000000000000000000000000000000000..0b18247da5850f3c2486373a3e179acd2772e8aa GIT binary patch literal 641 zcmV-{0)G98P);68^@7JE5sw#jpE*579S@TLkU(6yap1yN*Zuy>-hV%Q_v4Ar&!63c8OBr(ZRhFu z_kWs36-AmgZCT>x!RqM;Zu9tqvoHI~k@UmYo_g(*J3c%2{N8}7I+|qKPQzv}7t>%W zsu&9G)UmCzkDYSw{fBnuW4j;1fKV_nicw`$8C6D=F_qu`zUiK$2Oc?5UVY+D(`I@R zW`KlwqLftWHH3Z2_XVNfKn>VgT~k=@- z+N>c>0|@A_HbI9Jn`v0~7cfIF(TS69zaomDS1QtgvaBBfGEPLHccO2~3jc>n^6}^HAEh-2#VxC7YYcDXv!L9X= z-R*SOUvIs;n`8(LxP4~^2|JsiN^hq6cU}5dn4v1~Kl{eT7pm&f$PoY`hyVe%y8G*S bxB&kH@RuR86sIAS00000NkvXXu0mjfZBi!% literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_refresh.png b/node_modules/connect/lib/public/icons/page_refresh.png new file mode 100644 index 0000000000000000000000000000000000000000..cf347c7d4685128a4a447abb9fb8e939417644f4 GIT binary patch literal 858 zcmV-g1Eu_lP)`6pHR2Ufr z!EI=jWf;Km|8+n2IrHqe<9xxFVk)&(Nh?w$Xk`TAyvb=#e=0aySC z00NkRDM597_LiNIJ2M^qhuTvB004REvvU8@of{r?P8tmo3;+Pk0F0@*jAMhdOkS&1 zhJPPfQa;pP0|4+Yk%#j>X}o-s#EF1_DMV93FsfPP`G*>Ks>L&)Q}w2g%slu0kBfW1 z+*$*0BC^oTl6>OGIq(9BgG4|C90Dk-N_mPazGrQ7uHZ|>BLD!-KmZ)z1e^#?1Sf(M z!6m}K(^b|i%$TcA5bC}r$tAA?0C)g1@CgWliJ;NAk&ZF+-w#}$`-3nZ32C6IVKrHp zr+(!L2hRfF&AsTw>_@ z1y23;E%Oz}?q^Q2d($ayO;-sON2t7$w(Z|o0Pw1YnSp^}0PI+I5HnDNsFCA?oorkG z5sUIGIq=FSyxcj+xlhkm0en=52Bx3@02o12gdAU$_i?v6iyFMuc7P9#zQ-Hf; zVuV$t9P5`m)F2w1?t6{<8%wk{w-PP#Sj#%1MbsjrSI6n;D_@8q9`~W98dNQf$j=iI z6~hpgww&be%X_HI50Hhx@W==u4TLPB;ei-J-1}G8wH}|{i#Lk-WZAyfv}k4y0|fvU zZTy^$u6L>2nWo(NDSV2@MRD}JQ4(c%G%=dG@_vxH?>gcH#*Ue2HC}9sapf8X?R$Z;XEnm&g zW99mh)5jNw008mK8)r^`_{yH0rNn%u1|SpC(tjf#om=+r#lh+?Kb>DVb9`|C0Bvbv zN3U(>f4-tAC1hosRoA7p(b(hL*V}(j>ug<`&U)|l$6o$)!>PBQ9RQSwn9asj2p*|xhU*R^vq?*Twb0t!lm5}`yW5lRy-U0ZYK?8to!;o!r!XeOE$ z0HB3T+6EEoI4PlR=wonwqJ+TvCoWh&$?CAPVYcU= zD{DS0?AkOtb@-hh^ZLq~FMjxYf19X?pa_YqtgZGvv2TaxcF#KT?O%=_*a-kW_;N|D zakkWsOe!)HsT5WRBiC+p;N-c>0Qwy(1D2MDBC595oXSiR07)sKNk-%9*rDBOO^HUD zZW#;)R&EZpqha<(HK$(tZYU#V29<@0qCXgU{gXeGpc_|pTqQD-WO|}%yKZbeX7k*H z2W~CK$v8NBAq~czrc5A(v51g0Wma7`G8}f=ZcuAiYYxZan@gP(;Ku66M6?bquGiHe z3Q0ya)%Lvk@kLixZfZyU@#UFbv+>pYhcj8TRKSr_sWG8i^X~UA**LvbD3(_Lba3xm ziYcpup*A9qJ$?AA=Og05lndxfwr`!C+O~h|B~4 z01q8H`StcY);%&mId7_+)76ovRpeNWRp&4M?#jx@|E-)x%P*A6t^fc407*qoM6N<$ Ef@ddc(f|Me literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white.png b/node_modules/connect/lib/public/icons/page_white.png new file mode 100644 index 0000000000000000000000000000000000000000..8b8b1ca0000bc8fa8d0379926736029f8fabe364 GIT binary patch literal 294 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-&H;pyTSqH(@-Vl>|&1p(LP>kg~E zYiz5X^`c$+%8#zC{u)yfe-5 zmgid={Z3k(ERKCKrE7DF;=x4^O+ pzO8rLO8p|Ip=x)jHOtWj`bJBmKdh_V<`47(gQu&X%Q~loCIFbEay|e6 literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_acrobat.png b/node_modules/connect/lib/public/icons/page_white_acrobat.png new file mode 100644 index 0000000000000000000000000000000000000000..8f8095e46fa4965700afe1f9d065d8a37b101676 GIT binary patch literal 591 zcmV-V0~O9lw>B8WRlD)Gm}Jrz31u-X&&gn2lvjs=i{7nIaL6v2==uw+8Lcs(8j27 z;|c`rmSv@Lx!heopGP^^Ieb3f=R!%Lpp$}iMS-&P3EJ)s48wrJ_Ni0~k|c47D2nj= z{jS6bt|kFpFf|p5cM`_&0Zh|`rfEp0(}=}lT#(6RpzAsUfxv^LSYX>WlAaN$>)*J5 z0#sE+JRUD8iT9*fz{)_^7@6P&!sEjTcD+I9Z4YjT1`wH@fV{cEvneYGFU%maIEU2s55&K(LixD|{p-uiS@?KNj zk-Go8G$hH6g002ovPDHLkV1hVj1#|!a literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_actionscript.png b/node_modules/connect/lib/public/icons/page_white_actionscript.png new file mode 100644 index 0000000000000000000000000000000000000000..159b24075191fc259cfd80c797a1b0d74c168422 GIT binary patch literal 664 zcmV;J0%!e+P)7Z7t2}reCh0o`+ zAlt$F2tW%oO@m<=(B8a-_VgLl#~yUMUDWG!0qFPppd^03e+x1WpkO1NhIaKD2A)-@ z=Py8(Wi%R%JtYZG#sTKH@6Z+&!S3Edf8jFJJNKuva#KJQD3X^7;H^fd2di znEN&c58aUG>`>P{Vqq$kLb+TP{?I!d4(|o59X_%|nVEZq2Rk60n7072SWJ{64CV?3 zgS!EB=eYxwQ>P2&$}(iT6UMvuFgHHIEdNA29!EBtg=v~X!DxxEH~}L2zn|52%xalaq@DTdhh{EVwv0IaQ=!?daer zTKp4I`l8SDt;d{8Q`5Ko;BXUi&oAG1l4}59P-{|^S(Rmord5s6qsh<&m@Ab^wqCD) zHyRD}lKLDzpYN&@q5&*47mGzGiqcXpmqR9#K|CH8kXS4RNs`(iEF%HjP%f8ItyaZK z6$%Apvsok(2>~dTO5jTZfq;N?0ch4l01f$k9?4{~Youl-#x{UDMr#AFIkz@SDwPtQ z$gQ^$2|*(Ps9LQiav_8o8Ne<=Zx1*M*syo80sEO1tB%>5 zfdHB`1z+!R@?ghPRKmL)hWEvZE$=*54ose*0JiUNTM_)cMDXhxEKg(?-pD=y<)L4J zT0dSyD0&NhJ$^_8Ko9uom%-ZM4BTM{Tw$9qyPj=-9W;N(Wi@3*-Q4pq`Gcp}^vvNr zyd&PsmG>fpCSZz?K}UIEd;HGgG%0MG>ymxKPwy{>wy(m*Atq7)0000~7 zMNw2LQirBVQoa8G3P(rY+l;L4iy+JwSqmy$9JlSkk z&*$^Eg+c)@!R|v4gdc8+TTn&eWHO0VD&>$!B%o;;WLf4CNs=Inq9d`xA4otCWHK38 zmc{pkX`0Y=9g3oGK{}lVy~OYL|C5lQ&U^l;wrg|7w=BcA9L4-r411?K7f`@348&rw zXD#uW)DK;H`hxO}u%=@Cj{;#u#_;bb1_KgUOT2Hp6;)MvC6P$vQP3=g1O5#aU%I!K zZ1dc@f}YvG&*Spnplm2rIp^VdA^HydZ0X1axdms2!RKi5x-SFA4p@ zC@N|PI$ryHL@t-(!zBsf2-+sYAukhDHU7Lxm88-p zDk^c;sHj}OKUc4lGZU}6umlGVNAJx0%sKDOFwQx|V2pVvxhYKe|L9TNk!~md3BVrm zYPDL8Hk*yU-ER3~LGwJ7N`0ZV&nOhBI{~~A;@ND*=kxg?#^W&`4u`zk?Mg_e)8XlK z`T#M+OaR1!<#Nf_>$S`xrqd}OjYhoJ>q)?3vEX8pY&I()ERjfjrXM$k7e+-Qs3Ihj zNyOuEQ2EGYG7ro!o6VOBQEwuV2z)*tR8>WxP{616FY)p1Pn1d}#9}cxolZC$4n(6- z35hJq0;FlHC{ zp*iF(lgUK(E`($(s9pJ8Kn?(M734H_63WHtf}6SQQ_MXEP!#0|&@>J8dL5TfG&tBw z#tYn{TCGZvAr>cca%YYn^!t73tg8OOJ2FvJ(`YpCyVZi*?Ur+1uUA$hAg8-aK)c;e zQ<)!XwHh|n&ND=$@^)>aF-`~n}#*WMkD*M|f8r$i*z7+W! qF|A!t*4fE(R`<_YIkN&?Jng?3oQ|aAqClPi0000t>5xmo{nArfL4CJwMMm+N`pQ3p^Le$?rMud6Rbxz!-yG7bz2z$^USP5(;udf(gfQ zG~f68y^)EvcNWp#bUoDt2=h+^%o-?-|mo~iieWqLNP<0m@2PTB7ftyb= z@H`K$>v9Pr5X`L|rw&CEN2(9SB7A2SE;d|j9@*F}sd(@*2l|P*fWfK>1drZUrtUA7 zNXO~pKn1cjf~~TLbje1g>EPPzN2GH#UIBxJ{}S9=E`{zs-w#hO?vcH+hJxroI5v?j zD!4lP0WXq8zUx3RAP@|Gq$}6wXCjFLZY^YSWBxN9#&g)ro$%5}aYn#y=tJ_aIT%4d z5d4u`rlp!};XGmbZkJE*kYJoi&N0pd*yxY{0${xD;;Q1h^8f$<07*qoM6N<$f(}t7 Ae*gdg literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_code.png b/node_modules/connect/lib/public/icons/page_white_code.png new file mode 100644 index 0000000000000000000000000000000000000000..0c76bd1297751b66230f74719504b2adb02b1615 GIT binary patch literal 603 zcmV-h0;K(kP)^~*-1fljz_B$LUvK}k?BNXe#Y!m=zM!!V#}8bncK5m;8VP zw86G*RI63?Cd%b9bX|ueNlZ|wR6rj|r_)VIP@r2imh3?SN+^{|kY%~8B{maJ@F*OK z&VH9LwOeGt#DRjj0~v~8`>iO7!Ybi;zE$va`A^T#yW`y44;k^#O~K5*jD=qcUhPSc zvyy~q;5H_1WT1l~cqje9yfa+l!hu6xjdOJ8s;8E^+=QQ$tw p?%p!Hy#YapB=@+^9(46X{{RQg%9y;OKjr`c002ovPDHLkV1g7l326WT literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_code_red.png b/node_modules/connect/lib/public/icons/page_white_code_red.png new file mode 100644 index 0000000000000000000000000000000000000000..87a69145075afd8f8fd8b391c5da1249ec8b2889 GIT binary patch literal 587 zcmV-R0<`^!P)LWh{^|hy<@Q*xw+qo|KpY<+vaXbbW{L4q( zTsjXEJvb}e%bgb=o%W0h?4u1;^bWTqH8}5Th002ovPDHLkV1nrS0P+9; literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_coldfusion.png b/node_modules/connect/lib/public/icons/page_white_coldfusion.png new file mode 100644 index 0000000000000000000000000000000000000000..c66011fb0fbdcbf210483d676b7131542a0e282b GIT binary patch literal 592 zcmV-W0k7R5;6x zlV4BMP#DI+Z{WQcKZBTk0lfkj5F$ztWhP#lcuyb@0@rA^#Kpu5KLA&Rgc}o#aSmis zrZC__xY^&#cI&!!{c|4Q_tcec*#b>|Y15wPcY2=o3;-Bl=(t4;6Ok*pL)-{*A;GX^ zS(@WGp6j~k1wBVR9)BB_gar`}HyRBXh7nM!)u5^>N~MyN6bc0-5{W?44iB<`2biXb zR;wk?jIQg@G!5l)SqhrXCU}x$GU-dY1sra}0uCq@153FUULT=jNwSk}0WBjKz}Jdu z<5gB*<^XtpAmp3m^ZEXQZWd1krhft}CoYaF4cSMvTJ01}X3X37KYdx-D0$c{doUe8 ztY{vlGr-e*;N!WAV%_hgUawyYrhegW>^F)pv%uUTFslHn; zvJ)l{%w(~{!O4`KTmK{Q{zCYltLfs&4?nz|6IdlqHCvX;|HGv~!QW?8P~_d#e0$v$ z)5XHEz{3>qMiH`1+qNYf?huS+@L`J9_$cjJF)Hf?@pu;)`9}BXwGUM{2!{y-4|Z{L zG>z?O%Cp8P5T#j1DID7u_*(Jg?7iss8AZQ+&;u_J{FmILf((9eoiL5nGUe>Fgq*U$z0000 zJ3A|*qoWOonz+4ZQ0KNhDB07SX1?#FrNy8%K)_l}y&kh`*KYdy`Y99&tgNgMLSSrc z?B?+B@HO@P-jS~z2Rgc6yy~Y~%>oJpBxsb$5<&nRLqiuR7K=@0SZj~jTs|sv_jWVX zGe?WflejOaq|Vec=s9+ahmXbyJ|T)Sl*?s82sr2H?Ce~HD5WI+Sz&tmWrN()wI2}+ zKqg92t*l^-#ae~;9%KFlWkmwnY=-UK`_|%ICZ#P1gdjK<2n38VXsuC7{WiU!fZFmm zW~Sda9(Qi@pxO}$ARY+;t##Ao27usOqNt7Hwq6K7G1il@xitj=LIM&{N&#SuX;x4x zmG6FhCg-$PI;hQ=;1iZ>F>^~@)IPi;l}fX?SZ!QiO=X<|pSVkNpJuLHzW(FT_~W-v z?vFpkyE>8ee4d=7wKauH5~dd_M7d2Aa=ICC{Nj7Blqv&DQEP#j_VeWV&WXL>c=LLK zsmYg^_JiDb;%U!UxO%qjFAvsDFj-kzT2$GbV(ZopPM$i$z`!7jvEk07BcC=6FMt4` z*0u3Sy`0b~%#(0000K literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_copy.png b/node_modules/connect/lib/public/icons/page_white_copy.png new file mode 100644 index 0000000000000000000000000000000000000000..a9f31a278e17993d8d4e13beac2f9d5f7b42d08f GIT binary patch literal 309 zcmV-50m}Y~P)sF~CC`eaI+m%Y8jfzomMvZQaNUIT3LIrJ$h)_W{ zwF|LDNlB-g`Hb_G$;>3F$9JF3WYR|3fy2C+_wH}*xp!_4fF2UN4lt#d26oXwru}hT z0+0%Vz-l&|Tdh_L-Ng1G2*RBtBncRx;99K)&+}s0whhxXp{go}$g&Jk6k|vfypI5M z!1sNGVaV?!*L7i87Bo%cfO@?S`bajL{R<($@$|PtgBRcCGIJ_2a|&kO>G-s2aR3E4 zjssoScUa;zIdOeGHBnH13G)W-zt$kUQgNfG;96b=v&4NzRt&@7nN%v3HsG`<<+F$cumMs448N!W3r&2Z*b~D5^$^d6Jxn@SFK5Q8*uKSR7x{I|H-_N1f+AD zSYC5@2K4OKL$==F9U@CH;ONNL(W}oZICHn;d?~pw?GRIsH*x-68Oy6SuK`)`{E)46 z9^3(-HXa#X89SBv?u_YP)WjsQrp;}0X?Bxrvf12IKW8>3t`e~W9|JS<{btTNbNT@EQIWBSNJTX8AMGXD z-SsH|s#>j9Xf~VMtyT-YMD}5^SWHTY5->o`k|d#AE_YQd79j`%GMS7FNvG3b7^Vy9 zn0HYCJy5MyQLoqKnW|JOp-?D*<2V^msZ>BOv0ANd2n7t@{=V;sZrQ>3c})5_%ms4z z7!qXwHHe~!QFj8aR~&*-3F?O|;#(ESIXP~Os%|~y^7c15*q5`gz2-5ol!fU92NIGT z_ves+>+Tf3gfcL?!nimYmR}cw*|BGULzI^7!;k#3K^YO#;!+vM@N~(99+<;fdqr zYPJm+pXYFYk;neQyXXEcTQDNQx57i`Okp9A#n?<7!{#tnKJdsF>utb@JH7dU01gfL zEK2hoPZAnO5+je3&^i*hWM`qCW^vLK!O*?U-#IvXV?#6koWqrwnD{j&K`7N>^tR3G z8zr1(qVOzcF#nF1&0MZ5C$l8*E^Uth0000zE0Ay_3@1Z_7#f-XWL#E{8Al7>L$ z0Rx7lnddoqAyfT%&#`$;v0@*5YdW3w z7mLNoa=FAshK% zDiy@zakyMAxr-H?iQDZi^!t5;Eno2A=?>mMx`Vg(Z!?<53LHLvfTPa`$mjDcX*Qdv zR;ylN4OH+m)fVX&Z#yZpUae;ss@a$K&})gHovkhr@w#xyPVlfVgXti1_357y%I-UHDvRWYvPEX+#g+j4Q9ayba zh7uQN1j%HQgA=Fp9DfODAU^*3*FCs^6IpO7xg`RUXyP)(;=d!ly=#I^l3e0Cub`{H Z`5PU3+D2e&<<>s`J(VpX#y^kqzQ;#=2x({YMw9Q&ndHT&`BD$#%Ql?{+)-OuSA`r}MWJ zVg+2Gc(GW}a=BERPNy^;kEz$|38dTYlFQ{%5S!g@|8f8D_!Nu9_Ni2glF1}xG8xi! zorc39&F6EPOeWOt_XS`W2H_Bo$MXugy}SEctJQj=(TLXTHL(jRXfzs>NF=0SHk;94 zF!&HjdZNX(3U3;LY64IMX__Xv%_wjLC!J2`0Jw?X=zPK$C$`&dYPDKaC={e16bcE@ zgun^<0k;ak*=xLE)@(Lqu~MmsFoMCLY&0Qog`NO(h@kyxaA%EbwJLy8sU*Vi`~52K zX0wrqW;_LmMq@evX4iAM9Od(Q0eHP$1%L|xAh@vrqB`HPQLon}f3aAka=9!3hr=O- z5F9`#J_7Jhah=U(4RjaRhkS4Xkk98kDz-`i!r|~~AQ1TFcDw(@<8g{aBE)l)PNxNE zI(RPyc>9e{@WGSMU%i7*v{!&P$WLz25)0oc=Dl-yy%xYZAm4b-rttL7UjR#%`#j_F R;_m(?iiXTHIMmcoLoO94I8;j@ zv^2DJ5#orqydFJX|Gm$_Bi_vyew+j6{r}$Qc@D1%fQqeAhJj)1!z4pP83k2MV2~s! zSt^w(<#HLFVBg_#xz1W8ioi(WY&Hu~6zil?DI^jJgu`K35(hkP)H%@Imesbg#5!Ps_$Ni*SiR8&sKb9?M`0-mH)gtg&YgRX#*TXz@Z+| z;|2H@xzE0TfuORhuO2k6#K8#sW^J`mQ0+E@$K`QkFV+DTlI$w{GJ;zid{*v9xeIe_ z$|Bp`@iKkgoFK3{4Z)#DWKV~W4K@5WZN+Ql_7%YxNqSx7%cWud&cX>)_PvD*UzxZg a%Kia9Rjz_59@~-t0000)l$0ECbfb-0$}>7z|u>IvuoHEmW&j4lzv=KA+EpIObc7e7{sGA)QVmnM@*^ z%|h38^m;wC+ilpk%>l#V5LCqP_y2_Cayd^XlX;j*r54R*lW!zbqtSpQNyz8(JVmWm zyV4S$2{Uhyc{Cb0QQbf{ZGT{Kr zvJi{K_&f+q^Pv4MK$hhS4TgFj_FD*rLOePdE-E^T7ZzTFCRUB`*?9&h(a#C!-v8lWG#k3AOJQaUey6Oasked^kDPe=Khg@7s584 zg`XfS1)&u*_c;I76#%`kkBfiZgKKo@0)9d6vZw=ExQUtV?eW{Y1Xv}=4X(2zy85d> Y0C^(qLv?Ui{{R3007*qoM6N<$f-gW7od5s; literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_edit.png b/node_modules/connect/lib/public/icons/page_white_edit.png new file mode 100644 index 0000000000000000000000000000000000000000..b93e77600def75c9a144d3d0a5088a62c02cbb0b GIT binary patch literal 618 zcmV-w0+s!VP)$>5Y&axjp2O=VLu>*f>1L;s0)kkvKC!*u?s6CVL=HJ6oP~pNfZc; zsKr=bq;7MITw8NXw{SZm%59TId2x_9BQ zV86`NuvGI!>o^V!Na!=$7GJE{Cq`b+XwknM{UcGHFTTfmuS+ zm-zYC!P3+zmY;SG$?!fYkOih`QYaLxyF}A86h$GGN}kFj)_o*0e zjPMP%zTG7FYMAfO2Nn1D`D0Cj?Wl>5q%@CE10nX)KxpNmwk+!IWkzywiYD( zqUXiYYIq3qcRyMGJ;IY`(Gz~E$J$zu2+R{)xGlE*88b3WK6V*J>}2iPY1HH|tER0W z_+^^FdppY?o)Gt5M2`%xwRDH@R3G}^i1l4|6uchm0X0f!@&YdVLB5K&dd7Rv{)DXX zt^&vP;}kqj3f>94j+4xd93>s|Q!Ezi>?r8(Il$P}PFxSqu{d*!Y%*#cX(R0f|Juz# z3o0_xI14Al->1uky@W-rCI_%l&>PK^TXNSN{byMk2AI5vbwp!K-%-@!-vPR3iikL1L7HA!^!~ChCFU#lnGzp88=I z67V8PHBo4(l$u?-AKmT8?#_0rKW9dUNRbpLc`}piywAM9$xZ-3fR1C75T(BjCn-l* zjUcci2oXXo-}iqun@#)+`W@kL_-U&|2>MxZy~3IdmRm&8b)9!2%ksg3R)nNnT*TJOC=6{2hG86Dz+<^p6qfG5$i^UNUh+u)CD7O2 zK>Ioazn;U|+X0x$=feveYZL1W*Fm%e5P1sajd#eW#^5(ddx76*pt$^)b}$Q4oPabL zLc^HF>Z{8za;f$LtN0P$6C?1{X*jtXkRJ8IEeyiSzencvH3Ux_y>y^}wfJrRCQN#9 z?&e+C>sSAfrE%mZD5RfZ`gSndD)=P?+nG5Oq$zmY&-v+gc7R6c0u8^Ke#|XOq?gF@othF3zFpM8Il<8BJrWqBtF>b#_ye4{0)Xbu6j&@UIhRE002ov JPDHLkV1nWI9dZBw literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_excel.png b/node_modules/connect/lib/public/icons/page_white_excel.png new file mode 100644 index 0000000000000000000000000000000000000000..b977d7e52e2446ea01201c5c7209ac3a05f12c9f GIT binary patch literal 663 zcmV;I0%-k-P)^@R5;6x zlTS!gQ5431_q{u#M2 zg&W%y6a}>qj1Z|7Vu&-DW6d~k-n;jnHsjb-q#u0C^W!_5^C=MlKq<8oNCQ6qS00!X z5eI;XP=g!^f}j{hku}E1zZ?XCjE;`p19k(Rh%^AQQ54xysU+ocx$c#f61Z4HnT#3u~FR(3>BnZniMIF4DouI8Hi4u>cAK%EN)5PO(ip3(% zIgBx+QYirR){Z8QwV$9Z(Mpt=L-Or3#bf-G@66}txq0yc*T(zNTBDT0T8rO^JeNbSI-Tzf5!pBioy4NwAN^?iN#{;fH1Jke4Xa`^fR8m z%h6dq%xX)S?7`zae))(Xst^Scp6B8FejQW?RLTM8@0=vnnntuRGBM2dpo>gbCnTD= z^<;=JuqdSf@O>Z8^XdR?s+KEfhDdB_#ahFj^giCtzT(s8kA$AViyTqaAR;KGaLzUU z<=GqA4bRwpX|IG~*x>pZ!@zLr`XQ`od>m(`;jz|M_*1GDO#$7;n74ppb8=eiqh760 x0yt}J1#p`gw$`o!R{d7zU9~!Un@nJV{4bstt4Au+Up@c;002ovPDHLkV1kWhGjjj{ literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_find.png b/node_modules/connect/lib/public/icons/page_white_find.png new file mode 100644 index 0000000000000000000000000000000000000000..581843637079359a6a58fcdccf0763690c67b063 GIT binary patch literal 676 zcmV;V0$crwP)_k3`4d{s8lK_6bi^@vq&To98fNoK}7)fx$e2^Y&@<^jR_Ee+8}KG;X`@ z@bCyiolqX>bb1ZIs%QGnjzFU~L8H~d?e;*XP(h(S262}XyZ3a0h07r{KV?E70l+e- zE`%3x|M5#q+;HOC(h@A^M)7Rn13dm0&>K$j%k_F4wOWlsNCIH+!c_#{eS&TL8v4yc zcpnPEY`cQzZ$ILq{U-MA6Z6Z|1p!FZjQ}tXSb25J@HphEqX-6Hqo?-_Zn@{d#>2Ml zJGhxTAd&emK$lV-QK&VM&ix0Xy{GyS3Wp(+E1^8BhD3T0a)m-Lw@Lu4zQRrP)9(3F z^>$hh@N>OAXrmPYunLi|fJ$_*5i`46;M>~*5D{bp>-OL3{+!MJa`3kv~Q#QfQ%c z)1s}QE<_XaYBG;IuRF=td#+}fi4h(6HgoUyJLi0t(*dA^B)%@8kkG&bdM5P5^Z5WF z%d%>m^SbN0XeV)wbUOXn5Ag#A$gJx+7-OCkMM1S%MWIlTkbFLmOeW(&n&wUd&;`>p zVcRy$Z{K0=?SpNnP^;BYEEXleFbq(UY&LrXX$6qkJ~)8+b{=jj3HEXds;Z(?D%}}L zX3`39&dy=Zyar!ehA}e>w)(*vrCct{PI9^2Jpj&OZS8<3-@{0(gNv%1{)zAiLY+_^ zl}e>Ofd4&#Irj#7>=o=Uhv5IJ@?sN0^J|(WL2Uun$4}si6}TG-s3T#p&6GE<<2W)O zf{^Y2HlO#*QDvTp3v&d@;8*}aUC4lisG9(w7@d5Y8y)}U#FwCkqp*Mcgme4{&gGRf zlBfd`nF9cQBKB2_L{F8G2)7pAf$i)Ds`|}-c>pc^LRW{w4SQ)3N^BbZx)6BlCZts! zKph%`(m#xg-q3I7=(us;9<)*2%iuQ1J`oV3gU6V~T}^JU5714JN33&GwEEru0d}Uo U{MPL+lmGw#07*qoM6N<$f^vibe*gdg literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_freehand.png b/node_modules/connect/lib/public/icons/page_white_freehand.png new file mode 100644 index 0000000000000000000000000000000000000000..8d719df5205f7415ce657e5c277db4533c82f346 GIT binary patch literal 639 zcmV-_0)YLAP)p{{sC7)XB-g4w*W1a1)XtvxrMYa1o?wn&v~3 zHnC|#(>B_M1d`_7gfzLiHy=0c<2kQQdXu*33(xYN_xYW39(cz9jEVT%VokB8|DoF~ z8u%Q5sdl@4VB7X#uh+v_;yOGY&pRi?378ghv)P1cngiAAb<}D#l*?rWDV0j_dc6Zk z-|P~AJZQCA=yWcQjG8fYnimzj*3KqTfN0Cy!G^$7)+bQ$+mHVd1J zvwOR^5Lm<|R+uyB1Nu4vL?d4qa3tn?9H7SZH@~u=fFHEDfSH|bHU6kh0O3%cLdyny z{`9S2Sw~WMy0MPy!64i`jdk4Z3>^+KIL_fN2V_d&ywBt`^IJpxUI$=YAph~5`;xCe Z{s%Y0vkUXDnO6V+002ovPDHLkV1loX8z=w( literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_gear.png b/node_modules/connect/lib/public/icons/page_white_gear.png new file mode 100644 index 0000000000000000000000000000000000000000..106f5aa3611a4807ec8c21701c631730275089a4 GIT binary patch literal 402 zcmV;D0d4+?P)<@FR}JvtGRKa0_WfK^c7uXaFH3q@Y!Hnl8VySc`OtkPN3;#l*y*l23+99h*9JzA00}rAC!#M1dZ#v9YOBH|eC*${MmzzYjBu!!-< zK8tujf&(6i)1biy*F>4{f*Kd(IU-JsG&#b_@NgTnx@40)2@2%c;*=?-2Za=}O}7&( w%_K#(S>e1j&gfY?mR})n>>0+8p`iTe2d1K2h8#$+)&Kwi07*qoM6N<$f(2cptN;K2 literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_get.png b/node_modules/connect/lib/public/icons/page_white_get.png new file mode 100644 index 0000000000000000000000000000000000000000..e4a1ecba1b60e54f3777717ed105cdde745b7184 GIT binary patch literal 516 zcmV+f0{i`mP)o)wchR-92qq~y6`XqbKmElbB3z{pkZs0VPF`CFvS?7jDn^mFo>d9Y&06* z&1MsS!M-CH3ee+h_sy)Ms%B*ec3R0RpVi9?*mU84yoq(Bw8 z<4(999dJJE!V%pWT~HGRIAb;(#O%2K3?uRpz}AfgE8e9q&OSdr^e^}lC$QXZz;S2A z)w>^oHy>?v)q--`!pmuBe96PxP0u*inQvyFW(llfv9 zXV1s*Jh`y2H%B3ZTA(AzpsQ?hb6_PyZ=c1?_B4fbl>G%!@ubJln=!)x0000#DY{xaiib^#X=YT4@yE_&2#eBulEdzjE`u&@G%2(&u{J-<}d(^uY4W_kMfEX z@!X)AR9F&FL?RJyJRUzvBoeqN{5kY`z3wcM0+du73~_0|*lac! z42Dw(Eg1o{Ash}P8jXrqN+1w`*XxDD;ShmPCZC7#4;wWbHoMvBl$=zF-`?*9j*Nbjd=v@OWt_BgKxP-3wd zy37?ATx&$b+&zRM!K;BD%Okw`Sb@&Pak8$KRX19jWZmC0&n*Ggv%j8nvSPDFw zEkV65AGOoBQ8kf`R|}Px*&INNS%osq9b{Fq2I(x6@xM>tg=vRLF?I`0rWzHyRc>}g~)F_Qn`A>)C_iwK%Z zrIJ;xR)UI1Y4Ozts|-Nho;q zVk9-bX)%F~!;63iu$Fk=VJn3~fmb5S@@)ZqjBT2{f`vT`b2}zxb0$o;EF@G3&BHK^ zc)`1kUzo^Qkk$?KFKHNBD?nP-MJ3b@&4fg;g5l2wMi^g?9qj+~@b;62o_U1_S1J`g z7m^UMg25FX1MJ5AQxAJ5F5WDt=$=-@JV-!LHA2vuxl9kN>PS8x??^AINH6LjF*#nbk4}=n3gfWp$kEX5IpHS zYiQ{@d7Nl&d$#+7-TckP&Q}N91e-C#5QQ<|d}62BjvZR2H60wE-&H<>}%WqH(_V;zPbB1rgSSSC(0? zWlQ#?N3UgnJ9m2C29w!SwoOo5_2Iq!<8vCyEoDoj@#oV($oJEg6Bj@;nD|2g8 s%L|>IZ381yx9RvPhV4J)*SeoEV4lyr#k*`nfWBbxboFyt=akR{0DpOPi2wiq literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_key.png b/node_modules/connect/lib/public/icons/page_white_key.png new file mode 100644 index 0000000000000000000000000000000000000000..d61648452284da1bc28b10385f95b5d2bf027901 GIT binary patch literal 616 zcmV-u0+;=XP)-tZUVHjYHp;RjQ0M0pRlXN=mLv{hk9Ebp9&~+Wj-T9IkpzWPWd#fZ)d=zV^~S`;LE*!&u-?g42^wwN&Xr1~#d5ifl_2*B1OoS}CDno^8a50ArfE8;stQF>AP54J@H~%T zFz84s;dO!QJKD36(~!QOg!t_^gfFcSKDU4yK0+Ypg$NT^mIYcQ6bk*3P(lBLh7Df_ zTu=2xC#+-_%)|{Cv8zz0t|0y4D5M`xAc{gwOKc`ou<*&VjUREFHs1qd<_xSkKeTBt zgyCi=@jj;&Ns^GsWWaIUl0Y2azcDlF@u{(P*!+EH;lnU~b|Lv{4|4Hdkh!qoQHiE$ zY#y>KFA0QEw=4Z|uV{0A^`Y=D}hB$GP&<$bi8q(u;p^0(my3Rz7fP}|# zGZ&#uor4@c3q9r|f?H6-UZmfgKx(iV(MQ`MPWB>iC~SxnN5H*zb*A3#zWwgu&c|}3 zn^g87H{pdeasl%Lhmab&jC?lES}7C?4BFDNA<}20hoY@w_IU%i*T;}}wh!589}7~7 z#Ug`-R~4j&+K_y4kW@X7qLr-)S5qVKU)tO;+kXJ++{vPI@{hVK|PhMVVx_`)vx~zUs}c9O-Ok{00000NkvXXu0mjf DS5_-g literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_link.png b/node_modules/connect/lib/public/icons/page_white_link.png new file mode 100644 index 0000000000000000000000000000000000000000..bf7bd1c9bfd78d689c73ba67cf914182933ee68c GIT binary patch literal 614 zcmV-s0-61ZP)OOAS;jTeL{ZSdz-%)SMH9tDF;N4B6%j=d15J&5qy`F#vB?Ar zqS1nH@%ny_XSI*Y>) z1f5QYdmzT>YciP<3WehS<{GovEaLGv27>{*-7f0&I$yJ^L%ZGPv1YT$V|u;*+ZCWz ztHI~CDVsuy($SfR6-`N~K?9GTB#l%%0h7 z-q`K-y~E)+s8lMyTrPL8^_pUo)9G|SluG5pPqw6!LJB_PzyJUM07*qoM6N<$f^=yZ AYybcN literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_magnify.png b/node_modules/connect/lib/public/icons/page_white_magnify.png new file mode 100644 index 0000000000000000000000000000000000000000..f6b74cc40f82fc83e4dfa6e9647ccc1b34e6ed7e GIT binary patch literal 554 zcmV+_0@eMAP)Vb2f>2}Fa82O3m(Ob=t*sniin`NpInLyMJgI`saru@YOPfh zy4g0#G*cV!#N%;Gq9_VH9?v%kjS3Rb1j8^;C={$Gp=lbj z*(?%?geA!5^Pok%UauwjA)v4g2`HedDw4_Mk4hhBQt?e7YJ5(hcj|3dNu^TOPGnjB zTTsqd3GIZ=Bb`n=7no)dflv&K(lsWw?lH6T1Yht0F9qgIuzh}ym0%n<3d3EBWB*pg z+G!I0lbAEXyd>k|QNuwr4=KX1D+tLPv)j@C1=N4sA4NF9A>HcO3G47*Y6!+SrUH-7 z1hb;^#S=r|`aMh>J#dWruAEf}gcR(DRUC`ZUev&$Sbh0SgLiTXeeHEU<$_YV;9281 zym`igIE%Sm8DpDw7@71Tv^EB5xSdUR*0$Mqp+Wq8OoaZtOg52&)zZ;;M=7#C1Yd6x svjx>8ad4e2x|*xHHwRjcjs6zA0XLDUqKT6dS^xk507*qoM6N<$f*wetruo^Ag2=LamM1T#~4RmC^m`_ zs}H7d&XJ}mg+hU?tu0noRvyjI&o2SRAeYZFesYkts79I^jJ7!A7%6nJwq8O?iT55M z1OQ` zbL{!Cp5o*IRmE9PInMCSPjTwfT~J+EYkz}tjxY=fg5Yf6EQ@DG$0kMJ9h^&$W}9BU zP1oj2;?MWVkKIEl)r=Y;L^Cx2q|>!)qJJ8zE7-V*-Cf7V8_2#1c0N975t~+&QUpQJ z5(uo(-O_`%Rj@U@t>JYAgd!>L?0Idxtd#oW2gc!jinsAEva8|kF4#Ic**mmml_{d^$s}Q5Q)KCys4sfck5bP1SyeHwh2`A<@N&t2j0^lgHC_^(pAAPCNjwl+>AN%C4Ll>_8Hjda%9 oS~i=#*e)>KsPlg0=2)Qg6BCqJ=F8HdGXMYp07*qoM6N<$f@i)wr~m)} literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_office.png b/node_modules/connect/lib/public/icons/page_white_office.png new file mode 100644 index 0000000000000000000000000000000000000000..a65bcb3e1e9613cd9e4950850db43d7025a5fdf9 GIT binary patch literal 779 zcmV+m1N8ifP)JNR5;6x zll@CmQ5eTZ^k*a#RQf}fVOdd`5NJh6S(>6Cf$wEW#f&JyAR#GAn9>Gml;nOf3WCDa z5({5&UB$(IF?G#$x4X@Ickg!Y-HU!Z_rzX=qAq-XI_LS^=lOCT0|0{#{kBkYDS7{3 zD`iu%E=`cDX_^#^#n$5SIQ|4Zhsk8>N|zXHXG@*41$i-7`Jr{8`3S_OEcmY|RF48wXkk?WpdVM4OePBSbfh z#4_=eXJg@3epx~gi>QbUmO}Bm(ENN3+@c?jWiKvSrm(o|W}Ud*?vy~fn1!V~Cl4kB zI-;c!8f~-v)jX82%EG($>?;KSD$64f2&4qQ#=Yyrcpy$57RAVuV#vKMP)0hT$r6m# zc^F^XaJ8R9Q|}x^NoJYIvYZkq-z}Tnj@UJK2l2H zG}p+VvtjP2Z%bsb$~7QLJ9#pC0dKi`ppOd^_V;ME6tdzC0PtV|r=@e@37O`%0k^=5^`%cf$eu00N17Ro!{^30krz>a%3j34C?*{Mt2^a4~ zK=P+Qq%|f;Tc&+9ps;@Mw`EE%rgs&#y=j6BUGg96oIqdwj9-fiy*N(|@o)eD002ov JPDHLkV1j#pUPb@_ literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_paint.png b/node_modules/connect/lib/public/icons/page_white_paint.png new file mode 100644 index 0000000000000000000000000000000000000000..23a37b891c2f5faa3b8128d45373ceab794ca609 GIT binary patch literal 688 zcmV;h0#E&kP)PK^TXt2QS_@2qt2T|9~baC-vaPn=ziepcfAwB$0!O2Q)E}1e8!q+9)KT5JVKU z7HNY}h##OS-BxWHWjD0wrPDeEfUClHs%Q6&2u@FTOJkKMQN|_Rlw6rQz$gPzqGNtj z#ruSeFeh835JJFiM6vp@6M5bXj%k7CMt%SIwfbF_fD-3*Os`9Ly_Q3WQ_SX33E{pX z9_WIeeTCGQ3wYALpBcK+P-iuw;3i&7xCua37k5# z`>c`M@sGeC7cdsdz`aE9lOz!hPholbyz%T85LYf6O*@SA+9&+^7k>+4M8$A8iNQq{ zQvn8k?-+dU`Z@gK0z$EtPV#+`^OH`R@cE-cuE&_!D)SZGxmQxeobP_Zwq zMEgi6ePN45N`|V1so0uE8^}1xw8s;VM%Ai@7} z2-&Cyvez_-O4?6uv{zTaj|YeYEk34i~K@`8YW2g{x* zc;7z3lItpVy_et{Z-ZZ)<@*%{l7Ao8mu@V7*gz<_1##mwW*%LEwCdzNsVLYx2*T-J z#HeQ*_a=R~KDdVNk$EVgAIRl$oQi_(`_IrdJciDpH|Xe{K-YsMtc!cRnFi$qzsr4z z5*$;ecov%3->1{YNy6-Gf(Ecy&_I$CjI#laeuE+S120^|Vjsf)W&i*H07*qoM6N<$ Ef)^4A_ab^avY?n0hpS-#mn_4{O$e%cm-@NH=3`90Wq+3`~HKArSdfX`&Z12 z(CY$VW-MNtXX4xy%yUeE?}*~0-|iByA@ZrwXgph4S*bhcc5{HB!DFVm_v}P*g7+Q~K}7K0lcp(^N@X>U zV`{ZpeIf${R6Hgg4FL^`X$Eu75k(PE6ycl$AW0Ic)#@rR7Z(7;V?i-dR1K935Jgcx zPfkwK>2wGokf!Nih^ARp6-6arYFG#(9Ta!x93nFEjoA==z(g?#sDg?Owk?Mg7K+>l zWYsf(<`#+$h9Sp6gFOg_dd+80SkUpk&xM7h0`Sov9W73spU;GP073|VfZ&Gd$J$*0<~TV5aPS|qWH57|VJz+d0000vYep8SaFV10Q$h+;hIUPX_=v5b}%>Tm<(&j1&5;I!55C)oN0s(P%ZB zP3Q#ahfpXKWF@S?jm4U#fv)QovMhrriclyNs6-G12#3R##4PSZ0VY(dRWJ;Lwuq{# zAW0Gwi$yA^R4RZ!;W+L`f&%x{=D^VK#BBWL4Ys{;*!A7Q;!=dN<&D8*GzGaF4`hV4 zDbY0{NrMX>ZqF=0((gR5-zL$kC*b)!fwu{Euru|XrG<$^n#@)7i_>rCmRxnDq>$Y%gJaCkRd|tE*a2x05Pe!I^e13o69#&RQZ36s0 zB=O|K2Yi(jsMqThn}9t?f5E-)L^naZ+db$&%M$!bCdm=jv7?t_lB?3&%Ltq(>ESw? c;MI421LCcoDG!2@;{X5v07*qoM6N<$f`UZt7XSbN literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_picture.png b/node_modules/connect/lib/public/icons/page_white_picture.png new file mode 100644 index 0000000000000000000000000000000000000000..134b6693687b2fa5fe36d48a9c0b8001f937c741 GIT binary patch literal 650 zcmV;50(Jd~P)VHAd+bMNh~)LLRqN>D)-jd9UvB%+hyKX5U|&4t0)fzgD-MPpQ$nHU%yoz=vI zMGb>1Xu!6Hw$NT~@Au<4P-+{9;Uw?&oj31uzH>xX0T7Xkz!(tn|Ed9-s_FqyReC13 z(ll)vW1O{Ck5ihay12Ob2ABc@RUI;zHpaMiyRDs0r>|D4rHw{ItJSJnYjt~jTbGuW z`X(~}?!&86q40R8<4zYw;$qi0^3ec=c&<&H;r`8W%H=Xymf^i;Wo6~<+}zx2UMzpC z*6MZN?(FMv`n|KO3(KFiUaucP0;Z!@LcUNa%8#vGK5aZ>wDgB0Gi=t*argWJcdlMQ z2#MpEX0wU+9&0U?N(F#OgpviU_Y{jYMsj65U3|PjwUOY}lUYj?MTiK_Il}NCVx-Eh zDx-TzMk7se+M#W_>?A1-x}ZXw3kkyz5kW)_hkjsi@RhKadN#H$Hq)$07*qoM6N<$f}lhwPXGV_ literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_powerpoint.png b/node_modules/connect/lib/public/icons/page_white_powerpoint.png new file mode 100644 index 0000000000000000000000000000000000000000..c4eff0387d5888c638ba09473ba6d2369f7b56f0 GIT binary patch literal 588 zcmV-S0<-;zP)HU2HvUSp%6 z*n}iP63IK?dpo;h@sj9~pcxo;VVTc-XLiP@DgefqE#NE=@oyUd-&HjLpsLIuSFXV-EMck)oQ(A`s%*^&wf0(rNiNHsU%=0Rw;WC z(kbc37l6fo`-0uR!pYkYv8U^3?nsh^@pw!K0TH3uYyx1_2>|JbXPmfskJ|1YAw9w! z9`N)1^Aesr;y5Nr5-ODn)oOL|CGi}f9!&iVwpK$khlIX10X$H6^A_stBJqvLhU$?V`QXqKme*s~gVDJ4A;LTs_e15jhc1;By a82kqHEPVYFAD2!50000JNR5;6( zlS@kiVHAe7MZY2;Xi-5)WxDDgv@tCUl*&p14T@Z~3ThM5LP4tuQfLu@EnG;nXc<8S z6&3BN?fx-cv-Kp6>HRiNTHE>$X( zD&=w+?GWC>?RLAGC6Yix;an~UmSt)tSf}1VS6N1N2ONORdD? zaj}w6DAZZdOud9Ep?M?{iQWbE5^9HLLZZF|1kdy0Tu4InEuboP9@nvbZ-P0n4AZTy zyMRIxRDmUE#LdqYuD=-Qz4N^bC`_#S7vcLn1M}{J(Wl3#c4VWczu&)AjUlh(11>gp>f`wv{KnjF%!aA*Jk N002ovPDHLkV1kkt*XsZP literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_ruby.png b/node_modules/connect/lib/public/icons/page_white_ruby.png new file mode 100644 index 0000000000000000000000000000000000000000..f59b7c4365fa1720af1aa04eb47167ddaa6eeed4 GIT binary patch literal 626 zcmV-&0*(ENP)ZS(e|#C2>JN4>y}l*tQ*E7zP@R2CCJnkW?xa6bgk%(hgtZ z0=~d?U3i`+Mvi4!&~+WPT1^NX#{u6&QIx+DE(oR{&T5&-ovF?@wGw)P&AtpHZa|G%V*GUUqL@@!d4V$`8=##4)ytY959JG zdc&Kho)&AL70^i z!PEmeeDWCB-UbK(*4JST44^tV2z_J(dn~+vBMJT97_7rzFio=~XczIv?PQ5$v%u~y zu(bteXb5I1h2zCV{Jc2~V{{yzZipgsP6;k264$*#5q?GzCm|CPa9CKqm4b116h3Pu z?+%Cm52plC8|5P0@igf2GV1KkCfk{Zecu=G@VNrf>s%g9c5D%@cfxVb6$nY`1IW=4 zt10QqSps_2JLp0f3I0j0u>#qA;v!+T))KEbCg|mo3q0pG{OR}p0fPds8+K~d>Hq)$ M07*qoM6N<$g1S2e3jhEB literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_stack.png b/node_modules/connect/lib/public/icons/page_white_stack.png new file mode 100644 index 0000000000000000000000000000000000000000..44084add79b9a0fc3354d16bbd4b4b5ff8095da7 GIT binary patch literal 317 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$R@9E+gqH(@-qA%AW0|7U8+xDRI z0k`B18}ImRw2g{jTGP$Pmx3yI6F_2s&$|`cJ!i0UN zB3H;=r{#{FwLaNVJ&hZl9+MTHGx1T^-A=Q0?hRb#8a~x50X%;`b6ik3cw=#XdxWy= zgrpBoDjpwP&g9<9h3x!k_B!?vuTJVkmIJ-U N;OXk;vd$@?2>|rNdMN+^ literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_star.png b/node_modules/connect/lib/public/icons/page_white_star.png new file mode 100644 index 0000000000000000000000000000000000000000..3a1441c9a12062a4bb3d706000d3ca14399aebca GIT binary patch literal 565 zcmV-50?Pe~P)SCZIX8XZzY2l?gCw6LlgWJ5Avz#QX4|&mI8LN)w~J1vgL=KLAhlWz*=#m~gyvxa z&;iC6gb?aZvMdXxX`<0+D1hs_pqJ!wxqlEH;CJ)je~uL(gpi@v>!I0f_Kl=E(E+Tq z26na*9gribxx-Oft(HnstyXUUy!39&E-cI%J5Rsy;(PGZH{g{ty!HVC&yGPT3H8x# zw{^gBPW)O0FMoh{k%l<`1a|To_Wl&u&-GXm8izU|&<&utILc4wc6s@u1bmTz6x{qg zTw@7=FQRcg&r`h+gcR$*Jbv+*DPk7v)B@e0o2 z6IlBXW&8xh@9)YKiV~2>+z&XKd24JT55YWz&JtfvCg4r^~bLP79-yS@n$OW00000NkvXXu0mjf DStt2z literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_swoosh.png b/node_modules/connect/lib/public/icons/page_white_swoosh.png new file mode 100644 index 0000000000000000000000000000000000000000..e7708292adabf4821612bfca032cbd019c63180b GIT binary patch literal 634 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 literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_width.png b/node_modules/connect/lib/public/icons/page_white_width.png new file mode 100644 index 0000000000000000000000000000000000000000..1eb880947ddf3e745c29e8d9dc90f09c7e6e323c GIT binary patch literal 309 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf6SkfJR9T^zbpD<_bdI{u9mbgZg z1m~xflqVLYGB~E>C#5QQ<|d}62BjvZR2H60wE-$R?&;zfqH(@;q9b3Efq-lM(nr^( z=EYR73-9e)UYMWsXy%?aZsD68Yyv^2$~6QgEcljw%kx>O(f-gQ?@fOOx3A-0+Qw?O zRx~W)kn~Qe2d6f9nMG#g9Q04Mk==M~N!Dglvxk!fgVh#w@ZV$IY1+Xc`d{d2UcaP~ zfWp)_Ivqj}l2SPy^9ZWy6rG9Yx4v67_uA&&9|XA~5-#3)W3%em1peD8RWH^#O%XoM zxMPud%}GTj#~*+7JMxTd!`{^Q+>(D3*|@KV`*G2;{QnANOxu1$r2xIe;OXk;vd$@? F2>@zac~<}c literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_word.png b/node_modules/connect/lib/public/icons/page_white_word.png new file mode 100644 index 0000000000000000000000000000000000000000..ae8ecbf47672a874c0958d0d113a56162c2bd364 GIT binary patch literal 651 zcmV;60(AX}P)hkjP zNW|QGv-YFNLN^qH@tJycPNG5ti6B7;r4mEr#lr@*T8*M85D`{ZR^BWwF23T<%MYIh zdC)S*p=|xk^!~H=+HSZ183~y8v4|mYmZxt&)5{{~>J`>E223Q5>T$=~mtA71q-jdG z+eJhOAyBW^0k9Gk1+rX8)zFx((CG^&tDY>6XaS~Fy!WJON|Gdujg5^~Vzt@o%BcYLiNiTQSD`zL^ociBz_>bDlpw3kriQ@Z`bVsGz-_6N>$&gTDiKDTKR^ z-hB*tHa^>!oD~5TK^0UK5rZ}RBm50Bv}S-yA%s=Ha5RYb{)!z2N&$&64gfhybBu8p lh~_|?8^bu;BRYt{<}Yrwd83Y=s?Goa002ovPDHLkV1l%3CP4rI literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_world.png b/node_modules/connect/lib/public/icons/page_white_world.png new file mode 100644 index 0000000000000000000000000000000000000000..6ed2490ed1432d5d667a76235360824a1088e928 GIT binary patch literal 734 zcmV<40wMj0P)JT{hN;C#tgf#9krG=I>5!<*aE1_(spcgF}<`n4i zJi-}^6UUeU4jUFwdCiVPDm%`Zx^UBa8J(mnR6wEgz^}o8;)M*Y(@l_!Kfv)}4+NuM zaPXE50z)r)9=D=SR|RIqfQ^j}Hu!fzMeQBo+@PZk1G8hOw|vBTvkx`HM)Xe9q3xao z@`p0`NO!2904FHSLA6E@Y-O6zH$DQzvq@aHsz}}<(!v(Z_+EodX%R&NZW75g+nENo zV0020rxE^;7d!067AN>6*+&YLp$9uH6F-=In`XC{Cn%+o|5)b&boEPr02w@|P*oGm QmjD0&07*qoM6N<$g78X0Q~&?~ literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_wrench.png b/node_modules/connect/lib/public/icons/page_white_wrench.png new file mode 100644 index 0000000000000000000000000000000000000000..fecadd08afed92536be91ab12d8e37b6bf410d5d GIT binary patch literal 613 zcmV-r0-F7aP)wK%m(L+9IV|s|#(WRl-O^4GvaQsnHq|OstfO zIJ3}3<01}YGARE4m!7=)QisvlHUo!Qymx-@-t*p_129Ko-#pVI)6#!*kLj-AGXWNR zyA_{wKii_amK7^YT-v z6#plaNm#8`-kz@OvjIt^4%IN{@J3bR zRI}ME1Mv85p|%;RK>ViR>APPLB4;;BpCtqE@P+*7!G>I4UjNx~e>r3HA^tWCQ@S)l z{BslcSwL-CxQ&_ZZSv_g0Tu{yi*X){Mt|W7)lbE`SQxFP00000NkvXXu0mjf;)M*S literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/icons/page_white_zip.png b/node_modules/connect/lib/public/icons/page_white_zip.png new file mode 100644 index 0000000000000000000000000000000000000000..fd4bbccdf1643f4ff5022fbc59b82546e259317e GIT binary patch literal 386 zcmV-|0e$|7P)_QM!1S$Bhw4w+iRuFWf;tfR6D%SMJrb+tx zC9R6{2>Ou6#juIy6u(I?|;&Owi$sRB4^20apB5xE2 z#B9XekY66S6lzfCL!eEQRgo0LokTA55@Y#%_wN!TXPw^Q4IIXsG~v#u_4t;x_HM16EQ@QRY+rut&97&UefsPmLrQ5P zBC2kcbux9L%2bJz$P$XV$*zSxb2e@6_3O#;&!FD<&hLjGn%~%en;7)djE^d6!t$lW7GyIOKlQ46hr`Z zjLNuRDP_53dNoN?wd&HMgL^m1DXFU<5dQsrceN>fSz00000)O9XRTN^$%%`*Fg>ryDtc(lF@?b>dE!20r+y z#Q*>(wbV5H`-E4Do={CJp7=ERhw15hgZi)?jRG88 zzVz(5;g?Td1izJyO33bhjg2Qc7FVY@f9!o)Gu?DII~vm-Dc?}3M!fsgjP?F(7`rgg z+xOk8XD)e?Zl=5+un`5!7kr?F=eq)K-5uqr%yU$1hLv){Vlm=)*5~`lwMciiXFu*g z)*Jkz6AF>#zb(Vx`Iv{bdGZHtlW)v(y5k^|xgSUc9%0}S20nrYrO}78ofk?bV!5)4 z=Ngz@+$9N1>>mA%IWx`Fqa240bWkiW;2TZgd8CZS0U}@mknC;!2;wi$eI@`h0y2JS`Eae0CW}q(2(%!m8 zWq$`PDU>LT1_y*bBv#P5<@q0@ttz$hIH}YMDvAigCc=y*)jY-VOpTd;A8@3t7Xh4r z0KTWOk;N2Ox4!&&^4B*no$WtTX!BXB)rg!y8dvGgKBQKLJNXRRp0}Bsjd1|LNQX~c zbC~fjrk2iL@4dYF*vt;}dFn(%h)n_-vzEIHMOKRkdF%3Lq|zBgKm_h>TEq!))nWjq zzn;B!?!(dQcHu$#=JF`cS&W~C`WHFW^B!~MI#k)>1Vk&eQy8P1O`J6V04{D@|7d6^ zyBABnh-d^H0FX&L07M||E0n_dp4v&Q%PSE9p#R#Hq)`5I_(B5CE#q dxjPz0{s-<+c#AC!i7@~G002ovPDHLkV1iPlpuqqD literal 0 HcmV?d00001 diff --git a/node_modules/connect/lib/public/style.css b/node_modules/connect/lib/public/style.css new file mode 100644 index 0000000000..32b6507103 --- /dev/null +++ b/node_modules/connect/lib/public/style.css @@ -0,0 +1,141 @@ +body { + margin: 0; + padding: 80px 100px; + font: 13px "Helvetica Neue", "Lucida Grande", "Arial"; + background: #ECE9E9 -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ECE9E9)); + background: #ECE9E9 -moz-linear-gradient(top, #fff, #ECE9E9); + background-repeat: no-repeat; + color: #555; + -webkit-font-smoothing: antialiased; +} +h1, h2, h3 { + margin: 0; + font-size: 22px; + color: #343434; +} +h1 em, h2 em { + padding: 0 5px; + font-weight: normal; +} +h1 { + font-size: 60px; +} +h2 { + margin-top: 10px; +} +h3 { + margin: 5px 0 10px 0; + padding-bottom: 5px; + border-bottom: 1px solid #eee; + font-size: 18px; +} +ul { + margin: 0; + padding: 0; +} +ul li { + margin: 5px 0; + padding: 3px 8px; + list-style: none; +} +ul li:hover { + cursor: pointer; + color: #2e2e2e; +} +ul li .path { + padding-left: 5px; + font-weight: bold; +} +ul li .line { + padding-right: 5px; + font-style: italic; +} +ul li:first-child .path { + padding-left: 0; +} +p { + line-height: 1.5; +} +a { + color: #555; + text-decoration: none; +} +a:hover { + color: #303030; +} +#stacktrace { + margin-top: 15px; +} +.directory h1 { + margin-bottom: 15px; + font-size: 18px; +} +ul#files { + width: 100%; + height: 500px; +} +ul#files li { + padding: 0; +} +ul#files li img { + position: absolute; + top: 5px; + left: 5px; +} +ul#files li a { + position: relative; + display: block; + margin: 1px; + width: 30%; + height: 25px; + line-height: 25px; + text-indent: 8px; + float: left; + border: 1px solid transparent; + -webkit-border-radius: 5px; + -moz-border-radius: 5px; + border-radius: 5px; + overflow: hidden; + text-overflow: ellipsis; +} +ul#files li a.icon { + text-indent: 25px; +} +ul#files li a:focus, +ul#files li a:hover { + outline: none; + background: rgba(255,255,255,0.65); + border: 1px solid #ececec; +} +ul#files li a.highlight { + -webkit-transition: background .4s ease-in-out; + background: #ffff4f; + border-color: #E9DC51; +} +#search { + display: block; + position: fixed; + top: 20px; + right: 20px; + width: 90px; + -webkit-transition: width ease 0.2s, opacity ease 0.4s; + -moz-transition: width ease 0.2s, opacity ease 0.4s; + -webkit-border-radius: 32px; + -moz-border-radius: 32px; + -webkit-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03); + -moz-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03); + -webkit-font-smoothing: antialiased; + text-align: left; + font: 13px "Helvetica Neue", Arial, sans-serif; + padding: 4px 10px; + border: none; + background: transparent; + margin-bottom: 0; + outline: none; + opacity: 0.7; + color: #888; +} +#search:focus { + width: 120px; + opacity: 1.0; +} diff --git a/node_modules/connect/lib/utils.js b/node_modules/connect/lib/utils.js new file mode 100644 index 0000000000..35738b8c14 --- /dev/null +++ b/node_modules/connect/lib/utils.js @@ -0,0 +1,404 @@ + +/*! + * Connect - utils + * Copyright(c) 2010 Sencha Inc. + * Copyright(c) 2011 TJ Holowaychuk + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var http = require('http') + , crypto = require('crypto') + , parse = require('url').parse + , signature = require('cookie-signature') + , nodeVersion = process.versions.node.split('.'); + +// pause is broken in node < 0.10 +exports.brokenPause = parseInt(nodeVersion[0], 10) === 0 + && parseInt(nodeVersion[1], 10) < 10; + +/** + * Return `true` if the request has a body, otherwise return `false`. + * + * @param {IncomingMessage} req + * @return {Boolean} + * @api private + */ + +exports.hasBody = function(req) { + return 'transfer-encoding' in req.headers || 'content-length' in req.headers; +}; + +/** + * Extract the mime type from the given request's + * _Content-Type_ header. + * + * @param {IncomingMessage} req + * @return {String} + * @api private + */ + +exports.mime = function(req) { + var str = req.headers['content-type'] || ''; + return str.split(';')[0]; +}; + +/** + * Generate an `Error` from the given status `code` + * and optional `msg`. + * + * @param {Number} code + * @param {String} msg + * @return {Error} + * @api private + */ + +exports.error = function(code, msg){ + var err = new Error(msg || http.STATUS_CODES[code]); + err.status = code; + return err; +}; + +/** + * Return md5 hash of the given string and optional encoding, + * defaulting to hex. + * + * utils.md5('wahoo'); + * // => "e493298061761236c96b02ea6aa8a2ad" + * + * @param {String} str + * @param {String} encoding + * @return {String} + * @api private + */ + +exports.md5 = function(str, encoding){ + return crypto + .createHash('md5') + .update(str) + .digest(encoding || 'hex'); +}; + +/** + * Merge object b with object a. + * + * var a = { foo: 'bar' } + * , b = { bar: 'baz' }; + * + * utils.merge(a, b); + * // => { foo: 'bar', bar: 'baz' } + * + * @param {Object} a + * @param {Object} b + * @return {Object} + * @api private + */ + +exports.merge = function(a, b){ + if (a && b) { + for (var key in b) { + a[key] = b[key]; + } + } + return a; +}; + +/** + * Escape the given string of `html`. + * + * @param {String} html + * @return {String} + * @api private + */ + +exports.escape = function(html){ + return String(html) + .replace(/&(?!\w+;)/g, '&') + .replace(//g, '>') + .replace(/"/g, '"'); +}; + + +/** + * Return a unique identifier with the given `len`. + * + * utils.uid(10); + * // => "FDaS435D2z" + * + * @param {Number} len + * @return {String} + * @api private + */ + +exports.uid = function(len) { + return crypto.randomBytes(Math.ceil(len * 3 / 4)) + .toString('base64') + .slice(0, len) + .replace(/\//g, '-') + .replace(/\+/g, '_'); +}; + +/** + * Sign the given `val` with `secret`. + * + * @param {String} val + * @param {String} secret + * @return {String} + * @api private + */ + +exports.sign = function(val, secret){ + console.warn('do not use utils.sign(), use https://github.com/visionmedia/node-cookie-signature') + return val + '.' + crypto + .createHmac('sha256', secret) + .update(val) + .digest('base64') + .replace(/=+$/, ''); +}; + +/** + * Unsign and decode the given `val` with `secret`, + * returning `false` if the signature is invalid. + * + * @param {String} val + * @param {String} secret + * @return {String|Boolean} + * @api private + */ + +exports.unsign = function(val, secret){ + console.warn('do not use utils.unsign(), use https://github.com/visionmedia/node-cookie-signature') + var str = val.slice(0, val.lastIndexOf('.')); + return exports.sign(str, secret) == val + ? str + : false; +}; + +/** + * Parse signed cookies, returning an object + * containing the decoded key/value pairs, + * while removing the signed key from `obj`. + * + * @param {Object} obj + * @return {Object} + * @api private + */ + +exports.parseSignedCookies = function(obj, secret){ + var ret = {}; + Object.keys(obj).forEach(function(key){ + var val = obj[key]; + if (0 == val.indexOf('s:')) { + val = signature.unsign(val.slice(2), secret); + if (val) { + ret[key] = val; + delete obj[key]; + } + } + }); + return ret; +}; + +/** + * Parse a signed cookie string, return the decoded value + * + * @param {String} str signed cookie string + * @param {String} secret + * @return {String} decoded value + * @api private + */ + +exports.parseSignedCookie = function(str, secret){ + return 0 == str.indexOf('s:') + ? signature.unsign(str.slice(2), secret) + : str; +}; + +/** + * Parse JSON cookies. + * + * @param {Object} obj + * @return {Object} + * @api private + */ + +exports.parseJSONCookies = function(obj){ + Object.keys(obj).forEach(function(key){ + var val = obj[key]; + var res = exports.parseJSONCookie(val); + if (res) obj[key] = res; + }); + return obj; +}; + +/** + * Parse JSON cookie string + * + * @param {String} str + * @return {Object} Parsed object or null if not json cookie + * @api private + */ + +exports.parseJSONCookie = function(str) { + if (0 == str.indexOf('j:')) { + try { + return JSON.parse(str.slice(2)); + } catch (err) { + // no op + } + } +}; + +/** + * Pause `data` and `end` events on the given `obj`. + * Middleware performing async tasks _should_ utilize + * this utility (or similar), to re-emit data once + * the async operation has completed, otherwise these + * events may be lost. Pause is only required for + * node versions less than 10, and is replaced with + * noop's otherwise. + * + * var pause = utils.pause(req); + * fs.readFile(path, function(){ + * next(); + * pause.resume(); + * }); + * + * @param {Object} obj + * @return {Object} + * @api private + */ + +exports.pause = exports.brokenPause + ? require('pause') + : function () { + return { + end: noop, + resume: noop + } + } + +/** + * Strip `Content-*` headers from `res`. + * + * @param {ServerResponse} res + * @api private + */ + +exports.removeContentHeaders = function(res){ + Object.keys(res._headers).forEach(function(field){ + if (0 == field.indexOf('content')) { + res.removeHeader(field); + } + }); +}; + +/** + * Check if `req` is a conditional GET request. + * + * @param {IncomingMessage} req + * @return {Boolean} + * @api private + */ + +exports.conditionalGET = function(req) { + return req.headers['if-modified-since'] + || req.headers['if-none-match']; +}; + +/** + * Respond with 401 "Unauthorized". + * + * @param {ServerResponse} res + * @param {String} realm + * @api private + */ + +exports.unauthorized = function(res, realm) { + res.statusCode = 401; + res.setHeader('WWW-Authenticate', 'Basic realm="' + realm + '"'); + res.end('Unauthorized'); +}; + +/** + * Respond with 304 "Not Modified". + * + * @param {ServerResponse} res + * @param {Object} headers + * @api private + */ + +exports.notModified = function(res) { + exports.removeContentHeaders(res); + res.statusCode = 304; + res.end(); +}; + +/** + * Return an ETag in the form of `"-"` + * from the given `stat`. + * + * @param {Object} stat + * @return {String} + * @api private + */ + +exports.etag = function(stat) { + return '"' + stat.size + '-' + Number(stat.mtime) + '"'; +}; + +/** + * Parse the given Cache-Control `str`. + * + * @param {String} str + * @return {Object} + * @api private + */ + +exports.parseCacheControl = function(str){ + var directives = str.split(',') + , obj = {}; + + for(var i = 0, len = directives.length; i < len; i++) { + var parts = directives[i].split('=') + , key = parts.shift().trim() + , val = parseInt(parts.shift(), 10); + + obj[key] = isNaN(val) ? true : val; + } + + return obj; +}; + +/** + * Parse the `req` url with memoization. + * + * @param {ServerRequest} req + * @return {Object} + * @api private + */ + +exports.parseUrl = function(req){ + var parsed = req._parsedUrl; + if (parsed && parsed.href == req.url) { + return parsed; + } else { + return req._parsedUrl = parse(req.url); + } +}; + +/** + * Parse byte `size` string. + * + * @param {String} size + * @return {Number} + * @api private + */ + +exports.parseBytes = require('bytes'); + +function noop() {} \ No newline at end of file diff --git a/node_modules/connect/package.json b/node_modules/connect/package.json new file mode 100644 index 0000000000..cd09b6824b --- /dev/null +++ b/node_modules/connect/package.json @@ -0,0 +1,53 @@ +{ + "name": "connect", + "version": "2.7.6", + "description": "High performance middleware framework", + "keywords": [ + "framework", + "web", + "middleware", + "connect", + "rack" + ], + "repository": { + "type": "git", + "url": "git://github.com/senchalabs/connect.git" + }, + "author": { + "name": "TJ Holowaychuk", + "email": "tj@vision-media.ca", + "url": "http://tjholowaychuk.com" + }, + "dependencies": { + "qs": "0.5.1", + "formidable": "1.0.11", + "cookie-signature": "1.0.1", + "buffer-crc32": "0.1.1", + "cookie": "0.0.5", + "send": "0.1.0", + "bytes": "0.2.0", + "fresh": "0.1.0", + "pause": "0.0.1", + "debug": "*" + }, + "devDependencies": { + "should": "*", + "mocha": "*", + "jade": "*", + "dox": "*" + }, + "main": "index", + "engines": { + "node": ">= 0.5.0" + }, + "scripts": { + "test": "make" + }, + "_id": "connect@2.7.6", + "optionalDependencies": {}, + "_engineSupported": true, + "_npmVersion": "1.1.4", + "_nodeVersion": "v0.6.12", + "_defaultsLoaded": true, + "_from": "connect@2.7.6" +} diff --git a/node_modules/connect/test.js b/node_modules/connect/test.js new file mode 100644 index 0000000000..92b7003d34 --- /dev/null +++ b/node_modules/connect/test.js @@ -0,0 +1,40 @@ + +var connect = require('./'); +var app = connect(); + +app.use(connect.logger('dev')); +app.use(connect.bodyParser()); + +app.use(function(req, res, next){ + if (req.checkContinue) { + res.writeContinue(); + } + res.end('hello'); +}); + +var server = app.listen(3000); + +server.on('checkContinue', function(req, res){ + req.checkContinue = true; + app(req, res); +}); + + +// var http = require('http'); + +// var app = http.createServer(function(req, res){ +// console.log(req.headers); +// }); + +// app.on('checkContinue', function(req, res){ +// if ('application/json' == req.headers['content-type']) { +// res.writeContinue(); +// console.log('ok'); +// res.end('thanks') +// } else { +// res.writeHead(400); +// res.end('bad request, json only'); +// } +// }); + +// app.listen(3000); diff --git a/node_modules/express/.npmignore b/node_modules/express/.npmignore new file mode 100644 index 0000000000..caf574de4d --- /dev/null +++ b/node_modules/express/.npmignore @@ -0,0 +1,9 @@ +.git* +docs/ +examples/ +support/ +test/ +testing.js +.DS_Store +coverage.html +lib-cov diff --git a/node_modules/express/.travis.yml b/node_modules/express/.travis.yml new file mode 100644 index 0000000000..cc4dba29d9 --- /dev/null +++ b/node_modules/express/.travis.yml @@ -0,0 +1,4 @@ +language: node_js +node_js: + - "0.8" + - "0.10" diff --git a/node_modules/express/History.md b/node_modules/express/History.md new file mode 100644 index 0000000000..b087720b36 --- /dev/null +++ b/node_modules/express/History.md @@ -0,0 +1,1115 @@ + +3.2.0 / 2013-04-15 +================== + + * add "view" constructor setting to override view behaviour + * add req.acceptsEncoding(name) + * add req.acceptedEncodings + * revert cookie signature change causing session race conditions + * fix sorting of Accept values of the same quality + +3.1.2 / 2013-04-12 +================== + + * add support for custom Accept parameters + * update cookie-signature + +3.1.1 / 2013-04-01 +================== + + * add X-Forwarded-Host support to `req.host` + * fix relative redirects + * update mkdirp + * update buffer-crc32 + * remove legacy app.configure() method from app template. + +3.1.0 / 2013-01-25 +================== + + * add support for leading "." in "view engine" setting + * add array support to `res.set()` + * add node 0.8.x to travis.yml + * add "subdomain offset" setting for tweaking `req.subdomains` + * add `res.location(url)` implementing `res.redirect()`-like setting of Location + * use app.get() for x-powered-by setting for inheritance + * fix colons in passwords for `req.auth` + +3.0.6 / 2013-01-04 +================== + + * add http verb methods to Router + * update connect + * fix mangling of the `res.cookie()` options object + * fix jsonp whitespace escape. Closes #1132 + +3.0.5 / 2012-12-19 +================== + + * add throwing when a non-function is passed to a route + * fix: explicitly remove Transfer-Encoding header from 204 and 304 responses + * revert "add 'etag' option" + +3.0.4 / 2012-12-05 +================== + + * add 'etag' option to disable `res.send()` Etags + * add escaping of urls in text/plain in `res.redirect()` + for old browsers interpreting as html + * change crc32 module for a more liberal license + * update connect + +3.0.3 / 2012-11-13 +================== + + * update connect + * update cookie module + * fix cookie max-age + +3.0.2 / 2012-11-08 +================== + + * add OPTIONS to cors example. Closes #1398 + * fix route chaining regression. Closes #1397 + +3.0.1 / 2012-11-01 +================== + + * update connect + +3.0.0 / 2012-10-23 +================== + + * add `make clean` + * add "Basic" check to req.auth + * add `req.auth` test coverage + * add cb && cb(payload) to `res.jsonp()`. Closes #1374 + * add backwards compat for `res.redirect()` status. Closes #1336 + * add support for `res.json()` to retain previously defined Content-Types. Closes #1349 + * update connect + * change `res.redirect()` to utilize a pathname-relative Location again. Closes #1382 + * remove non-primitive string support for `res.send()` + * fix view-locals example. Closes #1370 + * fix route-separation example + +3.0.0rc5 / 2012-09-18 +================== + + * update connect + * add redis search example + * add static-files example + * add "x-powered-by" setting (`app.disable('x-powered-by')`) + * add "application/octet-stream" redirect Accept test case. Closes #1317 + +3.0.0rc4 / 2012-08-30 +================== + + * add `res.jsonp()`. Closes #1307 + * add "verbose errors" option to error-pages example + * add another route example to express(1) so people are not so confused + * add redis online user activity tracking example + * update connect dep + * fix etag quoting. Closes #1310 + * fix error-pages 404 status + * fix jsonp callback char restrictions + * remove old OPTIONS default response + +3.0.0rc3 / 2012-08-13 +================== + + * update connect dep + * fix signed cookies to work with `connect.cookieParser()` ("s:" prefix was missing) [tnydwrds] + * fix `res.render()` clobbering of "locals" + +3.0.0rc2 / 2012-08-03 +================== + + * add CORS example + * update connect dep + * deprecate `.createServer()` & remove old stale examples + * fix: escape `res.redirect()` link + * fix vhost example + +3.0.0rc1 / 2012-07-24 +================== + + * add more examples to view-locals + * add scheme-relative redirects (`res.redirect("//foo.com")`) support + * update cookie dep + * update connect dep + * update send dep + * fix `express(1)` -h flag, use -H for hogan. Closes #1245 + * fix `res.sendfile()` socket error handling regression + +3.0.0beta7 / 2012-07-16 +================== + + * update connect dep for `send()` root normalization regression + +3.0.0beta6 / 2012-07-13 +================== + + * add `err.view` property for view errors. Closes #1226 + * add "jsonp callback name" setting + * add support for "/foo/:bar*" non-greedy matches + * change `res.sendfile()` to use `send()` module + * change `res.send` to use "response-send" module + * remove `app.locals.use` and `res.locals.use`, use regular middleware + +3.0.0beta5 / 2012-07-03 +================== + + * add "make check" support + * add route-map example + * add `res.json(obj, status)` support back for BC + * add "methods" dep, remove internal methods module + * update connect dep + * update auth example to utilize cores pbkdf2 + * updated tests to use "supertest" + +3.0.0beta4 / 2012-06-25 +================== + + * Added `req.auth` + * Added `req.range(size)` + * Added `res.links(obj)` + * Added `res.send(body, status)` support back for backwards compat + * Added `.default()` support to `res.format()` + * Added 2xx / 304 check to `req.fresh` + * Revert "Added + support to the router" + * Fixed `res.send()` freshness check, respect res.statusCode + +3.0.0beta3 / 2012-06-15 +================== + + * Added hogan `--hjs` to express(1) [nullfirm] + * Added another example to content-negotiation + * Added `fresh` dep + * Changed: `res.send()` always checks freshness + * Fixed: expose connects mime module. Cloases #1165 + +3.0.0beta2 / 2012-06-06 +================== + + * Added `+` support to the router + * Added `req.host` + * Changed `req.param()` to check route first + * Update connect dep + +3.0.0beta1 / 2012-06-01 +================== + + * Added `res.format()` callback to override default 406 behaviour + * Fixed `res.redirect()` 406. Closes #1154 + +3.0.0alpha5 / 2012-05-30 +================== + + * Added `req.ip` + * Added `{ signed: true }` option to `res.cookie()` + * Removed `res.signedCookie()` + * Changed: dont reverse `req.ips` + * Fixed "trust proxy" setting check for `req.ips` + +3.0.0alpha4 / 2012-05-09 +================== + + * Added: allow `[]` in jsonp callback. Closes #1128 + * Added `PORT` env var support in generated template. Closes #1118 [benatkin] + * Updated: connect 2.2.2 + +3.0.0alpha3 / 2012-05-04 +================== + + * Added public `app.routes`. Closes #887 + * Added _view-locals_ example + * Added _mvc_ example + * Added `res.locals.use()`. Closes #1120 + * Added conditional-GET support to `res.send()` + * Added: coerce `res.set()` values to strings + * Changed: moved `static()` in generated apps below router + * Changed: `res.send()` only set ETag when not previously set + * Changed connect 2.2.1 dep + * Changed: `make test` now runs unit / acceptance tests + * Fixed req/res proto inheritance + +3.0.0alpha2 / 2012-04-26 +================== + + * Added `make benchmark` back + * Added `res.send()` support for `String` objects + * Added client-side data exposing example + * Added `res.header()` and `req.header()` aliases for BC + * Added `express.createServer()` for BC + * Perf: memoize parsed urls + * Perf: connect 2.2.0 dep + * Changed: make `expressInit()` middleware self-aware + * Fixed: use app.get() for all core settings + * Fixed redis session example + * Fixed session example. Closes #1105 + * Fixed generated express dep. Closes #1078 + +3.0.0alpha1 / 2012-04-15 +================== + + * Added `app.locals.use(callback)` + * Added `app.locals` object + * Added `app.locals(obj)` + * Added `res.locals` object + * Added `res.locals(obj)` + * Added `res.format()` for content-negotiation + * Added `app.engine()` + * Added `res.cookie()` JSON cookie support + * Added "trust proxy" setting + * Added `req.subdomains` + * Added `req.protocol` + * Added `req.secure` + * Added `req.path` + * Added `req.ips` + * Added `req.fresh` + * Added `req.stale` + * Added comma-delmited / array support for `req.accepts()` + * Added debug instrumentation + * Added `res.set(obj)` + * Added `res.set(field, value)` + * Added `res.get(field)` + * Added `app.get(setting)`. Closes #842 + * Added `req.acceptsLanguage()` + * Added `req.acceptsCharset()` + * Added `req.accepted` + * Added `req.acceptedLanguages` + * Added `req.acceptedCharsets` + * Added "json replacer" setting + * Added "json spaces" setting + * Added X-Forwarded-Proto support to `res.redirect()`. Closes #92 + * Added `--less` support to express(1) + * Added `express.response` prototype + * Added `express.request` prototype + * Added `express.application` prototype + * Added `app.path()` + * Added `app.render()` + * Added `res.type()` to replace `res.contentType()` + * Changed: `res.redirect()` to add relative support + * Changed: enable "jsonp callback" by default + * Changed: renamed "case sensitive routes" to "case sensitive routing" + * Rewrite of all tests with mocha + * Removed "root" setting + * Removed `res.redirect('home')` support + * Removed `req.notify()` + * Removed `app.register()` + * Removed `app.redirect()` + * Removed `app.is()` + * Removed `app.helpers()` + * Removed `app.dynamicHelpers()` + * Fixed `res.sendfile()` with non-GET. Closes #723 + * Fixed express(1) public dir for windows. Closes #866 + +2.5.9/ 2012-04-02 +================== + + * Added support for PURGE request method [pbuyle] + * Fixed `express(1)` generated app `app.address()` before `listening` [mmalecki] + +2.5.8 / 2012-02-08 +================== + + * Update mkdirp dep. Closes #991 + +2.5.7 / 2012-02-06 +================== + + * Fixed `app.all` duplicate DELETE requests [mscdex] + +2.5.6 / 2012-01-13 +================== + + * Updated hamljs dev dep. Closes #953 + +2.5.5 / 2012-01-08 +================== + + * Fixed: set `filename` on cached templates [matthewleon] + +2.5.4 / 2012-01-02 +================== + + * Fixed `express(1)` eol on 0.4.x. Closes #947 + +2.5.3 / 2011-12-30 +================== + + * Fixed `req.is()` when a charset is present + +2.5.2 / 2011-12-10 +================== + + * Fixed: express(1) LF -> CRLF for windows + +2.5.1 / 2011-11-17 +================== + + * Changed: updated connect to 1.8.x + * Removed sass.js support from express(1) + +2.5.0 / 2011-10-24 +================== + + * Added ./routes dir for generated app by default + * Added npm install reminder to express(1) app gen + * Added 0.5.x support + * Removed `make test-cov` since it wont work with node 0.5.x + * Fixed express(1) public dir for windows. Closes #866 + +2.4.7 / 2011-10-05 +================== + + * Added mkdirp to express(1). Closes #795 + * Added simple _json-config_ example + * Added shorthand for the parsed request's pathname via `req.path` + * Changed connect dep to 1.7.x to fix npm issue... + * Fixed `res.redirect()` __HEAD__ support. [reported by xerox] + * Fixed `req.flash()`, only escape args + * Fixed absolute path checking on windows. Closes #829 [reported by andrewpmckenzie] + +2.4.6 / 2011-08-22 +================== + + * Fixed multiple param callback regression. Closes #824 [reported by TroyGoode] + +2.4.5 / 2011-08-19 +================== + + * Added support for routes to handle errors. Closes #809 + * Added `app.routes.all()`. Closes #803 + * Added "basepath" setting to work in conjunction with reverse proxies etc. + * Refactored `Route` to use a single array of callbacks + * Added support for multiple callbacks for `app.param()`. Closes #801 +Closes #805 + * Changed: removed .call(self) for route callbacks + * Dependency: `qs >= 0.3.1` + * Fixed `res.redirect()` on windows due to `join()` usage. Closes #808 + +2.4.4 / 2011-08-05 +================== + + * Fixed `res.header()` intention of a set, even when `undefined` + * Fixed `*`, value no longer required + * Fixed `res.send(204)` support. Closes #771 + +2.4.3 / 2011-07-14 +================== + + * Added docs for `status` option special-case. Closes #739 + * Fixed `options.filename`, exposing the view path to template engines + +2.4.2. / 2011-07-06 +================== + + * Revert "removed jsonp stripping" for XSS + +2.4.1 / 2011-07-06 +================== + + * Added `res.json()` JSONP support. Closes #737 + * Added _extending-templates_ example. Closes #730 + * Added "strict routing" setting for trailing slashes + * Added support for multiple envs in `app.configure()` calls. Closes #735 + * Changed: `res.send()` using `res.json()` + * Changed: when cookie `path === null` don't default it + * Changed; default cookie path to "home" setting. Closes #731 + * Removed _pids/logs_ creation from express(1) + +2.4.0 / 2011-06-28 +================== + + * Added chainable `res.status(code)` + * Added `res.json()`, an explicit version of `res.send(obj)` + * Added simple web-service example + +2.3.12 / 2011-06-22 +================== + + * \#express is now on freenode! come join! + * Added `req.get(field, param)` + * Added links to Japanese documentation, thanks @hideyukisaito! + * Added; the `express(1)` generated app outputs the env + * Added `content-negotiation` example + * Dependency: connect >= 1.5.1 < 2.0.0 + * Fixed view layout bug. Closes #720 + * Fixed; ignore body on 304. Closes #701 + +2.3.11 / 2011-06-04 +================== + + * Added `npm test` + * Removed generation of dummy test file from `express(1)` + * Fixed; `express(1)` adds express as a dep + * Fixed; prune on `prepublish` + +2.3.10 / 2011-05-27 +================== + + * Added `req.route`, exposing the current route + * Added _package.json_ generation support to `express(1)` + * Fixed call to `app.param()` function for optional params. Closes #682 + +2.3.9 / 2011-05-25 +================== + + * Fixed bug-ish with `../' in `res.partial()` calls + +2.3.8 / 2011-05-24 +================== + + * Fixed `app.options()` + +2.3.7 / 2011-05-23 +================== + + * Added route `Collection`, ex: `app.get('/user/:id').remove();` + * Added support for `app.param(fn)` to define param logic + * Removed `app.param()` support for callback with return value + * Removed module.parent check from express(1) generated app. Closes #670 + * Refactored router. Closes #639 + +2.3.6 / 2011-05-20 +================== + + * Changed; using devDependencies instead of git submodules + * Fixed redis session example + * Fixed markdown example + * Fixed view caching, should not be enabled in development + +2.3.5 / 2011-05-20 +================== + + * Added export `.view` as alias for `.View` + +2.3.4 / 2011-05-08 +================== + + * Added `./examples/say` + * Fixed `res.sendfile()` bug preventing the transfer of files with spaces + +2.3.3 / 2011-05-03 +================== + + * Added "case sensitive routes" option. + * Changed; split methods supported per rfc [slaskis] + * Fixed route-specific middleware when using the same callback function several times + +2.3.2 / 2011-04-27 +================== + + * Fixed view hints + +2.3.1 / 2011-04-26 +================== + + * Added `app.match()` as `app.match.all()` + * Added `app.lookup()` as `app.lookup.all()` + * Added `app.remove()` for `app.remove.all()` + * Added `app.remove.VERB()` + * Fixed template caching collision issue. Closes #644 + * Moved router over from connect and started refactor + +2.3.0 / 2011-04-25 +================== + + * Added options support to `res.clearCookie()` + * Added `res.helpers()` as alias of `res.locals()` + * Added; json defaults to UTF-8 with `res.send()`. Closes #632. [Daniel * Dependency `connect >= 1.4.0` + * Changed; auto set Content-Type in res.attachement [Aaron Heckmann] + * Renamed "cache views" to "view cache". Closes #628 + * Fixed caching of views when using several apps. Closes #637 + * Fixed gotcha invoking `app.param()` callbacks once per route middleware. +Closes #638 + * Fixed partial lookup precedence. Closes #631 +Shaw] + +2.2.2 / 2011-04-12 +================== + + * Added second callback support for `res.download()` connection errors + * Fixed `filename` option passing to template engine + +2.2.1 / 2011-04-04 +================== + + * Added `layout(path)` helper to change the layout within a view. Closes #610 + * Fixed `partial()` collection object support. + Previously only anything with `.length` would work. + When `.length` is present one must still be aware of holes, + however now `{ collection: {foo: 'bar'}}` is valid, exposes + `keyInCollection` and `keysInCollection`. + + * Performance improved with better view caching + * Removed `request` and `response` locals + * Changed; errorHandler page title is now `Express` instead of `Connect` + +2.2.0 / 2011-03-30 +================== + + * Added `app.lookup.VERB()`, ex `app.lookup.put('/user/:id')`. Closes #606 + * Added `app.match.VERB()`, ex `app.match.put('/user/12')`. Closes #606 + * Added `app.VERB(path)` as alias of `app.lookup.VERB()`. + * Dependency `connect >= 1.2.0` + +2.1.1 / 2011-03-29 +================== + + * Added; expose `err.view` object when failing to locate a view + * Fixed `res.partial()` call `next(err)` when no callback is given [reported by aheckmann] + * Fixed; `res.send(undefined)` responds with 204 [aheckmann] + +2.1.0 / 2011-03-24 +================== + + * Added `/_?` partial lookup support. Closes #447 + * Added `request`, `response`, and `app` local variables + * Added `settings` local variable, containing the app's settings + * Added `req.flash()` exception if `req.session` is not available + * Added `res.send(bool)` support (json response) + * Fixed stylus example for latest version + * Fixed; wrap try/catch around `res.render()` + +2.0.0 / 2011-03-17 +================== + + * Fixed up index view path alternative. + * Changed; `res.locals()` without object returns the locals + +2.0.0rc3 / 2011-03-17 +================== + + * Added `res.locals(obj)` to compliment `res.local(key, val)` + * Added `res.partial()` callback support + * Fixed recursive error reporting issue in `res.render()` + +2.0.0rc2 / 2011-03-17 +================== + + * Changed; `partial()` "locals" are now optional + * Fixed `SlowBuffer` support. Closes #584 [reported by tyrda01] + * Fixed .filename view engine option [reported by drudge] + * Fixed blog example + * Fixed `{req,res}.app` reference when mounting [Ben Weaver] + +2.0.0rc / 2011-03-14 +================== + + * Fixed; expose `HTTPSServer` constructor + * Fixed express(1) default test charset. Closes #579 [reported by secoif] + * Fixed; default charset to utf-8 instead of utf8 for lame IE [reported by NickP] + +2.0.0beta3 / 2011-03-09 +================== + + * Added support for `res.contentType()` literal + The original `res.contentType('.json')`, + `res.contentType('application/json')`, and `res.contentType('json')` + will work now. + * Added `res.render()` status option support back + * Added charset option for `res.render()` + * Added `.charset` support (via connect 1.0.4) + * Added view resolution hints when in development and a lookup fails + * Added layout lookup support relative to the page view. + For example while rendering `./views/user/index.jade` if you create + `./views/user/layout.jade` it will be used in favour of the root layout. + * Fixed `res.redirect()`. RFC states absolute url [reported by unlink] + * Fixed; default `res.send()` string charset to utf8 + * Removed `Partial` constructor (not currently used) + +2.0.0beta2 / 2011-03-07 +================== + + * Added res.render() `.locals` support back to aid in migration process + * Fixed flash example + +2.0.0beta / 2011-03-03 +================== + + * Added HTTPS support + * Added `res.cookie()` maxAge support + * Added `req.header()` _Referrer_ / _Referer_ special-case, either works + * Added mount support for `res.redirect()`, now respects the mount-point + * Added `union()` util, taking place of `merge(clone())` combo + * Added stylus support to express(1) generated app + * Added secret to session middleware used in examples and generated app + * Added `res.local(name, val)` for progressive view locals + * Added default param support to `req.param(name, default)` + * Added `app.disabled()` and `app.enabled()` + * Added `app.register()` support for omitting leading ".", either works + * Added `res.partial()`, using the same interface as `partial()` within a view. Closes #539 + * Added `app.param()` to map route params to async/sync logic + * Added; aliased `app.helpers()` as `app.locals()`. Closes #481 + * Added extname with no leading "." support to `res.contentType()` + * Added `cache views` setting, defaulting to enabled in "production" env + * Added index file partial resolution, eg: partial('user') may try _views/user/index.jade_. + * Added `req.accepts()` support for extensions + * Changed; `res.download()` and `res.sendfile()` now utilize Connect's + static file server `connect.static.send()`. + * Changed; replaced `connect.utils.mime()` with npm _mime_ module + * Changed; allow `req.query` to be pre-defined (via middleware or other parent + * Changed view partial resolution, now relative to parent view + * Changed view engine signature. no longer `engine.render(str, options, callback)`, now `engine.compile(str, options) -> Function`, the returned function accepts `fn(locals)`. + * Fixed `req.param()` bug returning Array.prototype methods. Closes #552 + * Fixed; using `Stream#pipe()` instead of `sys.pump()` in `res.sendfile()` + * Fixed; using _qs_ module instead of _querystring_ + * Fixed; strip unsafe chars from jsonp callbacks + * Removed "stream threshold" setting + +1.0.8 / 2011-03-01 +================== + + * Allow `req.query` to be pre-defined (via middleware or other parent app) + * "connect": ">= 0.5.0 < 1.0.0". Closes #547 + * Removed the long deprecated __EXPRESS_ENV__ support + +1.0.7 / 2011-02-07 +================== + + * Fixed `render()` setting inheritance. + Mounted apps would not inherit "view engine" + +1.0.6 / 2011-02-07 +================== + + * Fixed `view engine` setting bug when period is in dirname + +1.0.5 / 2011-02-05 +================== + + * Added secret to generated app `session()` call + +1.0.4 / 2011-02-05 +================== + + * Added `qs` dependency to _package.json_ + * Fixed namespaced `require()`s for latest connect support + +1.0.3 / 2011-01-13 +================== + + * Remove unsafe characters from JSONP callback names [Ryan Grove] + +1.0.2 / 2011-01-10 +================== + + * Removed nested require, using `connect.router` + +1.0.1 / 2010-12-29 +================== + + * Fixed for middleware stacked via `createServer()` + previously the `foo` middleware passed to `createServer(foo)` + would not have access to Express methods such as `res.send()` + or props like `req.query` etc. + +1.0.0 / 2010-11-16 +================== + + * Added; deduce partial object names from the last segment. + For example by default `partial('forum/post', postObject)` will + give you the _post_ object, providing a meaningful default. + * Added http status code string representation to `res.redirect()` body + * Added; `res.redirect()` supporting _text/plain_ and _text/html_ via __Accept__. + * Added `req.is()` to aid in content negotiation + * Added partial local inheritance [suggested by masylum]. Closes #102 + providing access to parent template locals. + * Added _-s, --session[s]_ flag to express(1) to add session related middleware + * Added _--template_ flag to express(1) to specify the + template engine to use. + * Added _--css_ flag to express(1) to specify the + stylesheet engine to use (or just plain css by default). + * Added `app.all()` support [thanks aheckmann] + * Added partial direct object support. + You may now `partial('user', user)` providing the "user" local, + vs previously `partial('user', { object: user })`. + * Added _route-separation_ example since many people question ways + to do this with CommonJS modules. Also view the _blog_ example for + an alternative. + * Performance; caching view path derived partial object names + * Fixed partial local inheritance precedence. [reported by Nick Poulden] Closes #454 + * Fixed jsonp support; _text/javascript_ as per mailinglist discussion + +1.0.0rc4 / 2010-10-14 +================== + + * Added _NODE_ENV_ support, _EXPRESS_ENV_ is deprecated and will be removed in 1.0.0 + * Added route-middleware support (very helpful, see the [docs](http://expressjs.com/guide.html#Route-Middleware)) + * Added _jsonp callback_ setting to enable/disable jsonp autowrapping [Dav Glass] + * Added callback query check on response.send to autowrap JSON objects for simple webservice implementations [Dav Glass] + * Added `partial()` support for array-like collections. Closes #434 + * Added support for swappable querystring parsers + * Added session usage docs. Closes #443 + * Added dynamic helper caching. Closes #439 [suggested by maritz] + * Added authentication example + * Added basic Range support to `res.sendfile()` (and `res.download()` etc) + * Changed; `express(1)` generated app using 2 spaces instead of 4 + * Default env to "development" again [aheckmann] + * Removed _context_ option is no more, use "scope" + * Fixed; exposing _./support_ libs to examples so they can run without installs + * Fixed mvc example + +1.0.0rc3 / 2010-09-20 +================== + + * Added confirmation for `express(1)` app generation. Closes #391 + * Added extending of flash formatters via `app.flashFormatters` + * Added flash formatter support. Closes #411 + * Added streaming support to `res.sendfile()` using `sys.pump()` when >= "stream threshold" + * Added _stream threshold_ setting for `res.sendfile()` + * Added `res.send()` __HEAD__ support + * Added `res.clearCookie()` + * Added `res.cookie()` + * Added `res.render()` headers option + * Added `res.redirect()` response bodies + * Added `res.render()` status option support. Closes #425 [thanks aheckmann] + * Fixed `res.sendfile()` responding with 403 on malicious path + * Fixed `res.download()` bug; when an error occurs remove _Content-Disposition_ + * Fixed; mounted apps settings now inherit from parent app [aheckmann] + * Fixed; stripping Content-Length / Content-Type when 204 + * Fixed `res.send()` 204. Closes #419 + * Fixed multiple _Set-Cookie_ headers via `res.header()`. Closes #402 + * Fixed bug messing with error handlers when `listenFD()` is called instead of `listen()`. [thanks guillermo] + + +1.0.0rc2 / 2010-08-17 +================== + + * Added `app.register()` for template engine mapping. Closes #390 + * Added `res.render()` callback support as second argument (no options) + * Added callback support to `res.download()` + * Added callback support for `res.sendfile()` + * Added support for middleware access via `express.middlewareName()` vs `connect.middlewareName()` + * Added "partials" setting to docs + * Added default expresso tests to `express(1)` generated app. Closes #384 + * Fixed `res.sendfile()` error handling, defer via `next()` + * Fixed `res.render()` callback when a layout is used [thanks guillermo] + * Fixed; `make install` creating ~/.node_libraries when not present + * Fixed issue preventing error handlers from being defined anywhere. Closes #387 + +1.0.0rc / 2010-07-28 +================== + + * Added mounted hook. Closes #369 + * Added connect dependency to _package.json_ + + * Removed "reload views" setting and support code + development env never caches, production always caches. + + * Removed _param_ in route callbacks, signature is now + simply (req, res, next), previously (req, res, params, next). + Use _req.params_ for path captures, _req.query_ for GET params. + + * Fixed "home" setting + * Fixed middleware/router precedence issue. Closes #366 + * Fixed; _configure()_ callbacks called immediately. Closes #368 + +1.0.0beta2 / 2010-07-23 +================== + + * Added more examples + * Added; exporting `Server` constructor + * Added `Server#helpers()` for view locals + * Added `Server#dynamicHelpers()` for dynamic view locals. Closes #349 + * Added support for absolute view paths + * Added; _home_ setting defaults to `Server#route` for mounted apps. Closes #363 + * Added Guillermo Rauch to the contributor list + * Added support for "as" for non-collection partials. Closes #341 + * Fixed _install.sh_, ensuring _~/.node_libraries_ exists. Closes #362 [thanks jf] + * Fixed `res.render()` exceptions, now passed to `next()` when no callback is given [thanks guillermo] + * Fixed instanceof `Array` checks, now `Array.isArray()` + * Fixed express(1) expansion of public dirs. Closes #348 + * Fixed middleware precedence. Closes #345 + * Fixed view watcher, now async [thanks aheckmann] + +1.0.0beta / 2010-07-15 +================== + + * Re-write + - much faster + - much lighter + - Check [ExpressJS.com](http://expressjs.com) for migration guide and updated docs + +0.14.0 / 2010-06-15 +================== + + * Utilize relative requires + * Added Static bufferSize option [aheckmann] + * Fixed caching of view and partial subdirectories [aheckmann] + * Fixed mime.type() comments now that ".ext" is not supported + * Updated haml submodule + * Updated class submodule + * Removed bin/express + +0.13.0 / 2010-06-01 +================== + + * Added node v0.1.97 compatibility + * Added support for deleting cookies via Request#cookie('key', null) + * Updated haml submodule + * Fixed not-found page, now using using charset utf-8 + * Fixed show-exceptions page, now using using charset utf-8 + * Fixed view support due to fs.readFile Buffers + * Changed; mime.type() no longer accepts ".type" due to node extname() changes + +0.12.0 / 2010-05-22 +================== + + * Added node v0.1.96 compatibility + * Added view `helpers` export which act as additional local variables + * Updated haml submodule + * Changed ETag; removed inode, modified time only + * Fixed LF to CRLF for setting multiple cookies + * Fixed cookie complation; values are now urlencoded + * Fixed cookies parsing; accepts quoted values and url escaped cookies + +0.11.0 / 2010-05-06 +================== + + * Added support for layouts using different engines + - this.render('page.html.haml', { layout: 'super-cool-layout.html.ejs' }) + - this.render('page.html.haml', { layout: 'foo' }) // assumes 'foo.html.haml' + - this.render('page.html.haml', { layout: false }) // no layout + * Updated ext submodule + * Updated haml submodule + * Fixed EJS partial support by passing along the context. Issue #307 + +0.10.1 / 2010-05-03 +================== + + * Fixed binary uploads. + +0.10.0 / 2010-04-30 +================== + + * Added charset support via Request#charset (automatically assigned to 'UTF-8' when respond()'s + encoding is set to 'utf8' or 'utf-8'. + * Added "encoding" option to Request#render(). Closes #299 + * Added "dump exceptions" setting, which is enabled by default. + * Added simple ejs template engine support + * Added error reponse support for text/plain, application/json. Closes #297 + * Added callback function param to Request#error() + * Added Request#sendHead() + * Added Request#stream() + * Added support for Request#respond(304, null) for empty response bodies + * Added ETag support to Request#sendfile() + * Added options to Request#sendfile(), passed to fs.createReadStream() + * Added filename arg to Request#download() + * Performance enhanced due to pre-reversing plugins so that plugins.reverse() is not called on each request + * Performance enhanced by preventing several calls to toLowerCase() in Router#match() + * Changed; Request#sendfile() now streams + * Changed; Renamed Request#halt() to Request#respond(). Closes #289 + * Changed; Using sys.inspect() instead of JSON.encode() for error output + * Changed; run() returns the http.Server instance. Closes #298 + * Changed; Defaulting Server#host to null (INADDR_ANY) + * Changed; Logger "common" format scale of 0.4f + * Removed Logger "request" format + * Fixed; Catching ENOENT in view caching, preventing error when "views/partials" is not found + * Fixed several issues with http client + * Fixed Logger Content-Length output + * Fixed bug preventing Opera from retaining the generated session id. Closes #292 + +0.9.0 / 2010-04-14 +================== + + * Added DSL level error() route support + * Added DSL level notFound() route support + * Added Request#error() + * Added Request#notFound() + * Added Request#render() callback function. Closes #258 + * Added "max upload size" setting + * Added "magic" variables to collection partials (\_\_index\_\_, \_\_length\_\_, \_\_isFirst\_\_, \_\_isLast\_\_). Closes #254 + * Added [haml.js](http://github.com/visionmedia/haml.js) submodule; removed haml-js + * Added callback function support to Request#halt() as 3rd/4th arg + * Added preprocessing of route param wildcards using param(). Closes #251 + * Added view partial support (with collections etc) + * Fixed bug preventing falsey params (such as ?page=0). Closes #286 + * Fixed setting of multiple cookies. Closes #199 + * Changed; view naming convention is now NAME.TYPE.ENGINE (for example page.html.haml) + * Changed; session cookie is now httpOnly + * Changed; Request is no longer global + * Changed; Event is no longer global + * Changed; "sys" module is no longer global + * Changed; moved Request#download to Static plugin where it belongs + * Changed; Request instance created before body parsing. Closes #262 + * Changed; Pre-caching views in memory when "cache view contents" is enabled. Closes #253 + * Changed; Pre-caching view partials in memory when "cache view partials" is enabled + * Updated support to node --version 0.1.90 + * Updated dependencies + * Removed set("session cookie") in favour of use(Session, { cookie: { ... }}) + * Removed utils.mixin(); use Object#mergeDeep() + +0.8.0 / 2010-03-19 +================== + + * Added coffeescript example app. Closes #242 + * Changed; cache api now async friendly. Closes #240 + * Removed deprecated 'express/static' support. Use 'express/plugins/static' + +0.7.6 / 2010-03-19 +================== + + * Added Request#isXHR. Closes #229 + * Added `make install` (for the executable) + * Added `express` executable for setting up simple app templates + * Added "GET /public/*" to Static plugin, defaulting to /public + * Added Static plugin + * Fixed; Request#render() only calls cache.get() once + * Fixed; Namespacing View caches with "view:" + * Fixed; Namespacing Static caches with "static:" + * Fixed; Both example apps now use the Static plugin + * Fixed set("views"). Closes #239 + * Fixed missing space for combined log format + * Deprecated Request#sendfile() and 'express/static' + * Removed Server#running + +0.7.5 / 2010-03-16 +================== + + * Added Request#flash() support without args, now returns all flashes + * Updated ext submodule + +0.7.4 / 2010-03-16 +================== + + * Fixed session reaper + * Changed; class.js replacing js-oo Class implementation (quite a bit faster, no browser cruft) + +0.7.3 / 2010-03-16 +================== + + * Added package.json + * Fixed requiring of haml / sass due to kiwi removal + +0.7.2 / 2010-03-16 +================== + + * Fixed GIT submodules (HAH!) + +0.7.1 / 2010-03-16 +================== + + * Changed; Express now using submodules again until a PM is adopted + * Changed; chat example using millisecond conversions from ext + +0.7.0 / 2010-03-15 +================== + + * Added Request#pass() support (finds the next matching route, or the given path) + * Added Logger plugin (default "common" format replaces CommonLogger) + * Removed Profiler plugin + * Removed CommonLogger plugin + +0.6.0 / 2010-03-11 +================== + + * Added seed.yml for kiwi package management support + * Added HTTP client query string support when method is GET. Closes #205 + + * Added support for arbitrary view engines. + For example "foo.engine.html" will now require('engine'), + the exports from this module are cached after the first require(). + + * Added async plugin support + + * Removed usage of RESTful route funcs as http client + get() etc, use http.get() and friends + + * Removed custom exceptions + +0.5.0 / 2010-03-10 +================== + + * Added ext dependency (library of js extensions) + * Removed extname() / basename() utils. Use path module + * Removed toArray() util. Use arguments.values + * Removed escapeRegexp() util. Use RegExp.escape() + * Removed process.mixin() dependency. Use utils.mixin() + * Removed Collection + * Removed ElementCollection + * Shameless self promotion of ebook "Advanced JavaScript" (http://dev-mag.com) ;) + +0.4.0 / 2010-02-11 +================== + + * Added flash() example to sample upload app + * Added high level restful http client module (express/http) + * Changed; RESTful route functions double as HTTP clients. Closes #69 + * Changed; throwing error when routes are added at runtime + * Changed; defaulting render() context to the current Request. Closes #197 + * Updated haml submodule + +0.3.0 / 2010-02-11 +================== + + * Updated haml / sass submodules. Closes #200 + * Added flash message support. Closes #64 + * Added accepts() now allows multiple args. fixes #117 + * Added support for plugins to halt. Closes #189 + * Added alternate layout support. Closes #119 + * Removed Route#run(). Closes #188 + * Fixed broken specs due to use(Cookie) missing + +0.2.1 / 2010-02-05 +================== + + * Added "plot" format option for Profiler (for gnuplot processing) + * Added request number to Profiler plugin + * Fixed binary encoding for multi-part file uploads, was previously defaulting to UTF8 + * Fixed issue with routes not firing when not files are present. Closes #184 + * Fixed process.Promise -> events.Promise + +0.2.0 / 2010-02-03 +================== + + * Added parseParam() support for name[] etc. (allows for file inputs with "multiple" attr) Closes #180 + * Added Both Cache and Session option "reapInterval" may be "reapEvery". Closes #174 + * Added expiration support to cache api with reaper. Closes #133 + * Added cache Store.Memory#reap() + * Added Cache; cache api now uses first class Cache instances + * Added abstract session Store. Closes #172 + * Changed; cache Memory.Store#get() utilizing Collection + * Renamed MemoryStore -> Store.Memory + * Fixed use() of the same plugin several time will always use latest options. Closes #176 + +0.1.0 / 2010-02-03 +================== + + * Changed; Hooks (before / after) pass request as arg as well as evaluated in their context + * Updated node support to 0.1.27 Closes #169 + * Updated dirname(__filename) -> __dirname + * Updated libxmljs support to v0.2.0 + * Added session support with memory store / reaping + * Added quick uid() helper + * Added multi-part upload support + * Added Sass.js support / submodule + * Added production env caching view contents and static files + * Added static file caching. Closes #136 + * Added cache plugin with memory stores + * Added support to StaticFile so that it works with non-textual files. + * Removed dirname() helper + * Removed several globals (now their modules must be required) + +0.0.2 / 2010-01-10 +================== + + * Added view benchmarks; currently haml vs ejs + * Added Request#attachment() specs. Closes #116 + * Added use of node's parseQuery() util. Closes #123 + * Added `make init` for submodules + * Updated Haml + * Updated sample chat app to show messages on load + * Updated libxmljs parseString -> parseHtmlString + * Fixed `make init` to work with older versions of git + * Fixed specs can now run independant specs for those who cant build deps. Closes #127 + * Fixed issues introduced by the node url module changes. Closes 126. + * Fixed two assertions failing due to Collection#keys() returning strings + * Fixed faulty Collection#toArray() spec due to keys() returning strings + * Fixed `make test` now builds libxmljs.node before testing + +0.0.1 / 2010-01-03 +================== + + * Initial release diff --git a/node_modules/express/LICENSE b/node_modules/express/LICENSE new file mode 100644 index 0000000000..36075a3b7f --- /dev/null +++ b/node_modules/express/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2009-2011 TJ Holowaychuk + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/node_modules/express/Makefile b/node_modules/express/Makefile new file mode 100644 index 0000000000..228f299a9e --- /dev/null +++ b/node_modules/express/Makefile @@ -0,0 +1,33 @@ + +MOCHA_OPTS= --check-leaks +REPORTER = dot + +check: test + +test: test-unit test-acceptance + +test-unit: + @NODE_ENV=test ./node_modules/.bin/mocha \ + --reporter $(REPORTER) \ + $(MOCHA_OPTS) + +test-acceptance: + @NODE_ENV=test ./node_modules/.bin/mocha \ + --reporter $(REPORTER) \ + --bail \ + test/acceptance/*.js + +test-cov: lib-cov + @EXPRESS_COV=1 $(MAKE) test REPORTER=html-cov > coverage.html + +lib-cov: + @jscoverage lib lib-cov + +benchmark: + @./support/bench + +clean: + rm -f coverage.html + rm -fr lib-cov + +.PHONY: test test-unit test-acceptance benchmark clean diff --git a/node_modules/express/Readme.md b/node_modules/express/Readme.md new file mode 100644 index 0000000000..61ee6eaa19 --- /dev/null +++ b/node_modules/express/Readme.md @@ -0,0 +1,180 @@ +![express logo](http://f.cl.ly/items/0V2S1n0K1i3y1c122g04/Screen%20Shot%202012-04-11%20at%209.59.42%20AM.png) + + Fast, unopinionated, minimalist web framework for [node](http://nodejs.org). [![Build Status](https://secure.travis-ci.org/visionmedia/express.png)](http://travis-ci.org/visionmedia/express) [![Dependency Status](https://gemnasium.com/visionmedia/express.png)](https://gemnasium.com/visionmedia/express) + +```js +var express = require('express'); +var app = express(); + +app.get('/', function(req, res){ + res.send('Hello World'); +}); + +app.listen(3000); +``` + +## Installation + + $ npm install -g express + +## Quick Start + + The quickest way to get started with express is to utilize the executable `express(1)` to generate an application as shown below: + + Create the app: + + $ npm install -g express + $ express /tmp/foo && cd /tmp/foo + + Install dependencies: + + $ npm install + + Start the server: + + $ node app + +## Features + + * Built on [Connect](http://github.com/senchalabs/connect) + * Robust routing + * HTTP helpers (redirection, caching, etc) + * View system supporting 14+ template engines + * Content negotiation + * Focus on high performance + * Environment based configuration + * Executable for generating applications quickly + * High test coverage + +## Philosophy + + The Express philosophy is to provide small, robust tooling for HTTP servers. Making + it a great solution for single page applications, web sites, hybrids, or public + HTTP APIs. + + Built on Connect you can use _only_ what you need, and nothing more, applications + can be as big or as small as you like, even a single file. Express does + not force you to use any specific ORM or template engine. With support for over + 14 template engines via [Consolidate.js](http://github.com/visionmedia/consolidate.js) + you can quickly craft your perfect framework. + +## More Information + + * Join #express on freenode + * [Google Group](http://groups.google.com/group/express-js) for discussion + * Follow [tjholowaychuk](http://twitter.com/tjholowaychuk) on twitter for updates + * Visit the [Wiki](http://github.com/visionmedia/express/wiki) + * [日本語ドキュメンテーション](http://hideyukisaito.com/doc/expressjs/) by [hideyukisaito](https://github.com/hideyukisaito) + * [Русскоязычная документация](http://jsman.ru/express/) + * Run express examples [online](https://runnable.com/express) + +## Viewing Examples + +Clone the Express repo, then install the dev dependencies to install all the example / test suite deps: + + $ git clone git://github.com/visionmedia/express.git --depth 1 + $ cd express + $ npm install + +then run whichever tests you want: + + $ node examples/content-negotiation + +## Running Tests + +To run the test suite first invoke the following command within the repo, installing the development dependencies: + + $ npm install + +then run the tests: + + $ make test + +## Contributors + +``` +project: express +commits: 3559 +active : 468 days +files : 237 +authors: + 1891 Tj Holowaychuk 53.1% + 1285 visionmedia 36.1% + 182 TJ Holowaychuk 5.1% + 54 Aaron Heckmann 1.5% + 34 csausdev 1.0% + 26 ciaranj 0.7% + 21 Robert Sköld 0.6% + 6 Guillermo Rauch 0.2% + 3 Dav Glass 0.1% + 3 Nick Poulden 0.1% + 2 Randy Merrill 0.1% + 2 Benny Wong 0.1% + 2 Hunter Loftis 0.1% + 2 Jake Gordon 0.1% + 2 Brian McKinney 0.1% + 2 Roman Shtylman 0.1% + 2 Ben Weaver 0.1% + 2 Dave Hoover 0.1% + 2 Eivind Fjeldstad 0.1% + 2 Daniel Shaw 0.1% + 1 Matt Colyer 0.0% + 1 Pau Ramon 0.0% + 1 Pero Pejovic 0.0% + 1 Peter Rekdal Sunde 0.0% + 1 Raynos 0.0% + 1 Teng Siong Ong 0.0% + 1 Viktor Kelemen 0.0% + 1 ctide 0.0% + 1 8bitDesigner 0.0% + 1 isaacs 0.0% + 1 mgutz 0.0% + 1 pikeas 0.0% + 1 shuwatto 0.0% + 1 tstrimple 0.0% + 1 ewoudj 0.0% + 1 Adam Sanderson 0.0% + 1 Andrii Kostenko 0.0% + 1 Andy Hiew 0.0% + 1 Arpad Borsos 0.0% + 1 Ashwin Purohit 0.0% + 1 Benjen 0.0% + 1 Darren Torpey 0.0% + 1 Greg Ritter 0.0% + 1 Gregory Ritter 0.0% + 1 James Herdman 0.0% + 1 Jim Snodgrass 0.0% + 1 Joe McCann 0.0% + 1 Jonathan Dumaine 0.0% + 1 Jonathan Palardy 0.0% + 1 Jonathan Zacsh 0.0% + 1 Justin Lilly 0.0% + 1 Ken Sato 0.0% + 1 Maciej Małecki 0.0% + 1 Masahiro Hayashi 0.0% +``` + +## License + +(The MIT License) + +Copyright (c) 2009-2012 TJ Holowaychuk <tj@vision-media.ca> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/express/bin/express b/node_modules/express/bin/express new file mode 100644 index 0000000000..337c74e0ae --- /dev/null +++ b/node_modules/express/bin/express @@ -0,0 +1,422 @@ +#!/usr/bin/env node + +/** + * Module dependencies. + */ + +var exec = require('child_process').exec + , program = require('commander') + , mkdirp = require('mkdirp') + , pkg = require('../package.json') + , version = pkg.version + , os = require('os') + , fs = require('fs'); + +// CLI + +program + .version(version) + .option('-s, --sessions', 'add session support') + .option('-e, --ejs', 'add ejs engine support (defaults to jade)') + .option('-J, --jshtml', 'add jshtml engine support (defaults to jade)') + .option('-H, --hogan', 'add hogan.js engine support') + .option('-c, --css ', 'add stylesheet support (less|stylus) (defaults to plain css)') + .option('-f, --force', 'force on non-empty directory') + .parse(process.argv); + +// Path + +var path = program.args.shift() || '.'; + +// end-of-line code + +var eol = os.EOL + +// Template engine + +program.template = 'jade'; +if (program.ejs) program.template = 'ejs'; +if (program.jshtml) program.template = 'jshtml'; +if (program.hogan) program.template = 'hjs'; + +/** + * Routes index template. + */ + +var index = [ + '' + , '/*' + , ' * GET home page.' + , ' */' + , '' + , 'exports.index = function(req, res){' + , ' res.render(\'index\', { title: \'Express\' });' + , '};' +].join(eol); + +/** + * Routes users template. + */ + +var users = [ + '' + , '/*' + , ' * GET users listing.' + , ' */' + , '' + , 'exports.list = function(req, res){' + , ' res.send("respond with a resource");' + , '};' +].join(eol); + +/** + * Jade layout template. + */ + +var jadeLayout = [ + 'doctype 5' + , 'html' + , ' head' + , ' title= title' + , ' link(rel=\'stylesheet\', href=\'/stylesheets/style.css\')' + , ' body' + , ' block content' +].join(eol); + +/** + * Jade index template. + */ + +var jadeIndex = [ + 'extends layout' + , '' + , 'block content' + , ' h1= title' + , ' p Welcome to #{title}' +].join(eol); + +/** + * EJS index template. + */ + +var ejsIndex = [ + '' + , '' + , ' ' + , ' <%= title %>' + , ' ' + , ' ' + , ' ' + , '

    <%= title %>

    ' + , '

    Welcome to <%= title %>

    ' + , ' ' + , '' +].join(eol); + +/** + * JSHTML layout template. + */ + +var jshtmlLayout = [ + '' + , '' + , ' ' + , ' @write(title) ' + , ' ' + , ' ' + , ' ' + , ' @write(body)' + , ' ' + , '' +].join(eol); + +/** + * JSHTML index template. + */ + +var jshtmlIndex = [ + '

    @write(title)

    ' + , '

    Welcome to @write(title)

    ' +].join(eol); + +/** + * Hogan.js index template. + */ +var hoganIndex = [ + '' + , '' + , ' ' + , ' {{ title }}' + , ' ' + , ' ' + , ' ' + , '

    {{ title }}

    ' + , '

    Welcome to {{ title }}

    ' + , ' ' + , '' +].join(eol); + +/** + * Default css template. + */ + +var css = [ + 'body {' + , ' padding: 50px;' + , ' font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;' + , '}' + , '' + , 'a {' + , ' color: #00B7FF;' + , '}' +].join(eol); + +/** + * Default less template. + */ + +var less = [ + 'body {' + , ' padding: 50px;' + , ' font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;' + , '}' + , '' + , 'a {' + , ' color: #00B7FF;' + , '}' +].join(eol); + +/** + * Default stylus template. + */ + +var stylus = [ + 'body' + , ' padding: 50px' + , ' font: 14px "Lucida Grande", Helvetica, Arial, sans-serif' + , 'a' + , ' color: #00B7FF' +].join(eol); + +/** + * App template. + */ + +var app = [ + '' + , '/**' + , ' * Module dependencies.' + , ' */' + , '' + , 'var express = require(\'express\')' + , ' , routes = require(\'./routes\')' + , ' , user = require(\'./routes/user\')' + , ' , http = require(\'http\')' + , ' , path = require(\'path\');' + , '' + , 'var app = express();' + , '' + , '// all environments' + , 'app.set(\'port\', process.env.PORT || 3000);' + , 'app.set(\'views\', __dirname + \'/views\');' + , 'app.set(\'view engine\', \':TEMPLATE\');' + , 'app.use(express.favicon());' + , 'app.use(express.logger(\'dev\'));' + , 'app.use(express.bodyParser());' + , 'app.use(express.methodOverride());{sess}' + , 'app.use(app.router);{css}' + , 'app.use(express.static(path.join(__dirname, \'public\')));' + , '' + , '// development only' + , 'if (\'development\' == app.get(\'env\')) {' + , ' app.use(express.errorHandler());' + , '}' + , '' + , 'app.get(\'/\', routes.index);' + , 'app.get(\'/users\', user.list);' + , '' + , 'http.createServer(app).listen(app.get(\'port\'), function(){' + , ' console.log(\'Express server listening on port \' + app.get(\'port\'));' + , '});' + , '' +].join(eol); + +// Generate application + +(function createApplication(path) { + emptyDirectory(path, function(empty){ + if (empty || program.force) { + createApplicationAt(path); + } else { + program.confirm('destination is not empty, continue? ', function(ok){ + if (ok) { + process.stdin.destroy(); + createApplicationAt(path); + } else { + abort('aborting'); + } + }); + } + }); +})(path); + +/** + * Create application at the given directory `path`. + * + * @param {String} path + */ + +function createApplicationAt(path) { + console.log(); + process.on('exit', function(){ + console.log(); + console.log(' install dependencies:'); + console.log(' $ cd %s && npm install', path); + console.log(); + console.log(' run the app:'); + console.log(' $ node app'); + console.log(); + }); + + mkdir(path, function(){ + mkdir(path + '/public'); + mkdir(path + '/public/javascripts'); + mkdir(path + '/public/images'); + mkdir(path + '/public/stylesheets', function(){ + switch (program.css) { + case 'less': + write(path + '/public/stylesheets/style.less', less); + break; + case 'stylus': + write(path + '/public/stylesheets/style.styl', stylus); + break; + default: + write(path + '/public/stylesheets/style.css', css); + } + }); + + mkdir(path + '/routes', function(){ + write(path + '/routes/index.js', index); + write(path + '/routes/user.js', users); + }); + + mkdir(path + '/views', function(){ + switch (program.template) { + case 'ejs': + write(path + '/views/index.ejs', ejsIndex); + break; + case 'jade': + write(path + '/views/layout.jade', jadeLayout); + write(path + '/views/index.jade', jadeIndex); + break; + case 'jshtml': + write(path + '/views/layout.jshtml', jshtmlLayout); + write(path + '/views/index.jshtml', jshtmlIndex); + break; + case 'hjs': + write(path + '/views/index.hjs', hoganIndex); + break; + + } + }); + + // CSS Engine support + switch (program.css) { + case 'less': + app = app.replace('{css}', eol + ' app.use(require(\'less-middleware\')({ src: __dirname + \'/public\' }));'); + break; + case 'stylus': + app = app.replace('{css}', eol + ' app.use(require(\'stylus\').middleware(__dirname + \'/public\'));'); + break; + default: + app = app.replace('{css}', ''); + } + + // Session support + app = app.replace('{sess}', program.sessions + ? eol + ' app.use(express.cookieParser(\'your secret here\'));' + eol + ' app.use(express.session());' + : ''); + + // Template support + app = app.replace(':TEMPLATE', program.template); + + // package.json + var pkg = { + name: 'application-name' + , version: '0.0.1' + , private: true + , scripts: { start: 'node app.js' } + , dependencies: { + express: version + } + } + + if (program.template) pkg.dependencies[program.template] = '*'; + + // CSS Engine support + switch (program.css) { + case 'less': + pkg.dependencies['less-middleware'] = '*'; + break; + default: + if (program.css) { + pkg.dependencies[program.css] = '*'; + } + } + + write(path + '/package.json', JSON.stringify(pkg, null, 2)); + write(path + '/app.js', app); + }); +} + +/** + * Check if the given directory `path` is empty. + * + * @param {String} path + * @param {Function} fn + */ + +function emptyDirectory(path, fn) { + fs.readdir(path, function(err, files){ + if (err && 'ENOENT' != err.code) throw err; + fn(!files || !files.length); + }); +} + +/** + * echo str > path. + * + * @param {String} path + * @param {String} str + */ + +function write(path, str) { + fs.writeFile(path, str); + console.log(' \x1b[36mcreate\x1b[0m : ' + path); +} + +/** + * Mkdir -p. + * + * @param {String} path + * @param {Function} fn + */ + +function mkdir(path, fn) { + mkdirp(path, 0755, function(err){ + if (err) throw err; + console.log(' \033[36mcreate\033[0m : ' + path); + fn && fn(); + }); +} + +/** + * Exit with the given `str`. + * + * @param {String} str + */ + +function abort(str) { + console.error(str); + process.exit(1); +} diff --git a/node_modules/express/client.js b/node_modules/express/client.js new file mode 100644 index 0000000000..8984c44d2d --- /dev/null +++ b/node_modules/express/client.js @@ -0,0 +1,25 @@ + +var http = require('http'); + +var times = 50; + +while (times--) { + var req = http.request({ + port: 3000 + , method: 'POST' + , headers: { 'Content-Type': 'application/x-www-form-urlencoded' } + }); + + req.on('response', function(res){ + console.log(res.statusCode); + }); + + var n = 500000; + while (n--) { + req.write('foo=bar&bar=baz&'); + } + + req.write('foo=bar&bar=baz'); + + req.end(); +} \ No newline at end of file diff --git a/node_modules/express/index.js b/node_modules/express/index.js new file mode 100644 index 0000000000..bfe99345b7 --- /dev/null +++ b/node_modules/express/index.js @@ -0,0 +1,4 @@ + +module.exports = process.env.EXPRESS_COV + ? require('./lib-cov/express') + : require('./lib/express'); \ No newline at end of file diff --git a/node_modules/express/lib/application.js b/node_modules/express/lib/application.js new file mode 100644 index 0000000000..e841c1289d --- /dev/null +++ b/node_modules/express/lib/application.js @@ -0,0 +1,531 @@ +/** + * Module dependencies. + */ + +var connect = require('connect') + , Router = require('./router') + , methods = require('methods') + , middleware = require('./middleware') + , debug = require('debug')('express:application') + , locals = require('./utils').locals + , View = require('./view') + , utils = connect.utils + , path = require('path') + , http = require('http') + , join = path.join; + +/** + * Application prototype. + */ + +var app = exports = module.exports = {}; + +/** + * Initialize the server. + * + * - setup default configuration + * - setup default middleware + * - setup route reflection methods + * + * @api private + */ + +app.init = function(){ + this.cache = {}; + this.settings = {}; + this.engines = {}; + this.defaultConfiguration(); +}; + +/** + * Initialize application configuration. + * + * @api private + */ + +app.defaultConfiguration = function(){ + // default settings + this.enable('x-powered-by'); + this.set('env', process.env.NODE_ENV || 'development'); + this.set('subdomain offset', 2); + debug('booting in %s mode', this.get('env')); + + // implicit middleware + this.use(connect.query()); + this.use(middleware.init(this)); + + // inherit protos + this.on('mount', function(parent){ + this.request.__proto__ = parent.request; + this.response.__proto__ = parent.response; + this.engines.__proto__ = parent.engines; + this.settings.__proto__ = parent.settings; + }); + + // router + this._router = new Router(this); + this.routes = this._router.map; + this.__defineGetter__('router', function(){ + this._usedRouter = true; + this._router.caseSensitive = this.enabled('case sensitive routing'); + this._router.strict = this.enabled('strict routing'); + return this._router.middleware; + }); + + // setup locals + this.locals = locals(this); + + // default locals + this.locals.settings = this.settings; + + // default configuration + this.set('view', View); + this.set('views', process.cwd() + '/views'); + this.set('jsonp callback name', 'callback'); + + this.configure('development', function(){ + this.set('json spaces', 2); + }); + + this.configure('production', function(){ + this.enable('view cache'); + }); +}; + +/** + * Proxy `connect#use()` to apply settings to + * mounted applications. + * + * @param {String|Function|Server} route + * @param {Function|Server} fn + * @return {app} for chaining + * @api public + */ + +app.use = function(route, fn){ + var app; + + // default route to '/' + if ('string' != typeof route) fn = route, route = '/'; + + // express app + if (fn.handle && fn.set) app = fn; + + // restore .app property on req and res + if (app) { + app.route = route; + fn = function(req, res, next) { + var orig = req.app; + app.handle(req, res, function(err){ + req.app = res.app = orig; + req.__proto__ = orig.request; + res.__proto__ = orig.response; + next(err); + }); + }; + } + + connect.proto.use.call(this, route, fn); + + // mounted an app + if (app) { + app.parent = this; + app.emit('mount', this); + } + + return this; +}; + +/** + * Register the given template engine callback `fn` + * as `ext`. + * + * By default will `require()` the engine based on the + * file extension. For example if you try to render + * a "foo.jade" file Express will invoke the following internally: + * + * app.engine('jade', require('jade').__express); + * + * For engines that do not provide `.__express` out of the box, + * or if you wish to "map" a different extension to the template engine + * you may use this method. For example mapping the EJS template engine to + * ".html" files: + * + * app.engine('html', require('ejs').renderFile); + * + * In this case EJS provides a `.renderFile()` method with + * the same signature that Express expects: `(path, options, callback)`, + * though note that it aliases this method as `ejs.__express` internally + * so if you're using ".ejs" extensions you dont need to do anything. + * + * Some template engines do not follow this convention, the + * [Consolidate.js](https://github.com/visionmedia/consolidate.js) + * library was created to map all of node's popular template + * engines to follow this convention, thus allowing them to + * work seamlessly within Express. + * + * @param {String} ext + * @param {Function} fn + * @return {app} for chaining + * @api public + */ + +app.engine = function(ext, fn){ + if ('function' != typeof fn) throw new Error('callback function required'); + if ('.' != ext[0]) ext = '.' + ext; + this.engines[ext] = fn; + return this; +}; + +/** + * Map the given param placeholder `name`(s) to the given callback(s). + * + * Parameter mapping is used to provide pre-conditions to routes + * which use normalized placeholders. For example a _:user_id_ parameter + * could automatically load a user's information from the database without + * any additional code, + * + * The callback uses the samesignature as middleware, the only differencing + * being that the value of the placeholder is passed, in this case the _id_ + * of the user. Once the `next()` function is invoked, just like middleware + * it will continue on to execute the route, or subsequent parameter functions. + * + * app.param('user_id', function(req, res, next, id){ + * User.find(id, function(err, user){ + * if (err) { + * next(err); + * } else if (user) { + * req.user = user; + * next(); + * } else { + * next(new Error('failed to load user')); + * } + * }); + * }); + * + * @param {String|Array} name + * @param {Function} fn + * @return {app} for chaining + * @api public + */ + +app.param = function(name, fn){ + var self = this + , fns = [].slice.call(arguments, 1); + + // array + if (Array.isArray(name)) { + name.forEach(function(name){ + fns.forEach(function(fn){ + self.param(name, fn); + }); + }); + // param logic + } else if ('function' == typeof name) { + this._router.param(name); + // single + } else { + if (':' == name[0]) name = name.substr(1); + fns.forEach(function(fn){ + self._router.param(name, fn); + }); + } + + return this; +}; + +/** + * Assign `setting` to `val`, or return `setting`'s value. + * + * app.set('foo', 'bar'); + * app.get('foo'); + * // => "bar" + * + * Mounted servers inherit their parent server's settings. + * + * @param {String} setting + * @param {String} val + * @return {Server} for chaining + * @api public + */ + +app.set = function(setting, val){ + if (1 == arguments.length) { + return this.settings[setting]; + } else { + this.settings[setting] = val; + return this; + } +}; + +/** + * Return the app's absolute pathname + * based on the parent(s) that have + * mounted it. + * + * For example if the application was + * mounted as "/admin", which itself + * was mounted as "/blog" then the + * return value would be "/blog/admin". + * + * @return {String} + * @api private + */ + +app.path = function(){ + return this.parent + ? this.parent.path() + this.route + : ''; +}; + +/** + * Check if `setting` is enabled (truthy). + * + * app.enabled('foo') + * // => false + * + * app.enable('foo') + * app.enabled('foo') + * // => true + * + * @param {String} setting + * @return {Boolean} + * @api public + */ + +app.enabled = function(setting){ + return !!this.set(setting); +}; + +/** + * Check if `setting` is disabled. + * + * app.disabled('foo') + * // => true + * + * app.enable('foo') + * app.disabled('foo') + * // => false + * + * @param {String} setting + * @return {Boolean} + * @api public + */ + +app.disabled = function(setting){ + return !this.set(setting); +}; + +/** + * Enable `setting`. + * + * @param {String} setting + * @return {app} for chaining + * @api public + */ + +app.enable = function(setting){ + return this.set(setting, true); +}; + +/** + * Disable `setting`. + * + * @param {String} setting + * @return {app} for chaining + * @api public + */ + +app.disable = function(setting){ + return this.set(setting, false); +}; + +/** + * Configure callback for zero or more envs, + * when no `env` is specified that callback will + * be invoked for all environments. Any combination + * can be used multiple times, in any order desired. + * + * Examples: + * + * app.configure(function(){ + * // executed for all envs + * }); + * + * app.configure('stage', function(){ + * // executed staging env + * }); + * + * app.configure('stage', 'production', function(){ + * // executed for stage and production + * }); + * + * Note: + * + * These callbacks are invoked immediately, and + * are effectively sugar for the following: + * + * var env = process.env.NODE_ENV || 'development'; + * + * switch (env) { + * case 'development': + * ... + * break; + * case 'stage': + * ... + * break; + * case 'production': + * ... + * break; + * } + * + * @param {String} env... + * @param {Function} fn + * @return {app} for chaining + * @api public + */ + +app.configure = function(env, fn){ + var envs = 'all' + , args = [].slice.call(arguments); + fn = args.pop(); + if (args.length) envs = args; + if ('all' == envs || ~envs.indexOf(this.settings.env)) fn.call(this); + return this; +}; + +/** + * Delegate `.VERB(...)` calls to `router.VERB(...)`. + */ + +methods.forEach(function(method){ + app[method] = function(path){ + if ('get' == method && 1 == arguments.length) return this.set(path); + + // if no router attached yet, attach the router + if (!this._usedRouter) this.use(this.router); + + // setup route + this._router[method].apply(this._router, arguments); + return this; + }; +}); + +/** + * Special-cased "all" method, applying the given route `path`, + * middleware, and callback to _every_ HTTP method. + * + * @param {String} path + * @param {Function} ... + * @return {app} for chaining + * @api public + */ + +app.all = function(path){ + var args = arguments; + methods.forEach(function(method){ + app[method].apply(this, args); + }, this); + return this; +}; + +// del -> delete alias + +app.del = app.delete; + +/** + * Render the given view `name` name with `options` + * and a callback accepting an error and the + * rendered template string. + * + * Example: + * + * app.render('email', { name: 'Tobi' }, function(err, html){ + * // ... + * }) + * + * @param {String} name + * @param {String|Function} options or fn + * @param {Function} fn + * @api public + */ + +app.render = function(name, options, fn){ + var opts = {} + , cache = this.cache + , engines = this.engines + , view; + + // support callback function as second arg + if ('function' == typeof options) { + fn = options, options = {}; + } + + // merge app.locals + utils.merge(opts, this.locals); + + // merge options._locals + if (options._locals) utils.merge(opts, options._locals); + + // merge options + utils.merge(opts, options); + + // set .cache unless explicitly provided + opts.cache = null == opts.cache + ? this.enabled('view cache') + : opts.cache; + + // primed cache + if (opts.cache) view = cache[name]; + + // view + if (!view) { + view = new (this.get('view'))(name, { + defaultEngine: this.get('view engine'), + root: this.get('views'), + engines: engines + }); + + if (!view.path) { + var err = new Error('Failed to lookup view "' + name + '"'); + err.view = view; + return fn(err); + } + + // prime the cache + if (opts.cache) cache[name] = view; + } + + // render + try { + view.render(opts, fn); + } catch (err) { + fn(err); + } +}; + +/** + * Listen for connections. + * + * A node `http.Server` is returned, with this + * application (which is a `Function`) as its + * callback. If you wish to create both an HTTP + * and HTTPS server you may do so with the "http" + * and "https" modules as shown here: + * + * var http = require('http') + * , https = require('https') + * , express = require('express') + * , app = express(); + * + * http.createServer(app).listen(80); + * https.createServer({ ... }, app).listen(443); + * + * @return {http.Server} + * @api public + */ + +app.listen = function(){ + var server = http.createServer(this); + return server.listen.apply(server, arguments); +}; diff --git a/node_modules/express/lib/express.js b/node_modules/express/lib/express.js new file mode 100644 index 0000000000..ab55889077 --- /dev/null +++ b/node_modules/express/lib/express.js @@ -0,0 +1,92 @@ +/** + * Module dependencies. + */ + +var connect = require('connect') + , proto = require('./application') + , Route = require('./router/route') + , Router = require('./router') + , req = require('./request') + , res = require('./response') + , utils = connect.utils; + +/** + * Expose `createApplication()`. + */ + +exports = module.exports = createApplication; + +/** + * Framework version. + */ + +exports.version = '3.2.0'; + +/** + * Expose mime. + */ + +exports.mime = connect.mime; + +/** + * Create an express application. + * + * @return {Function} + * @api public + */ + +function createApplication() { + var app = connect(); + utils.merge(app, proto); + app.request = { __proto__: req }; + app.response = { __proto__: res }; + app.init(); + return app; +} + +/** + * Expose connect.middleware as express.* + * for example `express.logger` etc. + */ + +for (var key in connect.middleware) { + Object.defineProperty( + exports + , key + , Object.getOwnPropertyDescriptor(connect.middleware, key)); +} + +/** + * Error on createServer(). + */ + +exports.createServer = function(){ + console.warn('Warning: express.createServer() is deprecated, express'); + console.warn('applications no longer inherit from http.Server,'); + console.warn('please use:'); + console.warn(''); + console.warn(' var express = require("express");'); + console.warn(' var app = express();'); + console.warn(''); + return createApplication(); +}; + +/** + * Expose the prototypes. + */ + +exports.application = proto; +exports.request = req; +exports.response = res; + +/** + * Expose constructors. + */ + +exports.Route = Route; +exports.Router = Router; + +// Error handler title + +exports.errorHandler.title = 'Express'; + diff --git a/node_modules/express/lib/middleware.js b/node_modules/express/lib/middleware.js new file mode 100644 index 0000000000..308c5bb6d5 --- /dev/null +++ b/node_modules/express/lib/middleware.js @@ -0,0 +1,33 @@ + +/** + * Module dependencies. + */ + +var utils = require('./utils'); + +/** + * Initialization middleware, exposing the + * request and response to eachother, as well + * as defaulting the X-Powered-By header field. + * + * @param {Function} app + * @return {Function} + * @api private + */ + +exports.init = function(app){ + return function expressInit(req, res, next){ + req.app = res.app = app; + if (app.enabled('x-powered-by')) res.setHeader('X-Powered-By', 'Express'); + req.res = res; + res.req = req; + req.next = next; + + req.__proto__ = app.request; + res.__proto__ = app.response; + + res.locals = res.locals || utils.locals(res); + + next(); + } +}; diff --git a/node_modules/express/lib/request.js b/node_modules/express/lib/request.js new file mode 100644 index 0000000000..fd201e3472 --- /dev/null +++ b/node_modules/express/lib/request.js @@ -0,0 +1,526 @@ + +/** + * Module dependencies. + */ + +var http = require('http') + , utils = require('./utils') + , connect = require('connect') + , fresh = require('fresh') + , parseRange = require('range-parser') + , parse = connect.utils.parseUrl + , mime = connect.mime; + +/** + * Request prototype. + */ + +var req = exports = module.exports = { + __proto__: http.IncomingMessage.prototype +}; + +/** + * Return request header. + * + * The `Referrer` header field is special-cased, + * both `Referrer` and `Referer` are interchangeable. + * + * Examples: + * + * req.get('Content-Type'); + * // => "text/plain" + * + * req.get('content-type'); + * // => "text/plain" + * + * req.get('Something'); + * // => undefined + * + * Aliased as `req.header()`. + * + * @param {String} name + * @return {String} + * @api public + */ + +req.get = +req.header = function(name){ + switch (name = name.toLowerCase()) { + case 'referer': + case 'referrer': + return this.headers.referrer + || this.headers.referer; + default: + return this.headers[name]; + } +}; + +/** + * Check if the given `type(s)` is acceptable, returning + * the best match when true, otherwise `undefined`, in which + * case you should respond with 406 "Not Acceptable". + * + * The `type` value may be a single mime type string + * such as "application/json", the extension name + * such as "json", a comma-delimted list such as "json, html, text/plain", + * or an array `["json", "html", "text/plain"]`. When a list + * or array is given the _best_ match, if any is returned. + * + * Examples: + * + * // Accept: text/html + * req.accepts('html'); + * // => "html" + * + * // Accept: text/*, application/json + * req.accepts('html'); + * // => "html" + * req.accepts('text/html'); + * // => "text/html" + * req.accepts('json, text'); + * // => "json" + * req.accepts('application/json'); + * // => "application/json" + * + * // Accept: text/*, application/json + * req.accepts('image/png'); + * req.accepts('png'); + * // => undefined + * + * // Accept: text/*;q=.5, application/json + * req.accepts(['html', 'json']); + * req.accepts('html, json'); + * // => "json" + * + * @param {String|Array} type(s) + * @return {String} + * @api public + */ + +req.accepts = function(type){ + return utils.accepts(type, this.get('Accept')); +}; + +/** + * Check if the given `encoding` is accepted. + * + * @param {String} encoding + * @return {Boolean} + * @api public + */ + +req.acceptsEncoding = function(encoding){ + return ~this.acceptedEncodings.indexOf(encoding); +}; + +/** + * Check if the given `charset` is acceptable, + * otherwise you should respond with 406 "Not Acceptable". + * + * @param {String} charset + * @return {Boolean} + * @api public + */ + +req.acceptsCharset = function(charset){ + var accepted = this.acceptedCharsets; + return accepted.length + ? ~accepted.indexOf(charset) + : true; +}; + +/** + * Check if the given `lang` is acceptable, + * otherwise you should respond with 406 "Not Acceptable". + * + * @param {String} lang + * @return {Boolean} + * @api public + */ + +req.acceptsLanguage = function(lang){ + var accepted = this.acceptedLanguages; + return accepted.length + ? ~accepted.indexOf(lang) + : true; +}; + +/** + * Parse Range header field, + * capping to the given `size`. + * + * Unspecified ranges such as "0-" require + * knowledge of your resource length. In + * the case of a byte range this is of course + * the total number of bytes. If the Range + * header field is not given `null` is returned, + * `-1` when unsatisfiable, `-2` when syntactically invalid. + * + * NOTE: remember that ranges are inclusive, so + * for example "Range: users=0-3" should respond + * with 4 users when available, not 3. + * + * @param {Number} size + * @return {Array} + * @api public + */ + +req.range = function(size){ + var range = this.get('Range'); + if (!range) return; + return parseRange(size, range); +}; + +/** + * Return an array of encodings. + * + * Examples: + * + * ['gzip', 'deflate'] + * + * @return {Array} + * @api public + */ + +req.__defineGetter__('acceptedEncodings', function(){ + var accept = this.get('Accept-Encoding'); + return accept + ? accept.trim().split(/ *, */) + : []; +}); + +/** + * Return an array of Accepted media types + * ordered from highest quality to lowest. + * + * Examples: + * + * [ { value: 'application/json', + * quality: 1, + * type: 'application', + * subtype: 'json' }, + * { value: 'text/html', + * quality: 0.5, + * type: 'text', + * subtype: 'html' } ] + * + * @return {Array} + * @api public + */ + +req.__defineGetter__('accepted', function(){ + var accept = this.get('Accept'); + return accept + ? utils.parseAccept(accept) + : []; +}); + +/** + * Return an array of Accepted languages + * ordered from highest quality to lowest. + * + * Examples: + * + * Accept-Language: en;q=.5, en-us + * ['en-us', 'en'] + * + * @return {Array} + * @api public + */ + +req.__defineGetter__('acceptedLanguages', function(){ + var accept = this.get('Accept-Language'); + return accept + ? utils + .parseParams(accept) + .map(function(obj){ + return obj.value; + }) + : []; +}); + +/** + * Return an array of Accepted charsets + * ordered from highest quality to lowest. + * + * Examples: + * + * Accept-Charset: iso-8859-5;q=.2, unicode-1-1;q=0.8 + * ['unicode-1-1', 'iso-8859-5'] + * + * @return {Array} + * @api public + */ + +req.__defineGetter__('acceptedCharsets', function(){ + var accept = this.get('Accept-Charset'); + return accept + ? utils + .parseParams(accept) + .map(function(obj){ + return obj.value; + }) + : []; +}); + +/** + * Return the value of param `name` when present or `defaultValue`. + * + * - Checks route placeholders, ex: _/user/:id_ + * - Checks body params, ex: id=12, {"id":12} + * - Checks query string params, ex: ?id=12 + * + * To utilize request bodies, `req.body` + * should be an object. This can be done by using + * the `connect.bodyParser()` middleware. + * + * @param {String} name + * @param {Mixed} [defaultValue] + * @return {String} + * @api public + */ + +req.param = function(name, defaultValue){ + var params = this.params || {}; + var body = this.body || {}; + var query = this.query || {}; + if (null != params[name] && params.hasOwnProperty(name)) return params[name]; + if (null != body[name]) return body[name]; + if (null != query[name]) return query[name]; + return defaultValue; +}; + +/** + * Check if the incoming request contains the "Content-Type" + * header field, and it contains the give mime `type`. + * + * Examples: + * + * // With Content-Type: text/html; charset=utf-8 + * req.is('html'); + * req.is('text/html'); + * req.is('text/*'); + * // => true + * + * // When Content-Type is application/json + * req.is('json'); + * req.is('application/json'); + * req.is('application/*'); + * // => true + * + * req.is('html'); + * // => false + * + * @param {String} type + * @return {Boolean} + * @api public + */ + +req.is = function(type){ + var ct = this.get('Content-Type'); + if (!ct) return false; + ct = ct.split(';')[0]; + if (!~type.indexOf('/')) type = mime.lookup(type); + if (~type.indexOf('*')) { + type = type.split('/'); + ct = ct.split('/'); + if ('*' == type[0] && type[1] == ct[1]) return true; + if ('*' == type[1] && type[0] == ct[0]) return true; + return false; + } + return !! ~ct.indexOf(type); +}; + +/** + * Return the protocol string "http" or "https" + * when requested with TLS. When the "trust proxy" + * setting is enabled the "X-Forwarded-Proto" header + * field will be trusted. If you're running behind + * a reverse proxy that supplies https for you this + * may be enabled. + * + * @return {String} + * @api public + */ + +req.__defineGetter__('protocol', function(){ + var trustProxy = this.app.get('trust proxy'); + return this.connection.encrypted + ? 'https' + : trustProxy + ? (this.get('X-Forwarded-Proto') || 'http') + : 'http'; +}); + +/** + * Short-hand for: + * + * req.protocol == 'https' + * + * @return {Boolean} + * @api public + */ + +req.__defineGetter__('secure', function(){ + return 'https' == this.protocol; +}); + +/** + * Return the remote address, or when + * "trust proxy" is `true` return + * the upstream addr. + * + * @return {String} + * @api public + */ + +req.__defineGetter__('ip', function(){ + return this.ips[0] || this.connection.remoteAddress; +}); + +/** + * When "trust proxy" is `true`, parse + * the "X-Forwarded-For" ip address list. + * + * For example if the value were "client, proxy1, proxy2" + * you would receive the array `["client", "proxy1", "proxy2"]` + * where "proxy2" is the furthest down-stream. + * + * @return {Array} + * @api public + */ + +req.__defineGetter__('ips', function(){ + var trustProxy = this.app.get('trust proxy'); + var val = this.get('X-Forwarded-For'); + return trustProxy && val + ? val.split(/ *, */) + : []; +}); + +/** + * Return basic auth credentials. + * + * Examples: + * + * // http://tobi:hello@example.com + * req.auth + * // => { username: 'tobi', password: 'hello' } + * + * @return {Object} or undefined + * @api public + */ + +req.__defineGetter__('auth', function(){ + // missing + var auth = this.get('Authorization'); + if (!auth) return; + + // malformed + var parts = auth.split(' '); + if ('basic' != parts[0].toLowerCase()) return; + if (!parts[1]) return; + auth = parts[1]; + + // credentials + auth = new Buffer(auth, 'base64').toString().match(/^([^:]*):(.*)$/); + if (!auth) return; + return { username: auth[1], password: auth[2] }; +}); + +/** + * Return subdomains as an array. + * + * Subdomains are the dot-separated parts of the host before the main domain of + * the app. By default, the domain of the app is assumed to be the last two + * parts of the host. This can be changed by setting "subdomain offset". + * + * For example, if the domain is "tobi.ferrets.example.com": + * If "subdomain offset" is not set, req.subdomains is `["ferrets", "tobi"]`. + * If "subdomain offset" is 3, req.subdomains is `["tobi"]`. + * + * @return {Array} + * @api public + */ + +req.__defineGetter__('subdomains', function(){ + var offset = this.app.get('subdomain offset'); + return this.get('Host') + .split('.') + .reverse() + .slice(offset); +}); + +/** + * Short-hand for `url.parse(req.url).pathname`. + * + * @return {String} + * @api public + */ + +req.__defineGetter__('path', function(){ + return parse(this).pathname; +}); + +/** + * Parse the "Host" header field hostname. + * + * @return {String} + * @api public + */ + +req.__defineGetter__('host', function(){ + var trustProxy = this.app.get('trust proxy'); + var host = trustProxy && this.get('X-Forwarded-Host'); + host = host || this.get('Host'); + return host.split(':')[0]; +}); + +/** + * Check if the request is fresh, aka + * Last-Modified and/or the ETag + * still match. + * + * @return {Boolean} + * @api public + */ + +req.__defineGetter__('fresh', function(){ + var method = this.method; + var s = this.res.statusCode; + + // GET or HEAD for weak freshness validation only + if ('GET' != method && 'HEAD' != method) return false; + + // 2xx or 304 as per rfc2616 14.26 + if ((s >= 200 && s < 300) || 304 == s) { + return fresh(this.headers, this.res._headers); + } + + return false; +}); + +/** + * Check if the request is stale, aka + * "Last-Modified" and / or the "ETag" for the + * resource has changed. + * + * @return {Boolean} + * @api public + */ + +req.__defineGetter__('stale', function(){ + return !this.fresh; +}); + +/** + * Check if the request was an _XMLHttpRequest_. + * + * @return {Boolean} + * @api public + */ + +req.__defineGetter__('xhr', function(){ + var val = this.get('X-Requested-With') || ''; + return 'xmlhttprequest' == val.toLowerCase(); +}); diff --git a/node_modules/express/lib/response.js b/node_modules/express/lib/response.js new file mode 100644 index 0000000000..50fcd051af --- /dev/null +++ b/node_modules/express/lib/response.js @@ -0,0 +1,756 @@ +/** + * Module dependencies. + */ + +var http = require('http') + , path = require('path') + , connect = require('connect') + , utils = connect.utils + , sign = require('cookie-signature').sign + , normalizeType = require('./utils').normalizeType + , normalizeTypes = require('./utils').normalizeTypes + , etag = require('./utils').etag + , statusCodes = http.STATUS_CODES + , cookie = require('cookie') + , send = require('send') + , mime = connect.mime + , basename = path.basename + , extname = path.extname + , join = path.join; + +/** + * Response prototype. + */ + +var res = module.exports = { + __proto__: http.ServerResponse.prototype +}; + +/** + * Set status `code`. + * + * @param {Number} code + * @return {ServerResponse} + * @api public + */ + +res.status = function(code){ + this.statusCode = code; + return this; +}; + +/** + * Set Link header field with the given `links`. + * + * Examples: + * + * res.links({ + * next: 'http://api.example.com/users?page=2', + * last: 'http://api.example.com/users?page=5' + * }); + * + * @param {Object} links + * @return {ServerResponse} + * @api public + */ + +res.links = function(links){ + return this.set('Link', Object.keys(links).map(function(rel){ + return '<' + links[rel] + '>; rel="' + rel + '"'; + }).join(', ')); +}; + +/** + * Send a response. + * + * Examples: + * + * res.send(new Buffer('wahoo')); + * res.send({ some: 'json' }); + * res.send('

    some html

    '); + * res.send(404, 'Sorry, cant find that'); + * res.send(404); + * + * @param {Mixed} body or status + * @param {Mixed} body + * @return {ServerResponse} + * @api public + */ + +res.send = function(body){ + var req = this.req + , head = 'HEAD' == req.method + , len; + + // allow status / body + if (2 == arguments.length) { + // res.send(body, status) backwards compat + if ('number' != typeof body && 'number' == typeof arguments[1]) { + this.statusCode = arguments[1]; + } else { + this.statusCode = body; + body = arguments[1]; + } + } + + switch (typeof body) { + // response status + case 'number': + this.get('Content-Type') || this.type('txt'); + this.statusCode = body; + body = http.STATUS_CODES[body]; + break; + // string defaulting to html + case 'string': + if (!this.get('Content-Type')) { + this.charset = this.charset || 'utf-8'; + this.type('html'); + } + break; + case 'boolean': + case 'object': + if (null == body) { + body = ''; + } else if (Buffer.isBuffer(body)) { + this.get('Content-Type') || this.type('bin'); + } else { + return this.json(body); + } + break; + } + + // populate Content-Length + if (undefined !== body && !this.get('Content-Length')) { + this.set('Content-Length', len = Buffer.isBuffer(body) + ? body.length + : Buffer.byteLength(body)); + } + + // ETag support + // TODO: W/ support + if (len > 1024) { + if (!this.get('ETag')) { + this.set('ETag', etag(body)); + } + } + + // freshness + if (req.fresh) this.statusCode = 304; + + // strip irrelevant headers + if (204 == this.statusCode || 304 == this.statusCode) { + this.removeHeader('Content-Type'); + this.removeHeader('Content-Length'); + this.removeHeader('Transfer-Encoding'); + body = ''; + } + + // respond + this.end(head ? null : body); + return this; +}; + +/** + * Send JSON response. + * + * Examples: + * + * res.json(null); + * res.json({ user: 'tj' }); + * res.json(500, 'oh noes!'); + * res.json(404, 'I dont have that'); + * + * @param {Mixed} obj or status + * @param {Mixed} obj + * @return {ServerResponse} + * @api public + */ + +res.json = function(obj){ + // allow status / body + if (2 == arguments.length) { + // res.json(body, status) backwards compat + if ('number' == typeof arguments[1]) { + this.statusCode = arguments[1]; + } else { + this.statusCode = obj; + obj = arguments[1]; + } + } + + // settings + var app = this.app; + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = JSON.stringify(obj, replacer, spaces); + + // content-type + this.charset = this.charset || 'utf-8'; + this.get('Content-Type') || this.set('Content-Type', 'application/json'); + + return this.send(body); +}; + +/** + * Send JSON response with JSONP callback support. + * + * Examples: + * + * res.jsonp(null); + * res.jsonp({ user: 'tj' }); + * res.jsonp(500, 'oh noes!'); + * res.jsonp(404, 'I dont have that'); + * + * @param {Mixed} obj or status + * @param {Mixed} obj + * @return {ServerResponse} + * @api public + */ + +res.jsonp = function(obj){ + // allow status / body + if (2 == arguments.length) { + // res.json(body, status) backwards compat + if ('number' == typeof arguments[1]) { + this.statusCode = arguments[1]; + } else { + this.statusCode = obj; + obj = arguments[1]; + } + } + + // settings + var app = this.app; + var replacer = app.get('json replacer'); + var spaces = app.get('json spaces'); + var body = JSON.stringify(obj, replacer, spaces) + .replace(/\u2028/g, '\\u2028') + .replace(/\u2029/g, '\\u2029'); + var callback = this.req.query[app.get('jsonp callback name')]; + + // content-type + this.charset = this.charset || 'utf-8'; + this.set('Content-Type', 'application/json'); + + // jsonp + if (callback) { + this.set('Content-Type', 'text/javascript'); + var cb = callback.replace(/[^\[\]\w$.]/g, ''); + body = cb + ' && ' + cb + '(' + body + ');'; + } + + return this.send(body); +}; + +/** + * Transfer the file at the given `path`. + * + * Automatically sets the _Content-Type_ response header field. + * The callback `fn(err)` is invoked when the transfer is complete + * or when an error occurs. Be sure to check `res.sentHeader` + * if you wish to attempt responding, as the header and some data + * may have already been transferred. + * + * Options: + * + * - `maxAge` defaulting to 0 + * - `root` root directory for relative filenames + * + * Examples: + * + * The following example illustrates how `res.sendfile()` may + * be used as an alternative for the `static()` middleware for + * dynamic situations. The code backing `res.sendfile()` is actually + * the same code, so HTTP cache support etc is identical. + * + * app.get('/user/:uid/photos/:file', function(req, res){ + * var uid = req.params.uid + * , file = req.params.file; + * + * req.user.mayViewFilesFrom(uid, function(yes){ + * if (yes) { + * res.sendfile('/uploads/' + uid + '/' + file); + * } else { + * res.send(403, 'Sorry! you cant see that.'); + * } + * }); + * }); + * + * @param {String} path + * @param {Object|Function} options or fn + * @param {Function} fn + * @api public + */ + +res.sendfile = function(path, options, fn){ + var self = this + , req = self.req + , next = this.req.next + , options = options || {} + , done; + + // support function as second arg + if ('function' == typeof options) { + fn = options; + options = {}; + } + + // socket errors + req.socket.on('error', error); + + // errors + function error(err) { + if (done) return; + done = true; + + // clean up + cleanup(); + if (!self.headerSent) self.removeHeader('Content-Disposition'); + + // callback available + if (fn) return fn(err); + + // list in limbo if there's no callback + if (self.headerSent) return; + + // delegate + next(err); + } + + // streaming + function stream() { + if (done) return; + cleanup(); + if (fn) self.on('finish', fn); + } + + // cleanup + function cleanup() { + req.socket.removeListener('error', error); + } + + // transfer + var file = send(req, path); + if (options.root) file.root(options.root); + file.maxage(options.maxAge || 0); + file.on('error', error); + file.on('directory', next); + file.on('stream', stream); + file.pipe(this); + this.on('finish', cleanup); +}; + +/** + * Transfer the file at the given `path` as an attachment. + * + * Optionally providing an alternate attachment `filename`, + * and optional callback `fn(err)`. The callback is invoked + * when the data transfer is complete, or when an error has + * ocurred. Be sure to check `res.headerSent` if you plan to respond. + * + * This method uses `res.sendfile()`. + * + * @param {String} path + * @param {String|Function} filename or fn + * @param {Function} fn + * @api public + */ + +res.download = function(path, filename, fn){ + // support function as second arg + if ('function' == typeof filename) { + fn = filename; + filename = null; + } + + filename = filename || path; + this.set('Content-Disposition', 'attachment; filename="' + basename(filename) + '"'); + return this.sendfile(path, fn); +}; + +/** + * Set _Content-Type_ response header with `type` through `mime.lookup()` + * when it does not contain "/", or set the Content-Type to `type` otherwise. + * + * Examples: + * + * res.type('.html'); + * res.type('html'); + * res.type('json'); + * res.type('application/json'); + * res.type('png'); + * + * @param {String} type + * @return {ServerResponse} for chaining + * @api public + */ + +res.contentType = +res.type = function(type){ + return this.set('Content-Type', ~type.indexOf('/') + ? type + : mime.lookup(type)); +}; + +/** + * Respond to the Acceptable formats using an `obj` + * of mime-type callbacks. + * + * This method uses `req.accepted`, an array of + * acceptable types ordered by their quality values. + * When "Accept" is not present the _first_ callback + * is invoked, otherwise the first match is used. When + * no match is performed the server responds with + * 406 "Not Acceptable". + * + * Content-Type is set for you, however if you choose + * you may alter this within the callback using `res.type()` + * or `res.set('Content-Type', ...)`. + * + * res.format({ + * 'text/plain': function(){ + * res.send('hey'); + * }, + * + * 'text/html': function(){ + * res.send('

    hey

    '); + * }, + * + * 'appliation/json': function(){ + * res.send({ message: 'hey' }); + * } + * }); + * + * In addition to canonicalized MIME types you may + * also use extnames mapped to these types: + * + * res.format({ + * text: function(){ + * res.send('hey'); + * }, + * + * html: function(){ + * res.send('

    hey

    '); + * }, + * + * json: function(){ + * res.send({ message: 'hey' }); + * } + * }); + * + * By default Express passes an `Error` + * with a `.status` of 406 to `next(err)` + * if a match is not made. If you provide + * a `.default` callback it will be invoked + * instead. + * + * @param {Object} obj + * @return {ServerResponse} for chaining + * @api public + */ + +res.format = function(obj){ + var req = this.req + , next = req.next; + + var fn = obj.default; + if (fn) delete obj.default; + var keys = Object.keys(obj); + + var key = req.accepts(keys); + + this.set('Vary', 'Accept'); + + if (key) { + this.set('Content-Type', normalizeType(key).value); + obj[key](req, this, next); + } else if (fn) { + fn(); + } else { + var err = new Error('Not Acceptable'); + err.status = 406; + err.types = normalizeTypes(keys).map(function(o){ return o.value }); + next(err); + } + + return this; +}; + +/** + * Set _Content-Disposition_ header to _attachment_ with optional `filename`. + * + * @param {String} filename + * @return {ServerResponse} + * @api public + */ + +res.attachment = function(filename){ + if (filename) this.type(extname(filename)); + this.set('Content-Disposition', filename + ? 'attachment; filename="' + basename(filename) + '"' + : 'attachment'); + return this; +}; + +/** + * Set header `field` to `val`, or pass + * an object of header fields. + * + * Examples: + * + * res.set('Foo', ['bar', 'baz']); + * res.set('Accept', 'application/json'); + * res.set({ Accept: 'text/plain', 'X-API-Key': 'tobi' }); + * + * Aliased as `res.header()`. + * + * @param {String|Object|Array} field + * @param {String} val + * @return {ServerResponse} for chaining + * @api public + */ + +res.set = +res.header = function(field, val){ + if (2 == arguments.length) { + if (Array.isArray(val)) val = val.map(String); + else val = String(val); + this.setHeader(field, val); + } else { + for (var key in field) { + this.set(key, field[key]); + } + } + return this; +}; + +/** + * Get value for header `field`. + * + * @param {String} field + * @return {String} + * @api public + */ + +res.get = function(field){ + return this.getHeader(field); +}; + +/** + * Clear cookie `name`. + * + * @param {String} name + * @param {Object} options + * @param {ServerResponse} for chaining + * @api public + */ + +res.clearCookie = function(name, options){ + var opts = { expires: new Date(1), path: '/' }; + return this.cookie(name, '', options + ? utils.merge(opts, options) + : opts); +}; + +/** + * Set cookie `name` to `val`, with the given `options`. + * + * Options: + * + * - `maxAge` max-age in milliseconds, converted to `expires` + * - `signed` sign the cookie + * - `path` defaults to "/" + * + * Examples: + * + * // "Remember Me" for 15 minutes + * res.cookie('rememberme', '1', { expires: new Date(Date.now() + 900000), httpOnly: true }); + * + * // save as above + * res.cookie('rememberme', '1', { maxAge: 900000, httpOnly: true }) + * + * @param {String} name + * @param {String|Object} val + * @param {Options} options + * @api public + */ + +res.cookie = function(name, val, options){ + options = utils.merge({}, options); + var secret = this.req.secret; + var signed = options.signed; + if (signed && !secret) throw new Error('connect.cookieParser("secret") required for signed cookies'); + if ('object' == typeof val) val = 'j:' + JSON.stringify(val); + if (signed) val = 's:' + sign(val, secret); + if ('maxAge' in options) { + options.expires = new Date(Date.now() + options.maxAge); + options.maxAge /= 1000; + } + if (null == options.path) options.path = '/'; + this.set('Set-Cookie', cookie.serialize(name, String(val), options)); + return this; +}; + + +/** + * Set the location header to `url`. + * + * The given `url` can also be the name of a mapped url, for + * example by default express supports "back" which redirects + * to the _Referrer_ or _Referer_ headers or "/". + * + * Examples: + * + * res.location('/foo/bar').; + * res.location('http://example.com'); + * res.location('../login'); // /blog/post/1 -> /blog/login + * + * Mounting: + * + * When an application is mounted and `res.location()` + * is given a path that does _not_ lead with "/" it becomes + * relative to the mount-point. For example if the application + * is mounted at "/blog", the following would become "/blog/login". + * + * res.location('login'); + * + * While the leading slash would result in a location of "/login": + * + * res.location('/login'); + * + * @param {String} url + * @api public + */ + +res.location = function(url){ + var app = this.app + , req = this.req; + + // setup redirect map + var map = { back: req.get('Referrer') || '/' }; + + // perform redirect + url = map[url] || url; + + // relative + if (!~url.indexOf('://') && 0 != url.indexOf('//')) { + var path + + // relative to path + if ('.' == url[0]) { + path = req.originalUrl.split('?')[0] + url = path + ('/' == path[path.length - 1] ? '' : '/') + url; + // relative to mount-point + } else if ('/' != url[0]) { + path = app.path(); + url = path + '/' + url; + } + } + + // Respond + this.set('Location', url); + return this; +}; + +/** + * Redirect to the given `url` with optional response `status` + * defaulting to 302. + * + * The resulting `url` is determined by `res.location()`, so + * it will play nicely with mounted apps, relative paths, + * `"back"` etc. + * + * Examples: + * + * res.redirect('/foo/bar'); + * res.redirect('http://example.com'); + * res.redirect(301, 'http://example.com'); + * res.redirect('http://example.com', 301); + * res.redirect('../login'); // /blog/post/1 -> /blog/login + * + * @param {String} url + * @param {Number} code + * @api public + */ + +res.redirect = function(url){ + var app = this.app + , head = 'HEAD' == this.req.method + , status = 302 + , body; + + // allow status / url + if (2 == arguments.length) { + if ('number' == typeof url) { + status = url; + url = arguments[1]; + } else { + status = arguments[1]; + } + } + + // Set location header + this.location(url); + url = this.get('Location'); + + // Support text/{plain,html} by default + this.format({ + text: function(){ + body = statusCodes[status] + '. Redirecting to ' + encodeURI(url); + }, + + html: function(){ + var u = utils.escape(url); + body = '

    ' + statusCodes[status] + '. Redirecting to ' + u + '

    '; + }, + + default: function(){ + body = ''; + } + }); + + // Respond + this.statusCode = status; + this.set('Content-Length', Buffer.byteLength(body)); + this.end(head ? null : body); +}; + +/** + * Render `view` with the given `options` and optional callback `fn`. + * When a callback function is given a response will _not_ be made + * automatically, otherwise a response of _200_ and _text/html_ is given. + * + * Options: + * + * - `cache` boolean hinting to the engine it should cache + * - `filename` filename of the view being rendered + * + * @param {String} view + * @param {Object|Function} options or callback function + * @param {Function} fn + * @api public + */ + +res.render = function(view, options, fn){ + var self = this + , options = options || {} + , req = this.req + , app = req.app; + + // support callback function as second arg + if ('function' == typeof options) { + fn = options, options = {}; + } + + // merge res.locals + options._locals = self.locals; + + // default callback to respond + fn = fn || function(err, str){ + if (err) return req.next(err); + self.send(str); + }; + + // render + app.render(view, options, fn); +}; diff --git a/node_modules/express/lib/router/index.js b/node_modules/express/lib/router/index.js new file mode 100644 index 0000000000..662dc29bff --- /dev/null +++ b/node_modules/express/lib/router/index.js @@ -0,0 +1,273 @@ +/** + * Module dependencies. + */ + +var Route = require('./route') + , utils = require('../utils') + , methods = require('methods') + , debug = require('debug')('express:router') + , parse = require('connect').utils.parseUrl; + +/** + * Expose `Router` constructor. + */ + +exports = module.exports = Router; + +/** + * Initialize a new `Router` with the given `options`. + * + * @param {Object} options + * @api private + */ + +function Router(options) { + options = options || {}; + var self = this; + this.map = {}; + this.params = {}; + this._params = []; + this.caseSensitive = options.caseSensitive; + this.strict = options.strict; + this.middleware = function router(req, res, next){ + self._dispatch(req, res, next); + }; +} + +/** + * Register a param callback `fn` for the given `name`. + * + * @param {String|Function} name + * @param {Function} fn + * @return {Router} for chaining + * @api public + */ + +Router.prototype.param = function(name, fn){ + // param logic + if ('function' == typeof name) { + this._params.push(name); + return; + } + + // apply param functions + var params = this._params + , len = params.length + , ret; + + for (var i = 0; i < len; ++i) { + if (ret = params[i](name, fn)) { + fn = ret; + } + } + + // ensure we end up with a + // middleware function + if ('function' != typeof fn) { + throw new Error('invalid param() call for ' + name + ', got ' + fn); + } + + (this.params[name] = this.params[name] || []).push(fn); + return this; +}; + +/** + * Route dispatcher aka the route "middleware". + * + * @param {IncomingMessage} req + * @param {ServerResponse} res + * @param {Function} next + * @api private + */ + +Router.prototype._dispatch = function(req, res, next){ + var params = this.params + , self = this; + + debug('dispatching %s %s (%s)', req.method, req.url, req.originalUrl); + + // route dispatch + (function pass(i, err){ + var paramCallbacks + , paramIndex = 0 + , paramVal + , route + , keys + , key; + + // match next route + function nextRoute(err) { + pass(req._route_index + 1, err); + } + + // match route + req.route = route = self.matchRequest(req, i); + + // no route + if (!route) return next(err); + debug('matched %s %s', route.method, route.path); + + // we have a route + // start at param 0 + req.params = route.params; + keys = route.keys; + i = 0; + + // param callbacks + function param(err) { + paramIndex = 0; + key = keys[i++]; + paramVal = key && req.params[key.name]; + paramCallbacks = key && params[key.name]; + + try { + if ('route' == err) { + nextRoute(); + } else if (err) { + i = 0; + callbacks(err); + } else if (paramCallbacks && undefined !== paramVal) { + paramCallback(); + } else if (key) { + param(); + } else { + i = 0; + callbacks(); + } + } catch (err) { + param(err); + } + }; + + param(err); + + // single param callbacks + function paramCallback(err) { + var fn = paramCallbacks[paramIndex++]; + if (err || !fn) return param(err); + fn(req, res, paramCallback, paramVal, key.name); + } + + // invoke route callbacks + function callbacks(err) { + var fn = route.callbacks[i++]; + try { + if ('route' == err) { + nextRoute(); + } else if (err && fn) { + if (fn.length < 4) return callbacks(err); + fn(err, req, res, callbacks); + } else if (fn) { + if (fn.length < 4) return fn(req, res, callbacks); + callbacks(); + } else { + nextRoute(err); + } + } catch (err) { + callbacks(err); + } + } + })(0); +}; + +/** + * Attempt to match a route for `req` + * with optional starting index of `i` + * defaulting to 0. + * + * @param {IncomingMessage} req + * @param {Number} i + * @return {Route} + * @api private + */ + +Router.prototype.matchRequest = function(req, i, head){ + var method = req.method.toLowerCase() + , url = parse(req) + , path = url.pathname + , routes = this.map + , i = i || 0 + , route; + + // HEAD support + if (!head && 'head' == method) { + route = this.matchRequest(req, i, true); + if (route) return route; + method = 'get'; + } + + // routes for this method + if (routes = routes[method]) { + + // matching routes + for (var len = routes.length; i < len; ++i) { + route = routes[i]; + if (route.match(path)) { + req._route_index = i; + return route; + } + } + } +}; + +/** + * Attempt to match a route for `method` + * and `url` with optional starting + * index of `i` defaulting to 0. + * + * @param {String} method + * @param {String} url + * @param {Number} i + * @return {Route} + * @api private + */ + +Router.prototype.match = function(method, url, i, head){ + var req = { method: method, url: url }; + return this.matchRequest(req, i, head); +}; + +/** + * Route `method`, `path`, and one or more callbacks. + * + * @param {String} method + * @param {String} path + * @param {Function} callback... + * @return {Router} for chaining + * @api private + */ + +Router.prototype.route = function(method, path, callbacks){ + var method = method.toLowerCase() + , callbacks = utils.flatten([].slice.call(arguments, 2)); + + // ensure path was given + if (!path) throw new Error('Router#' + method + '() requires a path'); + + // ensure all callbacks are functions + callbacks.forEach(function(fn, i){ + if ('function' == typeof fn) return; + var type = {}.toString.call(fn); + var msg = '.' + method + '() requires callback functions but got a ' + type; + throw new Error(msg); + }); + + // create the route + debug('defined %s %s', method, path); + var route = new Route(method, path, callbacks, { + sensitive: this.caseSensitive, + strict: this.strict + }); + + // add it + (this.map[method] = this.map[method] || []).push(route); + return this; +}; + +methods.forEach(function(method){ + Router.prototype[method] = function(path){ + var args = [method].concat([].slice.call(arguments)); + this.route.apply(this, args); + return this; + }; +}); diff --git a/node_modules/express/lib/router/route.js b/node_modules/express/lib/router/route.js new file mode 100644 index 0000000000..c1a0b5ea80 --- /dev/null +++ b/node_modules/express/lib/router/route.js @@ -0,0 +1,72 @@ + +/** + * Module dependencies. + */ + +var utils = require('../utils'); + +/** + * Expose `Route`. + */ + +module.exports = Route; + +/** + * Initialize `Route` with the given HTTP `method`, `path`, + * and an array of `callbacks` and `options`. + * + * Options: + * + * - `sensitive` enable case-sensitive routes + * - `strict` enable strict matching for trailing slashes + * + * @param {String} method + * @param {String} path + * @param {Array} callbacks + * @param {Object} options. + * @api private + */ + +function Route(method, path, callbacks, options) { + options = options || {}; + this.path = path; + this.method = method; + this.callbacks = callbacks; + this.regexp = utils.pathRegexp(path + , this.keys = [] + , options.sensitive + , options.strict); +} + +/** + * Check if this route matches `path`, if so + * populate `.params`. + * + * @param {String} path + * @return {Boolean} + * @api private + */ + +Route.prototype.match = function(path){ + var keys = this.keys + , params = this.params = [] + , m = this.regexp.exec(path); + + if (!m) return false; + + for (var i = 1, len = m.length; i < len; ++i) { + var key = keys[i - 1]; + + var val = 'string' == typeof m[i] + ? decodeURIComponent(m[i]) + : m[i]; + + if (key) { + params[key.name] = val; + } else { + params.push(val); + } + } + + return true; +}; diff --git a/node_modules/express/lib/utils.js b/node_modules/express/lib/utils.js new file mode 100644 index 0000000000..fd245a8fbb --- /dev/null +++ b/node_modules/express/lib/utils.js @@ -0,0 +1,313 @@ + +/** + * Module dependencies. + */ + +var mime = require('connect').mime + , crc32 = require('buffer-crc32'); + +/** + * toString ref. + */ + +var toString = {}.toString; + +/** + * Return ETag for `body`. + * + * @param {String|Buffer} body + * @return {String} + * @api private + */ + +exports.etag = function(body){ + return '"' + crc32.signed(body) + '"'; +}; + +/** + * Make `locals()` bound to the given `obj`. + * + * This is used for `app.locals` and `res.locals`. + * + * @param {Object} obj + * @return {Function} + * @api private + */ + +exports.locals = function(obj){ + function locals(obj){ + for (var key in obj) locals[key] = obj[key]; + return obj; + }; + + return locals; +}; + +/** + * Check if `path` looks absolute. + * + * @param {String} path + * @return {Boolean} + * @api private + */ + +exports.isAbsolute = function(path){ + if ('/' == path[0]) return true; + if (':' == path[1] && '\\' == path[2]) return true; +}; + +/** + * Flatten the given `arr`. + * + * @param {Array} arr + * @return {Array} + * @api private + */ + +exports.flatten = function(arr, ret){ + var ret = ret || [] + , len = arr.length; + for (var i = 0; i < len; ++i) { + if (Array.isArray(arr[i])) { + exports.flatten(arr[i], ret); + } else { + ret.push(arr[i]); + } + } + return ret; +}; + +/** + * Normalize the given `type`, for example "html" becomes "text/html". + * + * @param {String} type + * @return {Object} + * @api private + */ + +exports.normalizeType = function(type){ + return ~type.indexOf('/') + ? acceptParams(type) + : { value: mime.lookup(type), params: {} }; +}; + +/** + * Normalize `types`, for example "html" becomes "text/html". + * + * @param {Array} types + * @return {Array} + * @api private + */ + +exports.normalizeTypes = function(types){ + var ret = []; + + for (var i = 0; i < types.length; ++i) { + ret.push(exports.normalizeType(types[i])); + } + + return ret; +}; + +/** + * Return the acceptable type in `types`, if any. + * + * @param {Array} types + * @param {String} str + * @return {String} + * @api private + */ + +exports.acceptsArray = function(types, str){ + // accept anything when Accept is not present + if (!str) return types[0]; + + // parse + var accepted = exports.parseAccept(str) + , normalized = exports.normalizeTypes(types) + , len = accepted.length; + + for (var i = 0; i < len; ++i) { + for (var j = 0, jlen = types.length; j < jlen; ++j) { + if (exports.accept(normalized[j], accepted[i])) { + return types[j]; + } + } + } +}; + +/** + * Check if `type(s)` are acceptable based on + * the given `str`. + * + * @param {String|Array} type(s) + * @param {String} str + * @return {Boolean|String} + * @api private + */ + +exports.accepts = function(type, str){ + if ('string' == typeof type) type = type.split(/ *, */); + return exports.acceptsArray(type, str); +}; + +/** + * Check if `type` array is acceptable for `other`. + * + * @param {Object} type + * @param {Object} other + * @return {Boolean} + * @api private + */ + +exports.accept = function(type, other){ + var t = type.value.split('/'); + return (t[0] == other.type || '*' == other.type) + && (t[1] == other.subtype || '*' == other.subtype) + && paramsEqual(type.params, other.params); +}; + +/** + * Check if accept params are equal. + * + * @param {Object} a + * @param {Object} b + * @return {Boolean} + * @api private + */ + +function paramsEqual(a, b){ + return !Object.keys(a).some(function(k) { + return a[k] != b[k]; + }); +} + +/** + * Parse accept `str`, returning + * an array objects containing + * `.type` and `.subtype` along + * with the values provided by + * `parseQuality()`. + * + * @param {Type} name + * @return {Type} + * @api private + */ + +exports.parseAccept = function(str){ + return exports + .parseParams(str) + .map(function(obj){ + var parts = obj.value.split('/'); + obj.type = parts[0]; + obj.subtype = parts[1]; + return obj; + }); +}; + +/** + * Parse quality `str`, returning an + * array of objects with `.value`, + * `.quality` and optional `.params` + * + * @param {String} str + * @return {Array} + * @api private + */ + +exports.parseParams = function(str){ + return str + .split(/ *, */) + .map(acceptParams) + .filter(function(obj){ + return obj.quality; + }) + .sort(function(a, b){ + if (a.quality === b.quality) { + return a.originalIndex - b.originalIndex; + } else { + return b.quality - a.quality; + } + }); +}; + +/** + * Parse accept params `str` returning an + * object with `.value`, `.quality` and `.params`. + * also includes `.originalIndex` for stable sorting + * + * @param {String} str + * @return {Object} + * @api private + */ + +function acceptParams(str, index) { + var parts = str.split(/ *; */); + var ret = { value: parts[0], quality: 1, params: {}, originalIndex: index }; + + for (var i = 1; i < parts.length; ++i) { + var pms = parts[i].split(/ *= */); + if ('q' == pms[0]) { + ret.quality = parseFloat(pms[1]); + } else { + ret.params[pms[0]] = pms[1]; + } + } + + return ret; +} + +/** + * Escape special characters in the given string of html. + * + * @param {String} html + * @return {String} + * @api private + */ + +exports.escape = function(html) { + return String(html) + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +}; + +/** + * Normalize the given path string, + * returning a regular expression. + * + * An empty array should be passed, + * which will contain the placeholder + * key names. For example "/user/:id" will + * then contain ["id"]. + * + * @param {String|RegExp|Array} path + * @param {Array} keys + * @param {Boolean} sensitive + * @param {Boolean} strict + * @return {RegExp} + * @api private + */ + +exports.pathRegexp = function(path, keys, sensitive, strict) { + if (toString.call(path) == '[object RegExp]') return path; + if (Array.isArray(path)) path = '(' + path.join('|') + ')'; + path = path + .concat(strict ? '' : '/?') + .replace(/\/\(/g, '(?:/') + .replace(/(\/)?(\.)?:(\w+)(?:(\(.*?\)))?(\?)?(\*)?/g, function(_, slash, format, key, capture, optional, star){ + keys.push({ name: key, optional: !! optional }); + slash = slash || ''; + return '' + + (optional ? '' : slash) + + '(?:' + + (optional ? slash : '') + + (format || '') + (capture || (format && '([^/.]+?)' || '([^/]+?)')) + ')' + + (optional || '') + + (star ? '(/*)?' : ''); + }) + .replace(/([\/.])/g, '\\$1') + .replace(/\*/g, '(.*)'); + return new RegExp('^' + path + '$', sensitive ? '' : 'i'); +} diff --git a/node_modules/express/lib/view.js b/node_modules/express/lib/view.js new file mode 100644 index 0000000000..ae20b17415 --- /dev/null +++ b/node_modules/express/lib/view.js @@ -0,0 +1,76 @@ +/** + * Module dependencies. + */ + +var path = require('path') + , fs = require('fs') + , utils = require('./utils') + , dirname = path.dirname + , basename = path.basename + , extname = path.extname + , exists = fs.existsSync || path.existsSync + , join = path.join; + +/** + * Expose `View`. + */ + +module.exports = View; + +/** + * Initialize a new `View` with the given `name`. + * + * Options: + * + * - `defaultEngine` the default template engine name + * - `engines` template engine require() cache + * - `root` root path for view lookup + * + * @param {String} name + * @param {Object} options + * @api private + */ + +function View(name, options) { + options = options || {}; + this.name = name; + this.root = options.root; + var engines = options.engines; + this.defaultEngine = options.defaultEngine; + var ext = this.ext = extname(name); + if (!ext) name += (ext = this.ext = ('.' != this.defaultEngine[0] ? '.' : '') + this.defaultEngine); + this.engine = engines[ext] || (engines[ext] = require(ext.slice(1)).__express); + this.path = this.lookup(name); +} + +/** + * Lookup view by the given `path` + * + * @param {String} path + * @return {String} + * @api private + */ + +View.prototype.lookup = function(path){ + var ext = this.ext; + + // . + if (!utils.isAbsolute(path)) path = join(this.root, path); + if (exists(path)) return path; + + // /index. + path = join(dirname(path), basename(path, ext), 'index' + ext); + if (exists(path)) return path; +}; + +/** + * Render with the given `options` and callback `fn(err, str)`. + * + * @param {Object} options + * @param {Function} fn + * @api private + */ + +View.prototype.render = function(options, fn){ + this.engine(this.path, options, fn); +}; diff --git a/node_modules/express/package.json b/node_modules/express/package.json new file mode 100644 index 0000000000..1536979e5a --- /dev/null +++ b/node_modules/express/package.json @@ -0,0 +1,87 @@ +{ + "name": "express", + "description": "Sinatra inspired web development framework", + "version": "3.2.0", + "author": { + "name": "TJ Holowaychuk", + "email": "tj@vision-media.ca" + }, + "contributors": [ + { + "name": "TJ Holowaychuk", + "email": "tj@vision-media.ca" + }, + { + "name": "Aaron Heckmann", + "email": "aaron.heckmann+github@gmail.com" + }, + { + "name": "Ciaran Jessup", + "email": "ciaranj@gmail.com" + }, + { + "name": "Guillermo Rauch", + "email": "rauchg@gmail.com" + } + ], + "dependencies": { + "connect": "2.7.6", + "commander": "0.6.1", + "range-parser": "0.0.4", + "mkdirp": "~0.3.4", + "cookie": "0.0.5", + "buffer-crc32": "~0.2.1", + "fresh": "0.1.0", + "methods": "0.0.1", + "send": "0.1.0", + "cookie-signature": "1.0.1", + "debug": "*" + }, + "devDependencies": { + "ejs": "*", + "mocha": "*", + "jade": "*", + "hjs": "*", + "stylus": "*", + "should": "*", + "connect-redis": "*", + "github-flavored-markdown": "*", + "supertest": "0.0.1" + }, + "keywords": [ + "express", + "framework", + "sinatra", + "web", + "rest", + "restful", + "router", + "app", + "api" + ], + "repository": { + "type": "git", + "url": "git://github.com/visionmedia/express.git" + }, + "main": "index", + "bin": { + "express": "./bin/express" + }, + "scripts": { + "prepublish": "npm prune", + "test": "make test" + }, + "engines": { + "node": "*" + }, + "_id": "express@3.2.0", + "optionalDependencies": {}, + "_engineSupported": true, + "_npmVersion": "1.1.4", + "_nodeVersion": "v0.6.12", + "_defaultsLoaded": true, + "dist": { + "shasum": "c721c85435f232ed531e0a5fe698daf0e0316cdb" + }, + "_from": "express@3.2.0" +} diff --git a/node_modules/express/test.js b/node_modules/express/test.js new file mode 100644 index 0000000000..bb6f6d912d --- /dev/null +++ b/node_modules/express/test.js @@ -0,0 +1,20 @@ + +/** + * Module dependencies. + */ + +var express = require('./') + , app = express() + +app.get('/:foo?/:bar?', function(req, res){ + console.log(req.params); +}); + + +app.post('/:foo?/:bar?', function(req, res){ + console.log(req.params); +}); + + +app.listen(5555); +console.log('listening on 5555'); diff --git a/node_modules/redis/.npmignore b/node_modules/redis/.npmignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/node_modules/redis/.npmignore @@ -0,0 +1 @@ +node_modules diff --git a/node_modules/redis/README.md b/node_modules/redis/README.md new file mode 100644 index 0000000000..e0cda3d816 --- /dev/null +++ b/node_modules/redis/README.md @@ -0,0 +1,707 @@ +redis - a node.js redis client +=========================== + +This is a complete Redis client for node.js. It supports all Redis commands, including many recently added commands like EVAL from +experimental Redis server branches. + + +Install with: + + npm install redis + +Pieter Noordhuis has provided a binding to the official `hiredis` C library, which is non-blocking and fast. To use `hiredis`, do: + + npm install hiredis redis + +If `hiredis` is installed, `node_redis` will use it by default. Otherwise, a pure JavaScript parser will be used. + +If you use `hiredis`, be sure to rebuild it whenever you upgrade your version of node. There are mysterious failures that can +happen between node and native code modules after a node upgrade. + + +## Usage + +Simple example, included as `examples/simple.js`: + +```js + var redis = require("redis"), + client = redis.createClient(); + + // if you'd like to select database 3, instead of 0 (default), call + // client.select(3, function() { /* ... */ }); + + client.on("error", function (err) { + console.log("Error " + err); + }); + + client.set("string key", "string val", redis.print); + client.hset("hash key", "hashtest 1", "some value", redis.print); + client.hset(["hash key", "hashtest 2", "some other value"], redis.print); + client.hkeys("hash key", function (err, replies) { + console.log(replies.length + " replies:"); + replies.forEach(function (reply, i) { + console.log(" " + i + ": " + reply); + }); + client.quit(); + }); +``` + +This will display: + + mjr:~/work/node_redis (master)$ node example.js + Reply: OK + Reply: 0 + Reply: 0 + 2 replies: + 0: hashtest 1 + 1: hashtest 2 + mjr:~/work/node_redis (master)$ + + +## Performance + +Here are typical results of `multi_bench.js` which is similar to `redis-benchmark` from the Redis distribution. +It uses 50 concurrent connections with no pipelining. + +JavaScript parser: + + PING: 20000 ops 42283.30 ops/sec 0/5/1.182 + SET: 20000 ops 32948.93 ops/sec 1/7/1.515 + GET: 20000 ops 28694.40 ops/sec 0/9/1.740 + INCR: 20000 ops 39370.08 ops/sec 0/8/1.269 + LPUSH: 20000 ops 36429.87 ops/sec 0/8/1.370 + LRANGE (10 elements): 20000 ops 9891.20 ops/sec 1/9/5.048 + LRANGE (100 elements): 20000 ops 1384.56 ops/sec 10/91/36.072 + +hiredis parser: + + PING: 20000 ops 46189.38 ops/sec 1/4/1.082 + SET: 20000 ops 41237.11 ops/sec 0/6/1.210 + GET: 20000 ops 39682.54 ops/sec 1/7/1.257 + INCR: 20000 ops 40080.16 ops/sec 0/8/1.242 + LPUSH: 20000 ops 41152.26 ops/sec 0/3/1.212 + LRANGE (10 elements): 20000 ops 36563.07 ops/sec 1/8/1.363 + LRANGE (100 elements): 20000 ops 21834.06 ops/sec 0/9/2.287 + +The performance of `node_redis` improves dramatically with pipelining, which happens automatically in most normal programs. + + +### Sending Commands + +Each Redis command is exposed as a function on the `client` object. +All functions take either an `args` Array plus optional `callback` Function or +a variable number of individual arguments followed by an optional callback. +Here is an example of passing an array of arguments and a callback: + + client.mset(["test keys 1", "test val 1", "test keys 2", "test val 2"], function (err, res) {}); + +Here is that same call in the second style: + + client.mset("test keys 1", "test val 1", "test keys 2", "test val 2", function (err, res) {}); + +Note that in either form the `callback` is optional: + + client.set("some key", "some val"); + client.set(["some other key", "some val"]); + +If the key is missing, reply will be null (probably): + + client.get("missingkey", function(err, reply) { + // reply is null when the key is missing + console.log(reply); + }); + +For a list of Redis commands, see [Redis Command Reference](http://redis.io/commands) + +The commands can be specified in uppercase or lowercase for convenience. `client.get()` is the same as `client.GET()`. + +Minimal parsing is done on the replies. Commands that return a single line reply return JavaScript Strings, +integer replies return JavaScript Numbers, "bulk" replies return node Buffers, and "multi bulk" replies return a +JavaScript Array of node Buffers. `HGETALL` returns an Object with Buffers keyed by the hash keys. + +# API + +## Connection Events + +`client` will emit some events about the state of the connection to the Redis server. + +### "ready" + +`client` will emit `ready` a connection is established to the Redis server and the server reports +that it is ready to receive commands. Commands issued before the `ready` event are queued, +then replayed just before this event is emitted. + +### "connect" + +`client` will emit `connect` at the same time as it emits `ready` unless `client.options.no_ready_check` +is set. If this options is set, `connect` will be emitted when the stream is connected, and then +you are free to try to send commands. + +### "error" + +`client` will emit `error` when encountering an error connecting to the Redis server. + +Note that "error" is a special event type in node. If there are no listeners for an +"error" event, node will exit. This is usually what you want, but it can lead to some +cryptic error messages like this: + + mjr:~/work/node_redis (master)$ node example.js + + node.js:50 + throw e; + ^ + Error: ECONNREFUSED, Connection refused + at IOWatcher.callback (net:870:22) + at node.js:607:9 + +Not very useful in diagnosing the problem, but if your program isn't ready to handle this, +it is probably the right thing to just exit. + +`client` will also emit `error` if an exception is thrown inside of `node_redis` for whatever reason. +It would be nice to distinguish these two cases. + +### "end" + +`client` will emit `end` when an established Redis server connection has closed. + +### "drain" + +`client` will emit `drain` when the TCP connection to the Redis server has been buffering, but is now +writable. This event can be used to stream commands in to Redis and adapt to backpressure. Right now, +you need to check `client.command_queue.length` to decide when to reduce your send rate. Then you can +resume sending when you get `drain`. + +### "idle" + +`client` will emit `idle` when there are no outstanding commands that are awaiting a response. + +## redis.createClient(port, host, options) + +Create a new client connection. `port` defaults to `6379` and `host` defaults +to `127.0.0.1`. If you have `redis-server` running on the same computer as node, then the defaults for +port and host are probably fine. `options` in an object with the following possible properties: + +* `parser`: which Redis protocol reply parser to use. Defaults to `hiredis` if that module is installed. +This may also be set to `javascript`. +* `return_buffers`: defaults to `false`. If set to `true`, then all replies will be sent to callbacks as node Buffer +objects instead of JavaScript Strings. +* `detect_buffers`: default to `false`. If set to `true`, then replies will be sent to callbacks as node Buffer objects +if any of the input arguments to the original command were Buffer objects. +This option lets you switch between Buffers and Strings on a per-command basis, whereas `return_buffers` applies to +every command on a client. +* `socket_nodelay`: defaults to `true`. Whether to call setNoDelay() on the TCP stream, which disables the +Nagle algorithm on the underlying socket. Setting this option to `false` can result in additional throughput at the +cost of more latency. Most applications will want this set to `true`. +* `no_ready_check`: defaults to `false`. When a connection is established to the Redis server, the server might still +be loading the database from disk. While loading, the server not respond to any commands. To work around this, +`node_redis` has a "ready check" which sends the `INFO` command to the server. The response from the `INFO` command +indicates whether the server is ready for more commands. When ready, `node_redis` emits a `ready` event. +Setting `no_ready_check` to `true` will inhibit this check. +* `enable_offline_queue`: defaults to `true`. By default, if there is no active +connection to the redis server, commands are added to a queue and are executed +once the connection has been established. Setting `enable_offline_queue` to +`false` will disable this feature and the callback will be execute immediately +with an error, or an error will be thrown if no callback is specified. +* `retry_max_delay`: defaults to `null`. By default every time the client tries to connect and fails time before +reconnection (delay) almost doubles. This delay normally grows infinitely, but setting `retry_max_delay` limits delay +to maximum value, provided in milliseconds. +* `connect_timeout` defaults to `false`. By default client will try reconnecting until connected. Setting `connect_timeout` +limits total time for client to reconnect. Value is provided in milliseconds and is counted once the disconnect occured. +* `max_attempts` defaults to `null`. By default client will try reconnecting until connected. Setting `max_attempts` +limits total amount of reconnects. + +```js + var redis = require("redis"), + client = redis.createClient(null, null, {detect_buffers: true}); + + client.set("foo_rand000000000000", "OK"); + + // This will return a JavaScript String + client.get("foo_rand000000000000", function (err, reply) { + console.log(reply.toString()); // Will print `OK` + }); + + // This will return a Buffer since original key is specified as a Buffer + client.get(new Buffer("foo_rand000000000000"), function (err, reply) { + console.log(reply.toString()); // Will print `` + }); + client.end(); +``` + +`createClient()` returns a `RedisClient` object that is named `client` in all of the examples here. + +## client.auth(password, callback) + +When connecting to Redis servers that require authentication, the `AUTH` command must be sent as the +first command after connecting. This can be tricky to coordinate with reconnections, the ready check, +etc. To make this easier, `client.auth()` stashes `password` and will send it after each connection, +including reconnections. `callback` is invoked only once, after the response to the very first +`AUTH` command sent. +NOTE: Your call to `client.auth()` should not be inside the ready handler. If +you are doing this wrong, `client` will emit an error that looks +something like this `Error: Ready check failed: ERR operation not permitted`. + +## client.end() + +Forcibly close the connection to the Redis server. Note that this does not wait until all replies have been parsed. +If you want to exit cleanly, call `client.quit()` to send the `QUIT` command after you have handled all replies. + +This example closes the connection to the Redis server before the replies have been read. You probably don't +want to do this: + +```js + var redis = require("redis"), + client = redis.createClient(); + + client.set("foo_rand000000000000", "some fantastic value"); + client.get("foo_rand000000000000", function (err, reply) { + console.log(reply.toString()); + }); + client.end(); +``` + +`client.end()` is useful for timeout cases where something is stuck or taking too long and you want +to start over. + +## Friendlier hash commands + +Most Redis commands take a single String or an Array of Strings as arguments, and replies are sent back as a single String or an Array of Strings. +When dealing with hash values, there are a couple of useful exceptions to this. + +### client.hgetall(hash) + +The reply from an HGETALL command will be converted into a JavaScript Object by `node_redis`. That way you can interact +with the responses using JavaScript syntax. + +Example: + + client.hmset("hosts", "mjr", "1", "another", "23", "home", "1234"); + client.hgetall("hosts", function (err, obj) { + console.dir(obj); + }); + +Output: + + { mjr: '1', another: '23', home: '1234' } + +### client.hmset(hash, obj, [callback]) + +Multiple values in a hash can be set by supplying an object: + + client.HMSET(key2, { + "0123456789": "abcdefghij", // NOTE: key and value will be coerced to strings + "some manner of key": "a type of value" + }); + +The properties and values of this Object will be set as keys and values in the Redis hash. + +### client.hmset(hash, key1, val1, ... keyn, valn, [callback]) + +Multiple values may also be set by supplying a list: + + client.HMSET(key1, "0123456789", "abcdefghij", "some manner of key", "a type of value"); + + +## Publish / Subscribe + +Here is a simple example of the API for publish / subscribe. This program opens two +client connections, subscribes to a channel on one of them, and publishes to that +channel on the other: + +```js + var redis = require("redis"), + client1 = redis.createClient(), client2 = redis.createClient(), + msg_count = 0; + + client1.on("subscribe", function (channel, count) { + client2.publish("a nice channel", "I am sending a message."); + client2.publish("a nice channel", "I am sending a second message."); + client2.publish("a nice channel", "I am sending my last message."); + }); + + client1.on("message", function (channel, message) { + console.log("client1 channel " + channel + ": " + message); + msg_count += 1; + if (msg_count === 3) { + client1.unsubscribe(); + client1.end(); + client2.end(); + } + }); + + client1.incr("did a thing"); + client1.subscribe("a nice channel"); +``` + +When a client issues a `SUBSCRIBE` or `PSUBSCRIBE`, that connection is put into "pub/sub" mode. +At that point, only commands that modify the subscription set are valid. When the subscription +set is empty, the connection is put back into regular mode. + +If you need to send regular commands to Redis while in pub/sub mode, just open another connection. + +## Pub / Sub Events + +If a client has subscriptions active, it may emit these events: + +### "message" (channel, message) + +Client will emit `message` for every message received that matches an active subscription. +Listeners are passed the channel name as `channel` and the message Buffer as `message`. + +### "pmessage" (pattern, channel, message) + +Client will emit `pmessage` for every message received that matches an active subscription pattern. +Listeners are passed the original pattern used with `PSUBSCRIBE` as `pattern`, the sending channel +name as `channel`, and the message Buffer as `message`. + +### "subscribe" (channel, count) + +Client will emit `subscribe` in response to a `SUBSCRIBE` command. Listeners are passed the +channel name as `channel` and the new count of subscriptions for this client as `count`. + +### "psubscribe" (pattern, count) + +Client will emit `psubscribe` in response to a `PSUBSCRIBE` command. Listeners are passed the +original pattern as `pattern`, and the new count of subscriptions for this client as `count`. + +### "unsubscribe" (channel, count) + +Client will emit `unsubscribe` in response to a `UNSUBSCRIBE` command. Listeners are passed the +channel name as `channel` and the new count of subscriptions for this client as `count`. When +`count` is 0, this client has left pub/sub mode and no more pub/sub events will be emitted. + +### "punsubscribe" (pattern, count) + +Client will emit `punsubscribe` in response to a `PUNSUBSCRIBE` command. Listeners are passed the +channel name as `channel` and the new count of subscriptions for this client as `count`. When +`count` is 0, this client has left pub/sub mode and no more pub/sub events will be emitted. + +## client.multi([commands]) + +`MULTI` commands are queued up until an `EXEC` is issued, and then all commands are run atomically by +Redis. The interface in `node_redis` is to return an individual `Multi` object by calling `client.multi()`. + +```js + var redis = require("./index"), + client = redis.createClient(), set_size = 20; + + client.sadd("bigset", "a member"); + client.sadd("bigset", "another member"); + + while (set_size > 0) { + client.sadd("bigset", "member " + set_size); + set_size -= 1; + } + + // multi chain with an individual callback + client.multi() + .scard("bigset") + .smembers("bigset") + .keys("*", function (err, replies) { + // NOTE: code in this callback is NOT atomic + // this only happens after the the .exec call finishes. + client.mget(replies, redis.print); + }) + .dbsize() + .exec(function (err, replies) { + console.log("MULTI got " + replies.length + " replies"); + replies.forEach(function (reply, index) { + console.log("Reply " + index + ": " + reply.toString()); + }); + }); +``` + +`client.multi()` is a constructor that returns a `Multi` object. `Multi` objects share all of the +same command methods as `client` objects do. Commands are queued up inside the `Multi` object +until `Multi.exec()` is invoked. + +You can either chain together `MULTI` commands as in the above example, or you can queue individual +commands while still sending regular client command as in this example: + +```js + var redis = require("redis"), + client = redis.createClient(), multi; + + // start a separate multi command queue + multi = client.multi(); + multi.incr("incr thing", redis.print); + multi.incr("incr other thing", redis.print); + + // runs immediately + client.mset("incr thing", 100, "incr other thing", 1, redis.print); + + // drains multi queue and runs atomically + multi.exec(function (err, replies) { + console.log(replies); // 101, 2 + }); + + // you can re-run the same transaction if you like + multi.exec(function (err, replies) { + console.log(replies); // 102, 3 + client.quit(); + }); +``` + +In addition to adding commands to the `MULTI` queue individually, you can also pass an array +of commands and arguments to the constructor: + +```js + var redis = require("redis"), + client = redis.createClient(), multi; + + client.multi([ + ["mget", "multifoo", "multibar", redis.print], + ["incr", "multifoo"], + ["incr", "multibar"] + ]).exec(function (err, replies) { + console.log(replies); + }); +``` + + +## Monitor mode + +Redis supports the `MONITOR` command, which lets you see all commands received by the Redis server +across all client connections, including from other client libraries and other computers. + +After you send the `MONITOR` command, no other commands are valid on that connection. `node_redis` +will emit a `monitor` event for every new monitor message that comes across. The callback for the +`monitor` event takes a timestamp from the Redis server and an array of command arguments. + +Here is a simple example: + +```js + var client = require("redis").createClient(), + util = require("util"); + + client.monitor(function (err, res) { + console.log("Entering monitoring mode."); + }); + + client.on("monitor", function (time, args) { + console.log(time + ": " + util.inspect(args)); + }); +``` + +# Extras + +Some other things you might like to know about. + +## client.server_info + +After the ready probe completes, the results from the INFO command are saved in the `client.server_info` +object. + +The `versions` key contains an array of the elements of the version string for easy comparison. + + > client.server_info.redis_version + '2.3.0' + > client.server_info.versions + [ 2, 3, 0 ] + +## redis.print() + +A handy callback function for displaying return values when testing. Example: + +```js + var redis = require("redis"), + client = redis.createClient(); + + client.on("connect", function () { + client.set("foo_rand000000000000", "some fantastic value", redis.print); + client.get("foo_rand000000000000", redis.print); + }); +``` + +This will print: + + Reply: OK + Reply: some fantastic value + +Note that this program will not exit cleanly because the client is still connected. + +## redis.debug_mode + +Boolean to enable debug mode and protocol tracing. + +```js + var redis = require("redis"), + client = redis.createClient(); + + redis.debug_mode = true; + + client.on("connect", function () { + client.set("foo_rand000000000000", "some fantastic value"); + }); +``` + +This will display: + + mjr:~/work/node_redis (master)$ node ~/example.js + send command: *3 + $3 + SET + $20 + foo_rand000000000000 + $20 + some fantastic value + + on_data: +OK + +`send command` is data sent into Redis and `on_data` is data received from Redis. + +## Multi-word commands + +To execute redis multi-word commands like `SCRIPT LOAD` or `CLIENT LIST` pass +the second word as first parameter: + + client.script('load', 'return 1'); + client.multi().script('load', 'return 1').exec(...); + client.multi([['script', 'load', 'return 1']]).exec(...); + +## client.send_command(command_name, args, callback) + +Used internally to send commands to Redis. For convenience, nearly all commands that are published on the Redis +Wiki have been added to the `client` object. However, if I missed any, or if new commands are introduced before +this library is updated, you can use `send_command()` to send arbitrary commands to Redis. + +All commands are sent as multi-bulk commands. `args` can either be an Array of arguments, or omitted. + +## client.connected + +Boolean tracking the state of the connection to the Redis server. + +## client.command_queue.length + +The number of commands that have been sent to the Redis server but not yet replied to. You can use this to +enforce some kind of maximum queue depth for commands while connected. + +Don't mess with `client.command_queue` though unless you really know what you are doing. + +## client.offline_queue.length + +The number of commands that have been queued up for a future connection. You can use this to enforce +some kind of maximum queue depth for pre-connection commands. + +## client.retry_delay + +Current delay in milliseconds before a connection retry will be attempted. This starts at `250`. + +## client.retry_backoff + +Multiplier for future retry timeouts. This should be larger than 1 to add more time between retries. +Defaults to 1.7. The default initial connection retry is 250, so the second retry will be 425, followed by 723.5, etc. + +### Commands with Optional and Keyword arguments + +This applies to anything that uses an optional `[WITHSCORES]` or `[LIMIT offset count]` in the [redis.io/commands](http://redis.io/commands) documentation. + +Example: +```js +var args = [ 'myzset', 1, 'one', 2, 'two', 3, 'three', 99, 'ninety-nine' ]; +client.zadd(args, function (err, response) { + if (err) throw err; + console.log('added '+response+' items.'); + + // -Infinity and +Infinity also work + var args1 = [ 'myzset', '+inf', '-inf' ]; + client.zrevrangebyscore(args1, function (err, response) { + if (err) throw err; + console.log('example1', response); + // write your code here + }); + + var max = 3, min = 1, offset = 1, count = 2; + var args2 = [ 'myzset', max, min, 'WITHSCORES', 'LIMIT', offset, count ]; + client.zrevrangebyscore(args2, function (err, response) { + if (err) throw err; + console.log('example2', response); + // write your code here + }); +}); +``` + +## TODO + +Better tests for auth, disconnect/reconnect, and all combinations thereof. + +Stream large set/get values into and out of Redis. Otherwise the entire value must be in node's memory. + +Performance can be better for very large values. + +I think there are more performance improvements left in there for smaller values, especially for large lists of small values. + +## How to Contribute +- open a pull request and then wait for feedback (if + [DTrejo](http://github.com/dtrejo) does not get back to you within 2 days, + comment again with indignation!) + +## Contributors +Some people have have added features and fixed bugs in `node_redis` other than me. + +Ordered by date of first contribution. +[Auto-generated](http://github.com/dtrejo/node-authors) on Wed Jul 25 2012 19:14:59 GMT-0700 (PDT). + +- [Matt Ranney aka `mranney`](https://github.com/mranney) +- [Tim-Smart aka `tim-smart`](https://github.com/tim-smart) +- [Tj Holowaychuk aka `visionmedia`](https://github.com/visionmedia) +- [rick aka `technoweenie`](https://github.com/technoweenie) +- [Orion Henry aka `orionz`](https://github.com/orionz) +- [Aivo Paas aka `aivopaas`](https://github.com/aivopaas) +- [Hank Sims aka `hanksims`](https://github.com/hanksims) +- [Paul Carey aka `paulcarey`](https://github.com/paulcarey) +- [Pieter Noordhuis aka `pietern`](https://github.com/pietern) +- [nithesh aka `nithesh`](https://github.com/nithesh) +- [Andy Ray aka `andy2ray`](https://github.com/andy2ray) +- [unknown aka `unknowdna`](https://github.com/unknowdna) +- [Dave Hoover aka `redsquirrel`](https://github.com/redsquirrel) +- [Vladimir Dronnikov aka `dvv`](https://github.com/dvv) +- [Umair Siddique aka `umairsiddique`](https://github.com/umairsiddique) +- [Louis-Philippe Perron aka `lp`](https://github.com/lp) +- [Mark Dawson aka `markdaws`](https://github.com/markdaws) +- [Ian Babrou aka `bobrik`](https://github.com/bobrik) +- [Felix Geisendörfer aka `felixge`](https://github.com/felixge) +- [Jean-Hugues Pinson aka `undefined`](https://github.com/undefined) +- [Maksim Lin aka `maks`](https://github.com/maks) +- [Owen Smith aka `orls`](https://github.com/orls) +- [Zachary Scott aka `zzak`](https://github.com/zzak) +- [TEHEK Firefox aka `TEHEK`](https://github.com/TEHEK) +- [Isaac Z. Schlueter aka `isaacs`](https://github.com/isaacs) +- [David Trejo aka `DTrejo`](https://github.com/DTrejo) +- [Brian Noguchi aka `bnoguchi`](https://github.com/bnoguchi) +- [Philip Tellis aka `bluesmoon`](https://github.com/bluesmoon) +- [Marcus Westin aka `marcuswestin2`](https://github.com/marcuswestin2) +- [Jed Schmidt aka `jed`](https://github.com/jed) +- [Dave Peticolas aka `jdavisp3`](https://github.com/jdavisp3) +- [Trae Robrock aka `trobrock`](https://github.com/trobrock) +- [Shankar Karuppiah aka `shankar0306`](https://github.com/shankar0306) +- [Ignacio Burgueño aka `ignacio`](https://github.com/ignacio) + +Thanks. + +## LICENSE - "MIT License" + +Copyright (c) 2010 Matthew Ranney, http://ranney.com/ + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +![spacer](http://ranney.com/1px.gif) diff --git a/node_modules/redis/benches/buffer_bench.js b/node_modules/redis/benches/buffer_bench.js new file mode 100644 index 0000000000..a504fbc087 --- /dev/null +++ b/node_modules/redis/benches/buffer_bench.js @@ -0,0 +1,89 @@ +var source = new Buffer(100), + dest = new Buffer(100), i, j, k, tmp, count = 1000000, bytes = 100; + +for (i = 99 ; i >= 0 ; i--) { + source[i] = 120; +} + +var str = "This is a nice String.", + buf = new Buffer("This is a lovely Buffer."); + +var start = new Date(); +for (i = count * 100; i > 0 ; i--) { + if (Buffer.isBuffer(str)) {} +} +var end = new Date(); +console.log("Buffer.isBuffer(str) " + (end - start) + " ms"); + +var start = new Date(); +for (i = count * 100; i > 0 ; i--) { + if (Buffer.isBuffer(buf)) {} +} +var end = new Date(); +console.log("Buffer.isBuffer(buf) " + (end - start) + " ms"); + +var start = new Date(); +for (i = count * 100; i > 0 ; i--) { + if (str instanceof Buffer) {} +} +var end = new Date(); +console.log("str instanceof Buffer " + (end - start) + " ms"); + +var start = new Date(); +for (i = count * 100; i > 0 ; i--) { + if (buf instanceof Buffer) {} +} +var end = new Date(); +console.log("buf instanceof Buffer " + (end - start) + " ms"); + +for (i = bytes ; i > 0 ; i --) { + var start = new Date(); + for (j = count ; j > 0; j--) { + tmp = source.toString("ascii", 0, bytes); + } + var end = new Date(); + console.log("toString() " + i + " bytes " + (end - start) + " ms"); +} + +for (i = bytes ; i > 0 ; i --) { + var start = new Date(); + for (j = count ; j > 0; j--) { + tmp = ""; + for (k = 0; k <= i ; k++) { + tmp += String.fromCharCode(source[k]); + } + } + var end = new Date(); + console.log("manual string " + i + " bytes " + (end - start) + " ms"); +} + +for (i = bytes ; i > 0 ; i--) { + var start = new Date(); + for (j = count ; j > 0 ; j--) { + for (k = i ; k > 0 ; k--) { + dest[k] = source[k]; + } + } + var end = new Date(); + console.log("Manual copy " + i + " bytes " + (end - start) + " ms"); +} + +for (i = bytes ; i > 0 ; i--) { + var start = new Date(); + for (j = count ; j > 0 ; j--) { + for (k = i ; k > 0 ; k--) { + dest[k] = 120; + } + } + var end = new Date(); + console.log("Direct assignment " + i + " bytes " + (end - start) + " ms"); +} + +for (i = bytes ; i > 0 ; i--) { + var start = new Date(); + for (j = count ; j > 0 ; j--) { + source.copy(dest, 0, 0, i); + } + var end = new Date(); + console.log("Buffer.copy() " + i + " bytes " + (end - start) + " ms"); +} diff --git a/node_modules/redis/benches/hiredis_parser.js b/node_modules/redis/benches/hiredis_parser.js new file mode 100644 index 0000000000..f1515b110b --- /dev/null +++ b/node_modules/redis/benches/hiredis_parser.js @@ -0,0 +1,38 @@ +var Parser = require('../lib/parser/hiredis').Parser; +var assert = require('assert'); + +/* +This test makes sure that exceptions thrown inside of "reply" event handlers +are not trapped and mistakenly emitted as parse errors. +*/ +(function testExecuteDoesNotCatchReplyCallbackExceptions() { + var parser = new Parser(); + var replies = [{}]; + + parser.reader = { + feed: function() {}, + get: function() { + return replies.shift(); + } + }; + + var emittedError = false; + var caughtException = false; + + parser + .on('error', function() { + emittedError = true; + }) + .on('reply', function() { + throw new Error('bad'); + }); + + try { + parser.execute(); + } catch (err) { + caughtException = true; + } + + assert.equal(caughtException, true); + assert.equal(emittedError, false); +})(); diff --git a/node_modules/redis/benches/re_sub_test.js b/node_modules/redis/benches/re_sub_test.js new file mode 100644 index 0000000000..64b8f31287 --- /dev/null +++ b/node_modules/redis/benches/re_sub_test.js @@ -0,0 +1,14 @@ +var client = require('../index').createClient() + , client2 = require('../index').createClient() + , assert = require('assert'); + +client.once('subscribe', function (channel, count) { + client.unsubscribe('x'); + client.subscribe('x', function () { + client.quit(); + client2.quit(); + }); + client2.publish('x', 'hi'); +}); + +client.subscribe('x'); diff --git a/node_modules/redis/benches/reconnect_test.js b/node_modules/redis/benches/reconnect_test.js new file mode 100644 index 0000000000..7abdd51665 --- /dev/null +++ b/node_modules/redis/benches/reconnect_test.js @@ -0,0 +1,29 @@ +var redis = require("../index").createClient(null, null, { +// max_attempts: 4 +}); + +redis.on("error", function (err) { + console.log("Redis says: " + err); +}); + +redis.on("ready", function () { + console.log("Redis ready."); +}); + +redis.on("reconnecting", function (arg) { + console.log("Redis reconnecting: " + JSON.stringify(arg)); +}); +redis.on("connect", function () { + console.log("Redis connected."); +}); + +setInterval(function () { + var now = Date.now(); + redis.set("now", now, function (err, res) { + if (err) { + console.log(now + " Redis reply error: " + err); + } else { + console.log(now + " Redis reply: " + res); + } + }); +}, 100); diff --git a/node_modules/redis/benches/stress/codec.js b/node_modules/redis/benches/stress/codec.js new file mode 100644 index 0000000000..7d764f6072 --- /dev/null +++ b/node_modules/redis/benches/stress/codec.js @@ -0,0 +1,16 @@ +var json = { + encode: JSON.stringify, + decode: JSON.parse +}; + +var MsgPack = require('node-msgpack'); +msgpack = { + encode: MsgPack.pack, + decode: function(str) { return MsgPack.unpack(new Buffer(str)); } +}; + +bison = require('bison'); + +module.exports = json; +//module.exports = msgpack; +//module.exports = bison; diff --git a/node_modules/redis/benches/stress/pubsub/pub.js b/node_modules/redis/benches/stress/pubsub/pub.js new file mode 100644 index 0000000000..0acde7a6eb --- /dev/null +++ b/node_modules/redis/benches/stress/pubsub/pub.js @@ -0,0 +1,38 @@ +'use strict'; + +var freemem = require('os').freemem; +var profiler = require('v8-profiler'); +var codec = require('../codec'); + +var sent = 0; + +var pub = require('redis').createClient(null, null, { + //command_queue_high_water: 5, + //command_queue_low_water: 1 +}) +.on('ready', function() { + this.emit('drain'); +}) +.on('drain', function() { + process.nextTick(exec); +}); + +var payload = '1'; for (var i = 0; i < 12; ++i) payload += payload; +console.log('Message payload length', payload.length); + +function exec() { + pub.publish('timeline', codec.encode({ foo: payload })); + ++sent; + if (!pub.should_buffer) { + process.nextTick(exec); + } +} + +profiler.takeSnapshot('s_0'); + +exec(); + +setInterval(function() { + profiler.takeSnapshot('s_' + sent); + console.error('sent', sent, 'free', freemem(), 'cmdqlen', pub.command_queue.length, 'offqlen', pub.offline_queue.length); +}, 2000); diff --git a/node_modules/redis/benches/stress/pubsub/run b/node_modules/redis/benches/stress/pubsub/run new file mode 100644 index 0000000000..bd9ac39253 --- /dev/null +++ b/node_modules/redis/benches/stress/pubsub/run @@ -0,0 +1,10 @@ +#!/bin/sh +node server.js & +node server.js & +node server.js & +node server.js & +node server.js & +node server.js & +node server.js & +node server.js & +node --debug pub.js diff --git a/node_modules/redis/benches/stress/pubsub/server.js b/node_modules/redis/benches/stress/pubsub/server.js new file mode 100644 index 0000000000..035e6b7440 --- /dev/null +++ b/node_modules/redis/benches/stress/pubsub/server.js @@ -0,0 +1,23 @@ +'use strict'; + +var freemem = require('os').freemem; +var codec = require('../codec'); + +var id = Math.random(); +var recv = 0; + +var sub = require('redis').createClient() + .on('ready', function() { + this.subscribe('timeline'); + }) + .on('message', function(channel, message) { + var self = this; + if (message) { + message = codec.decode(message); + ++recv; + } + }); + +setInterval(function() { + console.error('id', id, 'received', recv, 'free', freemem()); +}, 2000); diff --git a/node_modules/redis/benches/stress/rpushblpop/pub.js b/node_modules/redis/benches/stress/rpushblpop/pub.js new file mode 100644 index 0000000000..9caf1d0b82 --- /dev/null +++ b/node_modules/redis/benches/stress/rpushblpop/pub.js @@ -0,0 +1,49 @@ +'use strict'; + +var freemem = require('os').freemem; +//var profiler = require('v8-profiler'); +var codec = require('../codec'); + +var sent = 0; + +var pub = require('redis').createClient(null, null, { + //command_queue_high_water: 5, + //command_queue_low_water: 1 +}) +.on('ready', function() { + this.del('timeline'); + this.emit('drain'); +}) +.on('drain', function() { + process.nextTick(exec); +}); + +var payload = '1'; for (var i = 0; i < 12; ++i) payload += payload; +console.log('Message payload length', payload.length); + +function exec() { + pub.rpush('timeline', codec.encode({ foo: payload })); + ++sent; + if (!pub.should_buffer) { + process.nextTick(exec); + } +} + +//profiler.takeSnapshot('s_0'); + +exec(); + +setInterval(function() { + //var ss = profiler.takeSnapshot('s_' + sent); + //console.error(ss.stringify()); + pub.llen('timeline', function(err, result) { + console.error('sent', sent, 'free', freemem(), + 'cmdqlen', pub.command_queue.length, 'offqlen', pub.offline_queue.length, + 'llen', result + ); + }); +}, 2000); + +/*setTimeout(function() { + process.exit(); +}, 30000);*/ diff --git a/node_modules/redis/benches/stress/rpushblpop/run b/node_modules/redis/benches/stress/rpushblpop/run new file mode 100644 index 0000000000..8045ae8045 --- /dev/null +++ b/node_modules/redis/benches/stress/rpushblpop/run @@ -0,0 +1,6 @@ +#!/bin/sh +node server.js & +#node server.js & +#node server.js & +#node server.js & +node --debug pub.js diff --git a/node_modules/redis/benches/stress/rpushblpop/server.js b/node_modules/redis/benches/stress/rpushblpop/server.js new file mode 100644 index 0000000000..9cbcdd9ed7 --- /dev/null +++ b/node_modules/redis/benches/stress/rpushblpop/server.js @@ -0,0 +1,30 @@ +'use strict'; + +var freemem = require('os').freemem; +var codec = require('../codec'); + +var id = Math.random(); +var recv = 0; + +var cmd = require('redis').createClient(); +var sub = require('redis').createClient() + .on('ready', function() { + this.emit('timeline'); + }) + .on('timeline', function() { + var self = this; + this.blpop('timeline', 0, function(err, result) { + var message = result[1]; + if (message) { + message = codec.decode(message); + ++recv; + } + self.emit('timeline'); + }); + }); + +setInterval(function() { + cmd.llen('timeline', function(err, result) { + console.error('id', id, 'received', recv, 'free', freemem(), 'llen', result); + }); +}, 2000); diff --git a/node_modules/redis/benches/stress/speed/00 b/node_modules/redis/benches/stress/speed/00 new file mode 100644 index 0000000000..29d7bf7c5d --- /dev/null +++ b/node_modules/redis/benches/stress/speed/00 @@ -0,0 +1,13 @@ +# size JSON msgpack bison +26602 2151.0170848180414 +25542 ? 2842.589272665782 +24835 ? ? 7280.4538397469805 +6104 6985.234528557929 +5045 ? 7217.461392841478 +4341 ? ? 14261.406335354604 +4180 15864.633685636572 +4143 ? 12954.806235781925 +4141 ? ? 44650.70733912719 +75 114227.07313350472 +40 ? 30162.440062810834 +39 ? ? 119815.66013519121 diff --git a/node_modules/redis/benches/stress/speed/plot b/node_modules/redis/benches/stress/speed/plot new file mode 100644 index 0000000000..2563797cf5 --- /dev/null +++ b/node_modules/redis/benches/stress/speed/plot @@ -0,0 +1,13 @@ +#!/bin/sh + +gnuplot >size-rate.jpg << _EOF_ + +set terminal png nocrop enhanced font verdana 12 size 640,480 +set logscale x +set logscale y +set grid +set xlabel 'Serialized object size, octets' +set ylabel 'decode(encode(obj)) rate, 1/sec' +plot '00' using 1:2 title 'json' smooth bezier, '00' using 1:3 title 'msgpack' smooth bezier, '00' using 1:4 title 'bison' smooth bezier + +_EOF_ diff --git a/node_modules/redis/benches/stress/speed/size-rate.png b/node_modules/redis/benches/stress/speed/size-rate.png new file mode 100644 index 0000000000000000000000000000000000000000..c9c2bee6b076040ccf7e6bc337c307732c400463 GIT binary patch literal 6672 zcmb_gcT^KkyG}v^p$MTvXrUJokPcD;(wj5^r5FTh(xga{5<-_^Kn-960g+w=L3$7= z3Me3;NLN5QD1!9djsEWUefOOE&pqev*|R%4JM+HJJp0bfK5w$Qnf@snBn=1zI%Q~} zV+jI5upkh47ES@wKo}&)fCpJ~6Kh=(i3HT-<>irYL=Xr|B7#VujSV6i)Y}Wv@|+_P zQ6Mauh$YEjuqhxA8Ve#}NztT{XA%-XSM=;4&<}}3%E`%@nwq+~xkX1u=j7zn*47e< z#Qy&Ng@pyc#g?oregj|ed2#FF)+~v%F@+=5cT{a`kUoI`Zy;CT8ZD3_AeCbK=Q)Lw z(;t8W0u3a-Uj>mNl0ff=NGx|r(dJuNEJ+u1fDKqCgl{dTwl-XUl|Z|*Gnt1)k)DAz za2xkPAE)xB@aE`?*QK#6AS??D77d(OEQW~1kg#O@Xi^?(EE* zk7)_z5_GydHtiN8^X5S(R-w4)Bkdp5^J_1CUpo5a1bugab382tSo&X%)A=P;Q6ujz z*B?A;tK?!F5GE$s)a!%2MlU{I`_--$-PzZWypEhD4py5&9KJMrg+I5Lc^SLWcI9}R zWdlr^8espWd4Wefft|msbaPmlGF0YW#iw=0_AvLb`;J$B3&dZ?QrsQXc{b5p&+BOm z9y`x0mWM_p8F~m#ge-o#9Ndwwq)GV-p^N>wmtNe>ZO&(1#8ow^s=mBov@_cAqdg(^ z+V+%HUtdPSwI}Vr7)(?!?rjD79bUV-eDJV?8rx4%p|y2()kn{wF+8x8&Cegw<)pOl zq;!R%!UK^ab@obya%oq-9Cl?nwcTG_Pd6L;){=^Z@fBS~&`K3CpA3fx1+DU(@1uSy zL7XipFh&t2o=7rv@AKySf;SOq@)ix)kR-ha#rEFCJ+dn5s*h+5=Vg6=c_#~S#1-mJ z75eo|{H}N)YW3QxVXb}f%xOM?7zAAO%;|ideMe=k?}Km#Iw;(X{0BH!BHXaBnM3h#BMu6FMIW-tAIKOgBgw(SM}n4?E;w_Zv$ z#_fMdu}Me*1D1KqKKEEu7pp1=C4jW#y&Q#PHBO)XK%tFAA%C2l=m?sQoRNaVWgdIZ zV>@#-rvtxO!;%6d*)Y5ap#+b@gbM3@^SsIwok#)Q$d@i@|`hJ51JiM*Tx-%2gj2QT%>7ga!ME84X_Q$gDO{D9BR0V za4v|F{3X6($haJq{=lQ~EF;gonx-hpnwk64Wy1G&eDJY_ z<3UQ&J;S$Zj)GL}N+}{&tMVrxMK|-j>mh2=t+^>bgPC8#&Bv&~C*%GLH0nu!+np0# zN!W+$w8ezYliOZ1kfXyl(G4@r2U1_0=>5tpwtm@VA|-Ey#PcFB=Jk+C!t{l0?6ES#=VkTswjEzy|LHYi z+38=X6`Qw9zBj{;WRYZ6D>5z4)2ox{z&5T^5}d)`Di~fI^_|fXOgqP~MtqDHL}08@ zXgo`PhR6!r7X6R`b8uOx*SG6xDR0p9*$I2mA@D&cL~`=$X<#U7+dj&U5q#HjBFN2c zex%03zeRn$L3k^Q^vw5V{os&Zb#l^t`54nT&zvS7K$P!a*jw4;bI*x@55_fsp4%Sp zjLPnl#{E4rxOJu^p47a&yFH`%T6P}*x+@Yqm%6Ywu#xi$-#33ZP0pg>w!2i%Cl$=S zYd@3qkSZ8t{Wccxl?XDmqm|J>pbTipXlX+e{}mp+;6@PmK{X3T^&|ASwV&0T5;^1!4In#b~(~;>rN+g*6+LU9>N|F*jC?#yV7| z1l0|&WKaZjNDW&qNqY^PkZVmQbekR%*8Dn^YjIM_QjKtbbBwyrQx3KlcUHkwcrt6` zd5=^|gqQZ=jwZfD9!x+66G?O)K4+R8y2hpxD<$ z^S?)H#ef=TM<6$|lcf3rv>gJ_ts(AqD6~{jg?>ly85a{Bss>QKt02+tKWKAdU#_&( zEm083r>%X2IK++cQ%E+U?mq(sz6I|H)e+S&H(Z5?HkYH{f#G=CF-QOHE;)W8AK^mR z+smHPM>+r#QV1U}vKM}|`85Gzo2RafCLQqnS*Pi6S2h#Q4;1xOGvQ<^_=od-%o*$m z3u*zxY(#LGT>vv`BMx){jYWwAAEu96hDR}@(iXQb_mIYC7D<5g3sQL#+%t^WP{uzI zdHC0JR%Uy$TQk__#xW>6fXb}-UOnt>(n&1cvUXk7_U70#xMufn$v|RIHDlgahuGX! z=p`DNJKS69`8kyn@4_AQ<_5I<8%k3Wwp{9QYhotY3h$oPn=d|+oOqxi*B5nS@fZo5 zP7~wD;GH*V$fgz*_{La*%Xv7gh~K)fXVu6e?V)OmO48gIn~T4i5{~RCj&=6u!6GWb zo~mAgTe#N}o3ntGzUkGgi+!LQ=WW%|4=qk;tg2j#r%sH=7+erP`&T2iyH{z{dqk&sj%EoLhNqOf zQS%vDhjHb)v5(7pR2G$}-us3nI__voLFQ{O^nQ;;J`nq?+cbSdOB3VB5-b5fmC&X5 zzGau1Q(y3he3DkL;N&iK4&|E+a#?N-D1UdQ*-WWr#7M#$Uz*UeRd(on)(w9WC9XoH z_Ts&X3ucYy(89mV4e{bds|u5EIAtuJDqLkihHo*g^{zj1(Sfr2xDkbPRfgB31CQQ&%`r)>GG|w4o(Z*bX7?OQb=*?IE77vRLTySeTFYfa`z)%XCW~E~ zFU?(gM$kNU74b8P<$!rn&ijO4f|!yYd4X*9hNRwX4u#pHtTMN0*6#vnae|GEWE?DX zXP!x<V z+dCW?i@$aAjzDs?LJ24B*0B>q(Lc7GzDjnu0;`;>d+(K>;ZGxFnM`7R9-XC$>N{-Z zePulO)q%GFbEP$-6=>f`Re&*`_6wCWLICEnQ*r87Yrb24O;@G`cgGdmkJ%aGugQ8a zE{Bgp3@yCb*Ek=heOERbR2YuQN)#8M;trdEpL`S3$~QIDf<#Db`KY8pYu|z=G~UUh zMoH|IcI%*r+PFq|^~GBwNtM!J&kJSnSr~~&ye3M&m408i0jZlzetZWyMOY*TcF?Oy z_`Bz%K-LA+*O@-uZ_&WK3R)`Rnzm2>0$+S_p%;HleV-zqqvKSWukzwZ5L+yMKr-m9 zqrZ81w*YIR1D?F2EPxIC8HNn9l;Jsz*0b>oFNBxsoP)PKiTSY5ak$fEdg%_y0HRSA zv{cJr_A2qVcjp@IM3nMgp@|gao_4f&c>Aof-RbI)C=TYKlTIdgE%VwY@&Z7tE?ER-HPO6s(Y1p7z5Zfjepseg0afb!hOE zy}i^^#=2JK;ZkA7HdB-z+iQjXI|3#I`bcicLraX5N*?lPtmD%{vG((#X!%>Ni8V>m zutfo4__nmzSxYONgo!41db$;@ow)*?!l@Tr^v(0w_=}w!XPld+4eW2OB+3!;KFaaK zq|AOb%*Q9gvfcDVvbbfCC6fXf>>?bgK)e}F8C7)zbO<$Y!g1lK}d!an1Ac@gCMzo3gKAP_XOXWndj#xw`rbTM`Z>ZgV;W< zD|ulFAlkN(agVM_zjp6~NgyEdKXT~D4Y%SK%tICf-h!WB6Mwd~*~^0Xcl=VAz3`x?3QqfwsZf=H;|tn5;&!zdEFU#0;VJdhzn0S=<=mSRCNlJc^>xS-Z}e{xw4Zli%H8Ak-d6DN~!z zm3OR(K6@;Z-`Q!3y}w;7^xM#Lb+iVu?@Sb?nw%S>bJJj{@dk62Gh_!PJ7q--CPILp zg#xXS3Nbt5!Om0^)B`_7zVt-(1GjD=pe> zu!hjGf+Zy7_a?$if#BB#no?~l>=}z>r-1q zOR}S%Nh`&`uOW$6ojwL69cDr~#nfTVRF@nidT2v;MR%fos?uEkZj8-Xftw#a_ta0i zDQ^Ec!&nss9O0u(nnZ8rz{FzJ{dJE#E|#*BDV|r&3-{@ucIv@hV>H%9AH`(63%Sm6 z4rcm1oQMteW$mbd`mh$i%q`?f53(}7?d4#(xQHsX7xov`dhVng4~@-o89n^;FqUuf z>pN4~r6S_qvHw;;`5H7^pZ=Atu)XSqJ!a*&fOGDv8GD{|?$VvR14>jIzWYM8TD|XbppSj~(sVLWLq(L3ppI5X zH3c5s7;9LzZ$tkhl|I-N@hykLIWDa-@?ksZPoTKfk6y&TN%wJ@@30{6JuT!n&lu`9`PC&_ZSQ&6iWREb}x6ArJ0}5!0X_M4=y# zhphwArQ7HCUl?u8Cze1xc)qh0%tka?VPyYE^c*=Q+&vZp`Qcu0r;H&UPu{l97>mb8 z>+hnmPnB&kQn7gP!|TRU24@~y0V^$v5h1hK$-S%%UpYwO1V z?}Ag)yoOD6Y7u!QL~oINU?-`-nNMw%5*;Y{hXCDTLB}LCNK17k|I;bB3?&N-M?|ud zk_|q-dG2neRuS$%w5|Xw zU*JpJGtm9z3z^iM);h6X!);Bc*&SnwlPGC(p;I!W!r`&@Aon+$34f%YK#&r*74DDp zsw@f)v(bTRWAnavMq6X#shP;h4m%k_aW@ck26tNx2VzQ0G!r0iyE#=bMgkIeZ`$nl z^2T6fkvmKekAlKW3wFOJF1!Y&KCsJoCF;gJ6$ZEmk{EA(68$k`djZ7x@;l{J7lD-s z5QFYT-^36Q=xF?If!NuoP@K^3ma@} zyw}*H_tzug&Nw{0S(Yq#Q_emm#|lRrUT`p7;14HlXL_| z5HjWris3iy5Pmc>0TSW!r5Pxhk9*8C{B1iMYBjI_ClJwd7ym+%)nJ8_g>>FC&pR}6 zAgyavkIi}we1DI0*W0) z_N+rsUe~Y2fBN^H$PjvAKz`vFD1!nJ^kslz1+?{lB*DmH`2Q0C;U`S&!CTAI*Tb?hkO6eHBY9*d>TfQjkuh)_X^ zN)Iiz8dT1KP(lTu_$NEe?o=SC%lam;2-m*vA?K(6eV~+khL3gJ1V1-KnD7@}+T5*3 z?M^#RcUg`SsAXK6&?rI-m4_VK=zZ&s`cM<3nK2IE_2IKh@)&E5JY8_ZC7LKUVdScq zeyk(xb;s@e=V(9qxM14hdwou0eIjR(pAQPg5xX;7X}pK(2o!W_rU~}Xtrwg@anRt~ z;a0~|Yd#~x)pK%xh&aYKkWfvU_V}78ucu$YzQX19?Q^^AzpR+<ffXJpQWv*itUYIncYVgkvp-rxs)wS2ThfShdV~R@&V#RxdSD$){~fkoK$T7r-Y62H7q9^z-^>ez6agpH_NnHg;JjpL5#t%`ohD-#3Pd(I*GK zY?DX6>UE{xV!}$z@0gn~#3#=M7#)9ScTy%D{i;0iZA0{C>@enBTPO-2bC|iU6*r(k zwbll7r8K1T;)))o--JiTXwhr$tn4g~+*E$Kt~5}PyB6$WCO3#65rlO^uudtYyd{5sT3vAGc@{Jx{k3Tfrf6neyAQVVJi{G4SqaV%!HL~B|w>&ppSwHn1X-hb#E(w>gH!|nuq=@9gLe?e`> z@s5T6`=HiG8*z1=9cmN>S4K=T^&(a;VXYQNE1tJKq;j~xYIH6@(cy#2?A!P(wt;M_ zM;e;Bz#C~OHs)5l*kFE4F5?EooBk~dR6mkj?+X4`XmOrUeXS8zTs@s^My@l|HPflo H#^C-7XAtZ( literal 0 HcmV?d00001 diff --git a/node_modules/redis/benches/stress/speed/speed.js b/node_modules/redis/benches/stress/speed/speed.js new file mode 100644 index 0000000000..8e43cbc03b --- /dev/null +++ b/node_modules/redis/benches/stress/speed/speed.js @@ -0,0 +1,84 @@ +var msgpack = require('node-msgpack'); +var bison = require('bison'); +var codec = { + JSON: { + encode: JSON.stringify, + decode: JSON.parse + }, + msgpack: { + encode: msgpack.pack, + decode: msgpack.unpack + }, + bison: bison +}; + +var obj, l; + +var s = '0'; +for (var i = 0; i < 12; ++i) s += s; + +obj = { + foo: s, + arrrrrr: [{a:1,b:false,c:null,d:1.0}, 1111, 2222, 33333333], + rand: [], + a: s, + ccc: s, + b: s + s + s +}; +for (i = 0; i < 100; ++i) obj.rand.push(Math.random()); +forObj(obj); + +obj = { + foo: s, + arrrrrr: [{a:1,b:false,c:null,d:1.0}, 1111, 2222, 33333333], + rand: [] +}; +for (i = 0; i < 100; ++i) obj.rand.push(Math.random()); +forObj(obj); + +obj = { + foo: s, + arrrrrr: [{a:1,b:false,c:null,d:1.0}, 1111, 2222, 33333333], + rand: [] +}; +forObj(obj); + +obj = { + arrrrrr: [{a:1,b:false,c:null,d:1.0}, 1111, 2222, 33333333], + rand: [] +}; +forObj(obj); + +function run(obj, codec) { + var t1 = Date.now(); + var n = 10000; + for (var i = 0; i < n; ++i) { + codec.decode(l = codec.encode(obj)); + } + var t2 = Date.now(); + //console.log('DONE', n*1000/(t2-t1), 'codecs/sec, length=', l.length); + return [n*1000/(t2-t1), l.length]; +} + +function series(obj, cname, n) { + var rate = 0; + var len = 0; + for (var i = 0; i < n; ++i) { + var r = run(obj, codec[cname]); + rate += r[0]; + len += r[1]; + } + rate /= n; + len /= n; + console.log(cname + ' ' + rate + ' ' + len); + return [rate, len]; +} + +function forObj(obj) { + var r = { + JSON: series(obj, 'JSON', 20), + msgpack: series(obj, 'msgpack', 20), + bison: series(obj, 'bison', 20) + }; + return r; +} diff --git a/node_modules/redis/benches/sub_quit_test.js b/node_modules/redis/benches/sub_quit_test.js new file mode 100644 index 0000000000..ad1f413228 --- /dev/null +++ b/node_modules/redis/benches/sub_quit_test.js @@ -0,0 +1,18 @@ +var client = require("redis").createClient(), + client2 = require("redis").createClient(); + +client.subscribe("something"); +client.on("subscribe", function (channel, count) { + console.log("Got sub: " + channel); + client.unsubscribe("something"); +}); + +client.on("unsubscribe", function (channel, count) { + console.log("Got unsub: " + channel + ", quitting"); + client.quit(); +}); + +// exercise unsub before sub +client2.unsubscribe("something"); +client2.subscribe("another thing"); +client2.quit(); diff --git a/node_modules/redis/changelog.md b/node_modules/redis/changelog.md new file mode 100644 index 0000000000..e4e3277b54 --- /dev/null +++ b/node_modules/redis/changelog.md @@ -0,0 +1,240 @@ +Changelog +========= + +## v0.8.2 - November 11, 2012 + +Another version bump because 0.8.1 didn't get applied properly for some mysterious reason. +Sorry about that. + +Changed name of "faster" parser to "javascript". + +## v0.8.1 - September 11, 2012 + +Important bug fix for null responses (Jerry Sievert) + +## v0.8.0 - September 10, 2012 + +Many contributed features and fixes, including: + +* Pure JavaScript reply parser that is usually faster than hiredis (Jerry Sievert) +* Remove hiredis as optionalDependency from package.json. It still works if you want it. +* Restore client state on reconnect, including select, subscribe, and monitor. (Ignacio Burgueño) +* Fix idle event (Trae Robrock) +* Many documentation improvements and bug fixes (David Trejo) + +## v0.7.2 - April 29, 2012 + +Many contributed fixes. Thank you, contributors. + +* [GH-190] - pub/sub mode fix (Brian Noguchi) +* [GH-165] - parser selection fix (TEHEK) +* numerous documentation and examples updates +* auth errors emit Errors instead of Strings (David Trejo) + +## v0.7.1 - November 15, 2011 + +Fix regression in reconnect logic. + +Very much need automated tests for reconnection and queue logic. + +## v0.7.0 - November 14, 2011 + +Many contributed fixes. Thanks everybody. + +* [GH-127] - properly re-initialize parser on reconnect +* [GH-136] - handle passing undefined as callback (Ian Babrou) +* [GH-139] - properly handle exceptions thrown in pub/sub event handlers (Felix Geisendörfer) +* [GH-141] - detect closing state on stream error (Felix Geisendörfer) +* [GH-142] - re-select database on reconnection (Jean-Hugues Pinson) +* [GH-146] - add sort example (Maksim Lin) + +Some more goodies: + +* Fix bugs with node 0.6 +* Performance improvements +* New version of `multi_bench.js` that tests more realistic scenarios +* [GH-140] - support optional callback for subscribe commands +* Properly flush and error out command queue when connection fails +* Initial work on reconnection thresholds + +## v0.6.7 - July 30, 2011 + +(accidentally skipped v0.6.6) + +Fix and test for [GH-123] + +Passing an Array as as the last argument should expand as users +expect. The old behavior was to coerce the arguments into Strings, +which did surprising things with Arrays. + +## v0.6.5 - July 6, 2011 + +Contributed changes: + +* Support SlowBuffers (Umair Siddique) +* Add Multi to exports (Louis-Philippe Perron) +* Fix for drain event calculation (Vladimir Dronnikov) + +Thanks! + +## v0.6.4 - June 30, 2011 + +Fix bug with optional callbacks for hmset. + +## v0.6.2 - June 30, 2011 + +Bugs fixed: + +* authentication retry while server is loading db (danmaz74) [GH-101] +* command arguments processing issue with arrays + +New features: + +* Auto update of new commands from redis.io (Dave Hoover) +* Performance improvements and backpressure controls. +* Commands now return the true/false value from the underlying socket write(s). +* Implement command_queue high water and low water for more better control of queueing. + +See `examples/backpressure_drain.js` for more information. + +## v0.6.1 - June 29, 2011 + +Add support and tests for Redis scripting through EXEC command. + +Bug fix for monitor mode. (forddg) + +Auto update of new commands from redis.io (Dave Hoover) + +## v0.6.0 - April 21, 2011 + +Lots of bugs fixed. + +* connection error did not properly trigger reconnection logic [GH-85] +* client.hmget(key, [val1, val2]) was not expanding properly [GH-66] +* client.quit() while in pub/sub mode would throw an error [GH-87] +* client.multi(['hmset', 'key', {foo: 'bar'}]) fails [GH-92] +* unsubscribe before subscribe would make things very confused [GH-88] +* Add BRPOPLPUSH [GH-79] + +## v0.5.11 - April 7, 2011 + +Added DISCARD + +I originally didn't think DISCARD would do anything here because of the clever MULTI interface, but somebody +pointed out to me that DISCARD can be used to flush the WATCH set. + +## v0.5.10 - April 6, 2011 + +Added HVALS + +## v0.5.9 - March 14, 2011 + +Fix bug with empty Array arguments - Andy Ray + +## v0.5.8 - March 14, 2011 + +Add `MONITOR` command and special monitor command reply parsing. + +## v0.5.7 - February 27, 2011 + +Add magical auth command. + +Authentication is now remembered by the client and will be automatically sent to the server +on every connection, including any reconnections. + +## v0.5.6 - February 22, 2011 + +Fix bug in ready check with `return_buffers` set to `true`. + +Thanks to Dean Mao and Austin Chau. + +## v0.5.5 - February 16, 2011 + +Add probe for server readiness. + +When a Redis server starts up, it might take a while to load the dataset into memory. +During this time, the server will accept connections, but will return errors for all non-INFO +commands. Now node_redis will send an INFO command whenever it connects to a server. +If the info command indicates that the server is not ready, the client will keep trying until +the server is ready. Once it is ready, the client will emit a "ready" event as well as the +"connect" event. The client will queue up all commands sent before the server is ready, just +like it did before. When the server is ready, all offline/non-ready commands will be replayed. +This should be backward compatible with previous versions. + +To disable this ready check behavior, set `options.no_ready_check` when creating the client. + +As a side effect of this change, the key/val params from the info command are available as +`client.server_options`. Further, the version string is decomposed into individual elements +in `client.server_options.versions`. + +## v0.5.4 - February 11, 2011 + +Fix excess memory consumption from Queue backing store. + +Thanks to Gustaf Sjöberg. + +## v0.5.3 - February 5, 2011 + +Fix multi/exec error reply callback logic. + +Thanks to Stella Laurenzo. + +## v0.5.2 - January 18, 2011 + +Fix bug where unhandled error replies confuse the parser. + +## v0.5.1 - January 18, 2011 + +Fix bug where subscribe commands would not handle redis-server startup error properly. + +## v0.5.0 - December 29, 2010 + +Some bug fixes: + +* An important bug fix in reconnection logic. Previously, reply callbacks would be invoked twice after + a reconnect. +* Changed error callback argument to be an actual Error object. + +New feature: + +* Add friendly syntax for HMSET using an object. + +## v0.4.1 - December 8, 2010 + +Remove warning about missing hiredis. You probably do want it though. + +## v0.4.0 - December 5, 2010 + +Support for multiple response parsers and hiredis C library from Pieter Noordhuis. +Return Strings instead of Buffers by default. +Empty nested mb reply bug fix. + +## v0.3.9 - November 30, 2010 + +Fix parser bug on failed EXECs. + +## v0.3.8 - November 10, 2010 + +Fix for null MULTI response when WATCH condition fails. + +## v0.3.7 - November 9, 2010 + +Add "drain" and "idle" events. + +## v0.3.6 - November 3, 2010 + +Add all known Redis commands from Redis master, even ones that are coming in 2.2 and beyond. + +Send a friendlier "error" event message on stream errors like connection refused / reset. + +## v0.3.5 - October 21, 2010 + +A few bug fixes. + +* Fixed bug with `nil` multi-bulk reply lengths that showed up with `BLPOP` timeouts. +* Only emit `end` once when connection goes away. +* Fixed bug in `test.js` where driver finished before all tests completed. + +## unversioned wasteland + +See the git history for what happened before. diff --git a/node_modules/redis/diff_multi_bench_output.js b/node_modules/redis/diff_multi_bench_output.js new file mode 100644 index 0000000000..7396472019 --- /dev/null +++ b/node_modules/redis/diff_multi_bench_output.js @@ -0,0 +1,90 @@ +#!/usr/bin/env node + +var colors = require('colors'), + fs = require('fs'), + _ = require('underscore'), + metrics = require('metrics'), + + // `node diff_multi_bench_output.js before.txt after.txt` + before = process.argv[2], + after = process.argv[3]; + +if (!before || !after) { + console.log('Please supply two file arguments:'); + var n = __filename; + n = n.substring(n.lastIndexOf('/', n.length)); + console.log(' ./' + n + ' multiBenchBefore.txt multiBenchAfter.txt'); + console.log('To generate multiBenchBefore.txt, run'); + console.log(' node multi_bench.js > multiBenchBefore.txt'); + console.log('Thank you for benchmarking responsibly.'); + return; +} + +var before_lines = fs.readFileSync(before, 'utf8').split('\n'), + after_lines = fs.readFileSync(after, 'utf8').split('\n'); + +console.log('Comparing before,', before.green, '(', before_lines.length, + 'lines)', 'to after,', after.green, '(', after_lines.length, 'lines)'); + +var total_ops = new metrics.Histogram.createUniformHistogram(); + +before_lines.forEach(function(b, i) { + var a = after_lines[i]; + if (!a || !b || !b.trim() || !a.trim()) { + // console.log('#ignored#', '>'+a+'<', '>'+b+'<'); + return; + } + + b_words = b.split(' ').filter(is_whitespace); + a_words = a.split(' ').filter(is_whitespace); + + var ops = + [b_words, a_words] + .map(function(words) { + // console.log(words); + return parseInt10(words.slice(-2, -1)); + }).filter(function(num) { + var isNaN = !num && num !== 0; + return !isNaN; + }); + if (ops.length != 2) return + + var delta = ops[1] - ops[0]; + var pct = ((delta / ops[0]) * 100).toPrecision(3); + + total_ops.update(delta); + + delta = humanize_diff(delta); + pct = humanize_diff(pct, '%'); + console.log( + // name of test + command_name(a_words) == command_name(b_words) + ? command_name(a_words) + ':' + : '404:', + // results of test + ops.join(' -> '), 'ops/sec (∆', delta, pct, ')'); +}); + +console.log('Mean difference in ops/sec:', humanize_diff(total_ops.mean().toPrecision(6))); + +function is_whitespace(s) { + return !!s.trim(); +} + +function parseInt10(s) { + return parseInt(s, 10); +} + +// green if greater than 0, red otherwise +function humanize_diff(num, unit) { + unit = unit || ""; + if (num > 0) { + return ('+' + num + unit).green; + } + return ('' + num + unit).red; +} + +function command_name(words) { + var line = words.join(' '); + return line.substr(0, line.indexOf(',')); +} diff --git a/node_modules/redis/examples/auth.js b/node_modules/redis/examples/auth.js new file mode 100644 index 0000000000..6c0a563cd8 --- /dev/null +++ b/node_modules/redis/examples/auth.js @@ -0,0 +1,5 @@ +var redis = require("redis"), + client = redis.createClient(); + +// This command is magical. Client stashes the password and will issue on every connect. +client.auth("somepass"); diff --git a/node_modules/redis/examples/backpressure_drain.js b/node_modules/redis/examples/backpressure_drain.js new file mode 100644 index 0000000000..3488ef4d3f --- /dev/null +++ b/node_modules/redis/examples/backpressure_drain.js @@ -0,0 +1,33 @@ +var redis = require("../index"), + client = redis.createClient(null, null, { + command_queue_high_water: 5, + command_queue_low_water: 1 + }), + remaining_ops = 100000, paused = false; + +function op() { + if (remaining_ops <= 0) { + console.error("Finished."); + process.exit(0); + } + + remaining_ops--; + if (client.hset("test hash", "val " + remaining_ops, remaining_ops) === false) { + console.log("Pausing at " + remaining_ops); + paused = true; + } else { + process.nextTick(op); + } +} + +client.on("drain", function () { + if (paused) { + console.log("Resuming at " + remaining_ops); + paused = false; + process.nextTick(op); + } else { + console.log("Got drain while not paused at " + remaining_ops); + } +}); + +op(); diff --git a/node_modules/redis/examples/eval.js b/node_modules/redis/examples/eval.js new file mode 100644 index 0000000000..a3ff6b0793 --- /dev/null +++ b/node_modules/redis/examples/eval.js @@ -0,0 +1,14 @@ +var redis = require("../index"), + client = redis.createClient(); + +redis.debug_mode = true; + +client.eval("return 100.5", 0, function (err, res) { + console.dir(err); + console.dir(res); +}); + +client.eval([ "return 100.5", 0 ], function (err, res) { + console.dir(err); + console.dir(res); +}); diff --git a/node_modules/redis/examples/extend.js b/node_modules/redis/examples/extend.js new file mode 100644 index 0000000000..488b8c2dc5 --- /dev/null +++ b/node_modules/redis/examples/extend.js @@ -0,0 +1,24 @@ +var redis = require("redis"), + client = redis.createClient(); + +// Extend the RedisClient prototype to add a custom method +// This one converts the results from "INFO" into a JavaScript Object + +redis.RedisClient.prototype.parse_info = function (callback) { + this.info(function (err, res) { + var lines = res.toString().split("\r\n").sort(); + var obj = {}; + lines.forEach(function (line) { + var parts = line.split(':'); + if (parts[1]) { + obj[parts[0]] = parts[1]; + } + }); + callback(obj) + }); +}; + +client.parse_info(function (info) { + console.dir(info); + client.quit(); +}); diff --git a/node_modules/redis/examples/file.js b/node_modules/redis/examples/file.js new file mode 100644 index 0000000000..4d2b5d1c98 --- /dev/null +++ b/node_modules/redis/examples/file.js @@ -0,0 +1,32 @@ +// Read a file from disk, store it in Redis, then read it back from Redis. + +var redis = require("redis"), + client = redis.createClient(), + fs = require("fs"), + filename = "kids_in_cart.jpg"; + +// Get the file I use for testing like this: +// curl http://ranney.com/kids_in_cart.jpg -o kids_in_cart.jpg +// or just use your own file. + +// Read a file from fs, store it in Redis, get it back from Redis, write it back to fs. +fs.readFile(filename, function (err, data) { + if (err) throw err + console.log("Read " + data.length + " bytes from filesystem."); + + client.set(filename, data, redis.print); // set entire file + client.get(filename, function (err, reply) { // get entire file + if (err) { + console.log("Get error: " + err); + } else { + fs.writeFile("duplicate_" + filename, reply, function (err) { + if (err) { + console.log("Error on write: " + err) + } else { + console.log("File written."); + } + client.end(); + }); + } + }); +}); diff --git a/node_modules/redis/examples/mget.js b/node_modules/redis/examples/mget.js new file mode 100644 index 0000000000..936740d32f --- /dev/null +++ b/node_modules/redis/examples/mget.js @@ -0,0 +1,5 @@ +var client = require("redis").createClient(); + +client.mget(["sessions started", "sessions started", "foo"], function (err, res) { + console.dir(res); +}); \ No newline at end of file diff --git a/node_modules/redis/examples/monitor.js b/node_modules/redis/examples/monitor.js new file mode 100644 index 0000000000..2cb6a4e1ec --- /dev/null +++ b/node_modules/redis/examples/monitor.js @@ -0,0 +1,10 @@ +var client = require("../index").createClient(), + util = require("util"); + +client.monitor(function (err, res) { + console.log("Entering monitoring mode."); +}); + +client.on("monitor", function (time, args) { + console.log(time + ": " + util.inspect(args)); +}); diff --git a/node_modules/redis/examples/multi.js b/node_modules/redis/examples/multi.js new file mode 100644 index 0000000000..35c08e1840 --- /dev/null +++ b/node_modules/redis/examples/multi.js @@ -0,0 +1,46 @@ +var redis = require("redis"), + client = redis.createClient(), set_size = 20; + +client.sadd("bigset", "a member"); +client.sadd("bigset", "another member"); + +while (set_size > 0) { + client.sadd("bigset", "member " + set_size); + set_size -= 1; +} + +// multi chain with an individual callback +client.multi() + .scard("bigset") + .smembers("bigset") + .keys("*", function (err, replies) { + client.mget(replies, redis.print); + }) + .dbsize() + .exec(function (err, replies) { + console.log("MULTI got " + replies.length + " replies"); + replies.forEach(function (reply, index) { + console.log("Reply " + index + ": " + reply.toString()); + }); + }); + +client.mset("incr thing", 100, "incr other thing", 1, redis.print); + +// start a separate multi command queue +var multi = client.multi(); +multi.incr("incr thing", redis.print); +multi.incr("incr other thing", redis.print); + +// runs immediately +client.get("incr thing", redis.print); // 100 + +// drains multi queue and runs atomically +multi.exec(function (err, replies) { + console.log(replies); // 101, 2 +}); + +// you can re-run the same transaction if you like +multi.exec(function (err, replies) { + console.log(replies); // 102, 3 + client.quit(); +}); diff --git a/node_modules/redis/examples/multi2.js b/node_modules/redis/examples/multi2.js new file mode 100644 index 0000000000..8be4d7313c --- /dev/null +++ b/node_modules/redis/examples/multi2.js @@ -0,0 +1,29 @@ +var redis = require("redis"), + client = redis.createClient(), multi; + +// start a separate command queue for multi +multi = client.multi(); +multi.incr("incr thing", redis.print); +multi.incr("incr other thing", redis.print); + +// runs immediately +client.mset("incr thing", 100, "incr other thing", 1, redis.print); + +// drains multi queue and runs atomically +multi.exec(function (err, replies) { + console.log(replies); // 101, 2 +}); + +// you can re-run the same transaction if you like +multi.exec(function (err, replies) { + console.log(replies); // 102, 3 + client.quit(); +}); + +client.multi([ + ["mget", "multifoo", "multibar", redis.print], + ["incr", "multifoo"], + ["incr", "multibar"] +]).exec(function (err, replies) { + console.log(replies.toString()); +}); diff --git a/node_modules/redis/examples/psubscribe.js b/node_modules/redis/examples/psubscribe.js new file mode 100644 index 0000000000..c57117b8a6 --- /dev/null +++ b/node_modules/redis/examples/psubscribe.js @@ -0,0 +1,33 @@ +var redis = require("redis"), + client1 = redis.createClient(), + client2 = redis.createClient(), + client3 = redis.createClient(), + client4 = redis.createClient(), + msg_count = 0; + +redis.debug_mode = false; + +client1.on("psubscribe", function (pattern, count) { + console.log("client1 psubscribed to " + pattern + ", " + count + " total subscriptions"); + client2.publish("channeltwo", "Me!"); + client3.publish("channelthree", "Me too!"); + client4.publish("channelfour", "And me too!"); +}); + +client1.on("punsubscribe", function (pattern, count) { + console.log("client1 punsubscribed from " + pattern + ", " + count + " total subscriptions"); + client4.end(); + client3.end(); + client2.end(); + client1.end(); +}); + +client1.on("pmessage", function (pattern, channel, message) { + console.log("("+ pattern +")" + " client1 received message on " + channel + ": " + message); + msg_count += 1; + if (msg_count === 3) { + client1.punsubscribe(); + } +}); + +client1.psubscribe("channel*"); diff --git a/node_modules/redis/examples/pub_sub.js b/node_modules/redis/examples/pub_sub.js new file mode 100644 index 0000000000..aa508d6c9d --- /dev/null +++ b/node_modules/redis/examples/pub_sub.js @@ -0,0 +1,41 @@ +var redis = require("redis"), + client1 = redis.createClient(), msg_count = 0, + client2 = redis.createClient(); + +redis.debug_mode = false; + +// Most clients probably don't do much on "subscribe". This example uses it to coordinate things within one program. +client1.on("subscribe", function (channel, count) { + console.log("client1 subscribed to " + channel + ", " + count + " total subscriptions"); + if (count === 2) { + client2.publish("a nice channel", "I am sending a message."); + client2.publish("another one", "I am sending a second message."); + client2.publish("a nice channel", "I am sending my last message."); + } +}); + +client1.on("unsubscribe", function (channel, count) { + console.log("client1 unsubscribed from " + channel + ", " + count + " total subscriptions"); + if (count === 0) { + client2.end(); + client1.end(); + } +}); + +client1.on("message", function (channel, message) { + console.log("client1 channel " + channel + ": " + message); + msg_count += 1; + if (msg_count === 3) { + client1.unsubscribe(); + } +}); + +client1.on("ready", function () { + // if you need auth, do it here + client1.incr("did a thing"); + client1.subscribe("a nice channel", "another one"); +}); + +client2.on("ready", function () { + // if you need auth, do it here +}); diff --git a/node_modules/redis/examples/simple.js b/node_modules/redis/examples/simple.js new file mode 100644 index 0000000000..f1f2e3209b --- /dev/null +++ b/node_modules/redis/examples/simple.js @@ -0,0 +1,24 @@ +var redis = require("redis"), + client = redis.createClient(); + +client.on("error", function (err) { + console.log("error event - " + client.host + ":" + client.port + " - " + err); +}); + +client.set("string key", "string val", redis.print); +client.hset("hash key", "hashtest 1", "some value", redis.print); +client.hset(["hash key", "hashtest 2", "some other value"], redis.print); +client.hkeys("hash key", function (err, replies) { + if (err) { + return console.error("error response - " + err); + } + + console.log(replies.length + " replies:"); + replies.forEach(function (reply, i) { + console.log(" " + i + ": " + reply); + }); +}); + +client.quit(function (err, res) { + console.log("Exiting from quit command."); +}); diff --git a/node_modules/redis/examples/sort.js b/node_modules/redis/examples/sort.js new file mode 100644 index 0000000000..e7c6249e40 --- /dev/null +++ b/node_modules/redis/examples/sort.js @@ -0,0 +1,17 @@ +var redis = require("redis"), + client = redis.createClient(); + +client.sadd("mylist", 1); +client.sadd("mylist", 2); +client.sadd("mylist", 3); + +client.set("weight_1", 5); +client.set("weight_2", 500); +client.set("weight_3", 1); + +client.set("object_1", "foo"); +client.set("object_2", "bar"); +client.set("object_3", "qux"); + +client.sort("mylist", "by", "weight_*", "get", "object_*", redis.print); +// Prints Reply: qux,foo,bar \ No newline at end of file diff --git a/node_modules/redis/examples/subqueries.js b/node_modules/redis/examples/subqueries.js new file mode 100644 index 0000000000..560db2404e --- /dev/null +++ b/node_modules/redis/examples/subqueries.js @@ -0,0 +1,15 @@ +// Sending commands in response to other commands. +// This example runs "type" against every key in the database +// +var client = require("redis").createClient(); + +client.keys("*", function (err, keys) { + keys.forEach(function (key, pos) { + client.type(key, function (err, keytype) { + console.log(key + " is " + keytype); + if (pos === (keys.length - 1)) { + client.quit(); + } + }); + }); +}); diff --git a/node_modules/redis/examples/subquery.js b/node_modules/redis/examples/subquery.js new file mode 100644 index 0000000000..861657e1f3 --- /dev/null +++ b/node_modules/redis/examples/subquery.js @@ -0,0 +1,19 @@ +var client = require("redis").createClient(); + +function print_results(obj) { + console.dir(obj); +} + +// build a map of all keys and their types +client.keys("*", function (err, all_keys) { + var key_types = {}; + + all_keys.forEach(function (key, pos) { // use second arg of forEach to get pos + client.type(key, function (err, type) { + key_types[key] = type; + if (pos === all_keys.length - 1) { // callbacks all run in order + print_results(key_types); + } + }); + }); +}); diff --git a/node_modules/redis/examples/unix_socket.js b/node_modules/redis/examples/unix_socket.js new file mode 100644 index 0000000000..4a5e0bb0e8 --- /dev/null +++ b/node_modules/redis/examples/unix_socket.js @@ -0,0 +1,29 @@ +var redis = require("redis"), + client = redis.createClient("/tmp/redis.sock"), + profiler = require("v8-profiler"); + +client.on("connect", function () { + console.log("Got Unix socket connection.") +}); + +client.on("error", function (err) { + console.log(err.message); +}); + +client.set("space chars", "space value"); + +setInterval(function () { + client.get("space chars"); +}, 100); + +function done() { + client.info(function (err, reply) { + console.log(reply.toString()); + client.quit(); + }); +} + +setTimeout(function () { + console.log("Taking snapshot."); + var snap = profiler.takeSnapshot(); +}, 5000); diff --git a/node_modules/redis/examples/web_server.js b/node_modules/redis/examples/web_server.js new file mode 100644 index 0000000000..9fd85923de --- /dev/null +++ b/node_modules/redis/examples/web_server.js @@ -0,0 +1,31 @@ +// A simple web server that generates dyanmic content based on responses from Redis + +var http = require("http"), server, + redis_client = require("redis").createClient(); + +server = http.createServer(function (request, response) { + response.writeHead(200, { + "Content-Type": "text/plain" + }); + + var redis_info, total_requests; + + redis_client.info(function (err, reply) { + redis_info = reply; // stash response in outer scope + }); + redis_client.incr("requests", function (err, reply) { + total_requests = reply; // stash response in outer scope + }); + redis_client.hincrby("ip", request.connection.remoteAddress, 1); + redis_client.hgetall("ip", function (err, reply) { + // This is the last reply, so all of the previous replies must have completed already + response.write("This page was generated after talking to redis.\n\n" + + "Redis info:\n" + redis_info + "\n" + + "Total requests: " + total_requests + "\n\n" + + "IP count: \n"); + Object.keys(reply).forEach(function (ip) { + response.write(" " + ip + ": " + reply[ip] + "\n"); + }); + response.end(); + }); +}).listen(80); diff --git a/node_modules/redis/generate_commands.js b/node_modules/redis/generate_commands.js new file mode 100644 index 0000000000..e6949d3a14 --- /dev/null +++ b/node_modules/redis/generate_commands.js @@ -0,0 +1,39 @@ +var http = require("http"), + fs = require("fs"); + +function prettyCurrentTime() { + var date = new Date(); + return date.toLocaleString(); +} + +function write_file(commands, path) { + var file_contents, out_commands; + + console.log("Writing " + Object.keys(commands).length + " commands to " + path); + + file_contents = "// This file was generated by ./generate_commands.js on " + prettyCurrentTime() + "\n"; + + out_commands = Object.keys(commands).map(function (key) { + return key.toLowerCase(); + }); + + file_contents += "module.exports = " + JSON.stringify(out_commands, null, " ") + ";\n"; + + fs.writeFile(path, file_contents); +} + +http.get({host: "redis.io", path: "/commands.json"}, function (res) { + var body = ""; + + console.log("Response from redis.io/commands.json: " + res.statusCode); + + res.on('data', function (chunk) { + body += chunk; + }); + + res.on('end', function () { + write_file(JSON.parse(body), "lib/commands.js"); + }); +}).on('error', function (e) { + console.log("Error fetching command list from redis.io: " + e.message); +}); diff --git a/node_modules/redis/index.js b/node_modules/redis/index.js new file mode 100644 index 0000000000..283c743fb1 --- /dev/null +++ b/node_modules/redis/index.js @@ -0,0 +1,1144 @@ +/*global Buffer require exports console setTimeout */ + +var net = require("net"), + util = require("./lib/util"), + Queue = require("./lib/queue"), + to_array = require("./lib/to_array"), + events = require("events"), + crypto = require("crypto"), + parsers = [], commands, + connection_id = 0, + default_port = 6379, + default_host = "127.0.0.1"; + +// can set this to true to enable for all connections +exports.debug_mode = false; + +// hiredis might not be installed +try { + require("./lib/parser/hiredis"); + parsers.push(require("./lib/parser/hiredis")); +} catch (err) { + if (exports.debug_mode) { + console.warn("hiredis parser not installed."); + } +} + +parsers.push(require("./lib/parser/javascript")); + +function RedisClient(stream, options) { + this.stream = stream; + this.options = options = options || {}; + + this.connection_id = ++connection_id; + this.connected = false; + this.ready = false; + this.connections = 0; + if (this.options.socket_nodelay === undefined) { + this.options.socket_nodelay = true; + } + this.should_buffer = false; + this.command_queue_high_water = this.options.command_queue_high_water || 1000; + this.command_queue_low_water = this.options.command_queue_low_water || 0; + this.max_attempts = null; + if (options.max_attempts && !isNaN(options.max_attempts) && options.max_attempts > 0) { + this.max_attempts = +options.max_attempts; + } + this.command_queue = new Queue(); // holds sent commands to de-pipeline them + this.offline_queue = new Queue(); // holds commands issued but not able to be sent + this.commands_sent = 0; + this.connect_timeout = false; + if (options.connect_timeout && !isNaN(options.connect_timeout) && options.connect_timeout > 0) { + this.connect_timeout = +options.connect_timeout; + } + this.enable_offline_queue = true; + if (typeof this.options.enable_offline_queue === "boolean") { + this.enable_offline_queue = this.options.enable_offline_queue; + } + this.retry_max_delay = null; + if (options.retry_max_delay !== undefined && !isNaN(options.retry_max_delay) && options.retry_max_delay > 0) { + this.retry_max_delay = options.retry_max_delay; + } + + this.initialize_retry_vars(); + this.pub_sub_mode = false; + this.subscription_set = {}; + this.monitoring = false; + this.closing = false; + this.server_info = {}; + this.auth_pass = null; + this.parser_module = null; + this.selected_db = null; // save the selected db here, used when reconnecting + + this.old_state = null; + + var self = this; + + this.stream.on("connect", function () { + self.on_connect(); + }); + + this.stream.on("data", function (buffer_from_socket) { + self.on_data(buffer_from_socket); + }); + + this.stream.on("error", function (msg) { + self.on_error(msg.message); + }); + + this.stream.on("close", function () { + self.connection_gone("close"); + }); + + this.stream.on("end", function () { + self.connection_gone("end"); + }); + + this.stream.on("drain", function () { + self.should_buffer = false; + self.emit("drain"); + }); + + events.EventEmitter.call(this); +} +util.inherits(RedisClient, events.EventEmitter); +exports.RedisClient = RedisClient; + +RedisClient.prototype.initialize_retry_vars = function () { + this.retry_timer = null; + this.retry_totaltime = 0; + this.retry_delay = 150; + this.retry_backoff = 1.7; + this.attempts = 1; +}; + +// flush offline_queue and command_queue, erroring any items with a callback first +RedisClient.prototype.flush_and_error = function (message) { + var command_obj; + while (this.offline_queue.length > 0) { + command_obj = this.offline_queue.shift(); + if (typeof command_obj.callback === "function") { + command_obj.callback(message); + } + } + this.offline_queue = new Queue(); + + while (this.command_queue.length > 0) { + command_obj = this.command_queue.shift(); + if (typeof command_obj.callback === "function") { + command_obj.callback(message); + } + } + this.command_queue = new Queue(); +}; + +RedisClient.prototype.on_error = function (msg) { + var message = "Redis connection to " + this.host + ":" + this.port + " failed - " + msg; + + if (this.closing) { + return; + } + + if (exports.debug_mode) { + console.warn(message); + } + + this.flush_and_error(message); + + this.connected = false; + this.ready = false; + + this.emit("error", new Error(message)); + // "error" events get turned into exceptions if they aren't listened for. If the user handled this error + // then we should try to reconnect. + this.connection_gone("error"); +}; + +RedisClient.prototype.do_auth = function () { + var self = this; + + if (exports.debug_mode) { + console.log("Sending auth to " + self.host + ":" + self.port + " id " + self.connection_id); + } + self.send_anyway = true; + self.send_command("auth", [this.auth_pass], function (err, res) { + if (err) { + if (err.toString().match("LOADING")) { + // if redis is still loading the db, it will not authenticate and everything else will fail + console.log("Redis still loading, trying to authenticate later"); + setTimeout(function () { + self.do_auth(); + }, 2000); // TODO - magic number alert + return; + } else { + return self.emit("error", new Error("Auth error: " + err.message)); + } + } + if (res.toString() !== "OK") { + return self.emit("error", new Error("Auth failed: " + res.toString())); + } + if (exports.debug_mode) { + console.log("Auth succeeded " + self.host + ":" + self.port + " id " + self.connection_id); + } + if (self.auth_callback) { + self.auth_callback(err, res); + self.auth_callback = null; + } + + // now we are really connected + self.emit("connect"); + if (self.options.no_ready_check) { + self.on_ready(); + } else { + self.ready_check(); + } + }); + self.send_anyway = false; +}; + +RedisClient.prototype.on_connect = function () { + if (exports.debug_mode) { + console.log("Stream connected " + this.host + ":" + this.port + " id " + this.connection_id); + } + + this.connected = true; + this.ready = false; + this.attempts = 0; + this.connections += 1; + this.command_queue = new Queue(); + this.emitted_end = false; + this.initialize_retry_vars(); + if (this.options.socket_nodelay) { + this.stream.setNoDelay(); + } + this.stream.setTimeout(0); + + this.init_parser(); + + if (this.auth_pass) { + this.do_auth(); + } else { + this.emit("connect"); + + if (this.options.no_ready_check) { + this.on_ready(); + } else { + this.ready_check(); + } + } +}; + +RedisClient.prototype.init_parser = function () { + var self = this; + + if (this.options.parser) { + if (! parsers.some(function (parser) { + if (parser.name === self.options.parser) { + self.parser_module = parser; + if (exports.debug_mode) { + console.log("Using parser module: " + self.parser_module.name); + } + return true; + } + })) { + throw new Error("Couldn't find named parser " + self.options.parser + " on this system"); + } + } else { + if (exports.debug_mode) { + console.log("Using default parser module: " + parsers[0].name); + } + this.parser_module = parsers[0]; + } + + this.parser_module.debug_mode = exports.debug_mode; + + // return_buffers sends back Buffers from parser to callback. detect_buffers sends back Buffers from parser, but + // converts to Strings if the input arguments are not Buffers. + this.reply_parser = new this.parser_module.Parser({ + return_buffers: self.options.return_buffers || self.options.detect_buffers || false + }); + + // "reply error" is an error sent back by Redis + this.reply_parser.on("reply error", function (reply) { + self.return_error(new Error(reply)); + }); + this.reply_parser.on("reply", function (reply) { + self.return_reply(reply); + }); + // "error" is bad. Somehow the parser got confused. It'll try to reset and continue. + this.reply_parser.on("error", function (err) { + self.emit("error", new Error("Redis reply parser error: " + err.stack)); + }); +}; + +RedisClient.prototype.on_ready = function () { + var self = this; + + this.ready = true; + + if (this.old_state !== null) { + this.monitoring = this.old_state.monitoring; + this.pub_sub_mode = this.old_state.pub_sub_mode; + this.selected_db = this.old_state.selected_db; + this.old_state = null; + } + + // magically restore any modal commands from a previous connection + if (this.selected_db !== null) { + // this trick works if and only if the following send_command + // never goes into the offline queue + var pub_sub_mode = this.pub_sub_mode; + this.pub_sub_mode = false; + this.send_command('select', [this.selected_db]); + this.pub_sub_mode = pub_sub_mode; + } + if (this.pub_sub_mode === true) { + // only emit "ready" when all subscriptions were made again + var callback_count = 0; + var callback = function () { + callback_count--; + if (callback_count === 0) { + self.emit("ready"); + } + }; + Object.keys(this.subscription_set).forEach(function (key) { + var parts = key.split(" "); + if (exports.debug_mode) { + console.warn("sending pub/sub on_ready " + parts[0] + ", " + parts[1]); + } + callback_count++; + self.send_command(parts[0] + "scribe", [parts[1]], callback); + }); + return; + } else if (this.monitoring) { + this.send_command("monitor"); + } else { + this.send_offline_queue(); + } + this.emit("ready"); +}; + +RedisClient.prototype.on_info_cmd = function (err, res) { + var self = this, obj = {}, lines, retry_time; + + if (err) { + return self.emit("error", new Error("Ready check failed: " + err.message)); + } + + lines = res.toString().split("\r\n"); + + lines.forEach(function (line) { + var parts = line.split(':'); + if (parts[1]) { + obj[parts[0]] = parts[1]; + } + }); + + obj.versions = []; + obj.redis_version.split('.').forEach(function (num) { + obj.versions.push(+num); + }); + + // expose info key/vals to users + this.server_info = obj; + + if (!obj.loading || (obj.loading && obj.loading === "0")) { + if (exports.debug_mode) { + console.log("Redis server ready."); + } + this.on_ready(); + } else { + retry_time = obj.loading_eta_seconds * 1000; + if (retry_time > 1000) { + retry_time = 1000; + } + if (exports.debug_mode) { + console.log("Redis server still loading, trying again in " + retry_time); + } + setTimeout(function () { + self.ready_check(); + }, retry_time); + } +}; + +RedisClient.prototype.ready_check = function () { + var self = this; + + if (exports.debug_mode) { + console.log("checking server ready state..."); + } + + this.send_anyway = true; // secret flag to send_command to send something even if not "ready" + this.info(function (err, res) { + self.on_info_cmd(err, res); + }); + this.send_anyway = false; +}; + +RedisClient.prototype.send_offline_queue = function () { + var command_obj, buffered_writes = 0; + + while (this.offline_queue.length > 0) { + command_obj = this.offline_queue.shift(); + if (exports.debug_mode) { + console.log("Sending offline command: " + command_obj.command); + } + buffered_writes += !this.send_command(command_obj.command, command_obj.args, command_obj.callback); + } + this.offline_queue = new Queue(); + // Even though items were shifted off, Queue backing store still uses memory until next add, so just get a new Queue + + if (!buffered_writes) { + this.should_buffer = false; + this.emit("drain"); + } +}; + +RedisClient.prototype.connection_gone = function (why) { + var self = this; + + // If a retry is already in progress, just let that happen + if (this.retry_timer) { + return; + } + + if (exports.debug_mode) { + console.warn("Redis connection is gone from " + why + " event."); + } + this.connected = false; + this.ready = false; + + if (this.old_state === null) { + var state = { + monitoring: this.monitoring, + pub_sub_mode: this.pub_sub_mode, + selected_db: this.selected_db + }; + this.old_state = state; + this.monitoring = false; + this.pub_sub_mode = false; + this.selected_db = null; + } + + // since we are collapsing end and close, users don't expect to be called twice + if (! this.emitted_end) { + this.emit("end"); + this.emitted_end = true; + } + + this.flush_and_error("Redis connection gone from " + why + " event."); + + // If this is a requested shutdown, then don't retry + if (this.closing) { + this.retry_timer = null; + if (exports.debug_mode) { + console.warn("connection ended from quit command, not retrying."); + } + return; + } + + if (this.retry_max_delay !== null && this.retry_delay > this.retry_max_delay) { + this.retry_delay = this.retry_max_delay; + } else { + this.retry_delay = Math.floor(this.retry_delay * this.retry_backoff); + } + + if (exports.debug_mode) { + console.log("Retry connection in " + this.retry_delay + " ms"); + } + + if (this.max_attempts && this.attempts >= this.max_attempts) { + this.retry_timer = null; + // TODO - some people need a "Redis is Broken mode" for future commands that errors immediately, and others + // want the program to exit. Right now, we just log, which doesn't really help in either case. + console.error("node_redis: Couldn't get Redis connection after " + this.max_attempts + " attempts."); + return; + } + + this.attempts += 1; + this.emit("reconnecting", { + delay: self.retry_delay, + attempt: self.attempts + }); + this.retry_timer = setTimeout(function () { + if (exports.debug_mode) { + console.log("Retrying connection..."); + } + + self.retry_totaltime += self.retry_delay; + + if (self.connect_timeout && self.retry_totaltime >= self.connect_timeout) { + self.retry_timer = null; + // TODO - engage Redis is Broken mode for future commands, or whatever + console.error("node_redis: Couldn't get Redis connection after " + self.retry_totaltime + "ms."); + return; + } + + self.stream.connect(self.port, self.host); + self.retry_timer = null; + }, this.retry_delay); +}; + +RedisClient.prototype.on_data = function (data) { + if (exports.debug_mode) { + console.log("net read " + this.host + ":" + this.port + " id " + this.connection_id + ": " + data.toString()); + } + + try { + this.reply_parser.execute(data); + } catch (err) { + // This is an unexpected parser problem, an exception that came from the parser code itself. + // Parser should emit "error" events if it notices things are out of whack. + // Callbacks that throw exceptions will land in return_reply(), below. + // TODO - it might be nice to have a different "error" event for different types of errors + this.emit("error", err); + } +}; + +RedisClient.prototype.return_error = function (err) { + var command_obj = this.command_queue.shift(), queue_len = this.command_queue.getLength(); + + if (this.pub_sub_mode === false && queue_len === 0) { + this.command_queue = new Queue(); + this.emit("idle"); + } + if (this.should_buffer && queue_len <= this.command_queue_low_water) { + this.emit("drain"); + this.should_buffer = false; + } + + if (command_obj && typeof command_obj.callback === "function") { + try { + command_obj.callback(err); + } catch (callback_err) { + // if a callback throws an exception, re-throw it on a new stack so the parser can keep going + process.nextTick(function () { + throw callback_err; + }); + } + } else { + console.log("node_redis: no callback to send error: " + err.message); + // this will probably not make it anywhere useful, but we might as well throw + process.nextTick(function () { + throw err; + }); + } +}; + +// if a callback throws an exception, re-throw it on a new stack so the parser can keep going. +// put this try/catch in its own function because V8 doesn't optimize this well yet. +function try_callback(callback, reply) { + try { + callback(null, reply); + } catch (err) { + process.nextTick(function () { + throw err; + }); + } +} + +// hgetall converts its replies to an Object. If the reply is empty, null is returned. +function reply_to_object(reply) { + var obj = {}, j, jl, key, val; + + if (reply.length === 0) { + return null; + } + + for (j = 0, jl = reply.length; j < jl; j += 2) { + key = reply[j].toString(); + val = reply[j + 1]; + obj[key] = val; + } + + return obj; +} + +function reply_to_strings(reply) { + var i; + + if (Buffer.isBuffer(reply)) { + return reply.toString(); + } + + if (Array.isArray(reply)) { + for (i = 0; i < reply.length; i++) { + if (reply[i] !== null && reply[i] !== undefined) { + reply[i] = reply[i].toString(); + } + } + return reply; + } + + return reply; +} + +RedisClient.prototype.return_reply = function (reply) { + var command_obj, len, type, timestamp, argindex, args, queue_len; + + // If the "reply" here is actually a message received asynchronously due to a + // pubsub subscription, don't pop the command queue as we'll only be consuming + // the head command prematurely. + if (Array.isArray(reply) && reply.length > 0 && reply[0]) { + type = reply[0].toString(); + } + + if (type !== 'message' && type !== 'pmessage') { + command_obj = this.command_queue.shift(); + } + + queue_len = this.command_queue.getLength(); + + if (this.pub_sub_mode === false && queue_len === 0) { + this.command_queue = new Queue(); // explicitly reclaim storage from old Queue + this.emit("idle"); + } + if (this.should_buffer && queue_len <= this.command_queue_low_water) { + this.emit("drain"); + this.should_buffer = false; + } + + if (command_obj && !command_obj.sub_command) { + if (typeof command_obj.callback === "function") { + if (this.options.detect_buffers && command_obj.buffer_args === false) { + // If detect_buffers option was specified, then the reply from the parser will be Buffers. + // If this command did not use Buffer arguments, then convert the reply to Strings here. + reply = reply_to_strings(reply); + } + + // TODO - confusing and error-prone that hgetall is special cased in two places + if (reply && 'hgetall' === command_obj.command.toLowerCase()) { + reply = reply_to_object(reply); + } + + try_callback(command_obj.callback, reply); + } else if (exports.debug_mode) { + console.log("no callback for reply: " + (reply && reply.toString && reply.toString())); + } + } else if (this.pub_sub_mode || (command_obj && command_obj.sub_command)) { + if (Array.isArray(reply)) { + type = reply[0].toString(); + + if (type === "message") { + this.emit("message", reply[1].toString(), reply[2]); // channel, message + } else if (type === "pmessage") { + this.emit("pmessage", reply[1].toString(), reply[2].toString(), reply[3]); // pattern, channel, message + } else if (type === "subscribe" || type === "unsubscribe" || type === "psubscribe" || type === "punsubscribe") { + if (reply[2] === 0) { + this.pub_sub_mode = false; + if (this.debug_mode) { + console.log("All subscriptions removed, exiting pub/sub mode"); + } + } else { + this.pub_sub_mode = true; + } + // subscribe commands take an optional callback and also emit an event, but only the first response is included in the callback + // TODO - document this or fix it so it works in a more obvious way + if (command_obj && typeof command_obj.callback === "function") { + try_callback(command_obj.callback, reply[1].toString()); + } + this.emit(type, reply[1].toString(), reply[2]); // channel, count + } else { + throw new Error("subscriptions are active but got unknown reply type " + type); + } + } else if (! this.closing) { + throw new Error("subscriptions are active but got an invalid reply: " + reply); + } + } else if (this.monitoring) { + len = reply.indexOf(" "); + timestamp = reply.slice(0, len); + argindex = reply.indexOf('"'); + args = reply.slice(argindex + 1, -1).split('" "').map(function (elem) { + return elem.replace(/\\"/g, '"'); + }); + this.emit("monitor", timestamp, args); + } else { + throw new Error("node_redis command queue state error. If you can reproduce this, please report it."); + } +}; + +// This Command constructor is ever so slightly faster than using an object literal, but more importantly, using +// a named constructor helps it show up meaningfully in the V8 CPU profiler and in heap snapshots. +function Command(command, args, sub_command, buffer_args, callback) { + this.command = command; + this.args = args; + this.sub_command = sub_command; + this.buffer_args = buffer_args; + this.callback = callback; +} + +RedisClient.prototype.send_command = function (command, args, callback) { + var arg, command_obj, i, il, elem_count, buffer_args, stream = this.stream, command_str = "", buffered_writes = 0, last_arg_type; + + if (typeof command !== "string") { + throw new Error("First argument to send_command must be the command name string, not " + typeof command); + } + + if (Array.isArray(args)) { + if (typeof callback === "function") { + // probably the fastest way: + // client.command([arg1, arg2], cb); (straight passthrough) + // send_command(command, [arg1, arg2], cb); + } else if (! callback) { + // most people find this variable argument length form more convenient, but it uses arguments, which is slower + // client.command(arg1, arg2, cb); (wraps up arguments into an array) + // send_command(command, [arg1, arg2, cb]); + // client.command(arg1, arg2); (callback is optional) + // send_command(command, [arg1, arg2]); + // client.command(arg1, arg2, undefined); (callback is undefined) + // send_command(command, [arg1, arg2, undefined]); + last_arg_type = typeof args[args.length - 1]; + if (last_arg_type === "function" || last_arg_type === "undefined") { + callback = args.pop(); + } + } else { + throw new Error("send_command: last argument must be a callback or undefined"); + } + } else { + throw new Error("send_command: second argument must be an array"); + } + + // if the last argument is an array and command is sadd, expand it out: + // client.sadd(arg1, [arg2, arg3, arg4], cb); + // converts to: + // client.sadd(arg1, arg2, arg3, arg4, cb); + if ((command === 'sadd' || command === 'SADD') && args.length > 0 && Array.isArray(args[args.length - 1])) { + args = args.slice(0, -1).concat(args[args.length - 1]); + } + + // if the value is undefined or null and command is set or setx, need not to send message to redis + if (command === 'set' || command === 'setex') { + if(args[args.length - 1] === undefined || args[args.length - 1] === null) { + var err = new Error('send_command: ' + command + ' value must not be undefined or null'); + return callback(err); + } + } + + buffer_args = false; + for (i = 0, il = args.length, arg; i < il; i += 1) { + if (Buffer.isBuffer(args[i])) { + buffer_args = true; + } + } + + command_obj = new Command(command, args, false, buffer_args, callback); + + if ((!this.ready && !this.send_anyway) || !stream.writable) { + if (exports.debug_mode) { + if (!stream.writable) { + console.log("send command: stream is not writeable."); + } + } + + if (this.enable_offline_queue) { + if (exports.debug_mode) { + console.log("Queueing " + command + " for next server connection."); + } + this.offline_queue.push(command_obj); + this.should_buffer = true; + } else { + var not_writeable_error = new Error('send_command: stream not writeable. enable_offline_queue is false'); + if (command_obj.callback) { + command_obj.callback(not_writeable_error); + } else { + throw not_writeable_error; + } + } + + return false; + } + + if (command === "subscribe" || command === "psubscribe" || command === "unsubscribe" || command === "punsubscribe") { + this.pub_sub_command(command_obj); + } else if (command === "monitor") { + this.monitoring = true; + } else if (command === "quit") { + this.closing = true; + } else if (this.pub_sub_mode === true) { + throw new Error("Connection in pub/sub mode, only pub/sub commands may be used"); + } + this.command_queue.push(command_obj); + this.commands_sent += 1; + + elem_count = args.length + 1; + + // Always use "Multi bulk commands", but if passed any Buffer args, then do multiple writes, one for each arg. + // This means that using Buffers in commands is going to be slower, so use Strings if you don't already have a Buffer. + + command_str = "*" + elem_count + "\r\n$" + command.length + "\r\n" + command + "\r\n"; + + if (! buffer_args) { // Build up a string and send entire command in one write + for (i = 0, il = args.length, arg; i < il; i += 1) { + arg = args[i]; + if (typeof arg !== "string") { + arg = String(arg); + } + command_str += "$" + Buffer.byteLength(arg) + "\r\n" + arg + "\r\n"; + } + if (exports.debug_mode) { + console.log("send " + this.host + ":" + this.port + " id " + this.connection_id + ": " + command_str); + } + buffered_writes += !stream.write(command_str); + } else { + if (exports.debug_mode) { + console.log("send command (" + command_str + ") has Buffer arguments"); + } + buffered_writes += !stream.write(command_str); + + for (i = 0, il = args.length, arg; i < il; i += 1) { + arg = args[i]; + if (!(Buffer.isBuffer(arg) || arg instanceof String)) { + arg = String(arg); + } + + if (Buffer.isBuffer(arg)) { + if (arg.length === 0) { + if (exports.debug_mode) { + console.log("send_command: using empty string for 0 length buffer"); + } + buffered_writes += !stream.write("$0\r\n\r\n"); + } else { + buffered_writes += !stream.write("$" + arg.length + "\r\n"); + buffered_writes += !stream.write(arg); + buffered_writes += !stream.write("\r\n"); + if (exports.debug_mode) { + console.log("send_command: buffer send " + arg.length + " bytes"); + } + } + } else { + if (exports.debug_mode) { + console.log("send_command: string send " + Buffer.byteLength(arg) + " bytes: " + arg); + } + buffered_writes += !stream.write("$" + Buffer.byteLength(arg) + "\r\n" + arg + "\r\n"); + } + } + } + if (exports.debug_mode) { + console.log("send_command buffered_writes: " + buffered_writes, " should_buffer: " + this.should_buffer); + } + if (buffered_writes || this.command_queue.getLength() >= this.command_queue_high_water) { + this.should_buffer = true; + } + return !this.should_buffer; +}; + +RedisClient.prototype.pub_sub_command = function (command_obj) { + var i, key, command, args; + + if (this.pub_sub_mode === false && exports.debug_mode) { + console.log("Entering pub/sub mode from " + command_obj.command); + } + this.pub_sub_mode = true; + command_obj.sub_command = true; + + command = command_obj.command; + args = command_obj.args; + if (command === "subscribe" || command === "psubscribe") { + if (command === "subscribe") { + key = "sub"; + } else { + key = "psub"; + } + for (i = 0; i < args.length; i++) { + this.subscription_set[key + " " + args[i]] = true; + } + } else { + if (command === "unsubscribe") { + key = "sub"; + } else { + key = "psub"; + } + for (i = 0; i < args.length; i++) { + delete this.subscription_set[key + " " + args[i]]; + } + } +}; + +RedisClient.prototype.end = function () { + this.stream._events = {}; + this.connected = false; + this.ready = false; + this.closing = true; + return this.stream.end(); +}; + +function Multi(client, args) { + this._client = client; + this.queue = [["MULTI"]]; + if (Array.isArray(args)) { + this.queue = this.queue.concat(args); + } +} + +exports.Multi = Multi; + +// take 2 arrays and return the union of their elements +function set_union(seta, setb) { + var obj = {}; + + seta.forEach(function (val) { + obj[val] = true; + }); + setb.forEach(function (val) { + obj[val] = true; + }); + return Object.keys(obj); +} + +// This static list of commands is updated from time to time. ./lib/commands.js can be updated with generate_commands.js +commands = set_union(["get", "set", "setnx", "setex", "append", "strlen", "del", "exists", "setbit", "getbit", "setrange", "getrange", "substr", + "incr", "decr", "mget", "rpush", "lpush", "rpushx", "lpushx", "linsert", "rpop", "lpop", "brpop", "brpoplpush", "blpop", "llen", "lindex", + "lset", "lrange", "ltrim", "lrem", "rpoplpush", "sadd", "srem", "smove", "sismember", "scard", "spop", "srandmember", "sinter", "sinterstore", + "sunion", "sunionstore", "sdiff", "sdiffstore", "smembers", "zadd", "zincrby", "zrem", "zremrangebyscore", "zremrangebyrank", "zunionstore", + "zinterstore", "zrange", "zrangebyscore", "zrevrangebyscore", "zcount", "zrevrange", "zcard", "zscore", "zrank", "zrevrank", "hset", "hsetnx", + "hget", "hmset", "hmget", "hincrby", "hdel", "hlen", "hkeys", "hvals", "hgetall", "hexists", "incrby", "decrby", "getset", "mset", "msetnx", + "randomkey", "select", "move", "rename", "renamenx", "expire", "expireat", "keys", "dbsize", "auth", "ping", "echo", "save", "bgsave", + "bgrewriteaof", "shutdown", "lastsave", "type", "multi", "exec", "discard", "sync", "flushdb", "flushall", "sort", "info", "monitor", "ttl", + "persist", "slaveof", "debug", "config", "subscribe", "unsubscribe", "psubscribe", "punsubscribe", "publish", "watch", "unwatch", "cluster", + "restore", "migrate", "dump", "object", "client", "eval", "evalsha"], require("./lib/commands")); + +commands.forEach(function (fullCommand) { + var command = fullCommand.split(' ')[0]; + + RedisClient.prototype[command] = function (args, callback) { + if (Array.isArray(args) && typeof callback === "function") { + return this.send_command(command, args, callback); + } else { + return this.send_command(command, to_array(arguments)); + } + }; + RedisClient.prototype[command.toUpperCase()] = RedisClient.prototype[command]; + + Multi.prototype[command] = function () { + this.queue.push([command].concat(to_array(arguments))); + return this; + }; + Multi.prototype[command.toUpperCase()] = Multi.prototype[command]; +}); + +// store db in this.select_db to restore it on reconnect +RedisClient.prototype.select = function (db, callback) { + var self = this; + + this.send_command('select', [db], function (err, res) { + if (err === null) { + self.selected_db = db; + } + if (typeof(callback) === 'function') { + callback(err, res); + } + }); +}; +RedisClient.prototype.SELECT = RedisClient.prototype.select; + +// Stash auth for connect and reconnect. Send immediately if already connected. +RedisClient.prototype.auth = function () { + var args = to_array(arguments); + this.auth_pass = args[0]; + this.auth_callback = args[1]; + if (exports.debug_mode) { + console.log("Saving auth as " + this.auth_pass); + } + + if (this.connected) { + this.send_command("auth", args); + } +}; +RedisClient.prototype.AUTH = RedisClient.prototype.auth; + +RedisClient.prototype.hmget = function (arg1, arg2, arg3) { + if (Array.isArray(arg2) && typeof arg3 === "function") { + return this.send_command("hmget", [arg1].concat(arg2), arg3); + } else if (Array.isArray(arg1) && typeof arg2 === "function") { + return this.send_command("hmget", arg1, arg2); + } else { + return this.send_command("hmget", to_array(arguments)); + } +}; +RedisClient.prototype.HMGET = RedisClient.prototype.hmget; + +RedisClient.prototype.hmset = function (args, callback) { + var tmp_args, tmp_keys, i, il, key; + + if (Array.isArray(args) && typeof callback === "function") { + return this.send_command("hmset", args, callback); + } + + args = to_array(arguments); + if (typeof args[args.length - 1] === "function") { + callback = args[args.length - 1]; + args.length -= 1; + } else { + callback = null; + } + + if (args.length === 2 && typeof args[0] === "string" && typeof args[1] === "object") { + // User does: client.hmset(key, {key1: val1, key2: val2}) + tmp_args = [ args[0] ]; + tmp_keys = Object.keys(args[1]); + for (i = 0, il = tmp_keys.length; i < il ; i++) { + key = tmp_keys[i]; + tmp_args.push(key); + tmp_args.push(args[1][key]); + } + args = tmp_args; + } + + return this.send_command("hmset", args, callback); +}; +RedisClient.prototype.HMSET = RedisClient.prototype.hmset; + +Multi.prototype.hmset = function () { + var args = to_array(arguments), tmp_args; + if (args.length >= 2 && typeof args[0] === "string" && typeof args[1] === "object") { + tmp_args = [ "hmset", args[0] ]; + Object.keys(args[1]).map(function (key) { + tmp_args.push(key); + tmp_args.push(args[1][key]); + }); + if (args[2]) { + tmp_args.push(args[2]); + } + args = tmp_args; + } else { + args.unshift("hmset"); + } + + this.queue.push(args); + return this; +}; +Multi.prototype.HMSET = Multi.prototype.hmset; + +Multi.prototype.exec = function (callback) { + var self = this; + + // drain queue, callback will catch "QUEUED" or error + // TODO - get rid of all of these anonymous functions which are elegant but slow + this.queue.forEach(function (args, index) { + var command = args[0], obj; + if (typeof args[args.length - 1] === "function") { + args = args.slice(1, -1); + } else { + args = args.slice(1); + } + if (args.length === 1 && Array.isArray(args[0])) { + args = args[0]; + } + if (command.toLowerCase() === 'hmset' && typeof args[1] === 'object') { + obj = args.pop(); + Object.keys(obj).forEach(function (key) { + args.push(key); + args.push(obj[key]); + }); + } + this._client.send_command(command, args, function (err, reply) { + if (err) { + var cur = self.queue[index]; + if (typeof cur[cur.length - 1] === "function") { + cur[cur.length - 1](err); + } else { + throw new Error(err); + } + self.queue.splice(index, 1); + } + }); + }, this); + + // TODO - make this callback part of Multi.prototype instead of creating it each time + return this._client.send_command("EXEC", [], function (err, replies) { + if (err) { + if (callback) { + callback(new Error(err)); + return; + } else { + throw new Error(err); + } + } + + var i, il, reply, args; + + if (replies) { + for (i = 1, il = self.queue.length; i < il; i += 1) { + reply = replies[i - 1]; + args = self.queue[i]; + + // TODO - confusing and error-prone that hgetall is special cased in two places + if (reply && args[0].toLowerCase() === "hgetall") { + replies[i - 1] = reply = reply_to_object(reply); + } + + if (typeof args[args.length - 1] === "function") { + args[args.length - 1](null, reply); + } + } + } + + if (callback) { + callback(null, replies); + } + }); +}; +Multi.prototype.EXEC = Multi.prototype.exec; + +RedisClient.prototype.multi = function (args) { + return new Multi(this, args); +}; +RedisClient.prototype.MULTI = function (args) { + return new Multi(this, args); +}; + + +// stash original eval method +var eval_orig = RedisClient.prototype.eval; +// hook eval with an attempt to evalsha for cached scripts +RedisClient.prototype.eval = RedisClient.prototype.EVAL = function () { + var self = this, + args = to_array(arguments), + callback; + + if (typeof args[args.length - 1] === "function") { + callback = args.pop(); + } + + if (Array.isArray(args[0])) { + args = args[0]; + } + + // replace script source with sha value + var source = args[0]; + args[0] = crypto.createHash("sha1").update(source).digest("hex"); + + self.evalsha(args, function (err, reply) { + if (err && /NOSCRIPT/.test(err.message)) { + args[0] = source; + eval_orig.call(self, args, callback); + + } else if (callback) { + callback(err, reply); + } + }); +}; + + +exports.createClient = function (port_arg, host_arg, options) { + var port = port_arg || default_port, + host = host_arg || default_host, + redis_client, net_client; + + net_client = net.createConnection(port, host); + + redis_client = new RedisClient(net_client, options); + + redis_client.port = port; + redis_client.host = host; + + return redis_client; +}; + +exports.print = function (err, reply) { + if (err) { + console.log("Error: " + err); + } else { + console.log("Reply: " + reply); + } +}; diff --git a/node_modules/redis/lib/commands.js b/node_modules/redis/lib/commands.js new file mode 100644 index 0000000000..4abfe68668 --- /dev/null +++ b/node_modules/redis/lib/commands.js @@ -0,0 +1,149 @@ +// This file was generated by ./generate_commands.js on Sun Feb 17 2013 19:04:47 GMT-0500 (EST) +module.exports = [ + "append", + "auth", + "bgrewriteaof", + "bgsave", + "bitcount", + "bitop", + "blpop", + "brpop", + "brpoplpush", + "client kill", + "client list", + "client getname", + "client setname", + "config get", + "config set", + "config resetstat", + "dbsize", + "debug object", + "debug segfault", + "decr", + "decrby", + "del", + "discard", + "dump", + "echo", + "eval", + "evalsha", + "exec", + "exists", + "expire", + "expireat", + "flushall", + "flushdb", + "get", + "getbit", + "getrange", + "getset", + "hdel", + "hexists", + "hget", + "hgetall", + "hincrby", + "hincrbyfloat", + "hkeys", + "hlen", + "hmget", + "hmset", + "hset", + "hsetnx", + "hvals", + "incr", + "incrby", + "incrbyfloat", + "info", + "keys", + "lastsave", + "lindex", + "linsert", + "llen", + "lpop", + "lpush", + "lpushx", + "lrange", + "lrem", + "lset", + "ltrim", + "mget", + "migrate", + "monitor", + "move", + "mset", + "msetnx", + "multi", + "object", + "persist", + "pexpire", + "pexpireat", + "ping", + "psetex", + "psubscribe", + "pttl", + "publish", + "punsubscribe", + "quit", + "randomkey", + "rename", + "renamenx", + "restore", + "rpop", + "rpoplpush", + "rpush", + "rpushx", + "sadd", + "save", + "scard", + "script exists", + "script flush", + "script kill", + "script load", + "sdiff", + "sdiffstore", + "select", + "set", + "setbit", + "setex", + "setnx", + "setrange", + "shutdown", + "sinter", + "sinterstore", + "sismember", + "slaveof", + "slowlog", + "smembers", + "smove", + "sort", + "spop", + "srandmember", + "srem", + "strlen", + "subscribe", + "sunion", + "sunionstore", + "sync", + "time", + "ttl", + "type", + "unsubscribe", + "unwatch", + "watch", + "zadd", + "zcard", + "zcount", + "zincrby", + "zinterstore", + "zrange", + "zrangebyscore", + "zrank", + "zrem", + "zremrangebyrank", + "zremrangebyscore", + "zrevrange", + "zrevrangebyscore", + "zrevrank", + "zscore", + "zunionstore" +]; diff --git a/node_modules/redis/lib/parser/hiredis.js b/node_modules/redis/lib/parser/hiredis.js new file mode 100644 index 0000000000..940bfeeb76 --- /dev/null +++ b/node_modules/redis/lib/parser/hiredis.js @@ -0,0 +1,46 @@ +var events = require("events"), + util = require("../util"), + hiredis = require("hiredis"); + +exports.debug_mode = false; +exports.name = "hiredis"; + +function HiredisReplyParser(options) { + this.name = exports.name; + this.options = options || {}; + this.reset(); + events.EventEmitter.call(this); +} + +util.inherits(HiredisReplyParser, events.EventEmitter); + +exports.Parser = HiredisReplyParser; + +HiredisReplyParser.prototype.reset = function () { + this.reader = new hiredis.Reader({ + return_buffers: this.options.return_buffers || false + }); +}; + +HiredisReplyParser.prototype.execute = function (data) { + var reply; + this.reader.feed(data); + while (true) { + try { + reply = this.reader.get(); + } catch (err) { + this.emit("error", err); + break; + } + + if (reply === undefined) { + break; + } + + if (reply && reply.constructor === Error) { + this.emit("reply error", reply); + } else { + this.emit("reply", reply); + } + } +}; diff --git a/node_modules/redis/lib/parser/javascript.js b/node_modules/redis/lib/parser/javascript.js new file mode 100644 index 0000000000..0990cc098d --- /dev/null +++ b/node_modules/redis/lib/parser/javascript.js @@ -0,0 +1,301 @@ +var events = require("events"), + util = require("../util"); + +function Packet(type, size) { + this.type = type; + this.size = +size; +} + +exports.name = "javascript"; +exports.debug_mode = false; + +function ReplyParser(options) { + this.name = exports.name; + this.options = options || { }; + + this._buffer = null; + this._offset = 0; + this._encoding = "utf-8"; + this._debug_mode = options.debug_mode; + this._reply_type = null; +} + +util.inherits(ReplyParser, events.EventEmitter); + +exports.Parser = ReplyParser; + +function IncompleteReadBuffer(message) { + this.name = "IncompleteReadBuffer"; + this.message = message; +} +util.inherits(IncompleteReadBuffer, Error); + +// Buffer.toString() is quite slow for small strings +function small_toString(buf, start, end) { + var tmp = "", i; + + for (i = start; i < end; i++) { + tmp += String.fromCharCode(buf[i]); + } + + return tmp; +} + +ReplyParser.prototype._parseResult = function (type) { + var start, end, offset, packetHeader; + + if (type === 43 || type === 45) { // + or - + // up to the delimiter + end = this._packetEndOffset() - 1; + start = this._offset; + + // include the delimiter + this._offset = end + 2; + + if (end > this._buffer.length) { + this._offset = start; + throw new IncompleteReadBuffer("Wait for more data."); + } + + if (this.options.return_buffers) { + return this._buffer.slice(start, end); + } else { + if (end - start < 65536) { // completely arbitrary + return small_toString(this._buffer, start, end); + } else { + return this._buffer.toString(this._encoding, start, end); + } + } + } else if (type === 58) { // : + // up to the delimiter + end = this._packetEndOffset() - 1; + start = this._offset; + + // include the delimiter + this._offset = end + 2; + + if (end > this._buffer.length) { + this._offset = start; + throw new IncompleteReadBuffer("Wait for more data."); + } + + if (this.options.return_buffers) { + return this._buffer.slice(start, end); + } + + // return the coerced numeric value + return +small_toString(this._buffer, start, end); + } else if (type === 36) { // $ + // set a rewind point, as the packet could be larger than the + // buffer in memory + offset = this._offset - 1; + + packetHeader = new Packet(type, this.parseHeader()); + + // packets with a size of -1 are considered null + if (packetHeader.size === -1) { + return undefined; + } + + end = this._offset + packetHeader.size; + start = this._offset; + + // set the offset to after the delimiter + this._offset = end + 2; + + if (end > this._buffer.length) { + this._offset = offset; + throw new IncompleteReadBuffer("Wait for more data."); + } + + if (this.options.return_buffers) { + return this._buffer.slice(start, end); + } else { + return this._buffer.toString(this._encoding, start, end); + } + } else if (type === 42) { // * + offset = this._offset; + packetHeader = new Packet(type, this.parseHeader()); + + if (packetHeader.size < 0) { + return null; + } + + if (packetHeader.size > this._bytesRemaining()) { + this._offset = offset - 1; + throw new IncompleteReadBuffer("Wait for more data."); + } + + var reply = [ ]; + var ntype, i, res; + + offset = this._offset - 1; + + for (i = 0; i < packetHeader.size; i++) { + ntype = this._buffer[this._offset++]; + + if (this._offset > this._buffer.length) { + throw new IncompleteReadBuffer("Wait for more data."); + } + res = this._parseResult(ntype); + if (res === undefined) { + res = null; + } + reply.push(res); + } + + return reply; + } +}; + +ReplyParser.prototype.execute = function (buffer) { + this.append(buffer); + + var type, ret, offset; + + while (true) { + offset = this._offset; + try { + // at least 4 bytes: :1\r\n + if (this._bytesRemaining() < 4) { + break; + } + + type = this._buffer[this._offset++]; + + if (type === 43) { // + + ret = this._parseResult(type); + + if (ret === null) { + break; + } + + this.send_reply(ret); + } else if (type === 45) { // - + ret = this._parseResult(type); + + if (ret === null) { + break; + } + + this.send_error(ret); + } else if (type === 58) { // : + ret = this._parseResult(type); + + if (ret === null) { + break; + } + + this.send_reply(ret); + } else if (type === 36) { // $ + ret = this._parseResult(type); + + if (ret === null) { + break; + } + + // check the state for what is the result of + // a -1, set it back up for a null reply + if (ret === undefined) { + ret = null; + } + + this.send_reply(ret); + } else if (type === 42) { // * + // set a rewind point. if a failure occurs, + // wait for the next execute()/append() and try again + offset = this._offset - 1; + + ret = this._parseResult(type); + + this.send_reply(ret); + } + } catch (err) { + // catch the error (not enough data), rewind, and wait + // for the next packet to appear + if (! (err instanceof IncompleteReadBuffer)) { + throw err; + } + this._offset = offset; + break; + } + } +}; + +ReplyParser.prototype.append = function (newBuffer) { + if (!newBuffer) { + return; + } + + // first run + if (this._buffer === null) { + this._buffer = newBuffer; + + return; + } + + // out of data + if (this._offset >= this._buffer.length) { + this._buffer = newBuffer; + this._offset = 0; + + return; + } + + // very large packet + // check for concat, if we have it, use it + if (Buffer.concat !== undefined) { + this._buffer = Buffer.concat([this._buffer.slice(this._offset), newBuffer]); + } else { + var remaining = this._bytesRemaining(), + newLength = remaining + newBuffer.length, + tmpBuffer = new Buffer(newLength); + + this._buffer.copy(tmpBuffer, 0, this._offset); + newBuffer.copy(tmpBuffer, remaining, 0); + + this._buffer = tmpBuffer; + } + + this._offset = 0; +}; + +ReplyParser.prototype.parseHeader = function () { + var end = this._packetEndOffset(), + value = small_toString(this._buffer, this._offset, end - 1); + + this._offset = end + 1; + + return value; +}; + +ReplyParser.prototype._packetEndOffset = function () { + var offset = this._offset; + + while (this._buffer[offset] !== 0x0d && this._buffer[offset + 1] !== 0x0a) { + offset++; + + if (offset >= this._buffer.length) { + throw new IncompleteReadBuffer("didn't see LF after NL reading multi bulk count (" + offset + " => " + this._buffer.length + ", " + this._offset + ")"); + } + } + + offset++; + return offset; +}; + +ReplyParser.prototype._bytesRemaining = function () { + return (this._buffer.length - this._offset) < 0 ? 0 : (this._buffer.length - this._offset); +}; + +ReplyParser.prototype.parser_error = function (message) { + this.emit("error", message); +}; + +ReplyParser.prototype.send_error = function (reply) { + this.emit("reply error", reply); +}; + +ReplyParser.prototype.send_reply = function (reply) { + this.emit("reply", reply); +}; diff --git a/node_modules/redis/lib/queue.js b/node_modules/redis/lib/queue.js new file mode 100644 index 0000000000..3fc87ab102 --- /dev/null +++ b/node_modules/redis/lib/queue.js @@ -0,0 +1,59 @@ +// Queue class adapted from Tim Caswell's pattern library +// http://github.com/creationix/pattern/blob/master/lib/pattern/queue.js + +function Queue() { + this.tail = []; + this.head = []; + this.offset = 0; +} + +Queue.prototype.shift = function () { + if (this.offset === this.head.length) { + var tmp = this.head; + tmp.length = 0; + this.head = this.tail; + this.tail = tmp; + this.offset = 0; + if (this.head.length === 0) { + return; + } + } + return this.head[this.offset++]; // sorry, JSLint +}; + +Queue.prototype.push = function (item) { + return this.tail.push(item); +}; + +Queue.prototype.forEach = function (fn, thisv) { + var array = this.head.slice(this.offset), i, il; + + array.push.apply(array, this.tail); + + if (thisv) { + for (i = 0, il = array.length; i < il; i += 1) { + fn.call(thisv, array[i], i, array); + } + } else { + for (i = 0, il = array.length; i < il; i += 1) { + fn(array[i], i, array); + } + } + + return array; +}; + +Queue.prototype.getLength = function () { + return this.head.length - this.offset + this.tail.length; +}; + +Object.defineProperty(Queue.prototype, "length", { + get: function () { + return this.getLength(); + } +}); + + +if (typeof module !== "undefined" && module.exports) { + module.exports = Queue; +} diff --git a/node_modules/redis/lib/to_array.js b/node_modules/redis/lib/to_array.js new file mode 100644 index 0000000000..88a57e18a4 --- /dev/null +++ b/node_modules/redis/lib/to_array.js @@ -0,0 +1,12 @@ +function to_array(args) { + var len = args.length, + arr = new Array(len), i; + + for (i = 0; i < len; i += 1) { + arr[i] = args[i]; + } + + return arr; +} + +module.exports = to_array; diff --git a/node_modules/redis/lib/util.js b/node_modules/redis/lib/util.js new file mode 100644 index 0000000000..fc255ae953 --- /dev/null +++ b/node_modules/redis/lib/util.js @@ -0,0 +1,11 @@ +// Support for very old versions of node where the module was called "sys". At some point, we should abandon this. + +var util; + +try { + util = require("util"); +} catch (err) { + util = require("sys"); +} + +module.exports = util; diff --git a/node_modules/redis/multi_bench.js b/node_modules/redis/multi_bench.js new file mode 100644 index 0000000000..3a0d92da14 --- /dev/null +++ b/node_modules/redis/multi_bench.js @@ -0,0 +1,222 @@ +var redis = require("./index"), + metrics = require("metrics"), + num_clients = parseInt(process.argv[2], 10) || 5, + num_requests = 20000, + tests = [], + versions_logged = false, + client_options = { + return_buffers: false + }, + small_str, large_str, small_buf, large_buf; + +redis.debug_mode = false; + +function lpad(input, len, chr) { + var str = input.toString(); + chr = chr || " "; + + while (str.length < len) { + str = chr + str; + } + return str; +} + +metrics.Histogram.prototype.print_line = function () { + var obj = this.printObj(); + + return lpad(obj.min, 4) + "/" + lpad(obj.max, 4) + "/" + lpad(obj.mean.toFixed(2), 7) + "/" + lpad(obj.p95.toFixed(2), 7); +}; + +function Test(args) { + this.args = args; + + this.callback = null; + this.clients = []; + this.clients_ready = 0; + this.commands_sent = 0; + this.commands_completed = 0; + this.max_pipeline = this.args.pipeline || num_requests; + this.client_options = args.client_options || client_options; + + this.connect_latency = new metrics.Histogram(); + this.ready_latency = new metrics.Histogram(); + this.command_latency = new metrics.Histogram(); +} + +Test.prototype.run = function (callback) { + var i; + + this.callback = callback; + + for (i = 0; i < num_clients ; i++) { + this.new_client(i); + } +}; + +Test.prototype.new_client = function (id) { + var self = this, new_client; + + new_client = redis.createClient(6379, "127.0.0.1", this.client_options); + new_client.create_time = Date.now(); + + new_client.on("connect", function () { + self.connect_latency.update(Date.now() - new_client.create_time); + }); + + new_client.on("ready", function () { + if (! versions_logged) { + console.log("Client count: " + num_clients + ", node version: " + process.versions.node + ", server version: " + + new_client.server_info.redis_version + ", parser: " + new_client.reply_parser.name); + versions_logged = true; + } + self.ready_latency.update(Date.now() - new_client.create_time); + self.clients_ready++; + if (self.clients_ready === self.clients.length) { + self.on_clients_ready(); + } + }); + + self.clients[id] = new_client; +}; + +Test.prototype.on_clients_ready = function () { + process.stdout.write(lpad(this.args.descr, 13) + ", " + lpad(this.args.pipeline, 5) + "/" + this.clients_ready + " "); + this.test_start = Date.now(); + + this.fill_pipeline(); +}; + +Test.prototype.fill_pipeline = function () { + var pipeline = this.commands_sent - this.commands_completed; + + while (this.commands_sent < num_requests && pipeline < this.max_pipeline) { + this.commands_sent++; + pipeline++; + this.send_next(); + } + + if (this.commands_completed === num_requests) { + this.print_stats(); + this.stop_clients(); + } +}; + +Test.prototype.stop_clients = function () { + var self = this; + + this.clients.forEach(function (client, pos) { + if (pos === self.clients.length - 1) { + client.quit(function (err, res) { + self.callback(); + }); + } else { + client.quit(); + } + }); +}; + +Test.prototype.send_next = function () { + var self = this, + cur_client = this.commands_sent % this.clients.length, + start = Date.now(); + + this.clients[cur_client][this.args.command](this.args.args, function (err, res) { + if (err) { + throw err; + } + self.commands_completed++; + self.command_latency.update(Date.now() - start); + self.fill_pipeline(); + }); +}; + +Test.prototype.print_stats = function () { + var duration = Date.now() - this.test_start; + + console.log("min/max/avg/p95: " + this.command_latency.print_line() + " " + lpad(duration, 6) + "ms total, " + + lpad((num_requests / (duration / 1000)).toFixed(2), 8) + " ops/sec"); +}; + +small_str = "1234"; +small_buf = new Buffer(small_str); +large_str = (new Array(4097).join("-")); +large_buf = new Buffer(large_str); + +tests.push(new Test({descr: "PING", command: "ping", args: [], pipeline: 1})); +tests.push(new Test({descr: "PING", command: "ping", args: [], pipeline: 50})); +tests.push(new Test({descr: "PING", command: "ping", args: [], pipeline: 200})); +tests.push(new Test({descr: "PING", command: "ping", args: [], pipeline: 20000})); + +tests.push(new Test({descr: "SET small str", command: "set", args: ["foo_rand000000000000", small_str], pipeline: 1})); +tests.push(new Test({descr: "SET small str", command: "set", args: ["foo_rand000000000000", small_str], pipeline: 50})); +tests.push(new Test({descr: "SET small str", command: "set", args: ["foo_rand000000000000", small_str], pipeline: 200})); +tests.push(new Test({descr: "SET small str", command: "set", args: ["foo_rand000000000000", small_str], pipeline: 20000})); + +tests.push(new Test({descr: "SET small buf", command: "set", args: ["foo_rand000000000000", small_buf], pipeline: 1})); +tests.push(new Test({descr: "SET small buf", command: "set", args: ["foo_rand000000000000", small_buf], pipeline: 50})); +tests.push(new Test({descr: "SET small buf", command: "set", args: ["foo_rand000000000000", small_buf], pipeline: 200})); +tests.push(new Test({descr: "SET small buf", command: "set", args: ["foo_rand000000000000", small_buf], pipeline: 20000})); + +tests.push(new Test({descr: "GET small str", command: "get", args: ["foo_rand000000000000"], pipeline: 1})); +tests.push(new Test({descr: "GET small str", command: "get", args: ["foo_rand000000000000"], pipeline: 50})); +tests.push(new Test({descr: "GET small str", command: "get", args: ["foo_rand000000000000"], pipeline: 200})); +tests.push(new Test({descr: "GET small str", command: "get", args: ["foo_rand000000000000"], pipeline: 20000})); + +tests.push(new Test({descr: "GET small buf", command: "get", args: ["foo_rand000000000000"], pipeline: 1, client_opts: { return_buffers: true} })); +tests.push(new Test({descr: "GET small buf", command: "get", args: ["foo_rand000000000000"], pipeline: 50, client_opts: { return_buffers: true} })); +tests.push(new Test({descr: "GET small buf", command: "get", args: ["foo_rand000000000000"], pipeline: 200, client_opts: { return_buffers: true} })); +tests.push(new Test({descr: "GET small buf", command: "get", args: ["foo_rand000000000000"], pipeline: 20000, client_opts: { return_buffers: true} })); + +tests.push(new Test({descr: "SET large str", command: "set", args: ["foo_rand000000000001", large_str], pipeline: 1})); +tests.push(new Test({descr: "SET large str", command: "set", args: ["foo_rand000000000001", large_str], pipeline: 50})); +tests.push(new Test({descr: "SET large str", command: "set", args: ["foo_rand000000000001", large_str], pipeline: 200})); +tests.push(new Test({descr: "SET large str", command: "set", args: ["foo_rand000000000001", large_str], pipeline: 20000})); + +tests.push(new Test({descr: "SET large buf", command: "set", args: ["foo_rand000000000001", large_buf], pipeline: 1})); +tests.push(new Test({descr: "SET large buf", command: "set", args: ["foo_rand000000000001", large_buf], pipeline: 50})); +tests.push(new Test({descr: "SET large buf", command: "set", args: ["foo_rand000000000001", large_buf], pipeline: 200})); +tests.push(new Test({descr: "SET large buf", command: "set", args: ["foo_rand000000000001", large_buf], pipeline: 20000})); + +tests.push(new Test({descr: "GET large str", command: "get", args: ["foo_rand000000000001"], pipeline: 1})); +tests.push(new Test({descr: "GET large str", command: "get", args: ["foo_rand000000000001"], pipeline: 50})); +tests.push(new Test({descr: "GET large str", command: "get", args: ["foo_rand000000000001"], pipeline: 200})); +tests.push(new Test({descr: "GET large str", command: "get", args: ["foo_rand000000000001"], pipeline: 20000})); + +tests.push(new Test({descr: "GET large buf", command: "get", args: ["foo_rand000000000001"], pipeline: 1, client_opts: { return_buffers: true} })); +tests.push(new Test({descr: "GET large buf", command: "get", args: ["foo_rand000000000001"], pipeline: 50, client_opts: { return_buffers: true} })); +tests.push(new Test({descr: "GET large buf", command: "get", args: ["foo_rand000000000001"], pipeline: 200, client_opts: { return_buffers: true} })); +tests.push(new Test({descr: "GET large buf", command: "get", args: ["foo_rand000000000001"], pipeline: 20000, client_opts: { return_buffers: true} })); + +tests.push(new Test({descr: "INCR", command: "incr", args: ["counter_rand000000000000"], pipeline: 1})); +tests.push(new Test({descr: "INCR", command: "incr", args: ["counter_rand000000000000"], pipeline: 50})); +tests.push(new Test({descr: "INCR", command: "incr", args: ["counter_rand000000000000"], pipeline: 200})); +tests.push(new Test({descr: "INCR", command: "incr", args: ["counter_rand000000000000"], pipeline: 20000})); + +tests.push(new Test({descr: "LPUSH", command: "lpush", args: ["mylist", small_str], pipeline: 1})); +tests.push(new Test({descr: "LPUSH", command: "lpush", args: ["mylist", small_str], pipeline: 50})); +tests.push(new Test({descr: "LPUSH", command: "lpush", args: ["mylist", small_str], pipeline: 200})); +tests.push(new Test({descr: "LPUSH", command: "lpush", args: ["mylist", small_str], pipeline: 20000})); + +tests.push(new Test({descr: "LRANGE 10", command: "lrange", args: ["mylist", "0", "9"], pipeline: 1})); +tests.push(new Test({descr: "LRANGE 10", command: "lrange", args: ["mylist", "0", "9"], pipeline: 50})); +tests.push(new Test({descr: "LRANGE 10", command: "lrange", args: ["mylist", "0", "9"], pipeline: 200})); +tests.push(new Test({descr: "LRANGE 10", command: "lrange", args: ["mylist", "0", "9"], pipeline: 20000})); + +tests.push(new Test({descr: "LRANGE 100", command: "lrange", args: ["mylist", "0", "99"], pipeline: 1})); +tests.push(new Test({descr: "LRANGE 100", command: "lrange", args: ["mylist", "0", "99"], pipeline: 50})); +tests.push(new Test({descr: "LRANGE 100", command: "lrange", args: ["mylist", "0", "99"], pipeline: 200})); +tests.push(new Test({descr: "LRANGE 100", command: "lrange", args: ["mylist", "0", "99"], pipeline: 20000})); + +function next() { + var test = tests.shift(); + if (test) { + test.run(function () { + next(); + }); + } else { + console.log("End of tests."); + process.exit(0); + } +} + +next(); diff --git a/node_modules/redis/package.json b/node_modules/redis/package.json new file mode 100644 index 0000000000..489f477030 --- /dev/null +++ b/node_modules/redis/package.json @@ -0,0 +1,40 @@ +{ + "name": "redis", + "version": "0.8.3", + "description": "Redis client library", + "keywords": [ + "redis", + "database" + ], + "author": { + "name": "Matt Ranney", + "email": "mjr@ranney.com" + }, + "main": "./index.js", + "scripts": { + "test": "node ./test.js" + }, + "devDependencies": { + "metrics": ">=0.1.5", + "colors": "~0.6.0-1", + "underscore": "~1.4.4" + }, + "repository": { + "type": "git", + "url": "git://github.com/mranney/node_redis.git" + }, + "_id": "redis@0.8.3", + "dependencies": {}, + "optionalDependencies": {}, + "engines": { + "node": "*" + }, + "_engineSupported": true, + "_npmVersion": "1.1.4", + "_nodeVersion": "v0.6.12", + "_defaultsLoaded": true, + "dist": { + "shasum": "bda57a05e1f0d9a69996dbcdcd6010c8ca1de0c9" + }, + "_from": "redis@0.8.3" +} diff --git a/node_modules/redis/test.js b/node_modules/redis/test.js new file mode 100644 index 0000000000..1d8d6343af --- /dev/null +++ b/node_modules/redis/test.js @@ -0,0 +1,1955 @@ +/*global require console setTimeout process Buffer */ +var PORT = 6379; +var HOST = '127.0.0.1'; + +var redis = require("./index"), + client = redis.createClient(PORT, HOST), + client2 = redis.createClient(PORT, HOST), + client3 = redis.createClient(PORT, HOST), + bclient = redis.createClient(PORT, HOST, { return_buffers: true }), + assert = require("assert"), + crypto = require("crypto"), + util = require("./lib/util"), + test_db_num = 15, // this DB will be flushed and used for testing + tests = {}, + connected = false, + ended = false, + next, cur_start, run_next_test, all_tests, all_start, test_count; + + +// Set this to truthy to see the wire protocol and other debugging info +redis.debug_mode = process.argv[2]; + +function server_version_at_least(connection, desired_version) { + // Return true if the server version >= desired_version + var version = connection.server_info.versions; + for (var i = 0; i < 3; i++) { + if (version[i] > desired_version[i]) return true; + if (version[i] < desired_version[i]) return false; + } + return true; +} + +function buffers_to_strings(arr) { + return arr.map(function (val) { + return val.toString(); + }); +} + +function require_number(expected, label) { + return function (err, results) { + assert.strictEqual(null, err, label + " expected " + expected + ", got error: " + err); + assert.strictEqual(expected, results, label + " " + expected + " !== " + results); + assert.strictEqual(typeof results, "number", label); + return true; + }; +} + +function require_number_any(label) { + return function (err, results) { + assert.strictEqual(null, err, label + " expected any number, got error: " + err); + assert.strictEqual(typeof results, "number", label + " " + results + " is not a number"); + return true; + }; +} + +function require_number_pos(label) { + return function (err, results) { + assert.strictEqual(null, err, label + " expected positive number, got error: " + err); + assert.strictEqual(true, (results > 0), label + " " + results + " is not a positive number"); + return true; + }; +} + +function require_string(str, label) { + return function (err, results) { + assert.strictEqual(null, err, label + " expected string '" + str + "', got error: " + err); + assert.equal(str, results, label + " " + str + " does not match " + results); + return true; + }; +} + +function require_null(label) { + return function (err, results) { + assert.strictEqual(null, err, label + " expected null, got error: " + err); + assert.strictEqual(null, results, label + ": " + results + " is not null"); + return true; + }; +} + +function require_error(label) { + return function (err, results) { + assert.notEqual(err, null, label + " err is null, but an error is expected here."); + return true; + }; +} + +function is_empty_array(obj) { + return Array.isArray(obj) && obj.length === 0; +} + +function last(name, fn) { + return function (err, results) { + fn(err, results); + next(name); + }; +} + +// Wraps the given callback in a timeout. If the returned function +// is not called within the timeout period, we fail the named test. +function with_timeout(name, cb, millis) { + var timeoutId = setTimeout(function() { + assert.fail("Callback timed out!", name); + }, millis); + return function() { + clearTimeout(timeoutId); + cb.apply(this, arguments); + }; +} + +next = function next(name) { + console.log(" \x1b[33m" + (Date.now() - cur_start) + "\x1b[0m ms"); + run_next_test(); +}; + +// Tests are run in the order they are defined, so FLUSHDB should always be first. + +tests.FLUSHDB = function () { + var name = "FLUSHDB"; + client.select(test_db_num, require_string("OK", name)); + client2.select(test_db_num, require_string("OK", name)); + client3.select(test_db_num, require_string("OK", name)); + client.mset("flush keys 1", "flush val 1", "flush keys 2", "flush val 2", require_string("OK", name)); + client.FLUSHDB(require_string("OK", name)); + client.dbsize(last(name, require_number(0, name))); +}; + +tests.INCR = function () { + var name = "INCR"; + + if (bclient.reply_parser.name == "hiredis") { + console.log("Skipping INCR buffer test with hiredis"); + return next(name); + } + + // Test incr with the maximum JavaScript number value. Since we are + // returning buffers we should get back one more as a Buffer. + bclient.set("seq", "9007199254740992", function (err, result) { + assert.strictEqual(result.toString(), "OK"); + bclient.incr("seq", function (err, result) { + assert.strictEqual("9007199254740993", result.toString()); + next(name); + }); + }); +}; + +tests.MULTI_1 = function () { + var name = "MULTI_1", multi1, multi2; + + // Provoke an error at queue time + multi1 = client.multi(); + multi1.mset("multifoo", "10", "multibar", "20", require_string("OK", name)); + multi1.set("foo2", require_error(name)); + multi1.incr("multifoo", require_number(11, name)); + multi1.incr("multibar", require_number(21, name)); + multi1.exec(function () { + require_error(name); + + // Redis 2.6.5+ will abort transactions with errors + // see: http://redis.io/topics/transactions + var multibar_expected = 22; + var multifoo_expected = 12; + if (server_version_at_least(client, [2, 6, 5])) { + multibar_expected = 1; + multifoo_expected = 1; + } + + // Confirm that the previous command, while containing an error, still worked. + multi2 = client.multi(); + multi2.incr("multibar", require_number(multibar_expected, name)); + multi2.incr("multifoo", require_number(multifoo_expected, name)); + multi2.exec(function (err, replies) { + assert.strictEqual(multibar_expected, replies[0]); + assert.strictEqual(multifoo_expected, replies[1]); + next(name); + }); + }); +}; + +tests.MULTI_2 = function () { + var name = "MULTI_2"; + + // test nested multi-bulk replies + client.multi([ + ["mget", "multifoo", "multibar", function (err, res) { + assert.strictEqual(2, res.length, name); + assert.strictEqual("12", res[0].toString(), name); + assert.strictEqual("22", res[1].toString(), name); + }], + ["set", "foo2", require_error(name)], + ["incr", "multifoo", require_number(13, name)], + ["incr", "multibar", require_number(23, name)] + + ]).exec(function (err, replies) { + + if (server_version_at_least(client, [2, 6, 5])) { + assert.notEqual(err, null, name); + assert.equal(replies, undefined, name); + } else { + assert.strictEqual(2, replies[0].length, name); + assert.strictEqual("12", replies[0][0].toString(), name); + assert.strictEqual("22", replies[0][1].toString(), name); + + assert.strictEqual("13", replies[1].toString()); + assert.strictEqual("23", replies[2].toString()); + } + next(name); + }); +}; + +tests.MULTI_3 = function () { + var name = "MULTI_3"; + + client.sadd("some set", "mem 1"); + client.sadd("some set", "mem 2"); + client.sadd("some set", "mem 3"); + client.sadd("some set", "mem 4"); + + // make sure empty mb reply works + client.del("some missing set"); + client.smembers("some missing set", function (err, reply) { + // make sure empty mb reply works + assert.strictEqual(true, is_empty_array(reply), name); + }); + + // test nested multi-bulk replies with empty mb elements. + client.multi([ + ["smembers", "some set"], + ["del", "some set"], + ["smembers", "some set"] + ]) + .scard("some set") + .exec(function (err, replies) { + assert.strictEqual(true, is_empty_array(replies[2]), name); + next(name); + }); +}; + +tests.MULTI_4 = function () { + var name = "MULTI_4"; + + client.multi() + .mset('some', '10', 'keys', '20') + .incr('some') + .incr('keys') + .mget('some', 'keys') + .exec(function (err, replies) { + assert.strictEqual(null, err); + assert.equal('OK', replies[0]); + assert.equal(11, replies[1]); + assert.equal(21, replies[2]); + assert.equal(11, replies[3][0].toString()); + assert.equal(21, replies[3][1].toString()); + next(name); + }); +}; + +tests.MULTI_5 = function () { + var name = "MULTI_5"; + + // test nested multi-bulk replies with nulls. + client.multi([ + ["mget", ["multifoo", "some", "random value", "keys"]], + ["incr", "multifoo"] + ]) + .exec(function (err, replies) { + assert.strictEqual(replies.length, 2, name); + assert.strictEqual(replies[0].length, 4, name); + next(name); + }); +}; + +tests.MULTI_6 = function () { + var name = "MULTI_6"; + + client.multi() + .hmset("multihash", "a", "foo", "b", 1) + .hmset("multihash", { + extra: "fancy", + things: "here" + }) + .hgetall("multihash") + .exec(function (err, replies) { + assert.strictEqual(null, err); + assert.equal("OK", replies[0]); + assert.equal(Object.keys(replies[2]).length, 4); + assert.equal("foo", replies[2].a); + assert.equal("1", replies[2].b); + assert.equal("fancy", replies[2].extra); + assert.equal("here", replies[2].things); + next(name); + }); +}; + +tests.MULTI_7 = function () { + var name = "MULTI_7"; + + if (bclient.reply_parser.name != "javascript") { + console.log("Skipping wire-protocol test for 3rd-party parser"); + return next(name); + } + + var p = require("./lib/parser/javascript"); + var parser = new p.Parser(false); + var reply_count = 0; + function check_reply(reply) { + assert.deepEqual(reply, [['a']], "Expecting multi-bulk reply of [['a']]"); + reply_count++; + assert.notEqual(reply_count, 4, "Should only parse 3 replies"); + } + parser.on("reply", check_reply); + + parser.execute(new Buffer('*1\r\n*1\r\n$1\r\na\r\n')); + + parser.execute(new Buffer('*1\r\n*1\r')); + parser.execute(new Buffer('\n$1\r\na\r\n')); + + parser.execute(new Buffer('*1\r\n*1\r\n')); + parser.execute(new Buffer('$1\r\na\r\n')); + + next(name); +}; + +tests.FWD_ERRORS_1 = function () { + var name = "FWD_ERRORS_1"; + + var toThrow = new Error("Forced exception"); + var recordedError = null; + + var originalHandlers = client3.listeners("error"); + client3.removeAllListeners("error"); + client3.once("error", function (err) { + recordedError = err; + }); + + client3.on("message", function (channel, data) { + console.log("incoming"); + if (channel == name) { + assert.equal(data, "Some message"); + throw toThrow; + } + }); + client3.subscribe(name); + + client.publish(name, "Some message"); + setTimeout(function () { + client3.listeners("error").push(originalHandlers); + assert.equal(recordedError, toThrow, "Should have caught our forced exception"); + next(name); + }, 150); +}; + +tests.EVAL_1 = function () { + var name = "EVAL_1"; + + if (!server_version_at_least(client, [2, 5, 0])) { + console.log("Skipping " + name + " for old Redis server version < 2.5.x"); + return next(name); + } + + // test {EVAL - Lua integer -> Redis protocol type conversion} + client.eval("return 100.5", 0, require_number(100, name)); + // test {EVAL - Lua string -> Redis protocol type conversion} + client.eval("return 'hello world'", 0, require_string("hello world", name)); + // test {EVAL - Lua true boolean -> Redis protocol type conversion} + client.eval("return true", 0, require_number(1, name)); + // test {EVAL - Lua false boolean -> Redis protocol type conversion} + client.eval("return false", 0, require_null(name)); + // test {EVAL - Lua status code reply -> Redis protocol type conversion} + client.eval("return {ok='fine'}", 0, require_string("fine", name)); + // test {EVAL - Lua error reply -> Redis protocol type conversion} + client.eval("return {err='this is an error'}", 0, require_error(name)); + // test {EVAL - Lua table -> Redis protocol type conversion} + client.eval("return {1,2,3,'ciao',{1,2}}", 0, function (err, res) { + assert.strictEqual(5, res.length, name); + assert.strictEqual(1, res[0], name); + assert.strictEqual(2, res[1], name); + assert.strictEqual(3, res[2], name); + assert.strictEqual("ciao", res[3], name); + assert.strictEqual(2, res[4].length, name); + assert.strictEqual(1, res[4][0], name); + assert.strictEqual(2, res[4][1], name); + }); + // test {EVAL - Are the KEYS and ARGS arrays populated correctly?} + client.eval("return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", 2, "a", "b", "c", "d", function (err, res) { + assert.strictEqual(4, res.length, name); + assert.strictEqual("a", res[0], name); + assert.strictEqual("b", res[1], name); + assert.strictEqual("c", res[2], name); + assert.strictEqual("d", res[3], name); + }); + + // test {EVAL - parameters in array format gives same result} + client.eval(["return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}", 2, "a", "b", "c", "d"], function (err, res) { + assert.strictEqual(4, res.length, name); + assert.strictEqual("a", res[0], name); + assert.strictEqual("b", res[1], name); + assert.strictEqual("c", res[2], name); + assert.strictEqual("d", res[3], name); + }); + + // prepare sha sum for evalsha cache test + var source = "return redis.call('get', 'sha test')", + sha = crypto.createHash('sha1').update(source).digest('hex'); + + client.set("sha test", "eval get sha test", function (err, res) { + if (err) throw err; + // test {EVAL - is Lua able to call Redis API?} + client.eval(source, 0, function (err, res) { + require_string("eval get sha test", name)(err, res); + // test {EVALSHA - Can we call a SHA1 if already defined?} + client.evalsha(sha, 0, require_string("eval get sha test", name)); + // test {EVALSHA - Do we get an error on non defined SHA1?} + client.evalsha("ffffffffffffffffffffffffffffffffffffffff", 0, require_error(name)); + }); + }); + + // test {EVAL - Redis integer -> Lua type conversion} + client.set("incr key", 0, function (err, reply) { + if (err) throw err; + client.eval("local foo = redis.call('incr','incr key')\n" + "return {type(foo),foo}", 0, function (err, res) { + if (err) throw err; + assert.strictEqual(2, res.length, name); + assert.strictEqual("number", res[0], name); + assert.strictEqual(1, res[1], name); + }); + }); + + client.set("bulk reply key", "bulk reply value", function (err, res) { + // test {EVAL - Redis bulk -> Lua type conversion} + client.eval("local foo = redis.call('get','bulk reply key'); return {type(foo),foo}", 0, function (err, res) { + if (err) throw err; + assert.strictEqual(2, res.length, name); + assert.strictEqual("string", res[0], name); + assert.strictEqual("bulk reply value", res[1], name); + }); + }); + + // test {EVAL - Redis multi bulk -> Lua type conversion} + client.multi() + .del("mylist") + .rpush("mylist", "a") + .rpush("mylist", "b") + .rpush("mylist", "c") + .exec(function (err, replies) { + if (err) throw err; + client.eval("local foo = redis.call('lrange','mylist',0,-1); return {type(foo),foo[1],foo[2],foo[3],# foo}", 0, function (err, res) { + assert.strictEqual(5, res.length, name); + assert.strictEqual("table", res[0], name); + assert.strictEqual("a", res[1], name); + assert.strictEqual("b", res[2], name); + assert.strictEqual("c", res[3], name); + assert.strictEqual(3, res[4], name); + }); + }); + // test {EVAL - Redis status reply -> Lua type conversion} + client.eval("local foo = redis.call('set','mykey','myval'); return {type(foo),foo['ok']}", 0, function (err, res) { + if (err) throw err; + assert.strictEqual(2, res.length, name); + assert.strictEqual("table", res[0], name); + assert.strictEqual("OK", res[1], name); + }); + // test {EVAL - Redis error reply -> Lua type conversion} + client.set("error reply key", "error reply value", function (err, res) { + if (err) throw err; + client.eval("local foo = redis.pcall('incr','error reply key'); return {type(foo),foo['err']}", 0, function (err, res) { + if (err) throw err; + assert.strictEqual(2, res.length, name); + assert.strictEqual("table", res[0], name); + assert.strictEqual("ERR value is not an integer or out of range", res[1], name); + }); + }); + // test {EVAL - Redis nil bulk reply -> Lua type conversion} + client.del("nil reply key", function (err, res) { + if (err) throw err; + client.eval("local foo = redis.call('get','nil reply key'); return {type(foo),foo == false}", 0, function (err, res) { + if (err) throw err; + assert.strictEqual(2, res.length, name); + assert.strictEqual("boolean", res[0], name); + assert.strictEqual(1, res[1], name); + next(name); + }); + }); +}; + +tests.SCRIPT_LOAD = function() { + var name = "SCRIPT_LOAD", + command = "return 1", + commandSha = crypto.createHash('sha1').update(command).digest('hex'); + + if (!server_version_at_least(client, [2, 6, 0])) { + console.log("Skipping " + name + " for old Redis server version < 2.6.x"); + return next(name); + } + + bclient.script("load", command, function(err, result) { + assert.strictEqual(result.toString(), commandSha); + client.multi().script("load", command).exec(function(err, result) { + assert.strictEqual(result[0].toString(), commandSha); + client.multi([['script', 'load', command]]).exec(function(err, result) { + assert.strictEqual(result[0].toString(), commandSha); + next(name); + }); + }); + }); +}; + +tests.CLIENT_LIST = function() { + var name = "CLIENT_LIST"; + + if (!server_version_at_least(client, [2, 4, 0])) { + console.log("Skipping " + name + " for old Redis server version < 2.4.x"); + return next(name); + } + + function checkResult(result) { + var lines = result.toString().split('\n').slice(0, -1); + assert.strictEqual(lines.length, 4); + assert(lines.every(function(line) { + return line.match(/^addr=/); + })); + } + + bclient.client("list", function(err, result) { + console.log(result.toString()); + checkResult(result); + client.multi().client("list").exec(function(err, result) { + console.log(result.toString()); + checkResult(result); + client.multi([['client', 'list']]).exec(function(err, result) { + console.log(result.toString()); + checkResult(result); + next(name); + }); + }); + }); +}; + +tests.WATCH_MULTI = function () { + var name = 'WATCH_MULTI', multi; + if (!server_version_at_least(client, [2, 2, 0])) { + console.log("Skipping " + name + " for old Redis server version < 2.2.x"); + return next(name); + } + + client.watch(name); + client.incr(name); + multi = client.multi(); + multi.incr(name); + multi.exec(last(name, require_null(name))); +}; + +tests.WATCH_TRANSACTION = function () { + var name = "WATCH_TRANSACTION"; + + if (!server_version_at_least(client, [2, 1, 0])) { + console.log("Skipping " + name + " because server version isn't new enough."); + return next(name); + } + + // Test WATCH command aborting transactions, look for parser offset errors. + + client.set("unwatched", 200); + + client.set(name, 0); + client.watch(name); + client.incr(name); + var multi = client.multi() + .incr(name) + .exec(function (err, replies) { + // Failure expected because of pre-multi incr + assert.strictEqual(replies, null, "Aborted transaction multi-bulk reply should be null."); + + client.get("unwatched", function (err, reply) { + assert.equal(err, null, name); + assert.equal(reply, 200, "Expected 200, got " + reply); + next(name); + }); + }); + + client.set("unrelated", 100, function (err, reply) { + assert.equal(err, null, name); + assert.equal(reply, "OK", "Expected 'OK', got " + reply); + }); +}; + + +tests.detect_buffers = function () { + var name = "detect_buffers", detect_client = redis.createClient(null, null, {detect_buffers: true}); + + detect_client.on("ready", function () { + // single Buffer or String + detect_client.set("string key 1", "string value"); + detect_client.get("string key 1", require_string("string value", name)); + detect_client.get(new Buffer("string key 1"), function (err, reply) { + assert.strictEqual(null, err, name); + assert.strictEqual(true, Buffer.isBuffer(reply), name); + assert.strictEqual("", reply.inspect(), name); + }); + + detect_client.hmset("hash key 2", "key 1", "val 1", "key 2", "val 2"); + // array of Buffers or Strings + detect_client.hmget("hash key 2", "key 1", "key 2", function (err, reply) { + assert.strictEqual(null, err, name); + assert.strictEqual(true, Array.isArray(reply), name); + assert.strictEqual(2, reply.length, name); + assert.strictEqual("val 1", reply[0], name); + assert.strictEqual("val 2", reply[1], name); + }); + detect_client.hmget(new Buffer("hash key 2"), "key 1", "key 2", function (err, reply) { + assert.strictEqual(null, err, name); + assert.strictEqual(true, Array.isArray(reply)); + assert.strictEqual(2, reply.length, name); + assert.strictEqual(true, Buffer.isBuffer(reply[0])); + assert.strictEqual(true, Buffer.isBuffer(reply[1])); + assert.strictEqual("", reply[0].inspect(), name); + assert.strictEqual("", reply[1].inspect(), name); + }); + + // array of strings with undefined values (repro #344) + detect_client.hmget("hash key 2", "key 3", "key 4", function(err, reply) { + assert.strictEqual(null, err, name); + assert.strictEqual(true, Array.isArray(reply), name); + assert.strictEqual(2, reply.length, name); + assert.equal(null, reply[0], name); + assert.equal(null, reply[1], name); + }); + + // Object of Buffers or Strings + detect_client.hgetall("hash key 2", function (err, reply) { + assert.strictEqual(null, err, name); + assert.strictEqual("object", typeof reply, name); + assert.strictEqual(2, Object.keys(reply).length, name); + assert.strictEqual("val 1", reply["key 1"], name); + assert.strictEqual("val 2", reply["key 2"], name); + }); + detect_client.hgetall(new Buffer("hash key 2"), function (err, reply) { + assert.strictEqual(null, err, name); + assert.strictEqual("object", typeof reply, name); + assert.strictEqual(2, Object.keys(reply).length, name); + assert.strictEqual(true, Buffer.isBuffer(reply["key 1"])); + assert.strictEqual(true, Buffer.isBuffer(reply["key 2"])); + assert.strictEqual("", reply["key 1"].inspect(), name); + assert.strictEqual("", reply["key 2"].inspect(), name); + }); + + detect_client.quit(function (err, res) { + next(name); + }); + }); +}; + +tests.socket_nodelay = function () { + var name = "socket_nodelay", c1, c2, c3, ready_count = 0, quit_count = 0; + + c1 = redis.createClient(null, null, {socket_nodelay: true}); + c2 = redis.createClient(null, null, {socket_nodelay: false}); + c3 = redis.createClient(null, null); + + function quit_check() { + quit_count++; + + if (quit_count === 3) { + next(name); + } + } + + function run() { + assert.strictEqual(true, c1.options.socket_nodelay, name); + assert.strictEqual(false, c2.options.socket_nodelay, name); + assert.strictEqual(true, c3.options.socket_nodelay, name); + + c1.set(["set key 1", "set val"], require_string("OK", name)); + c1.set(["set key 2", "set val"], require_string("OK", name)); + c1.get(["set key 1"], require_string("set val", name)); + c1.get(["set key 2"], require_string("set val", name)); + + c2.set(["set key 3", "set val"], require_string("OK", name)); + c2.set(["set key 4", "set val"], require_string("OK", name)); + c2.get(["set key 3"], require_string("set val", name)); + c2.get(["set key 4"], require_string("set val", name)); + + c3.set(["set key 5", "set val"], require_string("OK", name)); + c3.set(["set key 6", "set val"], require_string("OK", name)); + c3.get(["set key 5"], require_string("set val", name)); + c3.get(["set key 6"], require_string("set val", name)); + + c1.quit(quit_check); + c2.quit(quit_check); + c3.quit(quit_check); + } + + function ready_check() { + ready_count++; + if (ready_count === 3) { + run(); + } + } + + c1.on("ready", ready_check); + c2.on("ready", ready_check); + c3.on("ready", ready_check); +}; + +tests.reconnect = function () { + var name = "reconnect"; + + client.set("recon 1", "one"); + client.set("recon 2", "two", function (err, res) { + // Do not do this in normal programs. This is to simulate the server closing on us. + // For orderly shutdown in normal programs, do client.quit() + client.stream.destroy(); + }); + + client.on("reconnecting", function on_recon(params) { + client.on("connect", function on_connect() { + client.select(test_db_num, require_string("OK", name)); + client.get("recon 1", require_string("one", name)); + client.get("recon 1", require_string("one", name)); + client.get("recon 2", require_string("two", name)); + client.get("recon 2", require_string("two", name)); + client.removeListener("connect", on_connect); + client.removeListener("reconnecting", on_recon); + next(name); + }); + }); +}; + +tests.reconnect_select_db_after_pubsub = function() { + var name = "reconnect_select_db_after_pubsub"; + + client.select(test_db_num); + client.set(name, "one"); + client.subscribe('ChannelV', function (err, res) { + client.stream.destroy(); + }); + + client.on("reconnecting", function on_recon(params) { + client.on("ready", function on_connect() { + client.unsubscribe('ChannelV', function (err, res) { + client.get(name, require_string("one", name)); + client.removeListener("connect", on_connect); + client.removeListener("reconnecting", on_recon); + next(name); + }); + }); + }); +}; + +tests.idle = function () { + var name = "idle"; + + client.on("idle", function on_idle() { + client.removeListener("idle", on_idle); + next(name); + }); + + client.set("idle", "test"); +}; + +tests.HSET = function () { + var key = "test hash", + field1 = new Buffer("0123456789"), + value1 = new Buffer("abcdefghij"), + field2 = new Buffer(0), + value2 = new Buffer(0), + name = "HSET"; + + client.HSET(key, field1, value1, require_number(1, name)); + client.HGET(key, field1, require_string(value1.toString(), name)); + + // Empty value + client.HSET(key, field1, value2, require_number(0, name)); + client.HGET([key, field1], require_string("", name)); + + // Empty key, empty value + client.HSET([key, field2, value1], require_number(1, name)); + client.HSET(key, field2, value2, last(name, require_number(0, name))); +}; + +tests.HLEN = function () { + var key = "test hash", + field1 = new Buffer("0123456789"), + value1 = new Buffer("abcdefghij"), + field2 = new Buffer(0), + value2 = new Buffer(0), + name = "HSET", + timeout = 1000; + + client.HSET(key, field1, value1, function (err, results) { + client.HLEN(key, function (err, len) { + assert.ok(2 === +len); + next(name); + }); + }); +} + +tests.HMSET_BUFFER_AND_ARRAY = function () { + // Saving a buffer and an array to the same key should not error + var key = "test hash", + field1 = "buffer", + value1 = new Buffer("abcdefghij"), + field2 = "array", + value2 = ["array contents"], + name = "HSET"; + + client.HMSET(key, field1, value1, field2, value2, last(name, require_string("OK", name))); +}; + +// TODO - add test for HMSET with optional callbacks + +tests.HMGET = function () { + var key1 = "test hash 1", key2 = "test hash 2", name = "HMGET"; + + // redis-like hmset syntax + client.HMSET(key1, "0123456789", "abcdefghij", "some manner of key", "a type of value", require_string("OK", name)); + + // fancy hmset syntax + client.HMSET(key2, { + "0123456789": "abcdefghij", + "some manner of key": "a type of value" + }, require_string("OK", name)); + + client.HMGET(key1, "0123456789", "some manner of key", function (err, reply) { + assert.strictEqual("abcdefghij", reply[0].toString(), name); + assert.strictEqual("a type of value", reply[1].toString(), name); + }); + + client.HMGET(key2, "0123456789", "some manner of key", function (err, reply) { + assert.strictEqual("abcdefghij", reply[0].toString(), name); + assert.strictEqual("a type of value", reply[1].toString(), name); + }); + + client.HMGET(key1, ["0123456789"], function (err, reply) { + assert.strictEqual("abcdefghij", reply[0], name); + }); + + client.HMGET(key1, ["0123456789", "some manner of key"], function (err, reply) { + assert.strictEqual("abcdefghij", reply[0], name); + assert.strictEqual("a type of value", reply[1], name); + }); + + client.HMGET(key1, "missing thing", "another missing thing", function (err, reply) { + assert.strictEqual(null, reply[0], name); + assert.strictEqual(null, reply[1], name); + next(name); + }); +}; + +tests.HINCRBY = function () { + var name = "HINCRBY"; + client.hset("hash incr", "value", 10, require_number(1, name)); + client.HINCRBY("hash incr", "value", 1, require_number(11, name)); + client.HINCRBY("hash incr", "value 2", 1, last(name, require_number(1, name))); +}; + +tests.SUBSCRIBE = function () { + var client1 = client, msg_count = 0, name = "SUBSCRIBE"; + + client1.on("subscribe", function (channel, count) { + if (channel === "chan1") { + client2.publish("chan1", "message 1", require_number(1, name)); + client2.publish("chan2", "message 2", require_number(1, name)); + client2.publish("chan1", "message 3", require_number(1, name)); + } + }); + + client1.on("unsubscribe", function (channel, count) { + if (count === 0) { + // make sure this connection can go into and out of pub/sub mode + client1.incr("did a thing", last(name, require_number(2, name))); + } + }); + + client1.on("message", function (channel, message) { + msg_count += 1; + assert.strictEqual("message " + msg_count, message.toString()); + if (msg_count === 3) { + client1.unsubscribe("chan1", "chan2"); + } + }); + + client1.set("did a thing", 1, require_string("OK", name)); + client1.subscribe("chan1", "chan2", function (err, results) { + assert.strictEqual(null, err, "result sent back unexpected error: " + err); + assert.strictEqual("chan1", results.toString(), name); + }); +}; + +tests.SUB_UNSUB_SUB = function () { + var name = "SUB_UNSUB_SUB"; + client3.subscribe('chan3'); + client3.unsubscribe('chan3'); + client3.subscribe('chan3', function (err, results) { + assert.strictEqual(null, err, "unexpected error: " + err); + client2.publish('chan3', 'foo'); + }); + client3.on('message', function (channel, message) { + assert.strictEqual(channel, 'chan3'); + assert.strictEqual(message, 'foo'); + client3.removeAllListeners(); + next(name); + }); +}; + +tests.SUB_UNSUB_MSG_SUB = function () { + var name = "SUB_UNSUB_MSG_SUB"; + client3.subscribe('chan8'); + client3.subscribe('chan9'); + client3.unsubscribe('chan9'); + client2.publish('chan8', 'something'); + client3.subscribe('chan9', with_timeout(name, function (err, results) { + next(name); + }, 2000)); +}; + +tests.PSUB_UNSUB_PMSG_SUB = function () { + var name = "PSUB_UNSUB_PMSG_SUB"; + client3.psubscribe('abc*'); + client3.subscribe('xyz'); + client3.unsubscribe('xyz'); + client2.publish('abcd', 'something'); + client3.subscribe('xyz', with_timeout(name, function (err, results) { + next(name); + }, 2000)); +}; + +tests.SUBSCRIBE_QUIT = function () { + var name = "SUBSCRIBE_QUIT"; + client3.on("end", function () { + next(name); + }); + client3.on("subscribe", function (channel, count) { + client3.quit(); + }); + client3.subscribe("chan3"); +}; + +tests.SUBSCRIBE_CLOSE_RESUBSCRIBE = function () { + var name = "SUBSCRIBE_CLOSE_RESUBSCRIBE"; + var c1 = redis.createClient(); + var c2 = redis.createClient(); + var count = 0; + + /* Create two clients. c1 subscribes to two channels, c2 will publish to them. + c2 publishes the first message. + c1 gets the message and drops its connection. It must resubscribe itself. + When it resubscribes, c2 publishes the second message, on the same channel + c1 gets the message and drops its connection. It must resubscribe itself, again. + When it resubscribes, c2 publishes the third message, on the second channel + c1 gets the message and drops its connection. When it reconnects, the test ends. + */ + + c1.on("message", function(channel, message) { + if (channel === "chan1") { + assert.strictEqual(message, "hi on channel 1"); + c1.stream.end(); + + } else if (channel === "chan2") { + assert.strictEqual(message, "hi on channel 2"); + c1.stream.end(); + + } else { + c1.quit(); + c2.quit(); + assert.fail("test failed"); + } + }); + + c1.subscribe("chan1", "chan2"); + + c2.once("ready", function() { + console.log("c2 is ready"); + c1.on("ready", function(err, results) { + console.log("c1 is ready", count); + + count++; + if (count == 1) { + c2.publish("chan1", "hi on channel 1"); + return; + + } else if (count == 2) { + c2.publish("chan2", "hi on channel 2"); + + } else { + c1.quit(function() { + c2.quit(function() { + next(name); + }); + }); + } + }); + + c2.publish("chan1", "hi on channel 1"); + + }); +}; + +tests.EXISTS = function () { + var name = "EXISTS"; + client.del("foo", "foo2", require_number_any(name)); + client.set("foo", "bar", require_string("OK", name)); + client.EXISTS("foo", require_number(1, name)); + client.EXISTS("foo2", last(name, require_number(0, name))); +}; + +tests.DEL = function () { + var name = "DEL"; + client.DEL("delkey", require_number_any(name)); + client.set("delkey", "delvalue", require_string("OK", name)); + client.DEL("delkey", require_number(1, name)); + client.exists("delkey", require_number(0, name)); + client.DEL("delkey", require_number(0, name)); + client.mset("delkey", "delvalue", "delkey2", "delvalue2", require_string("OK", name)); + client.DEL("delkey", "delkey2", last(name, require_number(2, name))); +}; + +tests.TYPE = function () { + var name = "TYPE"; + client.set(["string key", "should be a string"], require_string("OK", name)); + client.rpush(["list key", "should be a list"], require_number_pos(name)); + client.sadd(["set key", "should be a set"], require_number_any(name)); + client.zadd(["zset key", "10.0", "should be a zset"], require_number_any(name)); + client.hset(["hash key", "hashtest", "should be a hash"], require_number_any(0, name)); + + client.TYPE(["string key"], require_string("string", name)); + client.TYPE(["list key"], require_string("list", name)); + client.TYPE(["set key"], require_string("set", name)); + client.TYPE(["zset key"], require_string("zset", name)); + client.TYPE("not here yet", require_string("none", name)); + client.TYPE(["hash key"], last(name, require_string("hash", name))); +}; + +tests.KEYS = function () { + var name = "KEYS"; + client.mset(["test keys 1", "test val 1", "test keys 2", "test val 2"], require_string("OK", name)); + client.KEYS(["test keys*"], function (err, results) { + assert.strictEqual(null, err, "result sent back unexpected error: " + err); + assert.strictEqual(2, results.length, name); + assert.ok(~results.indexOf("test keys 1")); + assert.ok(~results.indexOf("test keys 2")); + next(name); + }); +}; + +tests.MULTIBULK = function() { + var name = "MULTIBULK", + keys_values = []; + + for (var i = 0; i < 200; i++) { + var key_value = [ + "multibulk:" + crypto.randomBytes(256).toString("hex"), // use long strings as keys to ensure generation of large packet + "test val " + i + ]; + keys_values.push(key_value); + } + + client.mset(keys_values.reduce(function(a, b) { + return a.concat(b); + }), require_string("OK", name)); + + client.KEYS("multibulk:*", function(err, results) { + assert.strictEqual(null, err, "result sent back unexpected error: " + err); + assert.deepEqual(keys_values.map(function(val) { + return val[0]; + }).sort(), results.sort(), name); + }); + + next(name); +}; + +tests.MULTIBULK_ZERO_LENGTH = function () { + var name = "MULTIBULK_ZERO_LENGTH"; + client.KEYS(['users:*'], function (err, results) { + assert.strictEqual(null, err, 'error on empty multibulk reply'); + assert.strictEqual(true, is_empty_array(results), "not an empty array"); + next(name); + }); +}; + +tests.RANDOMKEY = function () { + var name = "RANDOMKEY"; + client.mset(["test keys 1", "test val 1", "test keys 2", "test val 2"], require_string("OK", name)); + client.RANDOMKEY([], function (err, results) { + assert.strictEqual(null, err, name + " result sent back unexpected error: " + err); + assert.strictEqual(true, /\w+/.test(results), name); + next(name); + }); +}; + +tests.RENAME = function () { + var name = "RENAME"; + client.set(['foo', 'bar'], require_string("OK", name)); + client.RENAME(["foo", "new foo"], require_string("OK", name)); + client.exists(["foo"], require_number(0, name)); + client.exists(["new foo"], last(name, require_number(1, name))); +}; + +tests.RENAMENX = function () { + var name = "RENAMENX"; + client.set(['foo', 'bar'], require_string("OK", name)); + client.set(['foo2', 'bar2'], require_string("OK", name)); + client.RENAMENX(["foo", "foo2"], require_number(0, name)); + client.exists(["foo"], require_number(1, name)); + client.exists(["foo2"], require_number(1, name)); + client.del(["foo2"], require_number(1, name)); + client.RENAMENX(["foo", "foo2"], require_number(1, name)); + client.exists(["foo"], require_number(0, name)); + client.exists(["foo2"], last(name, require_number(1, name))); +}; + +tests.DBSIZE = function () { + var name = "DBSIZE"; + client.set(['foo', 'bar'], require_string("OK", name)); + client.DBSIZE([], last(name, require_number_pos("DBSIZE"))); +}; + +tests.GET_1 = function () { + var name = "GET_1"; + client.set(["get key", "get val"], require_string("OK", name)); + client.GET(["get key"], last(name, require_string("get val", name))); +}; + +tests.GET_2 = function() { + var name = "GET_2"; + + // tests handling of non-existent keys + client.GET('this_key_shouldnt_exist', last(name, require_null(name))); +}; + +tests.SET = function () { + var name = "SET"; + client.SET(["set key", "set val"], require_string("OK", name)); + client.get(["set key"], last(name, require_string("set val", name))); + client.SET(["set key", undefined], require_error(name)); +}; + +tests.GETSET = function () { + var name = "GETSET"; + client.set(["getset key", "getset val"], require_string("OK", name)); + client.GETSET(["getset key", "new getset val"], require_string("getset val", name)); + client.get(["getset key"], last(name, require_string("new getset val", name))); +}; + +tests.MGET = function () { + var name = "MGET"; + client.mset(["mget keys 1", "mget val 1", "mget keys 2", "mget val 2", "mget keys 3", "mget val 3"], require_string("OK", name)); + client.MGET("mget keys 1", "mget keys 2", "mget keys 3", function (err, results) { + assert.strictEqual(null, err, "result sent back unexpected error: " + err); + assert.strictEqual(3, results.length, name); + assert.strictEqual("mget val 1", results[0].toString(), name); + assert.strictEqual("mget val 2", results[1].toString(), name); + assert.strictEqual("mget val 3", results[2].toString(), name); + }); + client.MGET(["mget keys 1", "mget keys 2", "mget keys 3"], function (err, results) { + assert.strictEqual(null, err, "result sent back unexpected error: " + err); + assert.strictEqual(3, results.length, name); + assert.strictEqual("mget val 1", results[0].toString(), name); + assert.strictEqual("mget val 2", results[1].toString(), name); + assert.strictEqual("mget val 3", results[2].toString(), name); + }); + client.MGET(["mget keys 1", "some random shit", "mget keys 2", "mget keys 3"], function (err, results) { + assert.strictEqual(null, err, "result sent back unexpected error: " + err); + assert.strictEqual(4, results.length, name); + assert.strictEqual("mget val 1", results[0].toString(), name); + assert.strictEqual(null, results[1], name); + assert.strictEqual("mget val 2", results[2].toString(), name); + assert.strictEqual("mget val 3", results[3].toString(), name); + next(name); + }); +}; + +tests.SETNX = function () { + var name = "SETNX"; + client.set(["setnx key", "setnx value"], require_string("OK", name)); + client.SETNX(["setnx key", "new setnx value"], require_number(0, name)); + client.del(["setnx key"], require_number(1, name)); + client.exists(["setnx key"], require_number(0, name)); + client.SETNX(["setnx key", "new setnx value"], require_number(1, name)); + client.exists(["setnx key"], last(name, require_number(1, name))); +}; + +tests.SETEX = function () { + var name = "SETEX"; + client.SETEX(["setex key", "100", "setex val"], require_string("OK", name)); + client.exists(["setex key"], require_number(1, name)); + client.ttl(["setex key"], last(name, require_number_pos(name))); + client.SETEX(["setex key", "100", undefined], require_error(name)); +}; + +tests.MSETNX = function () { + var name = "MSETNX"; + client.mset(["mset1", "val1", "mset2", "val2", "mset3", "val3"], require_string("OK", name)); + client.MSETNX(["mset3", "val3", "mset4", "val4"], require_number(0, name)); + client.del(["mset3"], require_number(1, name)); + client.MSETNX(["mset3", "val3", "mset4", "val4"], require_number(1, name)); + client.exists(["mset3"], require_number(1, name)); + client.exists(["mset4"], last(name, require_number(1, name))); +}; + +tests.HGETALL = function () { + var name = "HGETALL"; + client.hmset(["hosts", "mjr", "1", "another", "23", "home", "1234"], require_string("OK", name)); + client.HGETALL(["hosts"], function (err, obj) { + assert.strictEqual(null, err, name + " result sent back unexpected error: " + err); + assert.strictEqual(3, Object.keys(obj).length, name); + assert.strictEqual("1", obj.mjr.toString(), name); + assert.strictEqual("23", obj.another.toString(), name); + assert.strictEqual("1234", obj.home.toString(), name); + next(name); + }); +}; + +tests.HGETALL_NULL = function () { + var name = "HGETALL_NULL"; + + client.hgetall("missing", function (err, obj) { + assert.strictEqual(null, err); + assert.strictEqual(null, obj); + next(name); + }); +}; + +tests.UTF8 = function () { + var name = "UTF8", + utf8_sample = "ಠ_ಠ"; + + client.set(["utf8test", utf8_sample], require_string("OK", name)); + client.get(["utf8test"], function (err, obj) { + assert.strictEqual(null, err); + assert.strictEqual(utf8_sample, obj); + next(name); + }); +}; + +// Set tests were adapted from Brian Hammond's redis-node-client.js, which has a comprehensive test suite + +tests.SADD = function () { + var name = "SADD"; + + client.del('set0'); + client.SADD('set0', 'member0', require_number(1, name)); + client.sadd('set0', 'member0', last(name, require_number(0, name))); +}; + +tests.SADD2 = function () { + var name = "SADD2"; + + client.del("set0"); + client.sadd("set0", ["member0", "member1", "member2"], require_number(3, name)); + client.smembers("set0", function (err, res) { + assert.strictEqual(res.length, 3); + assert.ok(~res.indexOf("member0")); + assert.ok(~res.indexOf("member1")); + assert.ok(~res.indexOf("member2")); + }); + client.SADD("set1", ["member0", "member1", "member2"], require_number(3, name)); + client.smembers("set1", function (err, res) { + assert.strictEqual(res.length, 3); + assert.ok(~res.indexOf("member0")); + assert.ok(~res.indexOf("member1")); + assert.ok(~res.indexOf("member2")); + next(name); + }); +}; + +tests.SISMEMBER = function () { + var name = "SISMEMBER"; + + client.del('set0'); + client.sadd('set0', 'member0', require_number(1, name)); + client.sismember('set0', 'member0', require_number(1, name)); + client.sismember('set0', 'member1', last(name, require_number(0, name))); +}; + +tests.SCARD = function () { + var name = "SCARD"; + + client.del('set0'); + client.sadd('set0', 'member0', require_number(1, name)); + client.scard('set0', require_number(1, name)); + client.sadd('set0', 'member1', require_number(1, name)); + client.scard('set0', last(name, require_number(2, name))); +}; + +tests.SREM = function () { + var name = "SREM"; + + client.del('set0'); + client.sadd('set0', 'member0', require_number(1, name)); + client.srem('set0', 'foobar', require_number(0, name)); + client.srem('set0', 'member0', require_number(1, name)); + client.scard('set0', last(name, require_number(0, name))); +}; + +tests.SPOP = function () { + var name = "SPOP"; + + client.del('zzz'); + client.sadd('zzz', 'member0', require_number(1, name)); + client.scard('zzz', require_number(1, name)); + + client.spop('zzz', function (err, value) { + if (err) { + assert.fail(err); + } + assert.equal(value, 'member0', name); + }); + + client.scard('zzz', last(name, require_number(0, name))); +}; + +tests.SDIFF = function () { + var name = "SDIFF"; + + client.del('foo'); + client.sadd('foo', 'x', require_number(1, name)); + client.sadd('foo', 'a', require_number(1, name)); + client.sadd('foo', 'b', require_number(1, name)); + client.sadd('foo', 'c', require_number(1, name)); + + client.sadd('bar', 'c', require_number(1, name)); + + client.sadd('baz', 'a', require_number(1, name)); + client.sadd('baz', 'd', require_number(1, name)); + + client.sdiff('foo', 'bar', 'baz', function (err, values) { + if (err) { + assert.fail(err, name); + } + values.sort(); + assert.equal(values.length, 2, name); + assert.equal(values[0], 'b', name); + assert.equal(values[1], 'x', name); + next(name); + }); +}; + +tests.SDIFFSTORE = function () { + var name = "SDIFFSTORE"; + + client.del('foo'); + client.del('bar'); + client.del('baz'); + client.del('quux'); + + client.sadd('foo', 'x', require_number(1, name)); + client.sadd('foo', 'a', require_number(1, name)); + client.sadd('foo', 'b', require_number(1, name)); + client.sadd('foo', 'c', require_number(1, name)); + + client.sadd('bar', 'c', require_number(1, name)); + + client.sadd('baz', 'a', require_number(1, name)); + client.sadd('baz', 'd', require_number(1, name)); + + // NB: SDIFFSTORE returns the number of elements in the dstkey + + client.sdiffstore('quux', 'foo', 'bar', 'baz', require_number(2, name)); + + client.smembers('quux', function (err, values) { + if (err) { + assert.fail(err, name); + } + var members = buffers_to_strings(values).sort(); + + assert.deepEqual(members, [ 'b', 'x' ], name); + next(name); + }); +}; + +tests.SMEMBERS = function () { + var name = "SMEMBERS"; + + client.del('foo'); + client.sadd('foo', 'x', require_number(1, name)); + + client.smembers('foo', function (err, members) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(members), [ 'x' ], name); + }); + + client.sadd('foo', 'y', require_number(1, name)); + + client.smembers('foo', function (err, values) { + if (err) { + assert.fail(err, name); + } + assert.equal(values.length, 2, name); + var members = buffers_to_strings(values).sort(); + + assert.deepEqual(members, [ 'x', 'y' ], name); + next(name); + }); +}; + +tests.SMOVE = function () { + var name = "SMOVE"; + + client.del('foo'); + client.del('bar'); + + client.sadd('foo', 'x', require_number(1, name)); + client.smove('foo', 'bar', 'x', require_number(1, name)); + client.sismember('foo', 'x', require_number(0, name)); + client.sismember('bar', 'x', require_number(1, name)); + client.smove('foo', 'bar', 'x', last(name, require_number(0, name))); +}; + +tests.SINTER = function () { + var name = "SINTER"; + + client.del('sa'); + client.del('sb'); + client.del('sc'); + + client.sadd('sa', 'a', require_number(1, name)); + client.sadd('sa', 'b', require_number(1, name)); + client.sadd('sa', 'c', require_number(1, name)); + + client.sadd('sb', 'b', require_number(1, name)); + client.sadd('sb', 'c', require_number(1, name)); + client.sadd('sb', 'd', require_number(1, name)); + + client.sadd('sc', 'c', require_number(1, name)); + client.sadd('sc', 'd', require_number(1, name)); + client.sadd('sc', 'e', require_number(1, name)); + + client.sinter('sa', 'sb', function (err, intersection) { + if (err) { + assert.fail(err, name); + } + assert.equal(intersection.length, 2, name); + assert.deepEqual(buffers_to_strings(intersection).sort(), [ 'b', 'c' ], name); + }); + + client.sinter('sb', 'sc', function (err, intersection) { + if (err) { + assert.fail(err, name); + } + assert.equal(intersection.length, 2, name); + assert.deepEqual(buffers_to_strings(intersection).sort(), [ 'c', 'd' ], name); + }); + + client.sinter('sa', 'sc', function (err, intersection) { + if (err) { + assert.fail(err, name); + } + assert.equal(intersection.length, 1, name); + assert.equal(intersection[0], 'c', name); + }); + + // 3-way + + client.sinter('sa', 'sb', 'sc', function (err, intersection) { + if (err) { + assert.fail(err, name); + } + assert.equal(intersection.length, 1, name); + assert.equal(intersection[0], 'c', name); + next(name); + }); +}; + +tests.SINTERSTORE = function () { + var name = "SINTERSTORE"; + + client.del('sa'); + client.del('sb'); + client.del('sc'); + client.del('foo'); + + client.sadd('sa', 'a', require_number(1, name)); + client.sadd('sa', 'b', require_number(1, name)); + client.sadd('sa', 'c', require_number(1, name)); + + client.sadd('sb', 'b', require_number(1, name)); + client.sadd('sb', 'c', require_number(1, name)); + client.sadd('sb', 'd', require_number(1, name)); + + client.sadd('sc', 'c', require_number(1, name)); + client.sadd('sc', 'd', require_number(1, name)); + client.sadd('sc', 'e', require_number(1, name)); + + client.sinterstore('foo', 'sa', 'sb', 'sc', require_number(1, name)); + + client.smembers('foo', function (err, members) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(members), [ 'c' ], name); + next(name); + }); +}; + +tests.SUNION = function () { + var name = "SUNION"; + + client.del('sa'); + client.del('sb'); + client.del('sc'); + + client.sadd('sa', 'a', require_number(1, name)); + client.sadd('sa', 'b', require_number(1, name)); + client.sadd('sa', 'c', require_number(1, name)); + + client.sadd('sb', 'b', require_number(1, name)); + client.sadd('sb', 'c', require_number(1, name)); + client.sadd('sb', 'd', require_number(1, name)); + + client.sadd('sc', 'c', require_number(1, name)); + client.sadd('sc', 'd', require_number(1, name)); + client.sadd('sc', 'e', require_number(1, name)); + + client.sunion('sa', 'sb', 'sc', function (err, union) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(union).sort(), ['a', 'b', 'c', 'd', 'e'], name); + next(name); + }); +}; + +tests.SUNIONSTORE = function () { + var name = "SUNIONSTORE"; + + client.del('sa'); + client.del('sb'); + client.del('sc'); + client.del('foo'); + + client.sadd('sa', 'a', require_number(1, name)); + client.sadd('sa', 'b', require_number(1, name)); + client.sadd('sa', 'c', require_number(1, name)); + + client.sadd('sb', 'b', require_number(1, name)); + client.sadd('sb', 'c', require_number(1, name)); + client.sadd('sb', 'd', require_number(1, name)); + + client.sadd('sc', 'c', require_number(1, name)); + client.sadd('sc', 'd', require_number(1, name)); + client.sadd('sc', 'e', require_number(1, name)); + + client.sunionstore('foo', 'sa', 'sb', 'sc', function (err, cardinality) { + if (err) { + assert.fail(err, name); + } + assert.equal(cardinality, 5, name); + }); + + client.smembers('foo', function (err, members) { + if (err) { + assert.fail(err, name); + } + assert.equal(members.length, 5, name); + assert.deepEqual(buffers_to_strings(members).sort(), ['a', 'b', 'c', 'd', 'e'], name); + next(name); + }); +}; + +// SORT test adapted from Brian Hammond's redis-node-client.js, which has a comprehensive test suite + +tests.SORT = function () { + var name = "SORT"; + + client.del('y'); + client.del('x'); + + client.rpush('y', 'd', require_number(1, name)); + client.rpush('y', 'b', require_number(2, name)); + client.rpush('y', 'a', require_number(3, name)); + client.rpush('y', 'c', require_number(4, name)); + + client.rpush('x', '3', require_number(1, name)); + client.rpush('x', '9', require_number(2, name)); + client.rpush('x', '2', require_number(3, name)); + client.rpush('x', '4', require_number(4, name)); + + client.set('w3', '4', require_string("OK", name)); + client.set('w9', '5', require_string("OK", name)); + client.set('w2', '12', require_string("OK", name)); + client.set('w4', '6', require_string("OK", name)); + + client.set('o2', 'buz', require_string("OK", name)); + client.set('o3', 'foo', require_string("OK", name)); + client.set('o4', 'baz', require_string("OK", name)); + client.set('o9', 'bar', require_string("OK", name)); + + client.set('p2', 'qux', require_string("OK", name)); + client.set('p3', 'bux', require_string("OK", name)); + client.set('p4', 'lux', require_string("OK", name)); + client.set('p9', 'tux', require_string("OK", name)); + + // Now the data has been setup, we can test. + + // But first, test basic sorting. + + // y = [ d b a c ] + // sort y ascending = [ a b c d ] + // sort y descending = [ d c b a ] + + client.sort('y', 'asc', 'alpha', function (err, sorted) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(sorted), ['a', 'b', 'c', 'd'], name); + }); + + client.sort('y', 'desc', 'alpha', function (err, sorted) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(sorted), ['d', 'c', 'b', 'a'], name); + }); + + // Now try sorting numbers in a list. + // x = [ 3, 9, 2, 4 ] + + client.sort('x', 'asc', function (err, sorted) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(sorted), [2, 3, 4, 9], name); + }); + + client.sort('x', 'desc', function (err, sorted) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(sorted), [9, 4, 3, 2], name); + }); + + // Try sorting with a 'by' pattern. + + client.sort('x', 'by', 'w*', 'asc', function (err, sorted) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(sorted), [3, 9, 4, 2], name); + }); + + // Try sorting with a 'by' pattern and 1 'get' pattern. + + client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', function (err, sorted) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(sorted), ['foo', 'bar', 'baz', 'buz'], name); + }); + + // Try sorting with a 'by' pattern and 2 'get' patterns. + + client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*', function (err, sorted) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(sorted), ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux'], name); + }); + + // Try sorting with a 'by' pattern and 2 'get' patterns. + // Instead of getting back the sorted set/list, store the values to a list. + // Then check that the values are there in the expected order. + + client.sort('x', 'by', 'w*', 'asc', 'get', 'o*', 'get', 'p*', 'store', 'bacon', function (err) { + if (err) { + assert.fail(err, name); + } + }); + + client.lrange('bacon', 0, -1, function (err, values) { + if (err) { + assert.fail(err, name); + } + assert.deepEqual(buffers_to_strings(values), ['foo', 'bux', 'bar', 'tux', 'baz', 'lux', 'buz', 'qux'], name); + next(name); + }); + + // TODO - sort by hash value +}; + +tests.MONITOR = function () { + var name = "MONITOR", responses = [], monitor_client; + + if (!server_version_at_least(client, [2, 6, 0])) { + console.log("Skipping " + name + " for old Redis server version < 2.6.x"); + return next(name); + } + + monitor_client = redis.createClient(); + monitor_client.monitor(function (err, res) { + client.mget("some", "keys", "foo", "bar"); + client.set("json", JSON.stringify({ + foo: "123", + bar: "sdflkdfsjk", + another: false + })); + }); + monitor_client.on("monitor", function (time, args) { + // skip monitor command for Redis <= 2.4.16 + if (args[0] === "monitor") return; + + responses.push(args); + if (responses.length === 2) { + assert.strictEqual(5, responses[0].length); + assert.strictEqual("mget", responses[0][0]); + assert.strictEqual("some", responses[0][1]); + assert.strictEqual("keys", responses[0][2]); + assert.strictEqual("foo", responses[0][3]); + assert.strictEqual("bar", responses[0][4]); + assert.strictEqual(3, responses[1].length); + assert.strictEqual("set", responses[1][0]); + assert.strictEqual("json", responses[1][1]); + assert.strictEqual('{"foo":"123","bar":"sdflkdfsjk","another":false}', responses[1][2]); + monitor_client.quit(function (err, res) { + next(name); + }); + } + }); +}; + +tests.BLPOP = function () { + var name = "BLPOP"; + + client.rpush("blocking list", "initial value", function (err, res) { + client2.BLPOP("blocking list", 0, function (err, res) { + assert.strictEqual("blocking list", res[0].toString()); + assert.strictEqual("initial value", res[1].toString()); + + client.rpush("blocking list", "wait for this value"); + }); + client2.BLPOP("blocking list", 0, function (err, res) { + assert.strictEqual("blocking list", res[0].toString()); + assert.strictEqual("wait for this value", res[1].toString()); + next(name); + }); + }); +}; + +tests.BLPOP_TIMEOUT = function () { + var name = "BLPOP_TIMEOUT"; + + // try to BLPOP the list again, which should be empty. This should timeout and return null. + client2.BLPOP("blocking list", 1, function (err, res) { + if (err) { + throw err; + } + + assert.strictEqual(res, null); + next(name); + }); +}; + +tests.EXPIRE = function () { + var name = "EXPIRE"; + client.set(['expiry key', 'bar'], require_string("OK", name)); + client.EXPIRE(["expiry key", "1"], require_number_pos(name)); + setTimeout(function () { + client.exists(["expiry key"], last(name, require_number(0, name))); + }, 2000); +}; + +tests.TTL = function () { + var name = "TTL"; + client.set(["ttl key", "ttl val"], require_string("OK", name)); + client.expire(["ttl key", "100"], require_number_pos(name)); + setTimeout(function () { + client.TTL(["ttl key"], last(name, require_number_pos(0, name))); + }, 500); +}; + +tests.OPTIONAL_CALLBACK = function () { + var name = "OPTIONAL_CALLBACK"; + client.del("op_cb1"); + client.set("op_cb1", "x"); + client.get("op_cb1", last(name, require_string("x", name))); +}; + +tests.OPTIONAL_CALLBACK_UNDEFINED = function () { + var name = "OPTIONAL_CALLBACK_UNDEFINED"; + client.del("op_cb2"); + client.set("op_cb2", "y", undefined); + client.get("op_cb2", last(name, require_string("y", name))); +}; + +tests.ENABLE_OFFLINE_QUEUE_TRUE = function () { + var name = "ENABLE_OFFLINE_QUEUE_TRUE"; + var cli = redis.createClient(9999, null, { + max_attempts: 1 + // default :) + // enable_offline_queue: true + }); + cli.on('error', function(e) { + // ignore, b/c expecting a "can't connect" error + }); + return setTimeout(function() { + cli.set(name, name, function(err, result) { + assert.ifError(err); + }); + + return setTimeout(function(){ + assert.strictEqual(cli.offline_queue.length, 1); + return next(name); + }, 25); + }, 50); +}; + +tests.ENABLE_OFFLINE_QUEUE_FALSE = function () { + var name = "ENABLE_OFFLINE_QUEUE_FALSE"; + var cli = redis.createClient(9999, null, { + max_attempts: 1, + enable_offline_queue: false + }); + cli.on('error', function() { + // ignore, see above + }); + assert.throws(function () { + cli.set(name, name) + }) + assert.doesNotThrow(function () { + cli.set(name, name, function (err) { + // should callback with an error + assert.ok(err); + setTimeout(function () { + next(name); + }, 50); + }); + }); +}; + +tests.SLOWLOG = function () { + var name = "SLOWLOG"; + client.config("set", "slowlog-log-slower-than", 0, require_string("OK", name)); + client.slowlog("reset", require_string("OK", name)); + client.set("foo", "bar", require_string("OK", name)); + client.get("foo", require_string("bar", name)); + client.slowlog("get", function (err, res) { + assert.equal(res.length, 3, name); + assert.equal(res[0][3].length, 2, name); + assert.deepEqual(res[1][3], ["set", "foo", "bar"], name); + assert.deepEqual(res[2][3], ["slowlog", "reset"], name); + client.config("set", "slowlog-log-slower-than", 10000, require_string("OK", name)); + next(name); + }); +} + +// TODO - need a better way to test auth, maybe auto-config a local Redis server or something. +// Yes, this is the real password. Please be nice, thanks. +tests.auth = function () { + var name = "AUTH", client4, ready_count = 0; + + client4 = redis.createClient(9006, "filefish.redistogo.com"); + client4.auth("664b1b6aaf134e1ec281945a8de702a9", function (err, res) { + assert.strictEqual(null, err, name); + assert.strictEqual("OK", res.toString(), name); + }); + + // test auth, then kill the connection so it'll auto-reconnect and auto-re-auth + client4.on("ready", function () { + ready_count++; + if (ready_count === 1) { + client4.stream.destroy(); + } else { + client4.quit(function (err, res) { + next(name); + }); + } + }); +}; + +tests.reconnectRetryMaxDelay = function() { + var time = new Date().getTime(), + name = 'reconnectRetryMaxDelay', + reconnecting = false; + var client = redis.createClient(PORT, HOST, { + retry_max_delay: 1 + }); + client.on('ready', function() { + if (!reconnecting) { + reconnecting = true; + client.retry_delay = 1000; + client.retry_backoff = 1; + client.stream.end(); + } else { + client.end(); + var lasted = new Date().getTime() - time; + assert.ok(lasted < 1000); + next(name); + } + }); +}; + +all_tests = Object.keys(tests); +all_start = new Date(); +test_count = 0; + +run_next_test = function run_next_test() { + var test_name = all_tests.shift(); + if (typeof tests[test_name] === "function") { + util.print('- \x1b[1m' + test_name.toLowerCase() + '\x1b[0m:'); + cur_start = new Date(); + test_count += 1; + tests[test_name](); + } else { + console.log('\n completed \x1b[32m%d\x1b[0m tests in \x1b[33m%d\x1b[0m ms\n', test_count, new Date() - all_start); + client.quit(); + client2.quit(); + bclient.quit(); + } +}; + +client.once("ready", function start_tests() { + console.log("Connected to " + client.host + ":" + client.port + ", Redis server version " + client.server_info.redis_version + "\n"); + console.log("Using reply parser " + client.reply_parser.name); + + run_next_test(); + + connected = true; +}); + +client.on('end', function () { + ended = true; +}); + +// Exit immediately on connection failure, which triggers "exit", below, which fails the test +client.on("error", function (err) { + console.error("client: " + err.stack); + process.exit(); +}); +client2.on("error", function (err) { + console.error("client2: " + err.stack); + process.exit(); +}); +client3.on("error", function (err) { + console.error("client3: " + err.stack); + process.exit(); +}); +bclient.on("error", function (err) { + console.error("bclient: " + err.stack); + process.exit(); +}); + +client.on("reconnecting", function (params) { + console.log("reconnecting: " + util.inspect(params)); +}); + +process.on('uncaughtException', function (err) { + console.error("Uncaught exception: " + err.stack); + process.exit(1); +}); + +process.on('exit', function (code) { + assert.equal(true, connected); + assert.equal(true, ended); +}); diff --git a/node_modules/socket.io/.npmignore b/node_modules/socket.io/.npmignore new file mode 100644 index 0000000000..39e9864f5a --- /dev/null +++ b/node_modules/socket.io/.npmignore @@ -0,0 +1,3 @@ +support +test +examples diff --git a/node_modules/socket.io/.travis.yml b/node_modules/socket.io/.travis.yml new file mode 100644 index 0000000000..56eca033cd --- /dev/null +++ b/node_modules/socket.io/.travis.yml @@ -0,0 +1,6 @@ +language: node_js +node_js: + - 0.6 + +notifications: + irc: "irc.freenode.org#socket.io" diff --git a/node_modules/socket.io/History.md b/node_modules/socket.io/History.md new file mode 100644 index 0000000000..4f57fa3150 --- /dev/null +++ b/node_modules/socket.io/History.md @@ -0,0 +1,315 @@ + +0.9.14 / 2013-03-29 +=================== + + * manager: fix memory leak with SSL [jpallen] + +0.9.13 / 2012-12-13 +=================== + + * package: fixed `base64id` requirement + +0.9.12 / 2012-12-13 +=================== + + * manager: fix for latest node which is returning a clone with `listeners` [viirya] + +0.9.11 / 2012-11-02 +=================== + + * package: move redis to optionalDependenices [3rd-Eden] + * bumped client + +0.9.10 / 2012-08-10 +=================== + + * Don't lowercase log messages + * Always set the HTTP response in case an error should be returned to the client + * Create or destroy the flash policy server on configuration change + * Honour configuration to disable flash policy server + * Add express 3.0 instructions on Readme.md + * Bump client + +0.9.9 / 2012-08-01 +================== + + * Fixed sync disconnect xhrs handling + * Put license text in its own file (#965) + * Add warning to .listen() to ease the migration to Express 3.x + * Restored compatibility with node 0.4.x + +0.9.8 / 2012-07-24 +================== + + * Bumped client. + +0.9.7 / 2012-07-24 +================== + + * Prevent crash when socket leaves a room twice. + * Corrects unsafe usage of for..in + * Fix for node 0.8 with `gzip compression` [vadimi] + * Update redis to support Node 0.8.x + * Made ID generation securely random + * Fix Redis Store race condition in manager onOpen unsubscribe callback + * Fix for EventEmitters always reusing the same Array instance for listeners + +0.9.6 / 2012-04-17 +================== + + * Fixed XSS in jsonp-polling. + +0.9.5 / 2012-04-05 +================== + + * Added test for polling and socket close. + * Ensure close upon request close. + * Fix disconnection reason being lost for polling transports. + * Ensure that polling transports work with Connection: close. + * Log disconnection reason. + +0.9.4 / 2012-04-01 +================== + + * Disconnecting from namespace improvement (#795) [DanielBaulig] + * Bumped client with polling reconnection loop (#438) + +0.9.3 / 2012-03-28 +================== + + * Fix "Syntax error" on FF Web Console with XHR Polling [mikito] + +0.9.2 / 2012-03-13 +================== + + * More sensible close `timeout default` (fixes disconnect issue) + +0.9.1-1 / 2012-03-02 +==================== + + * Bumped client with NPM dependency fix. + +0.9.1 / 2012-03-02 +================== + + * Changed heartbeat timeout and interval defaults (60 and 25 seconds) + * Make tests work both on 0.4 and 0.6 + * Updated client (improvements + bug fixes). + +0.9.0 / 2012-02-26 +================== + + * Make it possible to use a regexp to match the socket.io resource URL. + We need this because we have to prefix the socket.io URL with a variable ID. + * Supplemental fix to gavinuhma/authfix, it looks like the same Access-Control-Origin logic is needed in the http and xhr-polling transports + * Updated express dep for windows compatibility. + * Combine two substr calls into one in decodePayload to improve performance + * Minor documentation fix + * Minor. Conform to style of other files. + * Switching setting to 'match origin protocol' + * Revert "Fixes leaking Redis subscriptions for #663. The local flag was not getting passed through onClientDisconnect()." + * Revert "Handle leaked dispatch:[id] subscription." + * Merge pull request #667 from dshaw/patch/redis-disconnect + * Handle leaked dispatch:[id] subscription. + * Fixes leaking Redis subscriptions for #663. The local flag was not getting passed through onClientDisconnect(). + * Prevent memory leaking on uncompleted requests & add max post size limitation + * Fix for testcase + * Set Access-Control-Allow-Credentials true, regardless of cookie + * Remove assertvarnish from package as it breaks on 0.6 + * Correct irc channel + * Added proper return after reserved field error + * Fixes manager.js failure to close connection after transport error has happened + * Added implicit port 80 for origin checks. fixes #638 + * Fixed bug #432 in 0.8.7 + * Set Access-Control-Allow-Origin header to origin to enable withCredentials + * Adding configuration variable matchOriginProtocol + * Fixes location mismatch error in Safari. + * Use tty to detect if we should add colors or not by default. + * Updated the package location. + +0.8.7 / 2011-11-05 +================== + + * Fixed memory leaks in closed clients. + * Fixed memory leaks in namespaces. + * Fixed websocket handling for malformed requests from proxies. [einaros] + * Node 0.6 compatibility. [einaros] [3rd-Eden] + * Adapted tests and examples. + +0.8.6 / 2011-10-27 +================== + + * Added JSON decoding on jsonp-polling transport. + * Fixed README example. + * Major speed optimizations [3rd-Eden] [einaros] [visionmedia] + * Added decode/encode benchmarks [visionmedia] + * Added support for black-listing client sent events. + * Fixed logging options, closes #540 [3rd-Eden] + * Added vary header for gzip [3rd-Eden] + * Properly cleaned up async websocket / flashsocket tests, after patching node-websocket-client + * Patched to properly shut down when a finishClose call is made during connection establishment + * Added support for socket.io version on url and far-future Expires [3rd-Eden] [getify] + * Began IE10 compatibility [einaros] [tbranyen] + * Misc WebSocket fixes [einaros] + * Added UTF8 to respone headers for htmlfile [3rd-Eden] + +0.8.5 / 2011-10-07 +================== + + * Added websocket draft HyBi-16 support. [einaros] + * Fixed websocket continuation bugs. [einaros] + * Fixed flashsocket transport name. + * Fixed websocket tests. + * Ensured `parser#decodePayload` doesn't choke. + * Added http referrer verification to manager verifyOrigin. + * Added access control for cross domain xhr handshakes [3rd-Eden] + * Added support for automatic generation of socket.io files [3rd-Eden] + * Added websocket binary support [einaros] + * Added gzip support for socket.io.js [3rd-Eden] + * Expose socket.transport [3rd-Eden] + * Updated client. + +0.8.4 / 2011-09-06 +================== + + * Client build + +0.8.3 / 2011-09-03 +================== + + * Fixed `\n` parsing for non-JSON packets (fixes #479). + * Fixed parsing of certain unicode characters (fixes #451). + * Fixed transport message packet logging. + * Fixed emission of `error` event resulting in an uncaught exception if unhandled (fixes #476). + * Fixed; allow for falsy values as the configuration value of `log level` (fixes #491). + * Fixed repository URI in `package.json`. Fixes #504. + * Added text/plain content-type to handshake responses [einaros] + * Improved single byte writes [einaros] + * Updated socket.io-flashsocket default port from 843 to 10843 [3rd-Eden] + * Updated client. + +0.8.2 / 2011-08-29 +================== + + * Updated client. + +0.8.1 / 2011-08-29 +================== + + * Fixed utf8 bug in send framing in websocket [einaros] + * Fixed typo in docs [Znarkus] + * Fixed bug in send framing for over 64kB of data in websocket [einaros] + * Corrected ping handling in websocket transport [einaros] + +0.8.0 / 2011-08-28 +================== + + * Updated to work with two-level websocket versioning. [einaros] + * Added hybi07 support. [einaros] + * Added hybi10 support. [einaros] + * Added http referrer verification to manager.js verifyOrigin. [einaors] + +0.7.11 / 2011-08-27 +=================== + + * Updated socket.io-client. + +0.7.10 / 2011-08-27 +=================== + + * Updated socket.io-client. + +0.7.9 / 2011-08-12 +================== + + * Updated socket.io-client. + * Make sure we only do garbage collection when the server we receive is actually run. + +0.7.8 / 2011-08-08 +================== + + * Changed; make sure sio#listen passes options to both HTTP server and socket.io manager. + * Added docs for sio#listen. + * Added options parameter support for Manager constructor. + * Added memory leaks tests and test-leaks Makefile task. + * Removed auto npm-linking from make test. + * Make sure that you can disable heartbeats. [3rd-Eden] + * Fixed rooms memory leak [3rd-Eden] + * Send response once we got all POST data, not immediately [Pita] + * Fixed onLeave behavior with missing clientsk [3rd-Eden] + * Prevent duplicate references in rooms. + * Added alias for `to` to `in` and `in` to `to`. + * Fixed roomClients definition. + * Removed dependency on redis for installation without npm [3rd-Eden] + * Expose path and querystring in handshakeData [3rd-Eden] + +0.7.7 / 2011-07-12 +================== + + * Fixed double dispatch handling with emit to closed clients. + * Added test for emitting to closed clients to prevent regression. + * Fixed race condition in redis test. + * Changed Transport#end instrumentation. + * Leveraged $emit instead of emit internally. + * Made tests faster. + * Fixed double disconnect events. + * Fixed disconnect logic + * Simplified remote events handling in Socket. + * Increased testcase timeout. + * Fixed unknown room emitting (GH-291). [3rd-Eden] + * Fixed `address` in handshakeData. [3rd-Eden] + * Removed transports definition in chat example. + * Fixed room cleanup + * Fixed; make sure the client is cleaned up after booting. + * Make sure to mark the client as non-open if the connection is closed. + * Removed unneeded `buffer` declarations. + * Fixed; make sure to clear socket handlers and subscriptions upon transport close. + +0.7.6 / 2011-06-30 +================== + + * Fixed general dispatching when a client has closed. + +0.7.5 / 2011-06-30 +================== + + * Fixed dispatching to clients that are disconnected. + +0.7.4 / 2011-06-30 +================== + + * Fixed; only clear handlers if they were set. [level09] + +0.7.3 / 2011-06-30 +================== + + * Exposed handshake data to clients. + * Refactored dispatcher interface. + * Changed; Moved id generation method into the manager. + * Added sub-namespace authorization. [3rd-Eden] + * Changed; normalized SocketNamespace local eventing [dvv] + * Changed; Use packet.reason or default to 'packet' [3rd-Eden] + * Changed console.error to console.log. + * Fixed; bind both servers at the same time do that the test never times out. + * Added 304 support. + * Removed `Transport#name` for abstract interface. + * Changed; lazily require http and https module only when needed. [3rd-Eden] + +0.7.2 / 2011-06-22 +================== + + * Make sure to write a packet (of type `noop`) when closing a poll. + This solves a problem with cross-domain requests being flagged as aborted and + reconnection being triggered. + * Added `noop` message type. + +0.7.1 / 2011-06-21 +================== + + * Fixed cross-domain XHR. + * Added CORS test to xhr-polling suite. + +0.7.0 / 2010-06-21 +================== + + * http://socket.io/announcement.html diff --git a/node_modules/socket.io/LICENSE b/node_modules/socket.io/LICENSE new file mode 100644 index 0000000000..0f4acd44d7 --- /dev/null +++ b/node_modules/socket.io/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2011 Guillermo Rauch + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/socket.io/Makefile b/node_modules/socket.io/Makefile new file mode 100644 index 0000000000..832cba8bdb --- /dev/null +++ b/node_modules/socket.io/Makefile @@ -0,0 +1,31 @@ + +ALL_TESTS = $(shell find test/ -name '*.test.js') +ALL_BENCH = $(shell find benchmarks -name '*.bench.js') + +run-tests: + @./node_modules/.bin/expresso \ + -t 3000 \ + -I support \ + --serial \ + $(TESTFLAGS) \ + $(TESTS) + +test: + @$(MAKE) NODE_PATH=lib TESTS="$(ALL_TESTS)" run-tests + +test-cov: + @TESTFLAGS=--cov $(MAKE) test + +test-leaks: + @ls test/leaks/* | xargs node --expose_debug_as=debug --expose_gc + +run-bench: + @node $(PROFILEFLAGS) benchmarks/runner.js + +bench: + @$(MAKE) BENCHMARKS="$(ALL_BENCH)" run-bench + +profile: + @PROFILEFLAGS='--prof --trace-opt --trace-bailout --trace-deopt' $(MAKE) bench + +.PHONY: test bench profile diff --git a/node_modules/socket.io/Readme.md b/node_modules/socket.io/Readme.md new file mode 100644 index 0000000000..41f21f6814 --- /dev/null +++ b/node_modules/socket.io/Readme.md @@ -0,0 +1,364 @@ +# Socket.IO + +Socket.IO is a Node.JS project that makes WebSockets and realtime possible in +all browsers. It also enhances WebSockets by providing built-in multiplexing, +horizontal scalability, automatic JSON encoding/decoding, and more. + +## How to Install + +```bash +npm install socket.io +``` + +## How to use + +First, require `socket.io`: + +```js +var io = require('socket.io'); +``` + +Next, attach it to a HTTP/HTTPS server. If you're using the fantastic `express` +web framework: + +#### Express 3.x + +```js +var app = express() + , server = require('http').createServer(app) + , io = io.listen(server); + +server.listen(80); + +io.sockets.on('connection', function (socket) { + socket.emit('news', { hello: 'world' }); + socket.on('my other event', function (data) { + console.log(data); + }); +}); +``` + +#### Express 2.x + +```js +var app = express.createServer() + , io = io.listen(app); + +app.listen(80); + +io.sockets.on('connection', function (socket) { + socket.emit('news', { hello: 'world' }); + socket.on('my other event', function (data) { + console.log(data); + }); +}); +``` + +Finally, load it from the client side code: + +```html + + +``` + +For more thorough examples, look at the `examples/` directory. + +## Short recipes + +### Sending and receiving events. + +Socket.IO allows you to emit and receive custom events. +Besides `connect`, `message` and `disconnect`, you can emit custom events: + +```js +// note, io.listen() will create a http server for you +var io = require('socket.io').listen(80); + +io.sockets.on('connection', function (socket) { + io.sockets.emit('this', { will: 'be received by everyone' }); + + socket.on('private message', function (from, msg) { + console.log('I received a private message by ', from, ' saying ', msg); + }); + + socket.on('disconnect', function () { + io.sockets.emit('user disconnected'); + }); +}); +``` + +### Storing data associated to a client + +Sometimes it's necessary to store data associated with a client that's +necessary for the duration of the session. + +#### Server side + +```js +var io = require('socket.io').listen(80); + +io.sockets.on('connection', function (socket) { + socket.on('set nickname', function (name) { + socket.set('nickname', name, function () { socket.emit('ready'); }); + }); + + socket.on('msg', function () { + socket.get('nickname', function (err, name) { + console.log('Chat message by ', name); + }); + }); +}); +``` + +#### Client side + +```html + +``` + +### Restricting yourself to a namespace + +If you have control over all the messages and events emitted for a particular +application, using the default `/` namespace works. + +If you want to leverage 3rd-party code, or produce code to share with others, +socket.io provides a way of namespacing a `socket`. + +This has the benefit of `multiplexing` a single connection. Instead of +socket.io using two `WebSocket` connections, it'll use one. + +The following example defines a socket that listens on '/chat' and one for +'/news': + +#### Server side + +```js +var io = require('socket.io').listen(80); + +var chat = io + .of('/chat') + .on('connection', function (socket) { + socket.emit('a message', { that: 'only', '/chat': 'will get' }); + chat.emit('a message', { everyone: 'in', '/chat': 'will get' }); + }); + +var news = io + .of('/news'); + .on('connection', function (socket) { + socket.emit('item', { news: 'item' }); + }); +``` + +#### Client side: + +```html + +``` + +### Sending volatile messages. + +Sometimes certain messages can be dropped. Let's say you have an app that +shows realtime tweets for the keyword `bieber`. + +If a certain client is not ready to receive messages (because of network slowness +or other issues, or because he's connected through long polling and is in the +middle of a request-response cycle), if he doesn't receive ALL the tweets related +to bieber your application won't suffer. + +In that case, you might want to send those messages as volatile messages. + +#### Server side + +```js +var io = require('socket.io').listen(80); + +io.sockets.on('connection', function (socket) { + var tweets = setInterval(function () { + getBieberTweet(function (tweet) { + socket.volatile.emit('bieber tweet', tweet); + }); + }, 100); + + socket.on('disconnect', function () { + clearInterval(tweets); + }); +}); +``` + +#### Client side + +In the client side, messages are received the same way whether they're volatile +or not. + +### Getting acknowledgements + +Sometimes, you might want to get a callback when the client confirmed the message +reception. + +To do this, simply pass a function as the last parameter of `.send` or `.emit`. +What's more, when you use `.emit`, the acknowledgement is done by you, which +means you can also pass data along: + +#### Server side + +```js +var io = require('socket.io').listen(80); + +io.sockets.on('connection', function (socket) { + socket.on('ferret', function (name, fn) { + fn('woot'); + }); +}); +``` + +#### Client side + +```html + +``` + +### Broadcasting messages + +To broadcast, simply add a `broadcast` flag to `emit` and `send` method calls. +Broadcasting means sending a message to everyone else except for the socket +that starts it. + +#### Server side + +```js +var io = require('socket.io').listen(80); + +io.sockets.on('connection', function (socket) { + socket.broadcast.emit('user connected'); + socket.broadcast.json.send({ a: 'message' }); +}); +``` + +### Rooms + +Sometimes you want to put certain sockets in the same room, so that it's easy +to broadcast to all of them together. + +Think of this as built-in channels for sockets. Sockets `join` and `leave` +rooms in each socket. + +#### Server side + +```js +var io = require('socket.io').listen(80); + +io.sockets.on('connection', function (socket) { + socket.join('justin bieber fans'); + socket.broadcast.to('justin bieber fans').emit('new fan'); + io.sockets.in('rammstein fans').emit('new non-fan'); +}); +``` + +### Using it just as a cross-browser WebSocket + +If you just want the WebSocket semantics, you can do that too. +Simply leverage `send` and listen on the `message` event: + +#### Server side + +```js +var io = require('socket.io').listen(80); + +io.sockets.on('connection', function (socket) { + socket.on('message', function () { }); + socket.on('disconnect', function () { }); +}); +``` + +#### Client side + +```html + +``` + +### Changing configuration + +Configuration in socket.io is TJ-style: + +#### Server side + +```js +var io = require('socket.io').listen(80); + +io.configure(function () { + io.set('transports', ['websocket', 'flashsocket', 'xhr-polling']); +}); + +io.configure('development', function () { + io.set('transports', ['websocket', 'xhr-polling']); + io.enable('log'); +}); +``` + +## License + +(The MIT License) + +Copyright (c) 2011 Guillermo Rauch <guillermo@learnboost.com> + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +'Software'), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/node_modules/socket.io/TODO b/node_modules/socket.io/TODO new file mode 100644 index 0000000000..cc74adcead --- /dev/null +++ b/node_modules/socket.io/TODO @@ -0,0 +1 @@ +- Serve .swf client for engine.io diff --git a/node_modules/socket.io/benchmarks/decode.bench.js b/node_modules/socket.io/benchmarks/decode.bench.js new file mode 100644 index 0000000000..4855d805ef --- /dev/null +++ b/node_modules/socket.io/benchmarks/decode.bench.js @@ -0,0 +1,64 @@ + +/** + * Module dependencies. + */ + +var benchmark = require('benchmark') + , colors = require('colors') + , io = require('../') + , parser = io.parser + , suite = new benchmark.Suite('Decode packet'); + +suite.add('string', function () { + parser.decodePacket('4:::"2"'); +}); + +suite.add('event', function () { + parser.decodePacket('5:::{"name":"woot"}'); +}); + +suite.add('event+ack', function () { + parser.decodePacket('5:1+::{"name":"tobi"}'); +}); + +suite.add('event+data', function () { + parser.decodePacket('5:::{"name":"edwald","args":[{"a": "b"},2,"3"]}'); +}); + +suite.add('heartbeat', function () { + parser.decodePacket('2:::'); +}); + +suite.add('error', function () { + parser.decodePacket('7:::2+0'); +}); + +var payload = parser.encodePayload([ + parser.encodePacket({ type: 'message', data: '5', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: '53d', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobar', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobarbaz', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobarbazfoobarbaz', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobarbaz', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobar', endpoint: '' }) +]); + +suite.add('payload', function () { + parser.decodePayload(payload); +}); + +suite.on('cycle', function (bench, details) { + console.log('\n' + suite.name.grey, details.name.white.bold); + console.log([ + details.hz.toFixed(2).cyan + ' ops/sec'.grey + , details.count.toString().white + ' times executed'.grey + , 'benchmark took '.grey + details.times.elapsed.toString().white + ' sec.'.grey + , + ].join(', '.grey)); +}); + +if (!module.parent) { + suite.run(); +} else { + module.exports = suite; +} diff --git a/node_modules/socket.io/benchmarks/encode.bench.js b/node_modules/socket.io/benchmarks/encode.bench.js new file mode 100644 index 0000000000..5037702d31 --- /dev/null +++ b/node_modules/socket.io/benchmarks/encode.bench.js @@ -0,0 +1,90 @@ + +/** + * Module dependencies. + */ + +var benchmark = require('benchmark') + , colors = require('colors') + , io = require('../') + , parser = io.parser + , suite = new benchmark.Suite('Encode packet'); + +suite.add('string', function () { + parser.encodePacket({ + type: 'json' + , endpoint: '' + , data: '2' + }); +}); + +suite.add('event', function () { + parser.encodePacket({ + type: 'event' + , name: 'woot' + , endpoint: '' + , args: [] + }); +}); + +suite.add('event+ack', function () { + parser.encodePacket({ + type: 'json' + , id: 1 + , ack: 'data' + , endpoint: '' + , data: { a: 'b' } + }); +}); + +suite.add('event+data', function () { + parser.encodePacket({ + type: 'event' + , name: 'edwald' + , endpoint: '' + , args: [{a: 'b'}, 2, '3'] + }); +}); + +suite.add('heartbeat', function () { + parser.encodePacket({ + type: 'heartbeat' + , endpoint: '' + }) +}); + +suite.add('error', function () { + parser.encodePacket({ + type: 'error' + , reason: 'unauthorized' + , advice: 'reconnect' + , endpoint: '' + }) +}) + +suite.add('payload', function () { + parser.encodePayload([ + parser.encodePacket({ type: 'message', data: '5', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: '53d', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobar', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobarbaz', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobarbazfoobarbaz', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobarbaz', endpoint: '' }) + , parser.encodePacket({ type: 'message', data: 'foobar', endpoint: '' }) + ]); +}); + +suite.on('cycle', function (bench, details) { + console.log('\n' + suite.name.grey, details.name.white.bold); + console.log([ + details.hz.toFixed(2).cyan + ' ops/sec'.grey + , details.count.toString().white + ' times executed'.grey + , 'benchmark took '.grey + details.times.elapsed.toString().white + ' sec.'.grey + , + ].join(', '.grey)); +}); + +if (!module.parent) { + suite.run(); +} else { + module.exports = suite; +} diff --git a/node_modules/socket.io/benchmarks/runner.js b/node_modules/socket.io/benchmarks/runner.js new file mode 100644 index 0000000000..81e55cae43 --- /dev/null +++ b/node_modules/socket.io/benchmarks/runner.js @@ -0,0 +1,55 @@ +/** + * Benchmark runner dependencies + */ + +var colors = require('colors') + , path = require('path'); + +/** + * Find all the benchmarks + */ + +var benchmarks_files = process.env.BENCHMARKS.split(' ') + , all = [].concat(benchmarks_files) + , first = all.shift() + , benchmarks = {}; + +// find the benchmarks and load them all in our obj +benchmarks_files.forEach(function (file) { + benchmarks[file] = require(path.join(__dirname, '..', file)); +}); + +// setup the complete listeners +benchmarks_files.forEach(function (file) { + var benchmark = benchmarks[file] + , next_file = all.shift() + , next = benchmarks[next_file]; + + /** + * Generate a oncomplete function for the tests, either we are done or we + * have more benchmarks to process. + */ + + function complete () { + if (!next) { + console.log( + '\n\nBenchmark completed in'.grey + , (Date.now() - start).toString().green + ' ms'.grey + ); + } else { + console.log('\nStarting benchmark '.grey + next_file.yellow); + next.run(); + } + } + + // attach the listener + benchmark.on('complete', complete); +}); + +/** + * Start the benchmark + */ + +var start = Date.now(); +console.log('Starting benchmark '.grey + first.yellow); +benchmarks[first].run(); diff --git a/node_modules/socket.io/index.js b/node_modules/socket.io/index.js new file mode 100644 index 0000000000..cc00c103e2 --- /dev/null +++ b/node_modules/socket.io/index.js @@ -0,0 +1,8 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +module.exports = require('./lib/socket.io'); diff --git a/node_modules/socket.io/lib/logger.js b/node_modules/socket.io/lib/logger.js new file mode 100644 index 0000000000..49d02c98df --- /dev/null +++ b/node_modules/socket.io/lib/logger.js @@ -0,0 +1,97 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var util = require('./util') + , toArray = util.toArray; + +/** + * Log levels. + */ + +var levels = [ + 'error' + , 'warn' + , 'info' + , 'debug' +]; + +/** + * Colors for log levels. + */ + +var colors = [ + 31 + , 33 + , 36 + , 90 +]; + +/** + * Pads the nice output to the longest log level. + */ + +function pad (str) { + var max = 0; + + for (var i = 0, l = levels.length; i < l; i++) + max = Math.max(max, levels[i].length); + + if (str.length < max) + return str + new Array(max - str.length + 1).join(' '); + + return str; +}; + +/** + * Logger (console). + * + * @api public + */ + +var Logger = module.exports = function (opts) { + opts = opts || {} + this.colors = false !== opts.colors; + this.level = 3; + this.enabled = true; +}; + +/** + * Log method. + * + * @api public + */ + +Logger.prototype.log = function (type) { + var index = levels.indexOf(type); + + if (index > this.level || !this.enabled) + return this; + + console.log.apply( + console + , [this.colors + ? ' \033[' + colors[index] + 'm' + pad(type) + ' -\033[39m' + : type + ':' + ].concat(toArray(arguments).slice(1)) + ); + + return this; +}; + +/** + * Generate methods. + */ + +levels.forEach(function (name) { + Logger.prototype[name] = function () { + this.log.apply(this, [name].concat(toArray(arguments))); + }; +}); diff --git a/node_modules/socket.io/lib/manager.js b/node_modules/socket.io/lib/manager.js new file mode 100644 index 0000000000..83937853f5 --- /dev/null +++ b/node_modules/socket.io/lib/manager.js @@ -0,0 +1,1027 @@ +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var fs = require('fs') + , url = require('url') + , tty = require('tty') + , crypto = require('crypto') + , util = require('./util') + , store = require('./store') + , client = require('socket.io-client') + , transports = require('./transports') + , Logger = require('./logger') + , Socket = require('./socket') + , MemoryStore = require('./stores/memory') + , SocketNamespace = require('./namespace') + , Static = require('./static') + , EventEmitter = process.EventEmitter; + +/** + * Export the constructor. + */ + +exports = module.exports = Manager; + +/** + * Default transports. + */ + +var defaultTransports = exports.defaultTransports = [ + 'websocket' + , 'htmlfile' + , 'xhr-polling' + , 'jsonp-polling' +]; + +/** + * Inherited defaults. + */ + +var parent = module.parent.exports + , protocol = parent.protocol + , jsonpolling_re = /^\d+$/; + +/** + * Manager constructor. + * + * @param {HTTPServer} server + * @param {Object} options, optional + * @api public + */ + +function Manager (server, options) { + this.server = server; + this.namespaces = {}; + this.sockets = this.of(''); + this.settings = { + origins: '*:*' + , log: true + , store: new MemoryStore + , logger: new Logger + , static: new Static(this) + , heartbeats: true + , resource: '/socket.io' + , transports: defaultTransports + , authorization: false + , blacklist: ['disconnect'] + , 'log level': 3 + , 'log colors': tty.isatty(process.stdout.fd) + , 'close timeout': 60 + , 'heartbeat interval': 25 + , 'heartbeat timeout': 60 + , 'polling duration': 20 + , 'flash policy server': true + , 'flash policy port': 10843 + , 'destroy upgrade': true + , 'destroy buffer size': 10E7 + , 'browser client': true + , 'browser client cache': true + , 'browser client minification': false + , 'browser client etag': false + , 'browser client expires': 315360000 + , 'browser client gzip': false + , 'browser client handler': false + , 'client store expiration': 15 + , 'match origin protocol': false + }; + + for (var i in options) { + if (options.hasOwnProperty(i)) { + this.settings[i] = options[i]; + } + } + + var self = this; + + // default error handler + server.on('error', function(err) { + self.log.warn('error raised: ' + err); + }); + + this.initStore(); + + this.on('set:store', function() { + self.initStore(); + }); + + // reset listeners + this.oldListeners = server.listeners('request').splice(0); + server.removeAllListeners('request'); + + server.on('request', function (req, res) { + self.handleRequest(req, res); + }); + + server.on('upgrade', function (req, socket, head) { + self.handleUpgrade(req, socket, head); + }); + + server.on('close', function () { + clearInterval(self.gc); + }); + + server.once('listening', function () { + self.gc = setInterval(self.garbageCollection.bind(self), 10000); + }); + + for (var i in transports) { + if (transports.hasOwnProperty(i)) { + if (transports[i].init) { + transports[i].init(this); + } + } + } + + // forward-compatibility with 1.0 + var self = this; + this.sockets.on('connection', function (conn) { + self.emit('connection', conn); + }); + + this.sequenceNumber = Date.now() | 0; + + this.log.info('socket.io started'); +}; + +Manager.prototype.__proto__ = EventEmitter.prototype + +/** + * Store accessor shortcut. + * + * @api public + */ + +Manager.prototype.__defineGetter__('store', function () { + var store = this.get('store'); + store.manager = this; + return store; +}); + +/** + * Logger accessor. + * + * @api public + */ + +Manager.prototype.__defineGetter__('log', function () { + var logger = this.get('logger'); + + logger.level = this.get('log level') || -1; + logger.colors = this.get('log colors'); + logger.enabled = this.enabled('log'); + + return logger; +}); + +/** + * Static accessor. + * + * @api public + */ + +Manager.prototype.__defineGetter__('static', function () { + return this.get('static'); +}); + +/** + * Get settings. + * + * @api public + */ + +Manager.prototype.get = function (key) { + return this.settings[key]; +}; + +/** + * Set settings + * + * @api public + */ + +Manager.prototype.set = function (key, value) { + if (arguments.length == 1) return this.get(key); + this.settings[key] = value; + this.emit('set:' + key, this.settings[key], key); + return this; +}; + +/** + * Enable a setting + * + * @api public + */ + +Manager.prototype.enable = function (key) { + this.settings[key] = true; + this.emit('set:' + key, this.settings[key], key); + return this; +}; + +/** + * Disable a setting + * + * @api public + */ + +Manager.prototype.disable = function (key) { + this.settings[key] = false; + this.emit('set:' + key, this.settings[key], key); + return this; +}; + +/** + * Checks if a setting is enabled + * + * @api public + */ + +Manager.prototype.enabled = function (key) { + return !!this.settings[key]; +}; + +/** + * Checks if a setting is disabled + * + * @api public + */ + +Manager.prototype.disabled = function (key) { + return !this.settings[key]; +}; + +/** + * Configure callbacks. + * + * @api public + */ + +Manager.prototype.configure = function (env, fn) { + if ('function' == typeof env) { + env.call(this); + } else if (env == (process.env.NODE_ENV || 'development')) { + fn.call(this); + } + + return this; +}; + +/** + * Initializes everything related to the message dispatcher. + * + * @api private + */ + +Manager.prototype.initStore = function () { + this.handshaken = {}; + this.connected = {}; + this.open = {}; + this.closed = {}; + this.rooms = {}; + this.roomClients = {}; + + var self = this; + + this.store.subscribe('handshake', function (id, data) { + self.onHandshake(id, data); + }); + + this.store.subscribe('connect', function (id) { + self.onConnect(id); + }); + + this.store.subscribe('open', function (id) { + self.onOpen(id); + }); + + this.store.subscribe('join', function (id, room) { + self.onJoin(id, room); + }); + + this.store.subscribe('leave', function (id, room) { + self.onLeave(id, room); + }); + + this.store.subscribe('close', function (id) { + self.onClose(id); + }); + + this.store.subscribe('dispatch', function (room, packet, volatile, exceptions) { + self.onDispatch(room, packet, volatile, exceptions); + }); + + this.store.subscribe('disconnect', function (id) { + self.onDisconnect(id); + }); +}; + +/** + * Called when a client handshakes. + * + * @param text + */ + +Manager.prototype.onHandshake = function (id, data) { + this.handshaken[id] = data; +}; + +/** + * Called when a client connects (ie: transport first opens) + * + * @api private + */ + +Manager.prototype.onConnect = function (id) { + this.connected[id] = true; +}; + +/** + * Called when a client opens a request in a different node. + * + * @api private + */ + +Manager.prototype.onOpen = function (id) { + this.open[id] = true; + + if (this.closed[id]) { + var self = this; + + this.store.unsubscribe('dispatch:' + id, function () { + var transport = self.transports[id]; + if (self.closed[id] && self.closed[id].length && transport) { + + // if we have buffered messages that accumulate between calling + // onOpen an this async callback, send them if the transport is + // still open, otherwise leave them buffered + if (transport.open) { + transport.payload(self.closed[id]); + self.closed[id] = []; + } + } + }); + } + + // clear the current transport + if (this.transports[id]) { + this.transports[id].discard(); + this.transports[id] = null; + } +}; + +/** + * Called when a message is sent to a namespace and/or room. + * + * @api private + */ + +Manager.prototype.onDispatch = function (room, packet, volatile, exceptions) { + if (this.rooms[room]) { + for (var i = 0, l = this.rooms[room].length; i < l; i++) { + var id = this.rooms[room][i]; + + if (!~exceptions.indexOf(id)) { + if (this.transports[id] && this.transports[id].open) { + this.transports[id].onDispatch(packet, volatile); + } else if (!volatile) { + this.onClientDispatch(id, packet); + } + } + } + } +}; + +/** + * Called when a client joins a nsp / room. + * + * @api private + */ + +Manager.prototype.onJoin = function (id, name) { + if (!this.roomClients[id]) { + this.roomClients[id] = {}; + } + + if (!this.rooms[name]) { + this.rooms[name] = []; + } + + if (!~this.rooms[name].indexOf(id)) { + this.rooms[name].push(id); + this.roomClients[id][name] = true; + } +}; + +/** + * Called when a client leaves a nsp / room. + * + * @param private + */ + +Manager.prototype.onLeave = function (id, room) { + if (this.rooms[room]) { + var index = this.rooms[room].indexOf(id); + + if (index >= 0) { + this.rooms[room].splice(index, 1); + } + + if (!this.rooms[room].length) { + delete this.rooms[room]; + } + + if (this.roomClients[id]) { + delete this.roomClients[id][room]; + } + } +}; + +/** + * Called when a client closes a request in different node. + * + * @api private + */ + +Manager.prototype.onClose = function (id) { + if (this.open[id]) { + delete this.open[id]; + } + + this.closed[id] = []; + + var self = this; + + this.store.subscribe('dispatch:' + id, function (packet, volatile) { + if (!volatile) { + self.onClientDispatch(id, packet); + } + }); +}; + +/** + * Dispatches a message for a closed client. + * + * @api private + */ + +Manager.prototype.onClientDispatch = function (id, packet) { + if (this.closed[id]) { + this.closed[id].push(packet); + } +}; + +/** + * Receives a message for a client. + * + * @api private + */ + +Manager.prototype.onClientMessage = function (id, packet) { + if (this.namespaces[packet.endpoint]) { + this.namespaces[packet.endpoint].handlePacket(id, packet); + } +}; + +/** + * Fired when a client disconnects (not triggered). + * + * @api private + */ + +Manager.prototype.onClientDisconnect = function (id, reason) { + for (var name in this.namespaces) { + if (this.namespaces.hasOwnProperty(name)) { + this.namespaces[name].handleDisconnect(id, reason, typeof this.roomClients[id] !== 'undefined' && + typeof this.roomClients[id][name] !== 'undefined'); + } + } + + this.onDisconnect(id); +}; + +/** + * Called when a client disconnects. + * + * @param text + */ + +Manager.prototype.onDisconnect = function (id, local) { + delete this.handshaken[id]; + + if (this.open[id]) { + delete this.open[id]; + } + + if (this.connected[id]) { + delete this.connected[id]; + } + + if (this.transports[id]) { + this.transports[id].discard(); + delete this.transports[id]; + } + + if (this.closed[id]) { + delete this.closed[id]; + } + + if (this.roomClients[id]) { + for (var room in this.roomClients[id]) { + if (this.roomClients[id].hasOwnProperty(room)) { + this.onLeave(id, room); + } + } + delete this.roomClients[id] + } + + this.store.destroyClient(id, this.get('client store expiration')); + + this.store.unsubscribe('dispatch:' + id); + + if (local) { + this.store.unsubscribe('message:' + id); + this.store.unsubscribe('disconnect:' + id); + } +}; + +/** + * Handles an HTTP request. + * + * @api private + */ + +Manager.prototype.handleRequest = function (req, res) { + var data = this.checkRequest(req); + + if (!data) { + for (var i = 0, l = this.oldListeners.length; i < l; i++) { + this.oldListeners[i].call(this.server, req, res); + } + + return; + } + + if (data.static || !data.transport && !data.protocol) { + if (data.static && this.enabled('browser client')) { + this.static.write(data.path, req, res); + } else { + res.writeHead(200); + res.end('Welcome to socket.io.'); + + this.log.info('unhandled socket.io url'); + } + + return; + } + + if (data.protocol != protocol) { + res.writeHead(500); + res.end('Protocol version not supported.'); + + this.log.info('client protocol version unsupported'); + } else { + if (data.id) { + this.handleHTTPRequest(data, req, res); + } else { + this.handleHandshake(data, req, res); + } + } +}; + +/** + * Handles an HTTP Upgrade. + * + * @api private + */ + +Manager.prototype.handleUpgrade = function (req, socket, head) { + var data = this.checkRequest(req) + , self = this; + + if (!data) { + if (this.enabled('destroy upgrade')) { + socket.end(); + this.log.debug('destroying non-socket.io upgrade'); + } + + return; + } + + req.head = head; + this.handleClient(data, req); + req.head = null; +}; + +/** + * Handles a normal handshaken HTTP request (eg: long-polling) + * + * @api private + */ + +Manager.prototype.handleHTTPRequest = function (data, req, res) { + req.res = res; + this.handleClient(data, req); +}; + +/** + * Intantiantes a new client. + * + * @api private + */ + +Manager.prototype.handleClient = function (data, req) { + var socket = req.socket + , store = this.store + , self = this; + + // handle sync disconnect xhrs + if (undefined != data.query.disconnect) { + if (this.transports[data.id] && this.transports[data.id].open) { + this.transports[data.id].onForcedDisconnect(); + } else { + this.store.publish('disconnect-force:' + data.id); + } + req.res.writeHead(200); + req.res.end(); + return; + } + + if (!~this.get('transports').indexOf(data.transport)) { + this.log.warn('unknown transport: "' + data.transport + '"'); + req.connection.end(); + return; + } + + var transport = new transports[data.transport](this, data, req) + , handshaken = this.handshaken[data.id]; + + if (transport.disconnected) { + // failed during transport setup + req.connection.end(); + return; + } + if (handshaken) { + if (transport.open) { + if (this.closed[data.id] && this.closed[data.id].length) { + transport.payload(this.closed[data.id]); + this.closed[data.id] = []; + } + + this.onOpen(data.id); + this.store.publish('open', data.id); + this.transports[data.id] = transport; + } + + if (!this.connected[data.id]) { + this.onConnect(data.id); + this.store.publish('connect', data.id); + + // flag as used + delete handshaken.issued; + this.onHandshake(data.id, handshaken); + this.store.publish('handshake', data.id, handshaken); + + // initialize the socket for all namespaces + for (var i in this.namespaces) { + if (this.namespaces.hasOwnProperty(i)) { + var socket = this.namespaces[i].socket(data.id, true); + + // echo back connect packet and fire connection event + if (i === '') { + this.namespaces[i].handlePacket(data.id, { type: 'connect' }); + } + } + } + + this.store.subscribe('message:' + data.id, function (packet) { + self.onClientMessage(data.id, packet); + }); + + this.store.subscribe('disconnect:' + data.id, function (reason) { + self.onClientDisconnect(data.id, reason); + }); + } + } else { + if (transport.open) { + transport.error('client not handshaken', 'reconnect'); + } + + transport.discard(); + } +}; + +/** + * Generates a session id. + * + * @api private + */ + +Manager.prototype.generateId = function () { + var rand = new Buffer(15); // multiple of 3 for base64 + if (!rand.writeInt32BE) { + return Math.abs(Math.random() * Math.random() * Date.now() | 0).toString() + + Math.abs(Math.random() * Math.random() * Date.now() | 0).toString(); + } + this.sequenceNumber = (this.sequenceNumber + 1) | 0; + rand.writeInt32BE(this.sequenceNumber, 11); + if (crypto.randomBytes) { + crypto.randomBytes(12).copy(rand); + } else { + // not secure for node 0.4 + [0, 4, 8].forEach(function(i) { + rand.writeInt32BE(Math.random() * Math.pow(2, 32) | 0, i); + }); + } + return rand.toString('base64').replace(/\//g, '_').replace(/\+/g, '-'); +}; + +/** + * Handles a handshake request. + * + * @api private + */ + +Manager.prototype.handleHandshake = function (data, req, res) { + var self = this + , origin = req.headers.origin + , headers = { + 'Content-Type': 'text/plain' + }; + + function writeErr (status, message) { + if (data.query.jsonp && jsonpolling_re.test(data.query.jsonp)) { + res.writeHead(200, { 'Content-Type': 'application/javascript' }); + res.end('io.j[' + data.query.jsonp + '](new Error("' + message + '"));'); + } else { + res.writeHead(status, headers); + res.end(message); + } + }; + + function error (err) { + writeErr(500, 'handshake error'); + self.log.warn('handshake error ' + err); + }; + + if (!this.verifyOrigin(req)) { + writeErr(403, 'handshake bad origin'); + return; + } + + var handshakeData = this.handshakeData(data); + + if (origin) { + // https://developer.mozilla.org/En/HTTP_Access_Control + headers['Access-Control-Allow-Origin'] = origin; + headers['Access-Control-Allow-Credentials'] = 'true'; + } + + this.authorize(handshakeData, function (err, authorized, newData) { + if (err) return error(err); + + if (authorized) { + var id = self.generateId() + , hs = [ + id + , self.enabled('heartbeats') ? self.get('heartbeat timeout') || '' : '' + , self.get('close timeout') || '' + , self.transports(data).join(',') + ].join(':'); + + if (data.query.jsonp && jsonpolling_re.test(data.query.jsonp)) { + hs = 'io.j[' + data.query.jsonp + '](' + JSON.stringify(hs) + ');'; + res.writeHead(200, { 'Content-Type': 'application/javascript' }); + } else { + res.writeHead(200, headers); + } + + res.end(hs); + + self.onHandshake(id, newData || handshakeData); + self.store.publish('handshake', id, newData || handshakeData); + + self.log.info('handshake authorized', id); + } else { + writeErr(403, 'handshake unauthorized'); + self.log.info('handshake unauthorized'); + } + }) +}; + +/** + * Gets normalized handshake data + * + * @api private + */ + +Manager.prototype.handshakeData = function (data) { + var connection = data.request.connection + , connectionAddress + , date = new Date; + + if (connection.remoteAddress) { + connectionAddress = { + address: connection.remoteAddress + , port: connection.remotePort + }; + } else if (connection.socket && connection.socket.remoteAddress) { + connectionAddress = { + address: connection.socket.remoteAddress + , port: connection.socket.remotePort + }; + } + + return { + headers: data.headers + , address: connectionAddress + , time: date.toString() + , query: data.query + , url: data.request.url + , xdomain: !!data.request.headers.origin + , secure: data.request.connection.secure + , issued: +date + }; +}; + +/** + * Verifies the origin of a request. + * + * @api private + */ + +Manager.prototype.verifyOrigin = function (request) { + var origin = request.headers.origin || request.headers.referer + , origins = this.get('origins'); + + if (origin === 'null') origin = '*'; + + if (origins.indexOf('*:*') !== -1) { + return true; + } + + if (origin) { + try { + var parts = url.parse(origin); + parts.port = parts.port || 80; + var ok = + ~origins.indexOf(parts.hostname + ':' + parts.port) || + ~origins.indexOf(parts.hostname + ':*') || + ~origins.indexOf('*:' + parts.port); + if (!ok) this.log.warn('illegal origin: ' + origin); + return ok; + } catch (ex) { + this.log.warn('error parsing origin'); + } + } + else { + this.log.warn('origin missing from handshake, yet required by config'); + } + return false; +}; + +/** + * Handles an incoming packet. + * + * @api private + */ + +Manager.prototype.handlePacket = function (sessid, packet) { + this.of(packet.endpoint || '').handlePacket(sessid, packet); +}; + +/** + * Performs authentication. + * + * @param Object client request data + * @api private + */ + +Manager.prototype.authorize = function (data, fn) { + if (this.get('authorization')) { + var self = this; + + this.get('authorization').call(this, data, function (err, authorized) { + self.log.debug('client ' + authorized ? 'authorized' : 'unauthorized'); + fn(err, authorized); + }); + } else { + this.log.debug('client authorized'); + fn(null, true); + } + + return this; +}; + +/** + * Retrieves the transports adviced to the user. + * + * @api private + */ + +Manager.prototype.transports = function (data) { + var transp = this.get('transports') + , ret = []; + + for (var i = 0, l = transp.length; i < l; i++) { + var transport = transp[i]; + + if (transport) { + if (!transport.checkClient || transport.checkClient(data)) { + ret.push(transport); + } + } + } + + return ret; +}; + +/** + * Checks whether a request is a socket.io one. + * + * @return {Object} a client request data object or `false` + * @api private + */ + +var regexp = /^\/([^\/]+)\/?([^\/]+)?\/?([^\/]+)?\/?$/ + +Manager.prototype.checkRequest = function (req) { + var resource = this.get('resource'); + + var match; + if (typeof resource === 'string') { + match = req.url.substr(0, resource.length); + if (match !== resource) match = null; + } else { + match = resource.exec(req.url); + if (match) match = match[0]; + } + + if (match) { + var uri = url.parse(req.url.substr(match.length), true) + , path = uri.pathname || '' + , pieces = path.match(regexp); + + // client request data + var data = { + query: uri.query || {} + , headers: req.headers + , request: req + , path: path + }; + + if (pieces) { + data.protocol = Number(pieces[1]); + data.transport = pieces[2]; + data.id = pieces[3]; + data.static = !!this.static.has(path); + }; + + return data; + } + + return false; +}; + +/** + * Declares a socket namespace + * + * @api public + */ + +Manager.prototype.of = function (nsp) { + if (this.namespaces[nsp]) { + return this.namespaces[nsp]; + } + + return this.namespaces[nsp] = new SocketNamespace(this, nsp); +}; + +/** + * Perform garbage collection on long living objects and properties that cannot + * be removed automatically. + * + * @api private + */ + +Manager.prototype.garbageCollection = function () { + // clean up unused handshakes + var ids = Object.keys(this.handshaken) + , i = ids.length + , now = Date.now() + , handshake; + + while (i--) { + handshake = this.handshaken[ids[i]]; + + if ('issued' in handshake && (now - handshake.issued) >= 3E4) { + this.onDisconnect(ids[i]); + } + } +}; diff --git a/node_modules/socket.io/lib/namespace.js b/node_modules/socket.io/lib/namespace.js new file mode 100644 index 0000000000..6e1e1c927b --- /dev/null +++ b/node_modules/socket.io/lib/namespace.js @@ -0,0 +1,355 @@ +/** + * Module dependencies. + */ + +var Socket = require('./socket') + , EventEmitter = process.EventEmitter + , parser = require('./parser') + , util = require('./util'); + +/** + * Exports the constructor. + */ + +exports = module.exports = SocketNamespace; + +/** + * Constructor. + * + * @api public. + */ + +function SocketNamespace (mgr, name) { + this.manager = mgr; + this.name = name || ''; + this.sockets = {}; + this.auth = false; + this.setFlags(); +}; + +/** + * Inherits from EventEmitter. + */ + +SocketNamespace.prototype.__proto__ = EventEmitter.prototype; + +/** + * Copies emit since we override it. + * + * @api private + */ + +SocketNamespace.prototype.$emit = EventEmitter.prototype.emit; + +/** + * Retrieves all clients as Socket instances as an array. + * + * @api public + */ + +SocketNamespace.prototype.clients = function (room) { + var room = this.name + (room !== undefined ? + '/' + room : ''); + + if (!this.manager.rooms[room]) { + return []; + } + + return this.manager.rooms[room].map(function (id) { + return this.socket(id); + }, this); +}; + +/** + * Access logger interface. + * + * @api public + */ + +SocketNamespace.prototype.__defineGetter__('log', function () { + return this.manager.log; +}); + +/** + * Access store. + * + * @api public + */ + +SocketNamespace.prototype.__defineGetter__('store', function () { + return this.manager.store; +}); + +/** + * JSON message flag. + * + * @api public + */ + +SocketNamespace.prototype.__defineGetter__('json', function () { + this.flags.json = true; + return this; +}); + +/** + * Volatile message flag. + * + * @api public + */ + +SocketNamespace.prototype.__defineGetter__('volatile', function () { + this.flags.volatile = true; + return this; +}); + +/** + * Overrides the room to relay messages to (flag). + * + * @api public + */ + +SocketNamespace.prototype.in = SocketNamespace.prototype.to = function (room) { + this.flags.endpoint = this.name + (room ? '/' + room : ''); + return this; +}; + +/** + * Adds a session id we should prevent relaying messages to (flag). + * + * @api public + */ + +SocketNamespace.prototype.except = function (id) { + this.flags.exceptions.push(id); + return this; +}; + +/** + * Sets the default flags. + * + * @api private + */ + +SocketNamespace.prototype.setFlags = function () { + this.flags = { + endpoint: this.name + , exceptions: [] + }; + return this; +}; + +/** + * Sends out a packet. + * + * @api private + */ + +SocketNamespace.prototype.packet = function (packet) { + packet.endpoint = this.name; + + var store = this.store + , log = this.log + , volatile = this.flags.volatile + , exceptions = this.flags.exceptions + , packet = parser.encodePacket(packet); + + this.manager.onDispatch(this.flags.endpoint, packet, volatile, exceptions); + this.store.publish('dispatch', this.flags.endpoint, packet, volatile, exceptions); + + this.setFlags(); + + return this; +}; + +/** + * Sends to everyone. + * + * @api public + */ + +SocketNamespace.prototype.send = function (data) { + return this.packet({ + type: this.flags.json ? 'json' : 'message' + , data: data + }); +}; + +/** + * Emits to everyone (override). + * + * @api public + */ + +SocketNamespace.prototype.emit = function (name) { + if (name == 'newListener') { + return this.$emit.apply(this, arguments); + } + + return this.packet({ + type: 'event' + , name: name + , args: util.toArray(arguments).slice(1) + }); +}; + +/** + * Retrieves or creates a write-only socket for a client, unless specified. + * + * @param {Boolean} whether the socket will be readable when initialized + * @api public + */ + +SocketNamespace.prototype.socket = function (sid, readable) { + if (!this.sockets[sid]) { + this.sockets[sid] = new Socket(this.manager, sid, this, readable); + } + + return this.sockets[sid]; +}; + +/** + * Sets authorization for this namespace. + * + * @api public + */ + +SocketNamespace.prototype.authorization = function (fn) { + this.auth = fn; + return this; +}; + +/** + * Called when a socket disconnects entirely. + * + * @api private + */ + +SocketNamespace.prototype.handleDisconnect = function (sid, reason, raiseOnDisconnect) { + if (this.sockets[sid] && this.sockets[sid].readable) { + if (raiseOnDisconnect) this.sockets[sid].onDisconnect(reason); + delete this.sockets[sid]; + } +}; + +/** + * Performs authentication. + * + * @param Object client request data + * @api private + */ + +SocketNamespace.prototype.authorize = function (data, fn) { + if (this.auth) { + var self = this; + + this.auth.call(this, data, function (err, authorized) { + self.log.debug('client ' + + (authorized ? '' : 'un') + 'authorized for ' + self.name); + fn(err, authorized); + }); + } else { + this.log.debug('client authorized for ' + this.name); + fn(null, true); + } + + return this; +}; + +/** + * Handles a packet. + * + * @api private + */ + +SocketNamespace.prototype.handlePacket = function (sessid, packet) { + var socket = this.socket(sessid) + , dataAck = packet.ack == 'data' + , manager = this.manager + , self = this; + + function ack () { + self.log.debug('sending data ack packet'); + socket.packet({ + type: 'ack' + , args: util.toArray(arguments) + , ackId: packet.id + }); + }; + + function error (err) { + self.log.warn('handshake error ' + err + ' for ' + self.name); + socket.packet({ type: 'error', reason: err }); + }; + + function connect () { + self.manager.onJoin(sessid, self.name); + self.store.publish('join', sessid, self.name); + + // packet echo + socket.packet({ type: 'connect' }); + + // emit connection event + self.$emit('connection', socket); + }; + + switch (packet.type) { + case 'connect': + if (packet.endpoint == '') { + connect(); + } else { + var handshakeData = manager.handshaken[sessid]; + + this.authorize(handshakeData, function (err, authorized, newData) { + if (err) return error(err); + + if (authorized) { + manager.onHandshake(sessid, newData || handshakeData); + self.store.publish('handshake', sessid, newData || handshakeData); + connect(); + } else { + error('unauthorized'); + } + }); + } + break; + + case 'ack': + if (socket.acks[packet.ackId]) { + socket.acks[packet.ackId].apply(socket, packet.args); + } else { + this.log.info('unknown ack packet'); + } + break; + + case 'event': + // check if the emitted event is not blacklisted + if (-~manager.get('blacklist').indexOf(packet.name)) { + this.log.debug('ignoring blacklisted event `' + packet.name + '`'); + } else { + var params = [packet.name].concat(packet.args); + + if (dataAck) { + params.push(ack); + } + + socket.$emit.apply(socket, params); + } + break; + + case 'disconnect': + this.manager.onLeave(sessid, this.name); + this.store.publish('leave', sessid, this.name); + + socket.$emit('disconnect', packet.reason || 'packet'); + break; + + case 'json': + case 'message': + var params = ['message', packet.data]; + + if (dataAck) + params.push(ack); + + socket.$emit.apply(socket, params); + }; +}; diff --git a/node_modules/socket.io/lib/parser.js b/node_modules/socket.io/lib/parser.js new file mode 100644 index 0000000000..d56b550064 --- /dev/null +++ b/node_modules/socket.io/lib/parser.js @@ -0,0 +1,249 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +/** + * Packet types. + */ + +var packets = exports.packets = { + 'disconnect': 0 + , 'connect': 1 + , 'heartbeat': 2 + , 'message': 3 + , 'json': 4 + , 'event': 5 + , 'ack': 6 + , 'error': 7 + , 'noop': 8 + } + , packetslist = Object.keys(packets); + +/** + * Errors reasons. + */ + +var reasons = exports.reasons = { + 'transport not supported': 0 + , 'client not handshaken': 1 + , 'unauthorized': 2 + } + , reasonslist = Object.keys(reasons); + +/** + * Errors advice. + */ + +var advice = exports.advice = { + 'reconnect': 0 + } + , advicelist = Object.keys(advice); + +/** + * Encodes a packet. + * + * @api private + */ + +exports.encodePacket = function (packet) { + var type = packets[packet.type] + , id = packet.id || '' + , endpoint = packet.endpoint || '' + , ack = packet.ack + , data = null; + + switch (packet.type) { + case 'message': + if (packet.data !== '') + data = packet.data; + break; + + case 'event': + var ev = { name: packet.name }; + + if (packet.args && packet.args.length) { + ev.args = packet.args; + } + + data = JSON.stringify(ev); + break; + + case 'json': + data = JSON.stringify(packet.data); + break; + + case 'ack': + data = packet.ackId + + (packet.args && packet.args.length + ? '+' + JSON.stringify(packet.args) : ''); + break; + + case 'connect': + if (packet.qs) + data = packet.qs; + break; + + case 'error': + var reason = packet.reason ? reasons[packet.reason] : '' + , adv = packet.advice ? advice[packet.advice] : '' + + if (reason !== '' || adv !== '') + data = reason + (adv !== '' ? ('+' + adv) : '') + + break; + } + + // construct packet with required fragments + var encoded = type + ':' + id + (ack == 'data' ? '+' : '') + ':' + endpoint; + + // data fragment is optional + if (data !== null && data !== undefined) + encoded += ':' + data; + + return encoded; +}; + +/** + * Encodes multiple messages (payload). + * + * @param {Array} messages + * @api private + */ + +exports.encodePayload = function (packets) { + var decoded = ''; + + if (packets.length == 1) + return packets[0]; + + for (var i = 0, l = packets.length; i < l; i++) { + var packet = packets[i]; + decoded += '\ufffd' + packet.length + '\ufffd' + packets[i] + } + + return decoded; +}; + +/** + * Decodes a packet + * + * @api private + */ + +var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/; + +/** + * Wrap the JSON.parse in a seperate function the crankshaft optimizer will + * only punish this function for the usage for try catch + * + * @api private + */ + +function parse (data) { + try { return JSON.parse(data) } + catch (e) { return false } +} + +exports.decodePacket = function (data) { + var pieces = data.match(regexp); + + if (!pieces) return {}; + + var id = pieces[2] || '' + , data = pieces[5] || '' + , packet = { + type: packetslist[pieces[1]] + , endpoint: pieces[4] || '' + }; + + // whether we need to acknowledge the packet + if (id) { + packet.id = id; + if (pieces[3]) + packet.ack = 'data'; + else + packet.ack = true; + } + + // handle different packet types + switch (packet.type) { + case 'message': + packet.data = data || ''; + break; + + case 'event': + pieces = parse(data); + if (pieces) { + packet.name = pieces.name; + packet.args = pieces.args; + } + + packet.args = packet.args || []; + break; + + case 'json': + packet.data = parse(data); + break; + + case 'connect': + packet.qs = data || ''; + break; + + case 'ack': + pieces = data.match(/^([0-9]+)(\+)?(.*)/); + if (pieces) { + packet.ackId = pieces[1]; + packet.args = []; + + if (pieces[3]) { + packet.args = parse(pieces[3]) || []; + } + } + break; + + case 'error': + pieces = data.split('+'); + packet.reason = reasonslist[pieces[0]] || ''; + packet.advice = advicelist[pieces[1]] || ''; + } + + return packet; +}; + +/** + * Decodes data payload. Detects multiple messages + * + * @return {Array} messages + * @api public + */ + +exports.decodePayload = function (data) { + if (undefined == data || null == data) { + return []; + } + + if (data[0] == '\ufffd') { + var ret = []; + + for (var i = 1, length = ''; i < data.length; i++) { + if (data[i] == '\ufffd') { + ret.push(exports.decodePacket(data.substr(i + 1, length))); + i += Number(length) + 1; + length = ''; + } else { + length += data[i]; + } + } + + return ret; + } else { + return [exports.decodePacket(data)]; + } +}; diff --git a/node_modules/socket.io/lib/socket.io.js b/node_modules/socket.io/lib/socket.io.js new file mode 100644 index 0000000000..bf590364ae --- /dev/null +++ b/node_modules/socket.io/lib/socket.io.js @@ -0,0 +1,143 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var client = require('socket.io-client'); + +/** + * Version. + */ + +exports.version = '0.9.11'; + +/** + * Supported protocol version. + */ + +exports.protocol = 1; + +/** + * Client that we serve. + */ + +exports.clientVersion = client.version; + +/** + * Attaches a manager + * + * @param {HTTPServer/Number} a HTTP/S server or a port number to listen on. + * @param {Object} opts to be passed to Manager and/or http server + * @param {Function} callback if a port is supplied + * @api public + */ + +exports.listen = function (server, options, fn) { + if ('function' == typeof server) { + console.warn('Socket.IO\'s `listen()` method expects an `http.Server` instance\n' + + 'as its first parameter. Are you migrating from Express 2.x to 3.x?\n' + + 'If so, check out the "Socket.IO compatibility" section at:\n' + + 'https://github.com/visionmedia/express/wiki/Migrating-from-2.x-to-3.x'); + } + + if ('function' == typeof options) { + fn = options; + options = {}; + } + + if ('undefined' == typeof server) { + // create a server that listens on port 80 + server = 80; + } + + if ('number' == typeof server) { + // if a port number is passed + var port = server; + + if (options && options.key) + server = require('https').createServer(options); + else + server = require('http').createServer(); + + // default response + server.on('request', function (req, res) { + res.writeHead(200); + res.end('Welcome to socket.io.'); + }); + + server.listen(port, fn); + } + + // otherwise assume a http/s server + return new exports.Manager(server, options); +}; + +/** + * Manager constructor. + * + * @api public + */ + +exports.Manager = require('./manager'); + +/** + * Transport constructor. + * + * @api public + */ + +exports.Transport = require('./transport'); + +/** + * Socket constructor. + * + * @api public + */ + +exports.Socket = require('./socket'); + +/** + * Static constructor. + * + * @api public + */ + +exports.Static = require('./static'); + +/** + * Store constructor. + * + * @api public + */ + +exports.Store = require('./store'); + +/** + * Memory Store constructor. + * + * @api public + */ + +exports.MemoryStore = require('./stores/memory'); + +/** + * Redis Store constructor. + * + * @api public + */ + +exports.RedisStore = require('./stores/redis'); + +/** + * Parser. + * + * @api public + */ + +exports.parser = require('./parser'); diff --git a/node_modules/socket.io/lib/socket.js b/node_modules/socket.io/lib/socket.js new file mode 100644 index 0000000000..d9807f6dd2 --- /dev/null +++ b/node_modules/socket.io/lib/socket.js @@ -0,0 +1,369 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var parser = require('./parser') + , util = require('./util') + , EventEmitter = process.EventEmitter + +/** + * Export the constructor. + */ + +exports = module.exports = Socket; + +/** + * Default error event listener to prevent uncaught exceptions. + */ + +var defaultError = function () {}; + +/** + * Socket constructor. + * + * @param {Manager} manager instance + * @param {String} session id + * @param {Namespace} namespace the socket belongs to + * @param {Boolean} whether the + * @api public + */ + +function Socket (manager, id, nsp, readable) { + this.id = id; + this.namespace = nsp; + this.manager = manager; + this.disconnected = false; + this.ackPackets = 0; + this.acks = {}; + this.setFlags(); + this.readable = readable; + this.store = this.manager.store.client(this.id); + this.on('error', defaultError); +}; + +/** + * Inherits from EventEmitter. + */ + +Socket.prototype.__proto__ = EventEmitter.prototype; + +/** + * Accessor shortcut for the handshake data + * + * @api private + */ + +Socket.prototype.__defineGetter__('handshake', function () { + return this.manager.handshaken[this.id]; +}); + +/** + * Accessor shortcut for the transport type + * + * @api private + */ + +Socket.prototype.__defineGetter__('transport', function () { + return this.manager.transports[this.id].name; +}); + +/** + * Accessor shortcut for the logger. + * + * @api private + */ + +Socket.prototype.__defineGetter__('log', function () { + return this.manager.log; +}); + +/** + * JSON message flag. + * + * @api public + */ + +Socket.prototype.__defineGetter__('json', function () { + this.flags.json = true; + return this; +}); + +/** + * Volatile message flag. + * + * @api public + */ + +Socket.prototype.__defineGetter__('volatile', function () { + this.flags.volatile = true; + return this; +}); + +/** + * Broadcast message flag. + * + * @api public + */ + +Socket.prototype.__defineGetter__('broadcast', function () { + this.flags.broadcast = true; + return this; +}); + +/** + * Overrides the room to broadcast messages to (flag) + * + * @api public + */ + +Socket.prototype.to = Socket.prototype.in = function (room) { + this.flags.room = room; + return this; +}; + +/** + * Resets flags + * + * @api private + */ + +Socket.prototype.setFlags = function () { + this.flags = { + endpoint: this.namespace.name + , room: '' + }; + return this; +}; + +/** + * Triggered on disconnect + * + * @api private + */ + +Socket.prototype.onDisconnect = function (reason) { + if (!this.disconnected) { + this.$emit('disconnect', reason); + this.disconnected = true; + } +}; + +/** + * Joins a user to a room. + * + * @api public + */ + +Socket.prototype.join = function (name, fn) { + var nsp = this.namespace.name + , name = (nsp + '/') + name; + + this.manager.onJoin(this.id, name); + this.manager.store.publish('join', this.id, name); + + if (fn) { + this.log.warn('Client#join callback is deprecated'); + fn(); + } + + return this; +}; + +/** + * Un-joins a user from a room. + * + * @api public + */ + +Socket.prototype.leave = function (name, fn) { + var nsp = this.namespace.name + , name = (nsp + '/') + name; + + this.manager.onLeave(this.id, name); + this.manager.store.publish('leave', this.id, name); + + if (fn) { + this.log.warn('Client#leave callback is deprecated'); + fn(); + } + + return this; +}; + +/** + * Transmits a packet. + * + * @api private + */ + +Socket.prototype.packet = function (packet) { + if (this.flags.broadcast) { + this.log.debug('broadcasting packet'); + this.namespace.in(this.flags.room).except(this.id).packet(packet); + } else { + packet.endpoint = this.flags.endpoint; + packet = parser.encodePacket(packet); + + this.dispatch(packet, this.flags.volatile); + } + + this.setFlags(); + + return this; +}; + +/** + * Dispatches a packet + * + * @api private + */ + +Socket.prototype.dispatch = function (packet, volatile) { + if (this.manager.transports[this.id] && this.manager.transports[this.id].open) { + this.manager.transports[this.id].onDispatch(packet, volatile); + } else { + if (!volatile) { + this.manager.onClientDispatch(this.id, packet, volatile); + } + + this.manager.store.publish('dispatch:' + this.id, packet, volatile); + } +}; + +/** + * Stores data for the client. + * + * @api public + */ + +Socket.prototype.set = function (key, value, fn) { + this.store.set(key, value, fn); + return this; +}; + +/** + * Retrieves data for the client + * + * @api public + */ + +Socket.prototype.get = function (key, fn) { + this.store.get(key, fn); + return this; +}; + +/** + * Checks data for the client + * + * @api public + */ + +Socket.prototype.has = function (key, fn) { + this.store.has(key, fn); + return this; +}; + +/** + * Deletes data for the client + * + * @api public + */ + +Socket.prototype.del = function (key, fn) { + this.store.del(key, fn); + return this; +}; + +/** + * Kicks client + * + * @api public + */ + +Socket.prototype.disconnect = function () { + if (!this.disconnected) { + this.log.info('booting client'); + + if ('' === this.namespace.name) { + if (this.manager.transports[this.id] && this.manager.transports[this.id].open) { + this.manager.transports[this.id].onForcedDisconnect(); + } else { + this.manager.onClientDisconnect(this.id); + this.manager.store.publish('disconnect:' + this.id); + } + } else { + this.packet({type: 'disconnect'}); + this.manager.onLeave(this.id, this.namespace.name); + this.$emit('disconnect', 'booted'); + } + + } + + return this; +}; + +/** + * Send a message. + * + * @api public + */ + +Socket.prototype.send = function (data, fn) { + var packet = { + type: this.flags.json ? 'json' : 'message' + , data: data + }; + + if (fn) { + packet.id = ++this.ackPackets; + packet.ack = true; + this.acks[packet.id] = fn; + } + + return this.packet(packet); +}; + +/** + * Original emit function. + * + * @api private + */ + +Socket.prototype.$emit = EventEmitter.prototype.emit; + +/** + * Emit override for custom events. + * + * @api public + */ + +Socket.prototype.emit = function (ev) { + if (ev == 'newListener') { + return this.$emit.apply(this, arguments); + } + + var args = util.toArray(arguments).slice(1) + , lastArg = args[args.length - 1] + , packet = { + type: 'event' + , name: ev + }; + + if ('function' == typeof lastArg) { + packet.id = ++this.ackPackets; + packet.ack = lastArg.length ? 'data' : true; + this.acks[packet.id] = lastArg; + args = args.slice(0, args.length - 1); + } + + packet.args = args; + + return this.packet(packet); +}; diff --git a/node_modules/socket.io/lib/static.js b/node_modules/socket.io/lib/static.js new file mode 100644 index 0000000000..fe50593757 --- /dev/null +++ b/node_modules/socket.io/lib/static.js @@ -0,0 +1,395 @@ + +/*! +* socket.io-node +* Copyright(c) 2011 LearnBoost +* MIT Licensed +*/ + +/** + * Module dependencies. + */ + +var client = require('socket.io-client') + , cp = require('child_process') + , fs = require('fs') + , util = require('./util'); + +/** + * File type details. + * + * @api private + */ + +var mime = { + js: { + type: 'application/javascript' + , encoding: 'utf8' + , gzip: true + } + , swf: { + type: 'application/x-shockwave-flash' + , encoding: 'binary' + , gzip: false + } +}; + +/** + * Regexp for matching custom transport patterns. Users can configure their own + * socket.io bundle based on the url structure. Different transport names are + * concatinated using the `+` char. /socket.io/socket.io+websocket.js should + * create a bundle that only contains support for the websocket. + * + * @api private + */ + +var bundle = /\+((?:\+)?[\w\-]+)*(?:\.v\d+\.\d+\.\d+)?(?:\.js)$/ + , versioning = /\.v\d+\.\d+\.\d+(?:\.js)$/; + +/** + * Export the constructor + */ + +exports = module.exports = Static; + +/** + * Static constructor + * + * @api public + */ + +function Static (manager) { + this.manager = manager; + this.cache = {}; + this.paths = {}; + + this.init(); +} + +/** + * Initialize the Static by adding default file paths. + * + * @api public + */ + +Static.prototype.init = function () { + /** + * Generates a unique id based the supplied transports array + * + * @param {Array} transports The array with transport types + * @api private + */ + function id (transports) { + var id = transports.join('').split('').map(function (char) { + return ('' + char.charCodeAt(0)).split('').pop(); + }).reduce(function (char, id) { + return char +id; + }); + + return client.version + ':' + id; + } + + /** + * Generates a socket.io-client file based on the supplied transports. + * + * @param {Array} transports The array with transport types + * @param {Function} callback Callback for the static.write + * @api private + */ + + function build (transports, callback) { + client.builder(transports, { + minify: self.manager.enabled('browser client minification') + }, function (err, content) { + callback(err, content ? new Buffer(content) : null, id(transports)); + } + ); + } + + var self = this; + + // add our default static files + this.add('/static/flashsocket/WebSocketMain.swf', { + file: client.dist + '/WebSocketMain.swf' + }); + + this.add('/static/flashsocket/WebSocketMainInsecure.swf', { + file: client.dist + '/WebSocketMainInsecure.swf' + }); + + // generates dedicated build based on the available transports + this.add('/socket.io.js', function (path, callback) { + build(self.manager.get('transports'), callback); + }); + + this.add('/socket.io.v', { mime: mime.js }, function (path, callback) { + build(self.manager.get('transports'), callback); + }); + + // allow custom builds based on url paths + this.add('/socket.io+', { mime: mime.js }, function (path, callback) { + var available = self.manager.get('transports') + , matches = path.match(bundle) + , transports = []; + + if (!matches) return callback('No valid transports'); + + // make sure they valid transports + matches[0].split('.')[0].split('+').slice(1).forEach(function (transport) { + if (!!~available.indexOf(transport)) { + transports.push(transport); + } + }); + + if (!transports.length) return callback('No valid transports'); + build(transports, callback); + }); + + // clear cache when transports change + this.manager.on('set:transports', function (key, value) { + delete self.cache['/socket.io.js']; + Object.keys(self.cache).forEach(function (key) { + if (bundle.test(key)) { + delete self.cache[key]; + } + }); + }); +}; + +/** + * Gzip compress buffers. + * + * @param {Buffer} data The buffer that needs gzip compression + * @param {Function} callback + * @api public + */ + +Static.prototype.gzip = function (data, callback) { + var gzip = cp.spawn('gzip', ['-9', '-c', '-f', '-n']) + , encoding = Buffer.isBuffer(data) ? 'binary' : 'utf8' + , buffer = [] + , err; + + gzip.stdout.on('data', function (data) { + buffer.push(data); + }); + + gzip.stderr.on('data', function (data) { + err = data +''; + buffer.length = 0; + }); + + gzip.on('close', function () { + if (err) return callback(err); + + var size = 0 + , index = 0 + , i = buffer.length + , content; + + while (i--) { + size += buffer[i].length; + } + + content = new Buffer(size); + i = buffer.length; + + buffer.forEach(function (buffer) { + var length = buffer.length; + + buffer.copy(content, index, 0, length); + index += length; + }); + + buffer.length = 0; + callback(null, content); + }); + + gzip.stdin.end(data, encoding); +}; + +/** + * Is the path a static file? + * + * @param {String} path The path that needs to be checked + * @api public + */ + +Static.prototype.has = function (path) { + // fast case + if (this.paths[path]) return this.paths[path]; + + var keys = Object.keys(this.paths) + , i = keys.length; + + while (i--) { + if (-~path.indexOf(keys[i])) return this.paths[keys[i]]; + } + + return false; +}; + +/** + * Add new paths new paths that can be served using the static provider. + * + * @param {String} path The path to respond to + * @param {Options} options Options for writing out the response + * @param {Function} [callback] Optional callback if no options.file is + * supplied this would be called instead. + * @api public + */ + +Static.prototype.add = function (path, options, callback) { + var extension = /(?:\.(\w{1,4}))$/.exec(path); + + if (!callback && typeof options == 'function') { + callback = options; + options = {}; + } + + options.mime = options.mime || (extension ? mime[extension[1]] : false); + + if (callback) options.callback = callback; + if (!(options.file || options.callback) || !options.mime) return false; + + this.paths[path] = options; + + return true; +}; + +/** + * Writes a static response. + * + * @param {String} path The path for the static content + * @param {HTTPRequest} req The request object + * @param {HTTPResponse} res The response object + * @api public + */ + +Static.prototype.write = function (path, req, res) { + /** + * Write a response without throwing errors because can throw error if the + * response is no longer writable etc. + * + * @api private + */ + + function write (status, headers, content, encoding) { + try { + res.writeHead(status, headers || undefined); + + // only write content if it's not a HEAD request and we actually have + // some content to write (304's doesn't have content). + res.end( + req.method !== 'HEAD' && content ? content : '' + , encoding || undefined + ); + } catch (e) {} + } + + /** + * Answers requests depending on the request properties and the reply object. + * + * @param {Object} reply The details and content to reply the response with + * @api private + */ + + function answer (reply) { + var cached = req.headers['if-none-match'] === reply.etag; + if (cached && self.manager.enabled('browser client etag')) { + return write(304); + } + + var accept = req.headers['accept-encoding'] || '' + , gzip = !!~accept.toLowerCase().indexOf('gzip') + , mime = reply.mime + , versioned = reply.versioned + , headers = { + 'Content-Type': mime.type + }; + + // check if we can add a etag + if (self.manager.enabled('browser client etag') && reply.etag && !versioned) { + headers['Etag'] = reply.etag; + } + + // see if we need to set Expire headers because the path is versioned + if (versioned) { + var expires = self.manager.get('browser client expires'); + headers['Cache-Control'] = 'private, x-gzip-ok="", max-age=' + expires; + headers['Date'] = new Date().toUTCString(); + headers['Expires'] = new Date(Date.now() + (expires * 1000)).toUTCString(); + } + + if (gzip && reply.gzip) { + headers['Content-Length'] = reply.gzip.length; + headers['Content-Encoding'] = 'gzip'; + headers['Vary'] = 'Accept-Encoding'; + write(200, headers, reply.gzip.content, mime.encoding); + } else { + headers['Content-Length'] = reply.length; + write(200, headers, reply.content, mime.encoding); + } + + self.manager.log.debug('served static content ' + path); + } + + var self = this + , details; + + // most common case first + if (this.manager.enabled('browser client cache') && this.cache[path]) { + return answer(this.cache[path]); + } else if (this.manager.get('browser client handler')) { + return this.manager.get('browser client handler').call(this, req, res); + } else if ((details = this.has(path))) { + /** + * A small helper function that will let us deal with fs and dynamic files + * + * @param {Object} err Optional error + * @param {Buffer} content The data + * @api private + */ + + function ready (err, content, etag) { + if (err) { + self.manager.log.warn('Unable to serve file. ' + (err.message || err)); + return write(500, null, 'Error serving static ' + path); + } + + // store the result in the cache + var reply = self.cache[path] = { + content: content + , length: content.length + , mime: details.mime + , etag: etag || client.version + , versioned: versioning.test(path) + }; + + // check if gzip is enabled + if (details.mime.gzip && self.manager.enabled('browser client gzip')) { + self.gzip(content, function (err, content) { + if (!err) { + reply.gzip = { + content: content + , length: content.length + } + } + + answer(reply); + }); + } else { + answer(reply); + } + } + + if (details.file) { + fs.readFile(details.file, ready); + } else if(details.callback) { + details.callback.call(this, path, ready); + } else { + write(404, null, 'File handle not found'); + } + } else { + write(404, null, 'File not found'); + } +}; diff --git a/node_modules/socket.io/lib/store.js b/node_modules/socket.io/lib/store.js new file mode 100644 index 0000000000..06c0389a62 --- /dev/null +++ b/node_modules/socket.io/lib/store.js @@ -0,0 +1,98 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Expose the constructor. + */ + +exports = module.exports = Store; + +/** + * Module dependencies. + */ + +var EventEmitter = process.EventEmitter; + +/** + * Store interface + * + * @api public + */ + +function Store (options) { + this.options = options; + this.clients = {}; +}; + +/** + * Inherit from EventEmitter. + */ + +Store.prototype.__proto__ = EventEmitter.prototype; + +/** + * Initializes a client store + * + * @param {String} id + * @api public + */ + +Store.prototype.client = function (id) { + if (!this.clients[id]) { + this.clients[id] = new (this.constructor.Client)(this, id); + } + + return this.clients[id]; +}; + +/** + * Destroys a client + * + * @api {String} sid + * @param {Number} number of seconds to expire client data + * @api private + */ + +Store.prototype.destroyClient = function (id, expiration) { + if (this.clients[id]) { + this.clients[id].destroy(expiration); + delete this.clients[id]; + } + + return this; +}; + +/** + * Destroys the store + * + * @param {Number} number of seconds to expire client data + * @api private + */ + +Store.prototype.destroy = function (clientExpiration) { + var keys = Object.keys(this.clients) + , count = keys.length; + + for (var i = 0, l = count; i < l; i++) { + this.destroyClient(keys[i], clientExpiration); + } + + this.clients = {}; + + return this; +}; + +/** + * Client. + * + * @api public + */ + +Store.Client = function (store, id) { + this.store = store; + this.id = id; +}; diff --git a/node_modules/socket.io/lib/stores/memory.js b/node_modules/socket.io/lib/stores/memory.js new file mode 100644 index 0000000000..8b731a7904 --- /dev/null +++ b/node_modules/socket.io/lib/stores/memory.js @@ -0,0 +1,143 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var crypto = require('crypto') + , Store = require('../store'); + +/** + * Exports the constructor. + */ + +exports = module.exports = Memory; +Memory.Client = Client; + +/** + * Memory store + * + * @api public + */ + +function Memory (opts) { + Store.call(this, opts); +}; + +/** + * Inherits from Store. + */ + +Memory.prototype.__proto__ = Store.prototype; + +/** + * Publishes a message. + * + * @api private + */ + +Memory.prototype.publish = function () { }; + +/** + * Subscribes to a channel + * + * @api private + */ + +Memory.prototype.subscribe = function () { }; + +/** + * Unsubscribes + * + * @api private + */ + +Memory.prototype.unsubscribe = function () { }; + +/** + * Client constructor + * + * @api private + */ + +function Client () { + Store.Client.apply(this, arguments); + this.data = {}; +}; + +/** + * Inherits from Store.Client + */ + +Client.prototype.__proto__ = Store.Client; + +/** + * Gets a key + * + * @api public + */ + +Client.prototype.get = function (key, fn) { + fn(null, this.data[key] === undefined ? null : this.data[key]); + return this; +}; + +/** + * Sets a key + * + * @api public + */ + +Client.prototype.set = function (key, value, fn) { + this.data[key] = value; + fn && fn(null); + return this; +}; + +/** + * Has a key + * + * @api public + */ + +Client.prototype.has = function (key, fn) { + fn(null, key in this.data); +}; + +/** + * Deletes a key + * + * @api public + */ + +Client.prototype.del = function (key, fn) { + delete this.data[key]; + fn && fn(null); + return this; +}; + +/** + * Destroys the client. + * + * @param {Number} number of seconds to expire data + * @api private + */ + +Client.prototype.destroy = function (expiration) { + if ('number' != typeof expiration) { + this.data = {}; + } else { + var self = this; + + setTimeout(function () { + self.data = {}; + }, expiration * 1000); + } + + return this; +}; diff --git a/node_modules/socket.io/lib/stores/redis.js b/node_modules/socket.io/lib/stores/redis.js new file mode 100644 index 0000000000..8fea235f0d --- /dev/null +++ b/node_modules/socket.io/lib/stores/redis.js @@ -0,0 +1,269 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var crypto = require('crypto') + , Store = require('../store') + , assert = require('assert'); + +/** + * Exports the constructor. + */ + +exports = module.exports = Redis; +Redis.Client = Client; + +/** + * Redis store. + * Options: + * - nodeId (fn) gets an id that uniquely identifies this node + * - redis (fn) redis constructor, defaults to redis + * - redisPub (object) options to pass to the pub redis client + * - redisSub (object) options to pass to the sub redis client + * - redisClient (object) options to pass to the general redis client + * - pack (fn) custom packing, defaults to JSON or msgpack if installed + * - unpack (fn) custom packing, defaults to JSON or msgpack if installed + * + * @api public + */ + +function Redis (opts) { + opts = opts || {}; + + // node id to uniquely identify this node + var nodeId = opts.nodeId || function () { + // by default, we generate a random id + return Math.abs(Math.random() * Math.random() * Date.now() | 0); + }; + + this.nodeId = nodeId(); + + // packing / unpacking mechanism + if (opts.pack) { + this.pack = opts.pack; + this.unpack = opts.unpack; + } else { + try { + var msgpack = require('msgpack'); + this.pack = msgpack.pack; + this.unpack = msgpack.unpack; + } catch (e) { + this.pack = JSON.stringify; + this.unpack = JSON.parse; + } + } + + var redis = opts.redis || require('redis') + , RedisClient = redis.RedisClient; + + // initialize a pubsub client and a regular client + if (opts.redisPub instanceof RedisClient) { + this.pub = opts.redisPub; + } else { + opts.redisPub || (opts.redisPub = {}); + this.pub = redis.createClient(opts.redisPub.port, opts.redisPub.host, opts.redisPub); + } + if (opts.redisSub instanceof RedisClient) { + this.sub = opts.redisSub; + } else { + opts.redisSub || (opts.redisSub = {}); + this.sub = redis.createClient(opts.redisSub.port, opts.redisSub.host, opts.redisSub); + } + if (opts.redisClient instanceof RedisClient) { + this.cmd = opts.redisClient; + } else { + opts.redisClient || (opts.redisClient = {}); + this.cmd = redis.createClient(opts.redisClient.port, opts.redisClient.host, opts.redisClient); + } + + Store.call(this, opts); + + this.sub.setMaxListeners(0); + this.setMaxListeners(0); +}; + +/** + * Inherits from Store. + */ + +Redis.prototype.__proto__ = Store.prototype; + +/** + * Publishes a message. + * + * @api private + */ + +Redis.prototype.publish = function (name) { + var args = Array.prototype.slice.call(arguments, 1); + this.pub.publish(name, this.pack({ nodeId: this.nodeId, args: args })); + this.emit.apply(this, ['publish', name].concat(args)); +}; + +/** + * Subscribes to a channel + * + * @api private + */ + +Redis.prototype.subscribe = function (name, consumer, fn) { + this.sub.subscribe(name); + + if (consumer || fn) { + var self = this; + + self.sub.on('subscribe', function subscribe (ch) { + if (name == ch) { + function message (ch, msg) { + if (name == ch) { + msg = self.unpack(msg); + + // we check that the message consumed wasnt emitted by this node + if (self.nodeId != msg.nodeId) { + consumer.apply(null, msg.args); + } + } + }; + + self.sub.on('message', message); + + self.on('unsubscribe', function unsubscribe (ch) { + if (name == ch) { + self.sub.removeListener('message', message); + self.removeListener('unsubscribe', unsubscribe); + } + }); + + self.sub.removeListener('subscribe', subscribe); + + fn && fn(); + } + }); + } + + this.emit('subscribe', name, consumer, fn); +}; + +/** + * Unsubscribes + * + * @api private + */ + +Redis.prototype.unsubscribe = function (name, fn) { + this.sub.unsubscribe(name); + + if (fn) { + var client = this.sub; + + client.on('unsubscribe', function unsubscribe (ch) { + if (name == ch) { + fn(); + client.removeListener('unsubscribe', unsubscribe); + } + }); + } + + this.emit('unsubscribe', name, fn); +}; + +/** + * Destroys the store + * + * @api public + */ + +Redis.prototype.destroy = function () { + Store.prototype.destroy.call(this); + + this.pub.end(); + this.sub.end(); + this.cmd.end(); +}; + +/** + * Client constructor + * + * @api private + */ + +function Client (store, id) { + Store.Client.call(this, store, id); +}; + +/** + * Inherits from Store.Client + */ + +Client.prototype.__proto__ = Store.Client; + +/** + * Redis hash get + * + * @api private + */ + +Client.prototype.get = function (key, fn) { + this.store.cmd.hget(this.id, key, fn); + return this; +}; + +/** + * Redis hash set + * + * @api private + */ + +Client.prototype.set = function (key, value, fn) { + this.store.cmd.hset(this.id, key, value, fn); + return this; +}; + +/** + * Redis hash del + * + * @api private + */ + +Client.prototype.del = function (key, fn) { + this.store.cmd.hdel(this.id, key, fn); + return this; +}; + +/** + * Redis hash has + * + * @api private + */ + +Client.prototype.has = function (key, fn) { + this.store.cmd.hexists(this.id, key, function (err, has) { + if (err) return fn(err); + fn(null, !!has); + }); + return this; +}; + +/** + * Destroys client + * + * @param {Number} number of seconds to expire data + * @api private + */ + +Client.prototype.destroy = function (expiration) { + if ('number' != typeof expiration) { + this.store.cmd.del(this.id); + } else { + this.store.cmd.expire(this.id, expiration); + } + + return this; +}; diff --git a/node_modules/socket.io/lib/transport.js b/node_modules/socket.io/lib/transport.js new file mode 100644 index 0000000000..2e4c08bd43 --- /dev/null +++ b/node_modules/socket.io/lib/transport.js @@ -0,0 +1,534 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +var parser = require('./parser'); + +/** + * Expose the constructor. + */ + +exports = module.exports = Transport; + +/** + * Transport constructor. + * + * @api public + */ + +function Transport (mng, data, req) { + this.manager = mng; + this.id = data.id; + this.disconnected = false; + this.drained = true; + this.handleRequest(req); +}; + +/** + * Access the logger. + * + * @api public + */ + +Transport.prototype.__defineGetter__('log', function () { + return this.manager.log; +}); + +/** + * Access the store. + * + * @api public + */ + +Transport.prototype.__defineGetter__('store', function () { + return this.manager.store; +}); + +/** + * Handles a request when it's set. + * + * @api private + */ + +Transport.prototype.handleRequest = function (req) { + this.log.debug('setting request', req.method, req.url); + this.req = req; + + if (req.method == 'GET') { + this.socket = req.socket; + this.open = true; + this.drained = true; + this.setHeartbeatInterval(); + + this.setHandlers(); + this.onSocketConnect(); + } +}; + +/** + * Called when a connection is first set. + * + * @api private + */ + +Transport.prototype.onSocketConnect = function () { }; + +/** + * Sets transport handlers + * + * @api private + */ + +Transport.prototype.setHandlers = function () { + var self = this; + + // we need to do this in a pub/sub way since the client can POST the message + // over a different socket (ie: different Transport instance) + this.store.subscribe('heartbeat-clear:' + this.id, function () { + self.onHeartbeatClear(); + }); + + this.store.subscribe('disconnect-force:' + this.id, function () { + self.onForcedDisconnect(); + }); + + this.store.subscribe('dispatch:' + this.id, function (packet, volatile) { + self.onDispatch(packet, volatile); + }); + + this.bound = { + end: this.onSocketEnd.bind(this) + , close: this.onSocketClose.bind(this) + , error: this.onSocketError.bind(this) + , drain: this.onSocketDrain.bind(this) + }; + + this.socket.on('end', this.bound.end); + this.socket.on('close', this.bound.close); + this.socket.on('error', this.bound.error); + this.socket.on('drain', this.bound.drain); + + this.handlersSet = true; +}; + +/** + * Removes transport handlers + * + * @api private + */ + +Transport.prototype.clearHandlers = function () { + if (this.handlersSet) { + this.store.unsubscribe('disconnect-force:' + this.id); + this.store.unsubscribe('heartbeat-clear:' + this.id); + this.store.unsubscribe('dispatch:' + this.id); + + this.socket.removeListener('end', this.bound.end); + this.socket.removeListener('close', this.bound.close); + this.socket.removeListener('error', this.bound.error); + this.socket.removeListener('drain', this.bound.drain); + } +}; + +/** + * Called when the connection dies + * + * @api private + */ + +Transport.prototype.onSocketEnd = function () { + this.end('socket end'); +}; + +/** + * Called when the connection dies + * + * @api private + */ + +Transport.prototype.onSocketClose = function (error) { + this.end(error ? 'socket error' : 'socket close'); +}; + +/** + * Called when the connection has an error. + * + * @api private + */ + +Transport.prototype.onSocketError = function (err) { + if (this.open) { + this.socket.destroy(); + this.onClose(); + } + + this.log.info('socket error ' + err.stack); +}; + +/** + * Called when the connection is drained. + * + * @api private + */ + +Transport.prototype.onSocketDrain = function () { + this.drained = true; +}; + +/** + * Called upon receiving a heartbeat packet. + * + * @api private + */ + +Transport.prototype.onHeartbeatClear = function () { + this.clearHeartbeatTimeout(); + this.setHeartbeatInterval(); +}; + +/** + * Called upon a forced disconnection. + * + * @api private + */ + +Transport.prototype.onForcedDisconnect = function () { + if (!this.disconnected) { + this.log.info('transport end by forced client disconnection'); + if (this.open) { + this.packet({ type: 'disconnect' }); + } + this.end('booted'); + } +}; + +/** + * Dispatches a packet. + * + * @api private + */ + +Transport.prototype.onDispatch = function (packet, volatile) { + if (volatile) { + this.writeVolatile(packet); + } else { + this.write(packet); + } +}; + +/** + * Sets the close timeout. + */ + +Transport.prototype.setCloseTimeout = function () { + if (!this.closeTimeout) { + var self = this; + + this.closeTimeout = setTimeout(function () { + self.log.debug('fired close timeout for client', self.id); + self.closeTimeout = null; + self.end('close timeout'); + }, this.manager.get('close timeout') * 1000); + + this.log.debug('set close timeout for client', this.id); + } +}; + +/** + * Clears the close timeout. + */ + +Transport.prototype.clearCloseTimeout = function () { + if (this.closeTimeout) { + clearTimeout(this.closeTimeout); + this.closeTimeout = null; + + this.log.debug('cleared close timeout for client', this.id); + } +}; + +/** + * Sets the heartbeat timeout + */ + +Transport.prototype.setHeartbeatTimeout = function () { + if (!this.heartbeatTimeout && this.manager.enabled('heartbeats')) { + var self = this; + + this.heartbeatTimeout = setTimeout(function () { + self.log.debug('fired heartbeat timeout for client', self.id); + self.heartbeatTimeout = null; + self.end('heartbeat timeout'); + }, this.manager.get('heartbeat timeout') * 1000); + + this.log.debug('set heartbeat timeout for client', this.id); + } +}; + +/** + * Clears the heartbeat timeout + * + * @param text + */ + +Transport.prototype.clearHeartbeatTimeout = function () { + if (this.heartbeatTimeout && this.manager.enabled('heartbeats')) { + clearTimeout(this.heartbeatTimeout); + this.heartbeatTimeout = null; + this.log.debug('cleared heartbeat timeout for client', this.id); + } +}; + +/** + * Sets the heartbeat interval. To be called when a connection opens and when + * a heartbeat is received. + * + * @api private + */ + +Transport.prototype.setHeartbeatInterval = function () { + if (!this.heartbeatInterval && this.manager.enabled('heartbeats')) { + var self = this; + + this.heartbeatInterval = setTimeout(function () { + self.heartbeat(); + self.heartbeatInterval = null; + }, this.manager.get('heartbeat interval') * 1000); + + this.log.debug('set heartbeat interval for client', this.id); + } +}; + +/** + * Clears all timeouts. + * + * @api private + */ + +Transport.prototype.clearTimeouts = function () { + this.clearCloseTimeout(); + this.clearHeartbeatTimeout(); + this.clearHeartbeatInterval(); +}; + +/** + * Sends a heartbeat + * + * @api private + */ + +Transport.prototype.heartbeat = function () { + if (this.open) { + this.log.debug('emitting heartbeat for client', this.id); + this.packet({ type: 'heartbeat' }); + this.setHeartbeatTimeout(); + } + + return this; +}; + +/** + * Handles a message. + * + * @param {Object} packet object + * @api private + */ + +Transport.prototype.onMessage = function (packet) { + var current = this.manager.transports[this.id]; + + if ('heartbeat' == packet.type) { + this.log.debug('got heartbeat packet'); + + if (current && current.open) { + current.onHeartbeatClear(); + } else { + this.store.publish('heartbeat-clear:' + this.id); + } + } else { + if ('disconnect' == packet.type && packet.endpoint == '') { + this.log.debug('got disconnection packet'); + + if (current) { + current.onForcedDisconnect(); + } else { + this.store.publish('disconnect-force:' + this.id); + } + + return; + } + + if (packet.id && packet.ack != 'data') { + this.log.debug('acknowledging packet automatically'); + + var ack = parser.encodePacket({ + type: 'ack' + , ackId: packet.id + , endpoint: packet.endpoint || '' + }); + + if (current && current.open) { + current.onDispatch(ack); + } else { + this.manager.onClientDispatch(this.id, ack); + this.store.publish('dispatch:' + this.id, ack); + } + } + + // handle packet locally or publish it + if (current) { + this.manager.onClientMessage(this.id, packet); + } else { + this.store.publish('message:' + this.id, packet); + } + } +}; + +/** + * Clears the heartbeat interval + * + * @api private + */ + +Transport.prototype.clearHeartbeatInterval = function () { + if (this.heartbeatInterval && this.manager.enabled('heartbeats')) { + clearTimeout(this.heartbeatInterval); + this.heartbeatInterval = null; + this.log.debug('cleared heartbeat interval for client', this.id); + } +}; + +/** + * Finishes the connection and makes sure client doesn't reopen + * + * @api private + */ + +Transport.prototype.disconnect = function (reason) { + this.packet({ type: 'disconnect' }); + this.end(reason); + + return this; +}; + +/** + * Closes the connection. + * + * @api private + */ + +Transport.prototype.close = function () { + if (this.open) { + this.doClose(); + this.onClose(); + } +}; + +/** + * Called upon a connection close. + * + * @api private + */ + +Transport.prototype.onClose = function () { + if (this.open) { + this.setCloseTimeout(); + this.clearHandlers(); + this.open = false; + this.manager.onClose(this.id); + this.store.publish('close', this.id); + } +}; + +/** + * Cleans up the connection, considers the client disconnected. + * + * @api private + */ + +Transport.prototype.end = function (reason) { + if (!this.disconnected) { + this.log.info('transport end (' + reason + ')'); + + var local = this.manager.transports[this.id]; + + this.close(); + this.clearTimeouts(); + this.disconnected = true; + + if (local) { + this.manager.onClientDisconnect(this.id, reason, true); + } else { + this.store.publish('disconnect:' + this.id, reason); + } + } +}; + +/** + * Signals that the transport should pause and buffer data. + * + * @api public + */ + +Transport.prototype.discard = function () { + this.log.debug('discarding transport'); + this.discarded = true; + this.clearTimeouts(); + this.clearHandlers(); + + return this; +}; + +/** + * Writes an error packet with the specified reason and advice. + * + * @param {Number} advice + * @param {Number} reason + * @api public + */ + +Transport.prototype.error = function (reason, advice) { + this.packet({ + type: 'error' + , reason: reason + , advice: advice + }); + + this.log.warn(reason, advice ? ('client should ' + advice) : ''); + this.end('error'); +}; + +/** + * Write a packet. + * + * @api public + */ + +Transport.prototype.packet = function (obj) { + return this.write(parser.encodePacket(obj)); +}; + +/** + * Writes a volatile message. + * + * @api private + */ + +Transport.prototype.writeVolatile = function (msg) { + if (this.open) { + if (this.drained) { + this.write(msg); + } else { + this.log.debug('ignoring volatile packet, buffer not drained'); + } + } else { + this.log.debug('ignoring volatile packet, transport not open'); + } +}; diff --git a/node_modules/socket.io/lib/transports/flashsocket.js b/node_modules/socket.io/lib/transports/flashsocket.js new file mode 100644 index 0000000000..dc2d78b489 --- /dev/null +++ b/node_modules/socket.io/lib/transports/flashsocket.js @@ -0,0 +1,129 @@ +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ +var WebSocket = require('./websocket'); + +/** + * Export the constructor. + */ + +exports = module.exports = FlashSocket; + +/** + * The FlashSocket transport is just a proxy + * for WebSocket connections. + * + * @api public + */ + +function FlashSocket (mng, data, req) { + return WebSocket.call(this, mng, data, req); +} + +/** + * Inherits from WebSocket. + */ + +FlashSocket.prototype.__proto__ = WebSocket.prototype; + +/** + * Transport name + * + * @api public + */ + +FlashSocket.prototype.name = 'flashsocket'; + +/** + * Listens for new configuration changes of the Manager + * this way we can enable and disable the flash server. + * + * @param {Manager} Manager instance. + * @api private + */ + + +FlashSocket.init = function (manager) { + var server; + function create () { + + // Drop out immediately if the user has + // disabled the flash policy server + if (!manager.get('flash policy server')) { + return; + } + + server = require('policyfile').createServer({ + log: function(msg){ + manager.log.info(msg); + } + }, manager.get('origins')); + + server.on('close', function (e) { + server = null; + }); + + server.listen(manager.get('flash policy port'), manager.server); + + manager.flashPolicyServer = server; + } + + // listen for origin changes, so we can update the server + manager.on('set:origins', function (value, key) { + if (!server) return; + + // update the origins and compile a new response buffer + server.origins = Array.isArray(value) ? value : [value]; + server.compile(); + }); + + // destory the server and create a new server + manager.on('set:flash policy port', function (value, key) { + var transports = manager.get('transports'); + if (~transports.indexOf('flashsocket')) { + if (server) { + if (server.port === value) return; + // destroy the server and rebuild it on a new port + try { + server.close(); + } + catch (e) { /* ignore exception. could e.g. be that the server isn't started yet */ } + } + create(); + } + }); + + // create or destroy the server + manager.on('set:flash policy server', function (value, key) { + var transports = manager.get('transports'); + if (~transports.indexOf('flashsocket')) { + if (server && !value) { + // destroy the server + try { + server.close(); + } + catch (e) { /* ignore exception. could e.g. be that the server isn't started yet */ } + } + } else if (!server && value) { + // create the server + create(); + } + }); + + // only start the server + manager.on('set:transports', function (value, key){ + if (!server && ~manager.get('transports').indexOf('flashsocket')) { + create(); + } + }); + // check if we need to initialize at start + if (~manager.get('transports').indexOf('flashsocket')){ + create(); + } +}; diff --git a/node_modules/socket.io/lib/transports/htmlfile.js b/node_modules/socket.io/lib/transports/htmlfile.js new file mode 100644 index 0000000000..e8709a31ea --- /dev/null +++ b/node_modules/socket.io/lib/transports/htmlfile.js @@ -0,0 +1,82 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var HTTPTransport = require('./http'); + +/** + * Export the constructor. + */ + +exports = module.exports = HTMLFile; + +/** + * HTMLFile transport constructor. + * + * @api public + */ + +function HTMLFile (mng, data, req) { + HTTPTransport.call(this, mng, data, req); +}; + +/** + * Inherits from Transport. + */ + +HTMLFile.prototype.__proto__ = HTTPTransport.prototype; + +/** + * Transport name + * + * @api public + */ + +HTMLFile.prototype.name = 'htmlfile'; + +/** + * Handles the request. + * + * @api private + */ + +HTMLFile.prototype.handleRequest = function (req) { + HTTPTransport.prototype.handleRequest.call(this, req); + + if (req.method == 'GET') { + req.res.writeHead(200, { + 'Content-Type': 'text/html; charset=UTF-8' + , 'Connection': 'keep-alive' + , 'Transfer-Encoding': 'chunked' + }); + + req.res.write( + '' + + '' + + new Array(174).join(' ') + ); + } +}; + +/** + * Performs the write. + * + * @api private + */ + +HTMLFile.prototype.write = function (data) { + data = ''; + + if (this.response.write(data)) { + this.drained = true; + } + + this.log.debug(this.name + ' writing', data); +}; diff --git a/node_modules/socket.io/lib/transports/http-polling.js b/node_modules/socket.io/lib/transports/http-polling.js new file mode 100644 index 0000000000..89b7e0428b --- /dev/null +++ b/node_modules/socket.io/lib/transports/http-polling.js @@ -0,0 +1,147 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var HTTPTransport = require('./http'); + +/** + * Exports the constructor. + */ + +exports = module.exports = HTTPPolling; + +/** + * HTTP polling constructor. + * + * @api public. + */ + +function HTTPPolling (mng, data, req) { + HTTPTransport.call(this, mng, data, req); +}; + +/** + * Inherits from HTTPTransport. + * + * @api public. + */ + +HTTPPolling.prototype.__proto__ = HTTPTransport.prototype; + +/** + * Transport name + * + * @api public + */ + +HTTPPolling.prototype.name = 'httppolling'; + +/** + * Override setHandlers + * + * @api private + */ + +HTTPPolling.prototype.setHandlers = function () { + HTTPTransport.prototype.setHandlers.call(this); + this.socket.removeListener('end', this.bound.end); + this.socket.removeListener('close', this.bound.close); +}; + +/** + * Removes heartbeat timeouts for polling. + */ + +HTTPPolling.prototype.setHeartbeatInterval = function () { + return this; +}; + +/** + * Handles a request + * + * @api private + */ + +HTTPPolling.prototype.handleRequest = function (req) { + HTTPTransport.prototype.handleRequest.call(this, req); + + if (req.method == 'GET') { + var self = this; + + this.pollTimeout = setTimeout(function () { + self.packet({ type: 'noop' }); + self.log.debug(self.name + ' closed due to exceeded duration'); + }, this.manager.get('polling duration') * 1000); + + this.log.debug('setting poll timeout'); + } +}; + +/** + * Clears polling timeout + * + * @api private + */ + +HTTPPolling.prototype.clearPollTimeout = function () { + if (this.pollTimeout) { + clearTimeout(this.pollTimeout); + this.pollTimeout = null; + this.log.debug('clearing poll timeout'); + } + + return this; +}; + +/** + * Override clear timeouts to clear the poll timeout + * + * @api private + */ + +HTTPPolling.prototype.clearTimeouts = function () { + HTTPTransport.prototype.clearTimeouts.call(this); + + this.clearPollTimeout(); +}; + +/** + * doWrite to clear poll timeout + * + * @api private + */ + +HTTPPolling.prototype.doWrite = function () { + this.clearPollTimeout(); +}; + +/** + * Performs a write. + * + * @api private. + */ + +HTTPPolling.prototype.write = function (data, close) { + this.doWrite(data); + this.response.end(); + this.onClose(); +}; + +/** + * Override end. + * + * @api private + */ + +HTTPPolling.prototype.end = function (reason) { + this.clearPollTimeout(); + return HTTPTransport.prototype.end.call(this, reason); +}; + diff --git a/node_modules/socket.io/lib/transports/http.js b/node_modules/socket.io/lib/transports/http.js new file mode 100644 index 0000000000..28db794dfb --- /dev/null +++ b/node_modules/socket.io/lib/transports/http.js @@ -0,0 +1,121 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var Transport = require('../transport') + , parser = require('../parser') + , qs = require('querystring'); + +/** + * Export the constructor. + */ + +exports = module.exports = HTTPTransport; + +/** + * HTTP interface constructor. For all non-websocket transports. + * + * @api public + */ + +function HTTPTransport (mng, data, req) { + Transport.call(this, mng, data, req); +}; + +/** + * Inherits from Transport. + */ + +HTTPTransport.prototype.__proto__ = Transport.prototype; + +/** + * Handles a request. + * + * @api private + */ + +HTTPTransport.prototype.handleRequest = function (req) { + + // Always set the response in case an error is returned to the client + this.response = req.res; + + if (req.method == 'POST') { + var buffer = '' + , res = req.res + , origin = req.headers.origin + , headers = { 'Content-Length': 1, 'Content-Type': 'text/plain; charset=UTF-8' } + , self = this; + + req.on('data', function (data) { + buffer += data; + + if (Buffer.byteLength(buffer) >= self.manager.get('destroy buffer size')) { + buffer = ''; + req.connection.destroy(); + } + }); + + req.on('end', function () { + res.writeHead(200, headers); + res.end('1'); + + self.onData(self.postEncoded ? qs.parse(buffer).d : buffer); + }); + + // prevent memory leaks for uncompleted requests + req.on('close', function () { + buffer = ''; + self.onClose(); + }); + + if (origin) { + // https://developer.mozilla.org/En/HTTP_Access_Control + headers['Access-Control-Allow-Origin'] = origin; + headers['Access-Control-Allow-Credentials'] = 'true'; + } + } else { + Transport.prototype.handleRequest.call(this, req); + } +}; + +/** + * Handles data payload. + * + * @api private + */ + +HTTPTransport.prototype.onData = function (data) { + var messages = parser.decodePayload(data); + this.log.debug(this.name + ' received data packet', data); + + for (var i = 0, l = messages.length; i < l; i++) { + this.onMessage(messages[i]); + } +}; + +/** + * Closes the request-response cycle + * + * @api private + */ + +HTTPTransport.prototype.doClose = function () { + this.response.end(); +}; + +/** + * Writes a payload of messages + * + * @api private + */ + +HTTPTransport.prototype.payload = function (msgs) { + this.write(parser.encodePayload(msgs)); +}; diff --git a/node_modules/socket.io/lib/transports/index.js b/node_modules/socket.io/lib/transports/index.js new file mode 100644 index 0000000000..b86555940c --- /dev/null +++ b/node_modules/socket.io/lib/transports/index.js @@ -0,0 +1,12 @@ + +/** + * Export transports. + */ + +module.exports = { + websocket: require('./websocket') + , flashsocket: require('./flashsocket') + , htmlfile: require('./htmlfile') + , 'xhr-polling': require('./xhr-polling') + , 'jsonp-polling': require('./jsonp-polling') +}; diff --git a/node_modules/socket.io/lib/transports/jsonp-polling.js b/node_modules/socket.io/lib/transports/jsonp-polling.js new file mode 100644 index 0000000000..ad7d5aff3c --- /dev/null +++ b/node_modules/socket.io/lib/transports/jsonp-polling.js @@ -0,0 +1,97 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var HTTPPolling = require('./http-polling'); +var jsonpolling_re = /^\d+$/ + +/** + * Export the constructor. + */ + +exports = module.exports = JSONPPolling; + +/** + * JSON-P polling transport. + * + * @api public + */ + +function JSONPPolling (mng, data, req) { + HTTPPolling.call(this, mng, data, req); + + this.head = 'io.j[0]('; + this.foot = ');'; + + if (data.query.i && jsonpolling_re.test(data.query.i)) { + this.head = 'io.j[' + data.query.i + ']('; + } +}; + +/** + * Inherits from Transport. + */ + +JSONPPolling.prototype.__proto__ = HTTPPolling.prototype; + +/** + * Transport name + * + * @api public + */ + +JSONPPolling.prototype.name = 'jsonppolling'; + +/** + * Make sure POST are decoded. + */ + +JSONPPolling.prototype.postEncoded = true; + +/** + * Handles incoming data. + * Due to a bug in \n handling by browsers, we expect a JSONified string. + * + * @api private + */ + +JSONPPolling.prototype.onData = function (data) { + try { + data = JSON.parse(data); + } catch (e) { + this.error('parse', 'reconnect'); + return; + } + + HTTPPolling.prototype.onData.call(this, data); +}; + +/** + * Performs the write. + * + * @api private + */ + +JSONPPolling.prototype.doWrite = function (data) { + HTTPPolling.prototype.doWrite.call(this); + + var data = data === undefined + ? '' : this.head + JSON.stringify(data) + this.foot; + + this.response.writeHead(200, { + 'Content-Type': 'text/javascript; charset=UTF-8' + , 'Content-Length': Buffer.byteLength(data) + , 'Connection': 'Keep-Alive' + , 'X-XSS-Protection': '0' + }); + + this.response.write(data); + this.log.debug(this.name + ' writing', data); +}; diff --git a/node_modules/socket.io/lib/transports/websocket.js b/node_modules/socket.io/lib/transports/websocket.js new file mode 100644 index 0000000000..78a43043d5 --- /dev/null +++ b/node_modules/socket.io/lib/transports/websocket.js @@ -0,0 +1,36 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var protocolVersions = require('./websocket/'); + +/** + * Export the constructor. + */ + +exports = module.exports = WebSocket; + +/** + * HTTP interface constructor. Interface compatible with all transports that + * depend on request-response cycles. + * + * @api public + */ + +function WebSocket (mng, data, req) { + var transport + , version = req.headers['sec-websocket-version']; + if (typeof version !== 'undefined' && typeof protocolVersions[version] !== 'undefined') { + transport = new protocolVersions[version](mng, data, req); + } + else transport = new protocolVersions['default'](mng, data, req); + if (typeof this.name !== 'undefined') transport.name = this.name; + return transport; +}; diff --git a/node_modules/socket.io/lib/transports/websocket/default.js b/node_modules/socket.io/lib/transports/websocket/default.js new file mode 100644 index 0000000000..091fdd44fd --- /dev/null +++ b/node_modules/socket.io/lib/transports/websocket/default.js @@ -0,0 +1,362 @@ +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var Transport = require('../../transport') + , EventEmitter = process.EventEmitter + , crypto = require('crypto') + , parser = require('../../parser'); + +/** + * Export the constructor. + */ + +exports = module.exports = WebSocket; + +/** + * HTTP interface constructor. Interface compatible with all transports that + * depend on request-response cycles. + * + * @api public + */ + +function WebSocket (mng, data, req) { + // parser + var self = this; + + this.parser = new Parser(); + this.parser.on('data', function (packet) { + self.log.debug(self.name + ' received data packet', packet); + self.onMessage(parser.decodePacket(packet)); + }); + this.parser.on('close', function () { + self.end(); + }); + this.parser.on('error', function () { + self.end(); + }); + + Transport.call(this, mng, data, req); +}; + +/** + * Inherits from Transport. + */ + +WebSocket.prototype.__proto__ = Transport.prototype; + +/** + * Transport name + * + * @api public + */ + +WebSocket.prototype.name = 'websocket'; + +/** + * Websocket draft version + * + * @api public + */ + +WebSocket.prototype.protocolVersion = 'hixie-76'; + +/** + * Called when the socket connects. + * + * @api private + */ + +WebSocket.prototype.onSocketConnect = function () { + var self = this; + + this.socket.setNoDelay(true); + + this.buffer = true; + this.buffered = []; + + if (this.req.headers.upgrade !== 'WebSocket') { + this.log.warn(this.name + ' connection invalid'); + this.end(); + return; + } + + var origin = this.req.headers['origin'] + , waitingForNonce = false; + if(this.manager.settings['match origin protocol']){ + location = (origin.indexOf('https')>-1 ? 'wss' : 'ws') + '://' + this.req.headers.host + this.req.url; + }else if(this.socket.encrypted){ + location = 'wss://' + this.req.headers.host + this.req.url; + }else{ + location = 'ws://' + this.req.headers.host + this.req.url; + } + + if (this.req.headers['sec-websocket-key1']) { + // If we don't have the nonce yet, wait for it (HAProxy compatibility). + if (! (this.req.head && this.req.head.length >= 8)) { + waitingForNonce = true; + } + + var headers = [ + 'HTTP/1.1 101 WebSocket Protocol Handshake' + , 'Upgrade: WebSocket' + , 'Connection: Upgrade' + , 'Sec-WebSocket-Origin: ' + origin + , 'Sec-WebSocket-Location: ' + location + ]; + + if (this.req.headers['sec-websocket-protocol']){ + headers.push('Sec-WebSocket-Protocol: ' + + this.req.headers['sec-websocket-protocol']); + } + } else { + var headers = [ + 'HTTP/1.1 101 Web Socket Protocol Handshake' + , 'Upgrade: WebSocket' + , 'Connection: Upgrade' + , 'WebSocket-Origin: ' + origin + , 'WebSocket-Location: ' + location + ]; + } + + try { + this.socket.write(headers.concat('', '').join('\r\n')); + this.socket.setTimeout(0); + this.socket.setNoDelay(true); + this.socket.setEncoding('utf8'); + } catch (e) { + this.end(); + return; + } + + if (waitingForNonce) { + this.socket.setEncoding('binary'); + } else if (this.proveReception(headers)) { + self.flush(); + } + + var headBuffer = ''; + + this.socket.on('data', function (data) { + if (waitingForNonce) { + headBuffer += data; + + if (headBuffer.length < 8) { + return; + } + + // Restore the connection to utf8 encoding after receiving the nonce + self.socket.setEncoding('utf8'); + waitingForNonce = false; + + // Stuff the nonce into the location where it's expected to be + self.req.head = headBuffer.substr(0, 8); + headBuffer = ''; + + if (self.proveReception(headers)) { + self.flush(); + } + + return; + } + + self.parser.add(data); + }); +}; + +/** + * Writes to the socket. + * + * @api private + */ + +WebSocket.prototype.write = function (data) { + if (this.open) { + this.drained = false; + + if (this.buffer) { + this.buffered.push(data); + return this; + } + + var length = Buffer.byteLength(data) + , buffer = new Buffer(2 + length); + + buffer.write('\x00', 'binary'); + buffer.write(data, 1, 'utf8'); + buffer.write('\xff', 1 + length, 'binary'); + + try { + if (this.socket.write(buffer)) { + this.drained = true; + } + } catch (e) { + this.end(); + } + + this.log.debug(this.name + ' writing', data); + } +}; + +/** + * Flushes the internal buffer + * + * @api private + */ + +WebSocket.prototype.flush = function () { + this.buffer = false; + + for (var i = 0, l = this.buffered.length; i < l; i++) { + this.write(this.buffered.splice(0, 1)[0]); + } +}; + +/** + * Finishes the handshake. + * + * @api private + */ + +WebSocket.prototype.proveReception = function (headers) { + var self = this + , k1 = this.req.headers['sec-websocket-key1'] + , k2 = this.req.headers['sec-websocket-key2']; + + if (k1 && k2){ + var md5 = crypto.createHash('md5'); + + [k1, k2].forEach(function (k) { + var n = parseInt(k.replace(/[^\d]/g, '')) + , spaces = k.replace(/[^ ]/g, '').length; + + if (spaces === 0 || n % spaces !== 0){ + self.log.warn('Invalid ' + self.name + ' key: "' + k + '".'); + self.end(); + return false; + } + + n /= spaces; + + md5.update(String.fromCharCode( + n >> 24 & 0xFF, + n >> 16 & 0xFF, + n >> 8 & 0xFF, + n & 0xFF)); + }); + + md5.update(this.req.head.toString('binary')); + + try { + this.socket.write(md5.digest('binary'), 'binary'); + } catch (e) { + this.end(); + } + } + + return true; +}; + +/** + * Writes a payload. + * + * @api private + */ + +WebSocket.prototype.payload = function (msgs) { + for (var i = 0, l = msgs.length; i < l; i++) { + this.write(msgs[i]); + } + + return this; +}; + +/** + * Closes the connection. + * + * @api private + */ + +WebSocket.prototype.doClose = function () { + this.socket.end(); +}; + +/** + * WebSocket parser + * + * @api public + */ + +function Parser () { + this.buffer = ''; + this.i = 0; +}; + +/** + * Inherits from EventEmitter. + */ + +Parser.prototype.__proto__ = EventEmitter.prototype; + +/** + * Adds data to the buffer. + * + * @api public + */ + +Parser.prototype.add = function (data) { + this.buffer += data; + this.parse(); +}; + +/** + * Parses the buffer. + * + * @api private + */ + +Parser.prototype.parse = function () { + for (var i = this.i, chr, l = this.buffer.length; i < l; i++){ + chr = this.buffer[i]; + + if (this.buffer.length == 2 && this.buffer[1] == '\u0000') { + this.emit('close'); + this.buffer = ''; + this.i = 0; + return; + } + + if (i === 0){ + if (chr != '\u0000') + this.error('Bad framing. Expected null byte as first frame'); + else + continue; + } + + if (chr == '\ufffd'){ + this.emit('data', this.buffer.substr(1, i - 1)); + this.buffer = this.buffer.substr(i + 1); + this.i = 0; + return this.parse(); + } + } +}; + +/** + * Handles an error + * + * @api private + */ + +Parser.prototype.error = function (reason) { + this.buffer = ''; + this.i = 0; + this.emit('error', reason); + return this; +}; diff --git a/node_modules/socket.io/lib/transports/websocket/hybi-07-12.js b/node_modules/socket.io/lib/transports/websocket/hybi-07-12.js new file mode 100644 index 0000000000..44f666a543 --- /dev/null +++ b/node_modules/socket.io/lib/transports/websocket/hybi-07-12.js @@ -0,0 +1,622 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var Transport = require('../../transport') + , EventEmitter = process.EventEmitter + , crypto = require('crypto') + , url = require('url') + , parser = require('../../parser') + , util = require('../../util'); + +/** + * Export the constructor. + */ + +exports = module.exports = WebSocket; +exports.Parser = Parser; + +/** + * HTTP interface constructor. Interface compatible with all transports that + * depend on request-response cycles. + * + * @api public + */ + +function WebSocket (mng, data, req) { + // parser + var self = this; + + this.manager = mng; + this.parser = new Parser(); + this.parser.on('data', function (packet) { + self.onMessage(parser.decodePacket(packet)); + }); + this.parser.on('ping', function () { + // version 8 ping => pong + try { + self.socket.write('\u008a\u0000'); + } + catch (e) { + self.end(); + return; + } + }); + this.parser.on('close', function () { + self.end(); + }); + this.parser.on('error', function (reason) { + self.log.warn(self.name + ' parser error: ' + reason); + self.end(); + }); + + Transport.call(this, mng, data, req); +}; + +/** + * Inherits from Transport. + */ + +WebSocket.prototype.__proto__ = Transport.prototype; + +/** + * Transport name + * + * @api public + */ + +WebSocket.prototype.name = 'websocket'; + +/** + * Websocket draft version + * + * @api public + */ + +WebSocket.prototype.protocolVersion = '07-12'; + +/** + * Called when the socket connects. + * + * @api private + */ + +WebSocket.prototype.onSocketConnect = function () { + var self = this; + + if (typeof this.req.headers.upgrade === 'undefined' || + this.req.headers.upgrade.toLowerCase() !== 'websocket') { + this.log.warn(this.name + ' connection invalid'); + this.end(); + return; + } + + var origin = this.req.headers['sec-websocket-origin'] + , location = ((this.manager.settings['match origin protocol'] ? + origin.match(/^https/) : this.socket.encrypted) ? + 'wss' : 'ws') + + '://' + this.req.headers.host + this.req.url; + + if (!this.verifyOrigin(origin)) { + this.log.warn(this.name + ' connection invalid: origin mismatch'); + this.end(); + return; + } + + if (!this.req.headers['sec-websocket-key']) { + this.log.warn(this.name + ' connection invalid: received no key'); + this.end(); + return; + } + + // calc key + var key = this.req.headers['sec-websocket-key']; + var shasum = crypto.createHash('sha1'); + shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + key = shasum.digest('base64'); + + var headers = [ + 'HTTP/1.1 101 Switching Protocols' + , 'Upgrade: websocket' + , 'Connection: Upgrade' + , 'Sec-WebSocket-Accept: ' + key + ]; + + try { + this.socket.write(headers.concat('', '').join('\r\n')); + this.socket.setTimeout(0); + this.socket.setNoDelay(true); + } catch (e) { + this.end(); + return; + } + + this.socket.on('data', function (data) { + self.parser.add(data); + }); +}; + +/** + * Verifies the origin of a request. + * + * @api private + */ + +WebSocket.prototype.verifyOrigin = function (origin) { + var origins = this.manager.get('origins'); + + if (origin === 'null') origin = '*'; + + if (origins.indexOf('*:*') !== -1) { + return true; + } + + if (origin) { + try { + var parts = url.parse(origin); + parts.port = parts.port || 80; + var ok = + ~origins.indexOf(parts.hostname + ':' + parts.port) || + ~origins.indexOf(parts.hostname + ':*') || + ~origins.indexOf('*:' + parts.port); + if (!ok) this.log.warn('illegal origin: ' + origin); + return ok; + } catch (ex) { + this.log.warn('error parsing origin'); + } + } + else { + this.log.warn('origin missing from websocket call, yet required by config'); + } + return false; +}; + +/** + * Writes to the socket. + * + * @api private + */ + +WebSocket.prototype.write = function (data) { + if (this.open) { + var buf = this.frame(0x81, data); + try { + this.socket.write(buf, 'binary'); + } + catch (e) { + this.end(); + return; + } + this.log.debug(this.name + ' writing', data); + } +}; + +/** + * Writes a payload. + * + * @api private + */ + +WebSocket.prototype.payload = function (msgs) { + for (var i = 0, l = msgs.length; i < l; i++) { + this.write(msgs[i]); + } + + return this; +}; + +/** + * Frame server-to-client output as a text packet. + * + * @api private + */ + +WebSocket.prototype.frame = function (opcode, str) { + var dataBuffer = new Buffer(str) + , dataLength = dataBuffer.length + , startOffset = 2 + , secondByte = dataLength; + if (dataLength > 65536) { + startOffset = 10; + secondByte = 127; + } + else if (dataLength > 125) { + startOffset = 4; + secondByte = 126; + } + var outputBuffer = new Buffer(dataLength + startOffset); + outputBuffer[0] = opcode; + outputBuffer[1] = secondByte; + dataBuffer.copy(outputBuffer, startOffset); + switch (secondByte) { + case 126: + outputBuffer[2] = dataLength >>> 8; + outputBuffer[3] = dataLength % 256; + break; + case 127: + var l = dataLength; + for (var i = 1; i <= 8; ++i) { + outputBuffer[startOffset - i] = l & 0xff; + l >>>= 8; + } + } + return outputBuffer; +}; + +/** + * Closes the connection. + * + * @api private + */ + +WebSocket.prototype.doClose = function () { + this.socket.end(); +}; + +/** + * WebSocket parser + * + * @api public + */ + +function Parser () { + this.state = { + activeFragmentedOperation: null, + lastFragment: false, + masked: false, + opcode: 0 + }; + this.overflow = null; + this.expectOffset = 0; + this.expectBuffer = null; + this.expectHandler = null; + this.currentMessage = ''; + + var self = this; + this.opcodeHandlers = { + // text + '1': function(data) { + var finish = function(mask, data) { + self.currentMessage += self.unmask(mask, data); + if (self.state.lastFragment) { + self.emit('data', self.currentMessage); + self.currentMessage = ''; + } + self.endPacket(); + } + + var expectData = function(length) { + if (self.state.masked) { + self.expect('Mask', 4, function(data) { + var mask = data; + self.expect('Data', length, function(data) { + finish(mask, data); + }); + }); + } + else { + self.expect('Data', length, function(data) { + finish(null, data); + }); + } + } + + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength < 126) { + expectData(firstLength); + } + else if (firstLength == 126) { + self.expect('Length', 2, function(data) { + expectData(util.unpack(data)); + }); + } + else if (firstLength == 127) { + self.expect('Length', 8, function(data) { + if (util.unpack(data.slice(0, 4)) != 0) { + self.error('packets with length spanning more than 32 bit is currently not supported'); + return; + } + var lengthBytes = data.slice(4); // note: cap to 32 bit length + expectData(util.unpack(data)); + }); + } + }, + // binary + '2': function(data) { + var finish = function(mask, data) { + if (typeof self.currentMessage == 'string') self.currentMessage = []; // build a buffer list + self.currentMessage.push(self.unmask(mask, data, true)); + if (self.state.lastFragment) { + self.emit('binary', self.concatBuffers(self.currentMessage)); + self.currentMessage = ''; + } + self.endPacket(); + } + + var expectData = function(length) { + if (self.state.masked) { + self.expect('Mask', 4, function(data) { + var mask = data; + self.expect('Data', length, function(data) { + finish(mask, data); + }); + }); + } + else { + self.expect('Data', length, function(data) { + finish(null, data); + }); + } + } + + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength < 126) { + expectData(firstLength); + } + else if (firstLength == 126) { + self.expect('Length', 2, function(data) { + expectData(util.unpack(data)); + }); + } + else if (firstLength == 127) { + self.expect('Length', 8, function(data) { + if (util.unpack(data.slice(0, 4)) != 0) { + self.error('packets with length spanning more than 32 bit is currently not supported'); + return; + } + var lengthBytes = data.slice(4); // note: cap to 32 bit length + expectData(util.unpack(data)); + }); + } + }, + // close + '8': function(data) { + self.emit('close'); + self.reset(); + }, + // ping + '9': function(data) { + if (self.state.lastFragment == false) { + self.error('fragmented ping is not supported'); + return; + } + + var finish = function(mask, data) { + self.emit('ping', self.unmask(mask, data)); + self.endPacket(); + } + + var expectData = function(length) { + if (self.state.masked) { + self.expect('Mask', 4, function(data) { + var mask = data; + self.expect('Data', length, function(data) { + finish(mask, data); + }); + }); + } + else { + self.expect('Data', length, function(data) { + finish(null, data); + }); + } + } + + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength == 0) { + finish(null, null); + } + else if (firstLength < 126) { + expectData(firstLength); + } + else if (firstLength == 126) { + self.expect('Length', 2, function(data) { + expectData(util.unpack(data)); + }); + } + else if (firstLength == 127) { + self.expect('Length', 8, function(data) { + expectData(util.unpack(data)); + }); + } + } + } + + this.expect('Opcode', 2, this.processPacket); +}; + +/** + * Inherits from EventEmitter. + */ + +Parser.prototype.__proto__ = EventEmitter.prototype; + +/** + * Add new data to the parser. + * + * @api public + */ + +Parser.prototype.add = function(data) { + if (this.expectBuffer == null) { + this.addToOverflow(data); + return; + } + var toRead = Math.min(data.length, this.expectBuffer.length - this.expectOffset); + data.copy(this.expectBuffer, this.expectOffset, 0, toRead); + this.expectOffset += toRead; + if (toRead < data.length) { + // at this point the overflow buffer shouldn't at all exist + this.overflow = new Buffer(data.length - toRead); + data.copy(this.overflow, 0, toRead, toRead + this.overflow.length); + } + if (this.expectOffset == this.expectBuffer.length) { + var bufferForHandler = this.expectBuffer; + this.expectBuffer = null; + this.expectOffset = 0; + this.expectHandler.call(this, bufferForHandler); + } +} + +/** + * Adds a piece of data to the overflow. + * + * @api private + */ + +Parser.prototype.addToOverflow = function(data) { + if (this.overflow == null) this.overflow = data; + else { + var prevOverflow = this.overflow; + this.overflow = new Buffer(this.overflow.length + data.length); + prevOverflow.copy(this.overflow, 0); + data.copy(this.overflow, prevOverflow.length); + } +} + +/** + * Waits for a certain amount of bytes to be available, then fires a callback. + * + * @api private + */ + +Parser.prototype.expect = function(what, length, handler) { + this.expectBuffer = new Buffer(length); + this.expectOffset = 0; + this.expectHandler = handler; + if (this.overflow != null) { + var toOverflow = this.overflow; + this.overflow = null; + this.add(toOverflow); + } +} + +/** + * Start processing a new packet. + * + * @api private + */ + +Parser.prototype.processPacket = function (data) { + if ((data[0] & 0x70) != 0) { + this.error('reserved fields must be empty'); + } + this.state.lastFragment = (data[0] & 0x80) == 0x80; + this.state.masked = (data[1] & 0x80) == 0x80; + var opcode = data[0] & 0xf; + if (opcode == 0) { + // continuation frame + this.state.opcode = this.state.activeFragmentedOperation; + if (!(this.state.opcode == 1 || this.state.opcode == 2)) { + this.error('continuation frame cannot follow current opcode') + return; + } + } + else { + this.state.opcode = opcode; + if (this.state.lastFragment === false) { + this.state.activeFragmentedOperation = opcode; + } + } + var handler = this.opcodeHandlers[this.state.opcode]; + if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode); + else handler(data); +} + +/** + * Endprocessing a packet. + * + * @api private + */ + +Parser.prototype.endPacket = function() { + this.expectOffset = 0; + this.expectBuffer = null; + this.expectHandler = null; + if (this.state.lastFragment && this.state.opcode == this.state.activeFragmentedOperation) { + // end current fragmented operation + this.state.activeFragmentedOperation = null; + } + this.state.lastFragment = false; + this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0; + this.state.masked = false; + this.expect('Opcode', 2, this.processPacket); +} + +/** + * Reset the parser state. + * + * @api private + */ + +Parser.prototype.reset = function() { + this.state = { + activeFragmentedOperation: null, + lastFragment: false, + masked: false, + opcode: 0 + }; + this.expectOffset = 0; + this.expectBuffer = null; + this.expectHandler = null; + this.overflow = null; + this.currentMessage = ''; +} + +/** + * Unmask received data. + * + * @api private + */ + +Parser.prototype.unmask = function (mask, buf, binary) { + if (mask != null) { + for (var i = 0, ll = buf.length; i < ll; i++) { + buf[i] ^= mask[i % 4]; + } + } + if (binary) return buf; + return buf != null ? buf.toString('utf8') : ''; +} + +/** + * Concatenates a list of buffers. + * + * @api private + */ + +Parser.prototype.concatBuffers = function(buffers) { + var length = 0; + for (var i = 0, l = buffers.length; i < l; ++i) { + length += buffers[i].length; + } + var mergedBuffer = new Buffer(length); + var offset = 0; + for (var i = 0, l = buffers.length; i < l; ++i) { + buffers[i].copy(mergedBuffer, offset); + offset += buffers[i].length; + } + return mergedBuffer; +} + +/** + * Handles an error + * + * @api private + */ + +Parser.prototype.error = function (reason) { + this.reset(); + this.emit('error', reason); + return this; +}; diff --git a/node_modules/socket.io/lib/transports/websocket/hybi-16.js b/node_modules/socket.io/lib/transports/websocket/hybi-16.js new file mode 100644 index 0000000000..69967dad28 --- /dev/null +++ b/node_modules/socket.io/lib/transports/websocket/hybi-16.js @@ -0,0 +1,622 @@ +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var Transport = require('../../transport') + , EventEmitter = process.EventEmitter + , crypto = require('crypto') + , url = require('url') + , parser = require('../../parser') + , util = require('../../util'); + +/** + * Export the constructor. + */ + +exports = module.exports = WebSocket; +exports.Parser = Parser; + +/** + * HTTP interface constructor. Interface compatible with all transports that + * depend on request-response cycles. + * + * @api public + */ + +function WebSocket (mng, data, req) { + // parser + var self = this; + + this.manager = mng; + this.parser = new Parser(); + this.parser.on('data', function (packet) { + self.onMessage(parser.decodePacket(packet)); + }); + this.parser.on('ping', function () { + // version 8 ping => pong + try { + self.socket.write('\u008a\u0000'); + } + catch (e) { + self.end(); + return; + } + }); + this.parser.on('close', function () { + self.end(); + }); + this.parser.on('error', function (reason) { + self.log.warn(self.name + ' parser error: ' + reason); + self.end(); + }); + + Transport.call(this, mng, data, req); +}; + +/** + * Inherits from Transport. + */ + +WebSocket.prototype.__proto__ = Transport.prototype; + +/** + * Transport name + * + * @api public + */ + +WebSocket.prototype.name = 'websocket'; + +/** + * Websocket draft version + * + * @api public + */ + +WebSocket.prototype.protocolVersion = '16'; + +/** + * Called when the socket connects. + * + * @api private + */ + +WebSocket.prototype.onSocketConnect = function () { + var self = this; + + if (typeof this.req.headers.upgrade === 'undefined' || + this.req.headers.upgrade.toLowerCase() !== 'websocket') { + this.log.warn(this.name + ' connection invalid'); + this.end(); + return; + } + + var origin = this.req.headers['origin'] || '' + , location = ((this.manager.settings['match origin protocol'] ? + origin.match(/^https/) : this.socket.encrypted) ? + 'wss' : 'ws') + + '://' + this.req.headers.host + this.req.url; + + if (!this.verifyOrigin(origin)) { + this.log.warn(this.name + ' connection invalid: origin mismatch'); + this.end(); + return; + } + + if (!this.req.headers['sec-websocket-key']) { + this.log.warn(this.name + ' connection invalid: received no key'); + this.end(); + return; + } + + // calc key + var key = this.req.headers['sec-websocket-key']; + var shasum = crypto.createHash('sha1'); + shasum.update(key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"); + key = shasum.digest('base64'); + + var headers = [ + 'HTTP/1.1 101 Switching Protocols' + , 'Upgrade: websocket' + , 'Connection: Upgrade' + , 'Sec-WebSocket-Accept: ' + key + ]; + + try { + this.socket.write(headers.concat('', '').join('\r\n')); + this.socket.setTimeout(0); + this.socket.setNoDelay(true); + } catch (e) { + this.end(); + return; + } + + this.socket.on('data', function (data) { + self.parser.add(data); + }); +}; + +/** + * Verifies the origin of a request. + * + * @api private + */ + +WebSocket.prototype.verifyOrigin = function (origin) { + var origins = this.manager.get('origins'); + + if (origin === 'null') origin = '*'; + + if (origins.indexOf('*:*') !== -1) { + return true; + } + + if (origin) { + try { + var parts = url.parse(origin); + parts.port = parts.port || 80; + var ok = + ~origins.indexOf(parts.hostname + ':' + parts.port) || + ~origins.indexOf(parts.hostname + ':*') || + ~origins.indexOf('*:' + parts.port); + if (!ok) this.log.warn('illegal origin: ' + origin); + return ok; + } catch (ex) { + this.log.warn('error parsing origin'); + } + } + else { + this.log.warn('origin missing from websocket call, yet required by config'); + } + return false; +}; + +/** + * Writes to the socket. + * + * @api private + */ + +WebSocket.prototype.write = function (data) { + if (this.open) { + var buf = this.frame(0x81, data); + try { + this.socket.write(buf, 'binary'); + } + catch (e) { + this.end(); + return; + } + this.log.debug(this.name + ' writing', data); + } +}; + +/** + * Writes a payload. + * + * @api private + */ + +WebSocket.prototype.payload = function (msgs) { + for (var i = 0, l = msgs.length; i < l; i++) { + this.write(msgs[i]); + } + + return this; +}; + +/** + * Frame server-to-client output as a text packet. + * + * @api private + */ + +WebSocket.prototype.frame = function (opcode, str) { + var dataBuffer = new Buffer(str) + , dataLength = dataBuffer.length + , startOffset = 2 + , secondByte = dataLength; + if (dataLength > 65536) { + startOffset = 10; + secondByte = 127; + } + else if (dataLength > 125) { + startOffset = 4; + secondByte = 126; + } + var outputBuffer = new Buffer(dataLength + startOffset); + outputBuffer[0] = opcode; + outputBuffer[1] = secondByte; + dataBuffer.copy(outputBuffer, startOffset); + switch (secondByte) { + case 126: + outputBuffer[2] = dataLength >>> 8; + outputBuffer[3] = dataLength % 256; + break; + case 127: + var l = dataLength; + for (var i = 1; i <= 8; ++i) { + outputBuffer[startOffset - i] = l & 0xff; + l >>>= 8; + } + } + return outputBuffer; +}; + +/** + * Closes the connection. + * + * @api private + */ + +WebSocket.prototype.doClose = function () { + this.socket.end(); +}; + +/** + * WebSocket parser + * + * @api public + */ + +function Parser () { + this.state = { + activeFragmentedOperation: null, + lastFragment: false, + masked: false, + opcode: 0 + }; + this.overflow = null; + this.expectOffset = 0; + this.expectBuffer = null; + this.expectHandler = null; + this.currentMessage = ''; + + var self = this; + this.opcodeHandlers = { + // text + '1': function(data) { + var finish = function(mask, data) { + self.currentMessage += self.unmask(mask, data); + if (self.state.lastFragment) { + self.emit('data', self.currentMessage); + self.currentMessage = ''; + } + self.endPacket(); + } + + var expectData = function(length) { + if (self.state.masked) { + self.expect('Mask', 4, function(data) { + var mask = data; + self.expect('Data', length, function(data) { + finish(mask, data); + }); + }); + } + else { + self.expect('Data', length, function(data) { + finish(null, data); + }); + } + } + + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength < 126) { + expectData(firstLength); + } + else if (firstLength == 126) { + self.expect('Length', 2, function(data) { + expectData(util.unpack(data)); + }); + } + else if (firstLength == 127) { + self.expect('Length', 8, function(data) { + if (util.unpack(data.slice(0, 4)) != 0) { + self.error('packets with length spanning more than 32 bit is currently not supported'); + return; + } + var lengthBytes = data.slice(4); // note: cap to 32 bit length + expectData(util.unpack(data)); + }); + } + }, + // binary + '2': function(data) { + var finish = function(mask, data) { + if (typeof self.currentMessage == 'string') self.currentMessage = []; // build a buffer list + self.currentMessage.push(self.unmask(mask, data, true)); + if (self.state.lastFragment) { + self.emit('binary', self.concatBuffers(self.currentMessage)); + self.currentMessage = ''; + } + self.endPacket(); + } + + var expectData = function(length) { + if (self.state.masked) { + self.expect('Mask', 4, function(data) { + var mask = data; + self.expect('Data', length, function(data) { + finish(mask, data); + }); + }); + } + else { + self.expect('Data', length, function(data) { + finish(null, data); + }); + } + } + + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength < 126) { + expectData(firstLength); + } + else if (firstLength == 126) { + self.expect('Length', 2, function(data) { + expectData(util.unpack(data)); + }); + } + else if (firstLength == 127) { + self.expect('Length', 8, function(data) { + if (util.unpack(data.slice(0, 4)) != 0) { + self.error('packets with length spanning more than 32 bit is currently not supported'); + return; + } + var lengthBytes = data.slice(4); // note: cap to 32 bit length + expectData(util.unpack(data)); + }); + } + }, + // close + '8': function(data) { + self.emit('close'); + self.reset(); + }, + // ping + '9': function(data) { + if (self.state.lastFragment == false) { + self.error('fragmented ping is not supported'); + return; + } + + var finish = function(mask, data) { + self.emit('ping', self.unmask(mask, data)); + self.endPacket(); + } + + var expectData = function(length) { + if (self.state.masked) { + self.expect('Mask', 4, function(data) { + var mask = data; + self.expect('Data', length, function(data) { + finish(mask, data); + }); + }); + } + else { + self.expect('Data', length, function(data) { + finish(null, data); + }); + } + } + + // decode length + var firstLength = data[1] & 0x7f; + if (firstLength == 0) { + finish(null, null); + } + else if (firstLength < 126) { + expectData(firstLength); + } + else if (firstLength == 126) { + self.expect('Length', 2, function(data) { + expectData(util.unpack(data)); + }); + } + else if (firstLength == 127) { + self.expect('Length', 8, function(data) { + expectData(util.unpack(data)); + }); + } + } + } + + this.expect('Opcode', 2, this.processPacket); +}; + +/** + * Inherits from EventEmitter. + */ + +Parser.prototype.__proto__ = EventEmitter.prototype; + +/** + * Add new data to the parser. + * + * @api public + */ + +Parser.prototype.add = function(data) { + if (this.expectBuffer == null) { + this.addToOverflow(data); + return; + } + var toRead = Math.min(data.length, this.expectBuffer.length - this.expectOffset); + data.copy(this.expectBuffer, this.expectOffset, 0, toRead); + this.expectOffset += toRead; + if (toRead < data.length) { + // at this point the overflow buffer shouldn't at all exist + this.overflow = new Buffer(data.length - toRead); + data.copy(this.overflow, 0, toRead, toRead + this.overflow.length); + } + if (this.expectOffset == this.expectBuffer.length) { + var bufferForHandler = this.expectBuffer; + this.expectBuffer = null; + this.expectOffset = 0; + this.expectHandler.call(this, bufferForHandler); + } +} + +/** + * Adds a piece of data to the overflow. + * + * @api private + */ + +Parser.prototype.addToOverflow = function(data) { + if (this.overflow == null) this.overflow = data; + else { + var prevOverflow = this.overflow; + this.overflow = new Buffer(this.overflow.length + data.length); + prevOverflow.copy(this.overflow, 0); + data.copy(this.overflow, prevOverflow.length); + } +} + +/** + * Waits for a certain amount of bytes to be available, then fires a callback. + * + * @api private + */ + +Parser.prototype.expect = function(what, length, handler) { + this.expectBuffer = new Buffer(length); + this.expectOffset = 0; + this.expectHandler = handler; + if (this.overflow != null) { + var toOverflow = this.overflow; + this.overflow = null; + this.add(toOverflow); + } +} + +/** + * Start processing a new packet. + * + * @api private + */ + +Parser.prototype.processPacket = function (data) { + if ((data[0] & 0x70) != 0) { + this.error('reserved fields must be empty'); + return; + } + this.state.lastFragment = (data[0] & 0x80) == 0x80; + this.state.masked = (data[1] & 0x80) == 0x80; + var opcode = data[0] & 0xf; + if (opcode == 0) { + // continuation frame + this.state.opcode = this.state.activeFragmentedOperation; + if (!(this.state.opcode == 1 || this.state.opcode == 2)) { + this.error('continuation frame cannot follow current opcode') + return; + } + } + else { + this.state.opcode = opcode; + if (this.state.lastFragment === false) { + this.state.activeFragmentedOperation = opcode; + } + } + var handler = this.opcodeHandlers[this.state.opcode]; + if (typeof handler == 'undefined') this.error('no handler for opcode ' + this.state.opcode); + else handler(data); +} + +/** + * Endprocessing a packet. + * + * @api private + */ + +Parser.prototype.endPacket = function() { + this.expectOffset = 0; + this.expectBuffer = null; + this.expectHandler = null; + if (this.state.lastFragment && this.state.opcode == this.state.activeFragmentedOperation) { + // end current fragmented operation + this.state.activeFragmentedOperation = null; + } + this.state.lastFragment = false; + this.state.opcode = this.state.activeFragmentedOperation != null ? this.state.activeFragmentedOperation : 0; + this.state.masked = false; + this.expect('Opcode', 2, this.processPacket); +} + +/** + * Reset the parser state. + * + * @api private + */ + +Parser.prototype.reset = function() { + this.state = { + activeFragmentedOperation: null, + lastFragment: false, + masked: false, + opcode: 0 + }; + this.expectOffset = 0; + this.expectBuffer = null; + this.expectHandler = null; + this.overflow = null; + this.currentMessage = ''; +} + +/** + * Unmask received data. + * + * @api private + */ + +Parser.prototype.unmask = function (mask, buf, binary) { + if (mask != null) { + for (var i = 0, ll = buf.length; i < ll; i++) { + buf[i] ^= mask[i % 4]; + } + } + if (binary) return buf; + return buf != null ? buf.toString('utf8') : ''; +} + +/** + * Concatenates a list of buffers. + * + * @api private + */ + +Parser.prototype.concatBuffers = function(buffers) { + var length = 0; + for (var i = 0, l = buffers.length; i < l; ++i) { + length += buffers[i].length; + } + var mergedBuffer = new Buffer(length); + var offset = 0; + for (var i = 0, l = buffers.length; i < l; ++i) { + buffers[i].copy(mergedBuffer, offset); + offset += buffers[i].length; + } + return mergedBuffer; +} + +/** + * Handles an error + * + * @api private + */ + +Parser.prototype.error = function (reason) { + this.reset(); + this.emit('error', reason); + return this; +}; diff --git a/node_modules/socket.io/lib/transports/websocket/index.js b/node_modules/socket.io/lib/transports/websocket/index.js new file mode 100644 index 0000000000..3a952b7598 --- /dev/null +++ b/node_modules/socket.io/lib/transports/websocket/index.js @@ -0,0 +1,11 @@ + +/** + * Export websocket versions. + */ + +module.exports = { + 7: require('./hybi-07-12'), + 8: require('./hybi-07-12'), + 13: require('./hybi-16'), + default: require('./default') +}; diff --git a/node_modules/socket.io/lib/transports/xhr-polling.js b/node_modules/socket.io/lib/transports/xhr-polling.js new file mode 100644 index 0000000000..1db5aeee0f --- /dev/null +++ b/node_modules/socket.io/lib/transports/xhr-polling.js @@ -0,0 +1,69 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module requirements. + */ + +var HTTPPolling = require('./http-polling'); + +/** + * Export the constructor. + */ + +exports = module.exports = XHRPolling; + +/** + * Ajax polling transport. + * + * @api public + */ + +function XHRPolling (mng, data, req) { + HTTPPolling.call(this, mng, data, req); +}; + +/** + * Inherits from Transport. + */ + +XHRPolling.prototype.__proto__ = HTTPPolling.prototype; + +/** + * Transport name + * + * @api public + */ + +XHRPolling.prototype.name = 'xhr-polling'; + +/** + * Frames data prior to write. + * + * @api private + */ + +XHRPolling.prototype.doWrite = function (data) { + HTTPPolling.prototype.doWrite.call(this); + + var origin = this.req.headers.origin + , headers = { + 'Content-Type': 'text/plain; charset=UTF-8' + , 'Content-Length': data === undefined ? 0 : Buffer.byteLength(data) + , 'Connection': 'Keep-Alive' + }; + + if (origin) { + // https://developer.mozilla.org/En/HTTP_Access_Control + headers['Access-Control-Allow-Origin'] = origin; + headers['Access-Control-Allow-Credentials'] = 'true'; + } + + this.response.writeHead(200, headers); + this.response.write(data); + this.log.debug(this.name + ' writing', data); +}; diff --git a/node_modules/socket.io/lib/util.js b/node_modules/socket.io/lib/util.js new file mode 100644 index 0000000000..f7d9f2b4b4 --- /dev/null +++ b/node_modules/socket.io/lib/util.js @@ -0,0 +1,50 @@ + +/*! + * socket.io-node + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Module dependencies. + */ + +/** + * Converts an enumerable to an array. + * + * @api public + */ + +exports.toArray = function (enu) { + var arr = []; + + for (var i = 0, l = enu.length; i < l; i++) + arr.push(enu[i]); + + return arr; +}; + +/** + * Unpacks a buffer to a number. + * + * @api public + */ + +exports.unpack = function (buffer) { + var n = 0; + for (var i = 0; i < buffer.length; ++i) { + n = (i == 0) ? buffer[i] : (n * 256) + buffer[i]; + } + return n; +} + +/** + * Left pads a string. + * + * @api public + */ + +exports.padl = function (s,n,c) { + return new Array(1 + n - s.length).join(c) + s; +} + diff --git a/node_modules/socket.io/package.json b/node_modules/socket.io/package.json new file mode 100644 index 0000000000..b474ba7784 --- /dev/null +++ b/node_modules/socket.io/package.json @@ -0,0 +1,72 @@ +{ + "name": "socket.io", + "version": "0.9.14", + "description": "Real-time apps made cross-browser & easy with a WebSocket-like API", + "homepage": "http://socket.io", + "keywords": [ + "websocket", + "socket", + "realtime", + "socket.io", + "comet", + "ajax" + ], + "author": { + "name": "Guillermo Rauch", + "email": "guillermo@learnboost.com" + }, + "contributors": [ + { + "name": "Guillermo Rauch", + "email": "rauchg@gmail.com" + }, + { + "name": "Arnout Kazemier", + "email": "info@3rd-eden.com" + }, + { + "name": "Vladimir Dronnikov", + "email": "dronnikov@gmail.com" + }, + { + "name": "Einar Otto Stangvik", + "email": "einaros@gmail.com" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/LearnBoost/socket.io.git" + }, + "dependencies": { + "socket.io-client": "0.9.11", + "policyfile": "0.0.4", + "base64id": "0.1.0", + "redis": "0.7.3" + }, + "devDependencies": { + "expresso": "0.9.2", + "should": "*", + "benchmark": "0.2.2", + "microtime": "0.1.3-1", + "colors": "0.5.1" + }, + "optionalDependencies": { + "redis": "0.7.3" + }, + "main": "index", + "engines": { + "node": ">= 0.4.0" + }, + "scripts": { + "test": "make test" + }, + "_id": "socket.io@0.9.14", + "_engineSupported": true, + "_npmVersion": "1.1.4", + "_nodeVersion": "v0.6.12", + "_defaultsLoaded": true, + "dist": { + "shasum": "b865c2da3ae43df4f9933dc6f37e01e84a2fd1c6" + }, + "_from": "socket.io@0.9.14" +} diff --git a/package.json b/package.json new file mode 100644 index 0000000000..529a3439b6 --- /dev/null +++ b/package.json @@ -0,0 +1,22 @@ +{ + "author": "psychobunny ", + "name": "nodeforum", + "description": "nodeforum dev", + "version": "0.0.1", + "homepage": "http://www.designcreateplay.com", + "repository": { + "url": "" + }, + "main": "app.js", + "dependencies": { + "socket.io": "0.9.14", + "redis": "0.8.3", + "express": "3.2.0", + "connect": "2.7.6" + }, + "devDependencies": {}, + "optionalDependencies": {}, + "engines": { + "node": "*" + } +} diff --git a/public/oldindex.html b/public/oldindex.html new file mode 100644 index 0000000000..31528a03cd --- /dev/null +++ b/public/oldindex.html @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + +
    + Home +
    + + \ No newline at end of file diff --git a/public/socket.io/WebSocketMain.swf b/public/socket.io/WebSocketMain.swf new file mode 100644 index 0000000000000000000000000000000000000000..20a451f57ba342d27b1a485d2e3931ea79ed988b GIT binary patch literal 175830 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 z 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 literal 0 HcmV?d00001 diff --git a/public/socket.io/WebSocketMainInsecure.swf b/public/socket.io/WebSocketMainInsecure.swf new file mode 100644 index 0000000000000000000000000000000000000000..5949ff3d09e4dc669cb25144fa5775dba071b684 GIT binary patch literal 175953 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

    6_4TzBh#=34(fx0_pa7FIaK3=>Pm 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±k#VeO;J63S6QrHJIUFH4_gkHxsAIz7p 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 LboTr|Wyc*1(*E2! literal 0 HcmV?d00001 diff --git a/public/socket.io/socket.io.js b/public/socket.io/socket.io.js new file mode 100644 index 0000000000..6ea5dd0a21 --- /dev/null +++ b/public/socket.io/socket.io.js @@ -0,0 +1,3871 @@ +/*! Socket.IO.js build:0.9.11, development. Copyright(c) 2011 LearnBoost MIT Licensed */ + +var io = ('undefined' === typeof module ? {} : module.exports); +(function() { + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, global) { + + /** + * IO namespace. + * + * @namespace + */ + + var io = exports; + + /** + * Socket.IO version + * + * @api public + */ + + io.version = '0.9.11'; + + /** + * Protocol implemented. + * + * @api public + */ + + io.protocol = 1; + + /** + * Available transports, these will be populated with the available transports + * + * @api public + */ + + io.transports = []; + + /** + * Keep track of jsonp callbacks. + * + * @api private + */ + + io.j = []; + + /** + * Keep track of our io.Sockets + * + * @api private + */ + io.sockets = {}; + + + /** + * Manages connections to hosts. + * + * @param {String} uri + * @Param {Boolean} force creation of new socket (defaults to false) + * @api public + */ + + io.connect = function (host, details) { + var uri = io.util.parseUri(host) + , uuri + , socket; + + if (global && global.location) { + uri.protocol = uri.protocol || global.location.protocol.slice(0, -1); + uri.host = uri.host || (global.document + ? global.document.domain : global.location.hostname); + uri.port = uri.port || global.location.port; + } + + uuri = io.util.uniqueUri(uri); + + var options = { + host: uri.host + , secure: 'https' == uri.protocol + , port: uri.port || ('https' == uri.protocol ? 443 : 80) + , query: uri.query || '' + }; + + io.util.merge(options, details); + + if (options['force new connection'] || !io.sockets[uuri]) { + socket = new io.Socket(options); + } + + if (!options['force new connection'] && socket) { + io.sockets[uuri] = socket; + } + + socket = socket || io.sockets[uuri]; + + // if path is different from '' or / + return socket.of(uri.path.length > 1 ? uri.path : ''); + }; + +})('object' === typeof module ? module.exports : (this.io = {}), this); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, global) { + + /** + * Utilities namespace. + * + * @namespace + */ + + var util = exports.util = {}; + + /** + * Parses an URI + * + * @author Steven Levithan (MIT license) + * @api public + */ + + var re = /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/; + + var parts = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', + 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', + 'anchor']; + + util.parseUri = function (str) { + var m = re.exec(str || '') + , uri = {} + , i = 14; + + while (i--) { + uri[parts[i]] = m[i] || ''; + } + + return uri; + }; + + /** + * Produces a unique url that identifies a Socket.IO connection. + * + * @param {Object} uri + * @api public + */ + + util.uniqueUri = function (uri) { + var protocol = uri.protocol + , host = uri.host + , port = uri.port; + + if ('document' in global) { + host = host || document.domain; + port = port || (protocol == 'https' + && document.location.protocol !== 'https:' ? 443 : document.location.port); + } else { + host = host || 'localhost'; + + if (!port && protocol == 'https') { + port = 443; + } + } + + return (protocol || 'http') + '://' + host + ':' + (port || 80); + }; + + /** + * Mergest 2 query strings in to once unique query string + * + * @param {String} base + * @param {String} addition + * @api public + */ + + util.query = function (base, addition) { + var query = util.chunkQuery(base || '') + , components = []; + + util.merge(query, util.chunkQuery(addition || '')); + for (var part in query) { + if (query.hasOwnProperty(part)) { + components.push(part + '=' + query[part]); + } + } + + return components.length ? '?' + components.join('&') : ''; + }; + + /** + * Transforms a querystring in to an object + * + * @param {String} qs + * @api public + */ + + util.chunkQuery = function (qs) { + var query = {} + , params = qs.split('&') + , i = 0 + , l = params.length + , kv; + + for (; i < l; ++i) { + kv = params[i].split('='); + if (kv[0]) { + query[kv[0]] = kv[1]; + } + } + + return query; + }; + + /** + * Executes the given function when the page is loaded. + * + * io.util.load(function () { console.log('page loaded'); }); + * + * @param {Function} fn + * @api public + */ + + var pageLoaded = false; + + util.load = function (fn) { + if ('document' in global && document.readyState === 'complete' || pageLoaded) { + return fn(); + } + + util.on(global, 'load', fn, false); + }; + + /** + * Adds an event. + * + * @api private + */ + + util.on = function (element, event, fn, capture) { + if (element.attachEvent) { + element.attachEvent('on' + event, fn); + } else if (element.addEventListener) { + element.addEventListener(event, fn, capture); + } + }; + + /** + * Generates the correct `XMLHttpRequest` for regular and cross domain requests. + * + * @param {Boolean} [xdomain] Create a request that can be used cross domain. + * @returns {XMLHttpRequest|false} If we can create a XMLHttpRequest. + * @api private + */ + + util.request = function (xdomain) { + + if (xdomain && 'undefined' != typeof XDomainRequest && !util.ua.hasCORS) { + return new XDomainRequest(); + } + + if ('undefined' != typeof XMLHttpRequest && (!xdomain || util.ua.hasCORS)) { + return new XMLHttpRequest(); + } + + if (!xdomain) { + try { + return new window[(['Active'].concat('Object').join('X'))]('Microsoft.XMLHTTP'); + } catch(e) { } + } + + return null; + }; + + /** + * XHR based transport constructor. + * + * @constructor + * @api public + */ + + /** + * Change the internal pageLoaded value. + */ + + if ('undefined' != typeof window) { + util.load(function () { + pageLoaded = true; + }); + } + + /** + * Defers a function to ensure a spinner is not displayed by the browser + * + * @param {Function} fn + * @api public + */ + + util.defer = function (fn) { + if (!util.ua.webkit || 'undefined' != typeof importScripts) { + return fn(); + } + + util.load(function () { + setTimeout(fn, 100); + }); + }; + + /** + * Merges two objects. + * + * @api public + */ + + util.merge = function merge (target, additional, deep, lastseen) { + var seen = lastseen || [] + , depth = typeof deep == 'undefined' ? 2 : deep + , prop; + + for (prop in additional) { + if (additional.hasOwnProperty(prop) && util.indexOf(seen, prop) < 0) { + if (typeof target[prop] !== 'object' || !depth) { + target[prop] = additional[prop]; + seen.push(additional[prop]); + } else { + util.merge(target[prop], additional[prop], depth - 1, seen); + } + } + } + + return target; + }; + + /** + * Merges prototypes from objects + * + * @api public + */ + + util.mixin = function (ctor, ctor2) { + util.merge(ctor.prototype, ctor2.prototype); + }; + + /** + * Shortcut for prototypical and static inheritance. + * + * @api private + */ + + util.inherit = function (ctor, ctor2) { + function f() {}; + f.prototype = ctor2.prototype; + ctor.prototype = new f; + }; + + /** + * Checks if the given object is an Array. + * + * io.util.isArray([]); // true + * io.util.isArray({}); // false + * + * @param Object obj + * @api public + */ + + util.isArray = Array.isArray || function (obj) { + return Object.prototype.toString.call(obj) === '[object Array]'; + }; + + /** + * Intersects values of two arrays into a third + * + * @api public + */ + + util.intersect = function (arr, arr2) { + var ret = [] + , longest = arr.length > arr2.length ? arr : arr2 + , shortest = arr.length > arr2.length ? arr2 : arr; + + for (var i = 0, l = shortest.length; i < l; i++) { + if (~util.indexOf(longest, shortest[i])) + ret.push(shortest[i]); + } + + return ret; + }; + + /** + * Array indexOf compatibility. + * + * @see bit.ly/a5Dxa2 + * @api public + */ + + util.indexOf = function (arr, o, i) { + + for (var j = arr.length, i = i < 0 ? i + j < 0 ? 0 : i + j : i || 0; + i < j && arr[i] !== o; i++) {} + + return j <= i ? -1 : i; + }; + + /** + * Converts enumerables to array. + * + * @api public + */ + + util.toArray = function (enu) { + var arr = []; + + for (var i = 0, l = enu.length; i < l; i++) + arr.push(enu[i]); + + return arr; + }; + + /** + * UA / engines detection namespace. + * + * @namespace + */ + + util.ua = {}; + + /** + * Whether the UA supports CORS for XHR. + * + * @api public + */ + + util.ua.hasCORS = 'undefined' != typeof XMLHttpRequest && (function () { + try { + var a = new XMLHttpRequest(); + } catch (e) { + return false; + } + + return a.withCredentials != undefined; + })(); + + /** + * Detect webkit. + * + * @api public + */ + + util.ua.webkit = 'undefined' != typeof navigator + && /webkit/i.test(navigator.userAgent); + + /** + * Detect iPad/iPhone/iPod. + * + * @api public + */ + + util.ua.iDevice = 'undefined' != typeof navigator + && /iPad|iPhone|iPod/i.test(navigator.userAgent); + +})('undefined' != typeof io ? io : module.exports, this); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.EventEmitter = EventEmitter; + + /** + * Event emitter constructor. + * + * @api public. + */ + + function EventEmitter () {}; + + /** + * Adds a listener + * + * @api public + */ + + EventEmitter.prototype.on = function (name, fn) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = fn; + } else if (io.util.isArray(this.$events[name])) { + this.$events[name].push(fn); + } else { + this.$events[name] = [this.$events[name], fn]; + } + + return this; + }; + + EventEmitter.prototype.addListener = EventEmitter.prototype.on; + + /** + * Adds a volatile listener. + * + * @api public + */ + + EventEmitter.prototype.once = function (name, fn) { + var self = this; + + function on () { + self.removeListener(name, on); + fn.apply(this, arguments); + }; + + on.listener = fn; + this.on(name, on); + + return this; + }; + + /** + * Removes a listener. + * + * @api public + */ + + EventEmitter.prototype.removeListener = function (name, fn) { + if (this.$events && this.$events[name]) { + var list = this.$events[name]; + + if (io.util.isArray(list)) { + var pos = -1; + + for (var i = 0, l = list.length; i < l; i++) { + if (list[i] === fn || (list[i].listener && list[i].listener === fn)) { + pos = i; + break; + } + } + + if (pos < 0) { + return this; + } + + list.splice(pos, 1); + + if (!list.length) { + delete this.$events[name]; + } + } else if (list === fn || (list.listener && list.listener === fn)) { + delete this.$events[name]; + } + } + + return this; + }; + + /** + * Removes all listeners for an event. + * + * @api public + */ + + EventEmitter.prototype.removeAllListeners = function (name) { + if (name === undefined) { + this.$events = {}; + return this; + } + + if (this.$events && this.$events[name]) { + this.$events[name] = null; + } + + return this; + }; + + /** + * Gets all listeners for a certain event. + * + * @api publci + */ + + EventEmitter.prototype.listeners = function (name) { + if (!this.$events) { + this.$events = {}; + } + + if (!this.$events[name]) { + this.$events[name] = []; + } + + if (!io.util.isArray(this.$events[name])) { + this.$events[name] = [this.$events[name]]; + } + + return this.$events[name]; + }; + + /** + * Emits an event. + * + * @api public + */ + + EventEmitter.prototype.emit = function (name) { + if (!this.$events) { + return false; + } + + var handler = this.$events[name]; + + if (!handler) { + return false; + } + + var args = Array.prototype.slice.call(arguments, 1); + + if ('function' == typeof handler) { + handler.apply(this, args); + } else if (io.util.isArray(handler)) { + var listeners = handler.slice(); + + for (var i = 0, l = listeners.length; i < l; i++) { + listeners[i].apply(this, args); + } + } else { + return false; + } + + return true; + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +/** + * Based on JSON2 (http://www.JSON.org/js.html). + */ + +(function (exports, nativeJSON) { + "use strict"; + + // use native JSON if it's available + if (nativeJSON && nativeJSON.parse){ + return exports.JSON = { + parse: nativeJSON.parse + , stringify: nativeJSON.stringify + }; + } + + var JSON = exports.JSON = {}; + + function f(n) { + // Format integers to have at least two digits. + return n < 10 ? '0' + n : n; + } + + function date(d, key) { + return isFinite(d.valueOf()) ? + d.getUTCFullYear() + '-' + + f(d.getUTCMonth() + 1) + '-' + + f(d.getUTCDate()) + 'T' + + f(d.getUTCHours()) + ':' + + f(d.getUTCMinutes()) + ':' + + f(d.getUTCSeconds()) + 'Z' : null; + }; + + 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 = { // table of character substitutions + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '"' : '\\"', + '\\': '\\\\' + }, + rep; + + + function quote(string) { + +// If the string contains no control characters, no quote characters, and no +// backslash characters, then we can safely slap some quotes around it. +// Otherwise we must also replace the offending characters with safe escape +// sequences. + + 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) { + +// Produce a string from holder[key]. + + var i, // The loop counter. + k, // The member key. + v, // The member value. + length, + mind = gap, + partial, + value = holder[key]; + +// If the value has a toJSON method, call it to obtain a replacement value. + + if (value instanceof Date) { + value = date(key); + } + +// If we were called with a replacer function, then call the replacer to +// obtain a replacement value. + + if (typeof rep === 'function') { + value = rep.call(holder, key, value); + } + +// What happens next depends on the value's type. + + switch (typeof value) { + case 'string': + return quote(value); + + case 'number': + +// JSON numbers must be finite. Encode non-finite numbers as null. + + return isFinite(value) ? String(value) : 'null'; + + case 'boolean': + case 'null': + +// If the value is a boolean or null, convert it to a string. Note: +// typeof null does not produce 'null'. The case is included here in +// the remote chance that this gets fixed someday. + + return String(value); + +// If the type is 'object', we might be dealing with an object or an array or +// null. + + case 'object': + +// Due to a specification blunder in ECMAScript, typeof null is 'object', +// so watch out for that case. + + if (!value) { + return 'null'; + } + +// Make an array to hold the partial results of stringifying this object value. + + gap += indent; + partial = []; + +// Is the value an array? + + if (Object.prototype.toString.apply(value) === '[object Array]') { + +// The value is an array. Stringify every element. Use null as a placeholder +// for non-JSON values. + + length = value.length; + for (i = 0; i < length; i += 1) { + partial[i] = str(i, value) || 'null'; + } + +// Join all of the elements together, separated with commas, and wrap them in +// brackets. + + v = partial.length === 0 ? '[]' : gap ? + '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' : + '[' + partial.join(',') + ']'; + gap = mind; + return v; + } + +// If the replacer is an array, use it to select the members to be stringified. + + if (rep && typeof rep === 'object') { + length = rep.length; + for (i = 0; i < length; i += 1) { + if (typeof rep[i] === 'string') { + k = rep[i]; + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } else { + +// Otherwise, iterate through all of the keys in the object. + + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = str(k, value); + if (v) { + partial.push(quote(k) + (gap ? ': ' : ':') + v); + } + } + } + } + +// Join all of the member texts together, separated with commas, +// and wrap them in braces. + + v = partial.length === 0 ? '{}' : gap ? + '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' : + '{' + partial.join(',') + '}'; + gap = mind; + return v; + } + } + +// If the JSON object does not yet have a stringify method, give it one. + + JSON.stringify = function (value, replacer, space) { + +// The stringify method takes a value and an optional replacer, and an optional +// space parameter, and returns a JSON text. The replacer can be a function +// that can replace values, or an array of strings that will select the keys. +// A default replacer method can be provided. Use of the space parameter can +// produce text that is more easily readable. + + var i; + gap = ''; + indent = ''; + +// If the space parameter is a number, make an indent string containing that +// many spaces. + + if (typeof space === 'number') { + for (i = 0; i < space; i += 1) { + indent += ' '; + } + +// If the space parameter is a string, it will be used as the indent string. + + } else if (typeof space === 'string') { + indent = space; + } + +// If there is a replacer, it must be a function or an array. +// Otherwise, throw an error. + + rep = replacer; + if (replacer && typeof replacer !== 'function' && + (typeof replacer !== 'object' || + typeof replacer.length !== 'number')) { + throw new Error('JSON.stringify'); + } + +// Make a fake root object containing our value under the key of ''. +// Return the result of stringifying the value. + + return str('', {'': value}); + }; + +// If the JSON object does not yet have a parse method, give it one. + + JSON.parse = function (text, reviver) { + // The parse method takes a text and an optional reviver function, and returns + // a JavaScript value if the text is a valid JSON text. + + var j; + + function walk(holder, key) { + + // The walk method is used to recursively walk the resulting structure so + // that modifications can be made. + + var k, v, value = holder[key]; + if (value && typeof value === 'object') { + for (k in value) { + if (Object.prototype.hasOwnProperty.call(value, k)) { + v = walk(value, k); + if (v !== undefined) { + value[k] = v; + } else { + delete value[k]; + } + } + } + } + return reviver.call(holder, key, value); + } + + + // Parsing happens in four stages. In the first stage, we replace certain + // Unicode characters with escape sequences. JavaScript handles many characters + // incorrectly, either silently deleting them, or treating them as line endings. + + text = String(text); + cx.lastIndex = 0; + if (cx.test(text)) { + text = text.replace(cx, function (a) { + return '\\u' + + ('0000' + a.charCodeAt(0).toString(16)).slice(-4); + }); + } + + // In the second stage, we run the text against regular expressions that look + // for non-JSON patterns. We are especially concerned with '()' and 'new' + // because they can cause invocation, and '=' because it can cause mutation. + // But just to be safe, we want to reject all unexpected forms. + + // We split the second stage into 4 regexp operations in order to work around + // crippling inefficiencies in IE's and Safari's regexp engines. First we + // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we + // replace all simple value tokens with ']' characters. Third, we delete all + // open brackets that follow a colon or comma or that begin the text. Finally, + // we look to see that the remaining characters are only whitespace or ']' or + // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval. + + if (/^[\],:{}\s]*$/ + .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@') + .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']') + .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) { + + // In the third stage we use the eval function to compile the text into a + // JavaScript structure. The '{' operator is subject to a syntactic ambiguity + // in JavaScript: it can begin a block or an object literal. We wrap the text + // in parens to eliminate the ambiguity. + + j = eval('(' + text + ')'); + + // In the optional fourth stage, we recursively walk the new structure, passing + // each name/value pair to a reviver function for possible transformation. + + return typeof reviver === 'function' ? + walk({'': j}, '') : j; + } + + // If the text is not JSON parseable, then a SyntaxError is thrown. + + throw new SyntaxError('JSON.parse'); + }; + +})( + 'undefined' != typeof io ? io : module.exports + , typeof JSON !== 'undefined' ? JSON : undefined +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Parser namespace. + * + * @namespace + */ + + var parser = exports.parser = {}; + + /** + * Packet types. + */ + + var packets = parser.packets = [ + 'disconnect' + , 'connect' + , 'heartbeat' + , 'message' + , 'json' + , 'event' + , 'ack' + , 'error' + , 'noop' + ]; + + /** + * Errors reasons. + */ + + var reasons = parser.reasons = [ + 'transport not supported' + , 'client not handshaken' + , 'unauthorized' + ]; + + /** + * Errors advice. + */ + + var advice = parser.advice = [ + 'reconnect' + ]; + + /** + * Shortcuts. + */ + + var JSON = io.JSON + , indexOf = io.util.indexOf; + + /** + * Encodes a packet. + * + * @api private + */ + + parser.encodePacket = function (packet) { + var type = indexOf(packets, packet.type) + , id = packet.id || '' + , endpoint = packet.endpoint || '' + , ack = packet.ack + , data = null; + + switch (packet.type) { + case 'error': + var reason = packet.reason ? indexOf(reasons, packet.reason) : '' + , adv = packet.advice ? indexOf(advice, packet.advice) : ''; + + if (reason !== '' || adv !== '') + data = reason + (adv !== '' ? ('+' + adv) : ''); + + break; + + case 'message': + if (packet.data !== '') + data = packet.data; + break; + + case 'event': + var ev = { name: packet.name }; + + if (packet.args && packet.args.length) { + ev.args = packet.args; + } + + data = JSON.stringify(ev); + break; + + case 'json': + data = JSON.stringify(packet.data); + break; + + case 'connect': + if (packet.qs) + data = packet.qs; + break; + + case 'ack': + data = packet.ackId + + (packet.args && packet.args.length + ? '+' + JSON.stringify(packet.args) : ''); + break; + } + + // construct packet with required fragments + var encoded = [ + type + , id + (ack == 'data' ? '+' : '') + , endpoint + ]; + + // data fragment is optional + if (data !== null && data !== undefined) + encoded.push(data); + + return encoded.join(':'); + }; + + /** + * Encodes multiple messages (payload). + * + * @param {Array} messages + * @api private + */ + + parser.encodePayload = function (packets) { + var decoded = ''; + + if (packets.length == 1) + return packets[0]; + + for (var i = 0, l = packets.length; i < l; i++) { + var packet = packets[i]; + decoded += '\ufffd' + packet.length + '\ufffd' + packets[i]; + } + + return decoded; + }; + + /** + * Decodes a packet + * + * @api private + */ + + var regexp = /([^:]+):([0-9]+)?(\+)?:([^:]+)?:?([\s\S]*)?/; + + parser.decodePacket = function (data) { + var pieces = data.match(regexp); + + if (!pieces) return {}; + + var id = pieces[2] || '' + , data = pieces[5] || '' + , packet = { + type: packets[pieces[1]] + , endpoint: pieces[4] || '' + }; + + // whether we need to acknowledge the packet + if (id) { + packet.id = id; + if (pieces[3]) + packet.ack = 'data'; + else + packet.ack = true; + } + + // handle different packet types + switch (packet.type) { + case 'error': + var pieces = data.split('+'); + packet.reason = reasons[pieces[0]] || ''; + packet.advice = advice[pieces[1]] || ''; + break; + + case 'message': + packet.data = data || ''; + break; + + case 'event': + try { + var opts = JSON.parse(data); + packet.name = opts.name; + packet.args = opts.args; + } catch (e) { } + + packet.args = packet.args || []; + break; + + case 'json': + try { + packet.data = JSON.parse(data); + } catch (e) { } + break; + + case 'connect': + packet.qs = data || ''; + break; + + case 'ack': + var pieces = data.match(/^([0-9]+)(\+)?(.*)/); + if (pieces) { + packet.ackId = pieces[1]; + packet.args = []; + + if (pieces[3]) { + try { + packet.args = pieces[3] ? JSON.parse(pieces[3]) : []; + } catch (e) { } + } + } + break; + + case 'disconnect': + case 'heartbeat': + break; + }; + + return packet; + }; + + /** + * Decodes data payload. Detects multiple messages + * + * @return {Array} messages + * @api public + */ + + parser.decodePayload = function (data) { + // IE doesn't like data[i] for unicode chars, charAt works fine + if (data.charAt(0) == '\ufffd') { + var ret = []; + + for (var i = 1, length = ''; i < data.length; i++) { + if (data.charAt(i) == '\ufffd') { + ret.push(parser.decodePacket(data.substr(i + 1).substr(0, length))); + i += Number(length) + 1; + length = ''; + } else { + length += data.charAt(i); + } + } + + return ret; + } else { + return [parser.decodePacket(data)]; + } + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.Transport = Transport; + + /** + * This is the transport template for all supported transport methods. + * + * @constructor + * @api public + */ + + function Transport (socket, sessid) { + this.socket = socket; + this.sessid = sessid; + }; + + /** + * Apply EventEmitter mixin. + */ + + io.util.mixin(Transport, io.EventEmitter); + + + /** + * Indicates whether heartbeats is enabled for this transport + * + * @api private + */ + + Transport.prototype.heartbeats = function () { + return true; + }; + + /** + * Handles the response from the server. When a new response is received + * it will automatically update the timeout, decode the message and + * forwards the response to the onMessage function for further processing. + * + * @param {String} data Response from the server. + * @api private + */ + + Transport.prototype.onData = function (data) { + this.clearCloseTimeout(); + + // If the connection in currently open (or in a reopening state) reset the close + // timeout since we have just received data. This check is necessary so + // that we don't reset the timeout on an explicitly disconnected connection. + if (this.socket.connected || this.socket.connecting || this.socket.reconnecting) { + this.setCloseTimeout(); + } + + if (data !== '') { + // todo: we should only do decodePayload for xhr transports + var msgs = io.parser.decodePayload(data); + + if (msgs && msgs.length) { + for (var i = 0, l = msgs.length; i < l; i++) { + this.onPacket(msgs[i]); + } + } + } + + return this; + }; + + /** + * Handles packets. + * + * @api private + */ + + Transport.prototype.onPacket = function (packet) { + this.socket.setHeartbeatTimeout(); + + if (packet.type == 'heartbeat') { + return this.onHeartbeat(); + } + + if (packet.type == 'connect' && packet.endpoint == '') { + this.onConnect(); + } + + if (packet.type == 'error' && packet.advice == 'reconnect') { + this.isOpen = false; + } + + this.socket.onPacket(packet); + + return this; + }; + + /** + * Sets close timeout + * + * @api private + */ + + Transport.prototype.setCloseTimeout = function () { + if (!this.closeTimeout) { + var self = this; + + this.closeTimeout = setTimeout(function () { + self.onDisconnect(); + }, this.socket.closeTimeout); + } + }; + + /** + * Called when transport disconnects. + * + * @api private + */ + + Transport.prototype.onDisconnect = function () { + if (this.isOpen) this.close(); + this.clearTimeouts(); + this.socket.onDisconnect(); + return this; + }; + + /** + * Called when transport connects + * + * @api private + */ + + Transport.prototype.onConnect = function () { + this.socket.onConnect(); + return this; + }; + + /** + * Clears close timeout + * + * @api private + */ + + Transport.prototype.clearCloseTimeout = function () { + if (this.closeTimeout) { + clearTimeout(this.closeTimeout); + this.closeTimeout = null; + } + }; + + /** + * Clear timeouts + * + * @api private + */ + + Transport.prototype.clearTimeouts = function () { + this.clearCloseTimeout(); + + if (this.reopenTimeout) { + clearTimeout(this.reopenTimeout); + } + }; + + /** + * Sends a packet + * + * @param {Object} packet object. + * @api private + */ + + Transport.prototype.packet = function (packet) { + this.send(io.parser.encodePacket(packet)); + }; + + /** + * Send the received heartbeat message back to server. So the server + * knows we are still connected. + * + * @param {String} heartbeat Heartbeat response from the server. + * @api private + */ + + Transport.prototype.onHeartbeat = function (heartbeat) { + this.packet({ type: 'heartbeat' }); + }; + + /** + * Called when the transport opens. + * + * @api private + */ + + Transport.prototype.onOpen = function () { + this.isOpen = true; + this.clearCloseTimeout(); + this.socket.onOpen(); + }; + + /** + * Notifies the base when the connection with the Socket.IO server + * has been disconnected. + * + * @api private + */ + + Transport.prototype.onClose = function () { + var self = this; + + /* FIXME: reopen delay causing a infinit loop + this.reopenTimeout = setTimeout(function () { + self.open(); + }, this.socket.options['reopen delay']);*/ + + this.isOpen = false; + this.socket.onClose(); + this.onDisconnect(); + }; + + /** + * Generates a connection url based on the Socket.IO URL Protocol. + * See for more details. + * + * @returns {String} Connection url + * @api private + */ + + Transport.prototype.prepareUrl = function () { + var options = this.socket.options; + + return this.scheme() + '://' + + options.host + ':' + options.port + '/' + + options.resource + '/' + io.protocol + + '/' + this.name + '/' + this.sessid; + }; + + /** + * Checks if the transport is ready to start a connection. + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + Transport.prototype.ready = function (socket, fn) { + fn.call(this); + }; +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + + /** + * Expose constructor. + */ + + exports.Socket = Socket; + + /** + * Create a new `Socket.IO client` which can establish a persistent + * connection with a Socket.IO enabled server. + * + * @api public + */ + + function Socket (options) { + this.options = { + port: 80 + , secure: false + , document: 'document' in global ? document : false + , resource: 'socket.io' + , transports: io.transports + , 'connect timeout': 10000 + , 'try multiple transports': true + , 'reconnect': true + , 'reconnection delay': 500 + , 'reconnection limit': Infinity + , 'reopen delay': 3000 + , 'max reconnection attempts': 10 + , 'sync disconnect on unload': false + , 'auto connect': true + , 'flash policy port': 10843 + , 'manualFlush': false + }; + + io.util.merge(this.options, options); + + this.connected = false; + this.open = false; + this.connecting = false; + this.reconnecting = false; + this.namespaces = {}; + this.buffer = []; + this.doBuffer = false; + + if (this.options['sync disconnect on unload'] && + (!this.isXDomain() || io.util.ua.hasCORS)) { + var self = this; + io.util.on(global, 'beforeunload', function () { + self.disconnectSync(); + }, false); + } + + if (this.options['auto connect']) { + this.connect(); + } +}; + + /** + * Apply EventEmitter mixin. + */ + + io.util.mixin(Socket, io.EventEmitter); + + /** + * Returns a namespace listener/emitter for this socket + * + * @api public + */ + + Socket.prototype.of = function (name) { + if (!this.namespaces[name]) { + this.namespaces[name] = new io.SocketNamespace(this, name); + + if (name !== '') { + this.namespaces[name].packet({ type: 'connect' }); + } + } + + return this.namespaces[name]; + }; + + /** + * Emits the given event to the Socket and all namespaces + * + * @api private + */ + + Socket.prototype.publish = function () { + this.emit.apply(this, arguments); + + var nsp; + + for (var i in this.namespaces) { + if (this.namespaces.hasOwnProperty(i)) { + nsp = this.of(i); + nsp.$emit.apply(nsp, arguments); + } + } + }; + + /** + * Performs the handshake + * + * @api private + */ + + function empty () { }; + + Socket.prototype.handshake = function (fn) { + var self = this + , options = this.options; + + function complete (data) { + if (data instanceof Error) { + self.connecting = false; + self.onError(data.message); + } else { + fn.apply(null, data.split(':')); + } + }; + + var url = [ + 'http' + (options.secure ? 's' : '') + ':/' + , options.host + ':' + options.port + , options.resource + , io.protocol + , io.util.query(this.options.query, 't=' + +new Date) + ].join('/'); + + if (this.isXDomain() && !io.util.ua.hasCORS) { + var insertAt = document.getElementsByTagName('script')[0] + , script = document.createElement('script'); + + script.src = url + '&jsonp=' + io.j.length; + insertAt.parentNode.insertBefore(script, insertAt); + + io.j.push(function (data) { + complete(data); + script.parentNode.removeChild(script); + }); + } else { + var xhr = io.util.request(); + + xhr.open('GET', url, true); + if (this.isXDomain()) { + xhr.withCredentials = true; + } + xhr.onreadystatechange = function () { + if (xhr.readyState == 4) { + xhr.onreadystatechange = empty; + + if (xhr.status == 200) { + complete(xhr.responseText); + } else if (xhr.status == 403) { + self.onError(xhr.responseText); + } else { + self.connecting = false; + !self.reconnecting && self.onError(xhr.responseText); + } + } + }; + xhr.send(null); + } + }; + + /** + * Find an available transport based on the options supplied in the constructor. + * + * @api private + */ + + Socket.prototype.getTransport = function (override) { + var transports = override || this.transports, match; + + for (var i = 0, transport; transport = transports[i]; i++) { + if (io.Transport[transport] + && io.Transport[transport].check(this) + && (!this.isXDomain() || io.Transport[transport].xdomainCheck(this))) { + return new io.Transport[transport](this, this.sessionid); + } + } + + return null; + }; + + /** + * Connects to the server. + * + * @param {Function} [fn] Callback. + * @returns {io.Socket} + * @api public + */ + + Socket.prototype.connect = function (fn) { + if (this.connecting) { + return this; + } + + var self = this; + self.connecting = true; + + this.handshake(function (sid, heartbeat, close, transports) { + self.sessionid = sid; + self.closeTimeout = close * 1000; + self.heartbeatTimeout = heartbeat * 1000; + if(!self.transports) + self.transports = self.origTransports = (transports ? io.util.intersect( + transports.split(',') + , self.options.transports + ) : self.options.transports); + + self.setHeartbeatTimeout(); + + function connect (transports){ + if (self.transport) self.transport.clearTimeouts(); + + self.transport = self.getTransport(transports); + if (!self.transport) return self.publish('connect_failed'); + + // once the transport is ready + self.transport.ready(self, function () { + self.connecting = true; + self.publish('connecting', self.transport.name); + self.transport.open(); + + if (self.options['connect timeout']) { + self.connectTimeoutTimer = setTimeout(function () { + if (!self.connected) { + self.connecting = false; + + if (self.options['try multiple transports']) { + var remaining = self.transports; + + while (remaining.length > 0 && remaining.splice(0,1)[0] != + self.transport.name) {} + + if (remaining.length){ + connect(remaining); + } else { + self.publish('connect_failed'); + } + } + } + }, self.options['connect timeout']); + } + }); + } + + connect(self.transports); + + self.once('connect', function (){ + clearTimeout(self.connectTimeoutTimer); + + fn && typeof fn == 'function' && fn(); + }); + }); + + return this; + }; + + /** + * Clears and sets a new heartbeat timeout using the value given by the + * server during the handshake. + * + * @api private + */ + + Socket.prototype.setHeartbeatTimeout = function () { + clearTimeout(this.heartbeatTimeoutTimer); + if(this.transport && !this.transport.heartbeats()) return; + + var self = this; + this.heartbeatTimeoutTimer = setTimeout(function () { + self.transport.onClose(); + }, this.heartbeatTimeout); + }; + + /** + * Sends a message. + * + * @param {Object} data packet. + * @returns {io.Socket} + * @api public + */ + + Socket.prototype.packet = function (data) { + if (this.connected && !this.doBuffer) { + this.transport.packet(data); + } else { + this.buffer.push(data); + } + + return this; + }; + + /** + * Sets buffer state + * + * @api private + */ + + Socket.prototype.setBuffer = function (v) { + this.doBuffer = v; + + if (!v && this.connected && this.buffer.length) { + if (!this.options['manualFlush']) { + this.flushBuffer(); + } + } + }; + + /** + * Flushes the buffer data over the wire. + * To be invoked manually when 'manualFlush' is set to true. + * + * @api public + */ + + Socket.prototype.flushBuffer = function() { + this.transport.payload(this.buffer); + this.buffer = []; + }; + + + /** + * Disconnect the established connect. + * + * @returns {io.Socket} + * @api public + */ + + Socket.prototype.disconnect = function () { + if (this.connected || this.connecting) { + if (this.open) { + this.of('').packet({ type: 'disconnect' }); + } + + // handle disconnection immediately + this.onDisconnect('booted'); + } + + return this; + }; + + /** + * Disconnects the socket with a sync XHR. + * + * @api private + */ + + Socket.prototype.disconnectSync = function () { + // ensure disconnection + var xhr = io.util.request(); + var uri = [ + 'http' + (this.options.secure ? 's' : '') + ':/' + , this.options.host + ':' + this.options.port + , this.options.resource + , io.protocol + , '' + , this.sessionid + ].join('/') + '/?disconnect=1'; + + xhr.open('GET', uri, false); + xhr.send(null); + + // handle disconnection immediately + this.onDisconnect('booted'); + }; + + /** + * Check if we need to use cross domain enabled transports. Cross domain would + * be a different port or different domain name. + * + * @returns {Boolean} + * @api private + */ + + Socket.prototype.isXDomain = function () { + + var port = global.location.port || + ('https:' == global.location.protocol ? 443 : 80); + + return this.options.host !== global.location.hostname + || this.options.port != port; + }; + + /** + * Called upon handshake. + * + * @api private + */ + + Socket.prototype.onConnect = function () { + if (!this.connected) { + this.connected = true; + this.connecting = false; + if (!this.doBuffer) { + // make sure to flush the buffer + this.setBuffer(false); + } + this.emit('connect'); + } + }; + + /** + * Called when the transport opens + * + * @api private + */ + + Socket.prototype.onOpen = function () { + this.open = true; + }; + + /** + * Called when the transport closes. + * + * @api private + */ + + Socket.prototype.onClose = function () { + this.open = false; + clearTimeout(this.heartbeatTimeoutTimer); + }; + + /** + * Called when the transport first opens a connection + * + * @param text + */ + + Socket.prototype.onPacket = function (packet) { + this.of(packet.endpoint).onPacket(packet); + }; + + /** + * Handles an error. + * + * @api private + */ + + Socket.prototype.onError = function (err) { + if (err && err.advice) { + if (err.advice === 'reconnect' && (this.connected || this.connecting)) { + this.disconnect(); + if (this.options.reconnect) { + this.reconnect(); + } + } + } + + this.publish('error', err && err.reason ? err.reason : err); + }; + + /** + * Called when the transport disconnects. + * + * @api private + */ + + Socket.prototype.onDisconnect = function (reason) { + var wasConnected = this.connected + , wasConnecting = this.connecting; + + this.connected = false; + this.connecting = false; + this.open = false; + + if (wasConnected || wasConnecting) { + this.transport.close(); + this.transport.clearTimeouts(); + if (wasConnected) { + this.publish('disconnect', reason); + + if ('booted' != reason && this.options.reconnect && !this.reconnecting) { + this.reconnect(); + } + } + } + }; + + /** + * Called upon reconnection. + * + * @api private + */ + + Socket.prototype.reconnect = function () { + this.reconnecting = true; + this.reconnectionAttempts = 0; + this.reconnectionDelay = this.options['reconnection delay']; + + var self = this + , maxAttempts = this.options['max reconnection attempts'] + , tryMultiple = this.options['try multiple transports'] + , limit = this.options['reconnection limit']; + + function reset () { + if (self.connected) { + for (var i in self.namespaces) { + if (self.namespaces.hasOwnProperty(i) && '' !== i) { + self.namespaces[i].packet({ type: 'connect' }); + } + } + self.publish('reconnect', self.transport.name, self.reconnectionAttempts); + } + + clearTimeout(self.reconnectionTimer); + + self.removeListener('connect_failed', maybeReconnect); + self.removeListener('connect', maybeReconnect); + + self.reconnecting = false; + + delete self.reconnectionAttempts; + delete self.reconnectionDelay; + delete self.reconnectionTimer; + delete self.redoTransports; + + self.options['try multiple transports'] = tryMultiple; + }; + + function maybeReconnect () { + if (!self.reconnecting) { + return; + } + + if (self.connected) { + return reset(); + }; + + if (self.connecting && self.reconnecting) { + return self.reconnectionTimer = setTimeout(maybeReconnect, 1000); + } + + if (self.reconnectionAttempts++ >= maxAttempts) { + if (!self.redoTransports) { + self.on('connect_failed', maybeReconnect); + self.options['try multiple transports'] = true; + self.transports = self.origTransports; + self.transport = self.getTransport(); + self.redoTransports = true; + self.connect(); + } else { + self.publish('reconnect_failed'); + reset(); + } + } else { + if (self.reconnectionDelay < limit) { + self.reconnectionDelay *= 2; // exponential back off + } + + self.connect(); + self.publish('reconnecting', self.reconnectionDelay, self.reconnectionAttempts); + self.reconnectionTimer = setTimeout(maybeReconnect, self.reconnectionDelay); + } + }; + + this.options['try multiple transports'] = false; + this.reconnectionTimer = setTimeout(maybeReconnect, this.reconnectionDelay); + + this.on('connect', maybeReconnect); + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports + , this +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.SocketNamespace = SocketNamespace; + + /** + * Socket namespace constructor. + * + * @constructor + * @api public + */ + + function SocketNamespace (socket, name) { + this.socket = socket; + this.name = name || ''; + this.flags = {}; + this.json = new Flag(this, 'json'); + this.ackPackets = 0; + this.acks = {}; + }; + + /** + * Apply EventEmitter mixin. + */ + + io.util.mixin(SocketNamespace, io.EventEmitter); + + /** + * Copies emit since we override it + * + * @api private + */ + + SocketNamespace.prototype.$emit = io.EventEmitter.prototype.emit; + + /** + * Creates a new namespace, by proxying the request to the socket. This + * allows us to use the synax as we do on the server. + * + * @api public + */ + + SocketNamespace.prototype.of = function () { + return this.socket.of.apply(this.socket, arguments); + }; + + /** + * Sends a packet. + * + * @api private + */ + + SocketNamespace.prototype.packet = function (packet) { + packet.endpoint = this.name; + this.socket.packet(packet); + this.flags = {}; + return this; + }; + + /** + * Sends a message + * + * @api public + */ + + SocketNamespace.prototype.send = function (data, fn) { + var packet = { + type: this.flags.json ? 'json' : 'message' + , data: data + }; + + if ('function' == typeof fn) { + packet.id = ++this.ackPackets; + packet.ack = true; + this.acks[packet.id] = fn; + } + + return this.packet(packet); + }; + + /** + * Emits an event + * + * @api public + */ + + SocketNamespace.prototype.emit = function (name) { + var args = Array.prototype.slice.call(arguments, 1) + , lastArg = args[args.length - 1] + , packet = { + type: 'event' + , name: name + }; + + if ('function' == typeof lastArg) { + packet.id = ++this.ackPackets; + packet.ack = 'data'; + this.acks[packet.id] = lastArg; + args = args.slice(0, args.length - 1); + } + + packet.args = args; + + return this.packet(packet); + }; + + /** + * Disconnects the namespace + * + * @api private + */ + + SocketNamespace.prototype.disconnect = function () { + if (this.name === '') { + this.socket.disconnect(); + } else { + this.packet({ type: 'disconnect' }); + this.$emit('disconnect'); + } + + return this; + }; + + /** + * Handles a packet + * + * @api private + */ + + SocketNamespace.prototype.onPacket = function (packet) { + var self = this; + + function ack () { + self.packet({ + type: 'ack' + , args: io.util.toArray(arguments) + , ackId: packet.id + }); + }; + + switch (packet.type) { + case 'connect': + this.$emit('connect'); + break; + + case 'disconnect': + if (this.name === '') { + this.socket.onDisconnect(packet.reason || 'booted'); + } else { + this.$emit('disconnect', packet.reason); + } + break; + + case 'message': + case 'json': + var params = ['message', packet.data]; + + if (packet.ack == 'data') { + params.push(ack); + } else if (packet.ack) { + this.packet({ type: 'ack', ackId: packet.id }); + } + + this.$emit.apply(this, params); + break; + + case 'event': + var params = [packet.name].concat(packet.args); + + if (packet.ack == 'data') + params.push(ack); + + this.$emit.apply(this, params); + break; + + case 'ack': + if (this.acks[packet.ackId]) { + this.acks[packet.ackId].apply(this, packet.args); + delete this.acks[packet.ackId]; + } + break; + + case 'error': + if (packet.advice){ + this.socket.onError(packet); + } else { + if (packet.reason == 'unauthorized') { + this.$emit('connect_failed', packet.reason); + } else { + this.$emit('error', packet.reason); + } + } + break; + } + }; + + /** + * Flag interface. + * + * @api private + */ + + function Flag (nsp, name) { + this.namespace = nsp; + this.name = name; + }; + + /** + * Send a message + * + * @api public + */ + + Flag.prototype.send = function () { + this.namespace.flags[this.name] = true; + this.namespace.send.apply(this.namespace, arguments); + }; + + /** + * Emit an event + * + * @api public + */ + + Flag.prototype.emit = function () { + this.namespace.flags[this.name] = true; + this.namespace.emit.apply(this.namespace, arguments); + }; + +})( + 'undefined' != typeof io ? io : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + + /** + * Expose constructor. + */ + + exports.websocket = WS; + + /** + * The WebSocket transport uses the HTML5 WebSocket API to establish an + * persistent connection with the Socket.IO server. This transport will also + * be inherited by the FlashSocket fallback as it provides a API compatible + * polyfill for the WebSockets. + * + * @constructor + * @extends {io.Transport} + * @api public + */ + + function WS (socket) { + io.Transport.apply(this, arguments); + }; + + /** + * Inherits from Transport. + */ + + io.util.inherit(WS, io.Transport); + + /** + * Transport name + * + * @api public + */ + + WS.prototype.name = 'websocket'; + + /** + * Initializes a new `WebSocket` connection with the Socket.IO server. We attach + * all the appropriate listeners to handle the responses from the server. + * + * @returns {Transport} + * @api public + */ + + WS.prototype.open = function () { + var query = io.util.query(this.socket.options.query) + , self = this + , Socket + + + if (!Socket) { + Socket = global.MozWebSocket || global.WebSocket; + } + + this.websocket = new Socket(this.prepareUrl() + query); + + this.websocket.onopen = function () { + self.onOpen(); + self.socket.setBuffer(false); + }; + this.websocket.onmessage = function (ev) { + self.onData(ev.data); + }; + this.websocket.onclose = function () { + self.onClose(); + self.socket.setBuffer(true); + }; + this.websocket.onerror = function (e) { + self.onError(e); + }; + + return this; + }; + + /** + * Send a message to the Socket.IO server. The message will automatically be + * encoded in the correct message format. + * + * @returns {Transport} + * @api public + */ + + // Do to a bug in the current IDevices browser, we need to wrap the send in a + // setTimeout, when they resume from sleeping the browser will crash if + // we don't allow the browser time to detect the socket has been closed + if (io.util.ua.iDevice) { + WS.prototype.send = function (data) { + var self = this; + setTimeout(function() { + self.websocket.send(data); + },0); + return this; + }; + } else { + WS.prototype.send = function (data) { + this.websocket.send(data); + return this; + }; + } + + /** + * Payload + * + * @api private + */ + + WS.prototype.payload = function (arr) { + for (var i = 0, l = arr.length; i < l; i++) { + this.packet(arr[i]); + } + return this; + }; + + /** + * Disconnect the established `WebSocket` connection. + * + * @returns {Transport} + * @api public + */ + + WS.prototype.close = function () { + this.websocket.close(); + return this; + }; + + /** + * Handle the errors that `WebSocket` might be giving when we + * are attempting to connect or send messages. + * + * @param {Error} e The error. + * @api private + */ + + WS.prototype.onError = function (e) { + this.socket.onError(e); + }; + + /** + * Returns the appropriate scheme for the URI generation. + * + * @api private + */ + WS.prototype.scheme = function () { + return this.socket.options.secure ? 'wss' : 'ws'; + }; + + /** + * Checks if the browser has support for native `WebSockets` and that + * it's not the polyfill created for the FlashSocket transport. + * + * @return {Boolean} + * @api public + */ + + WS.check = function () { + return ('WebSocket' in global && !('__addTask' in WebSocket)) + || 'MozWebSocket' in global; + }; + + /** + * Check if the `WebSocket` transport support cross domain communications. + * + * @returns {Boolean} + * @api public + */ + + WS.xdomainCheck = function () { + return true; + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('websocket'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports + , this +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.flashsocket = Flashsocket; + + /** + * The FlashSocket transport. This is a API wrapper for the HTML5 WebSocket + * specification. It uses a .swf file to communicate with the server. If you want + * to serve the .swf file from a other server than where the Socket.IO script is + * coming from you need to use the insecure version of the .swf. More information + * about this can be found on the github page. + * + * @constructor + * @extends {io.Transport.websocket} + * @api public + */ + + function Flashsocket () { + io.Transport.websocket.apply(this, arguments); + }; + + /** + * Inherits from Transport. + */ + + io.util.inherit(Flashsocket, io.Transport.websocket); + + /** + * Transport name + * + * @api public + */ + + Flashsocket.prototype.name = 'flashsocket'; + + /** + * Disconnect the established `FlashSocket` connection. This is done by adding a + * new task to the FlashSocket. The rest will be handled off by the `WebSocket` + * transport. + * + * @returns {Transport} + * @api public + */ + + Flashsocket.prototype.open = function () { + var self = this + , args = arguments; + + WebSocket.__addTask(function () { + io.Transport.websocket.prototype.open.apply(self, args); + }); + return this; + }; + + /** + * Sends a message to the Socket.IO server. This is done by adding a new + * task to the FlashSocket. The rest will be handled off by the `WebSocket` + * transport. + * + * @returns {Transport} + * @api public + */ + + Flashsocket.prototype.send = function () { + var self = this, args = arguments; + WebSocket.__addTask(function () { + io.Transport.websocket.prototype.send.apply(self, args); + }); + return this; + }; + + /** + * Disconnects the established `FlashSocket` connection. + * + * @returns {Transport} + * @api public + */ + + Flashsocket.prototype.close = function () { + WebSocket.__tasks.length = 0; + io.Transport.websocket.prototype.close.call(this); + return this; + }; + + /** + * The WebSocket fall back needs to append the flash container to the body + * element, so we need to make sure we have access to it. Or defer the call + * until we are sure there is a body element. + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + Flashsocket.prototype.ready = function (socket, fn) { + function init () { + var options = socket.options + , port = options['flash policy port'] + , path = [ + 'http' + (options.secure ? 's' : '') + ':/' + , options.host + ':' + options.port + , options.resource + , 'static/flashsocket' + , 'WebSocketMain' + (socket.isXDomain() ? 'Insecure' : '') + '.swf' + ]; + + // Only start downloading the swf file when the checked that this browser + // actually supports it + if (!Flashsocket.loaded) { + if (typeof WEB_SOCKET_SWF_LOCATION === 'undefined') { + // Set the correct file based on the XDomain settings + WEB_SOCKET_SWF_LOCATION = path.join('/'); + } + + if (port !== 843) { + WebSocket.loadFlashPolicyFile('xmlsocket://' + options.host + ':' + port); + } + + WebSocket.__initialize(); + Flashsocket.loaded = true; + } + + fn.call(self); + } + + var self = this; + if (document.body) return init(); + + io.util.load(init); + }; + + /** + * Check if the FlashSocket transport is supported as it requires that the Adobe + * Flash Player plug-in version `10.0.0` or greater is installed. And also check if + * the polyfill is correctly loaded. + * + * @returns {Boolean} + * @api public + */ + + Flashsocket.check = function () { + if ( + typeof WebSocket == 'undefined' + || !('__initialize' in WebSocket) || !swfobject + ) return false; + + return swfobject.getFlashPlayerVersion().major >= 10; + }; + + /** + * Check if the FlashSocket transport can be used as cross domain / cross origin + * transport. Because we can't see which type (secure or insecure) of .swf is used + * we will just return true. + * + * @returns {Boolean} + * @api public + */ + + Flashsocket.xdomainCheck = function () { + return true; + }; + + /** + * Disable AUTO_INITIALIZATION + */ + + if (typeof window != 'undefined') { + WEB_SOCKET_DISABLE_AUTO_INITIALIZATION = true; + } + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('flashsocket'); +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); +/* SWFObject v2.2 + is released under the MIT License +*/ +if ('undefined' != typeof window) { +var swfobject=function(){var D="undefined",r="object",S="Shockwave Flash",W="ShockwaveFlash.ShockwaveFlash",q="application/x-shockwave-flash",R="SWFObjectExprInst",x="onreadystatechange",O=window,j=document,t=navigator,T=false,U=[h],o=[],N=[],I=[],l,Q,E,B,J=false,a=false,n,G,m=true,M=function(){var aa=typeof j.getElementById!=D&&typeof j.getElementsByTagName!=D&&typeof j.createElement!=D,ah=t.userAgent.toLowerCase(),Y=t.platform.toLowerCase(),ae=Y?/win/.test(Y):/win/.test(ah),ac=Y?/mac/.test(Y):/mac/.test(ah),af=/webkit/.test(ah)?parseFloat(ah.replace(/^.*webkit\/(\d+(\.\d+)?).*$/,"$1")):false,X=!+"\v1",ag=[0,0,0],ab=null;if(typeof t.plugins!=D&&typeof t.plugins[S]==r){ab=t.plugins[S].description;if(ab&&!(typeof t.mimeTypes!=D&&t.mimeTypes[q]&&!t.mimeTypes[q].enabledPlugin)){T=true;X=false;ab=ab.replace(/^.*\s+(\S+\s+\S+$)/,"$1");ag[0]=parseInt(ab.replace(/^(.*)\..*$/,"$1"),10);ag[1]=parseInt(ab.replace(/^.*\.(.*)\s.*$/,"$1"),10);ag[2]=/[a-zA-Z]/.test(ab)?parseInt(ab.replace(/^.*[a-zA-Z]+(.*)$/,"$1"),10):0}}else{if(typeof O[(['Active'].concat('Object').join('X'))]!=D){try{var ad=new window[(['Active'].concat('Object').join('X'))](W);if(ad){ab=ad.GetVariable("$version");if(ab){X=true;ab=ab.split(" ")[1].split(",");ag=[parseInt(ab[0],10),parseInt(ab[1],10),parseInt(ab[2],10)]}}}catch(Z){}}}return{w3:aa,pv:ag,wk:af,ie:X,win:ae,mac:ac}}(),k=function(){if(!M.w3){return}if((typeof j.readyState!=D&&j.readyState=="complete")||(typeof j.readyState==D&&(j.getElementsByTagName("body")[0]||j.body))){f()}if(!J){if(typeof j.addEventListener!=D){j.addEventListener("DOMContentLoaded",f,false)}if(M.ie&&M.win){j.attachEvent(x,function(){if(j.readyState=="complete"){j.detachEvent(x,arguments.callee);f()}});if(O==top){(function(){if(J){return}try{j.documentElement.doScroll("left")}catch(X){setTimeout(arguments.callee,0);return}f()})()}}if(M.wk){(function(){if(J){return}if(!/loaded|complete/.test(j.readyState)){setTimeout(arguments.callee,0);return}f()})()}s(f)}}();function f(){if(J){return}try{var Z=j.getElementsByTagName("body")[0].appendChild(C("span"));Z.parentNode.removeChild(Z)}catch(aa){return}J=true;var X=U.length;for(var Y=0;Y0){for(var af=0;af0){var ae=c(Y);if(ae){if(F(o[af].swfVersion)&&!(M.wk&&M.wk<312)){w(Y,true);if(ab){aa.success=true;aa.ref=z(Y);ab(aa)}}else{if(o[af].expressInstall&&A()){var ai={};ai.data=o[af].expressInstall;ai.width=ae.getAttribute("width")||"0";ai.height=ae.getAttribute("height")||"0";if(ae.getAttribute("class")){ai.styleclass=ae.getAttribute("class")}if(ae.getAttribute("align")){ai.align=ae.getAttribute("align")}var ah={};var X=ae.getElementsByTagName("param");var ac=X.length;for(var ad=0;ad'}}aa.outerHTML='"+af+"";N[N.length]=ai.id;X=c(ai.id)}else{var Z=C(r);Z.setAttribute("type",q);for(var ac in ai){if(ai[ac]!=Object.prototype[ac]){if(ac.toLowerCase()=="styleclass"){Z.setAttribute("class",ai[ac])}else{if(ac.toLowerCase()!="classid"){Z.setAttribute(ac,ai[ac])}}}}for(var ab in ag){if(ag[ab]!=Object.prototype[ab]&&ab.toLowerCase()!="movie"){e(Z,ab,ag[ab])}}aa.parentNode.replaceChild(Z,aa);X=Z}}return X}function e(Z,X,Y){var aa=C("param");aa.setAttribute("name",X);aa.setAttribute("value",Y);Z.appendChild(aa)}function y(Y){var X=c(Y);if(X&&X.nodeName=="OBJECT"){if(M.ie&&M.win){X.style.display="none";(function(){if(X.readyState==4){b(Y)}else{setTimeout(arguments.callee,10)}})()}else{X.parentNode.removeChild(X)}}}function b(Z){var Y=c(Z);if(Y){for(var X in Y){if(typeof Y[X]=="function"){Y[X]=null}}Y.parentNode.removeChild(Y)}}function c(Z){var X=null;try{X=j.getElementById(Z)}catch(Y){}return X}function C(X){return j.createElement(X)}function i(Z,X,Y){Z.attachEvent(X,Y);I[I.length]=[Z,X,Y]}function F(Z){var Y=M.pv,X=Z.split(".");X[0]=parseInt(X[0],10);X[1]=parseInt(X[1],10)||0;X[2]=parseInt(X[2],10)||0;return(Y[0]>X[0]||(Y[0]==X[0]&&Y[1]>X[1])||(Y[0]==X[0]&&Y[1]==X[1]&&Y[2]>=X[2]))?true:false}function v(ac,Y,ad,ab){if(M.ie&&M.mac){return}var aa=j.getElementsByTagName("head")[0];if(!aa){return}var X=(ad&&typeof ad=="string")?ad:"screen";if(ab){n=null;G=null}if(!n||G!=X){var Z=C("style");Z.setAttribute("type","text/css");Z.setAttribute("media",X);n=aa.appendChild(Z);if(M.ie&&M.win&&typeof j.styleSheets!=D&&j.styleSheets.length>0){n=j.styleSheets[j.styleSheets.length-1]}G=X}if(M.ie&&M.win){if(n&&typeof n.addRule==r){n.addRule(ac,Y)}}else{if(n&&typeof j.createTextNode!=D){n.appendChild(j.createTextNode(ac+" {"+Y+"}"))}}}function w(Z,X){if(!m){return}var Y=X?"visible":"hidden";if(J&&c(Z)){c(Z).style.visibility=Y}else{v("#"+Z,"visibility:"+Y)}}function L(Y){var Z=/[\\\"<>\.;]/;var X=Z.exec(Y)!=null;return X&&typeof encodeURIComponent!=D?encodeURIComponent(Y):Y}var d=function(){if(M.ie&&M.win){window.attachEvent("onunload",function(){var ac=I.length;for(var ab=0;ab +// License: New BSD License +// Reference: http://dev.w3.org/html5/websockets/ +// Reference: http://tools.ietf.org/html/draft-hixie-thewebsocketprotocol + +(function() { + + if ('undefined' == typeof window || window.WebSocket) return; + + var console = window.console; + if (!console || !console.log || !console.error) { + console = {log: function(){ }, error: function(){ }}; + } + + if (!swfobject.hasFlashPlayerVersion("10.0.0")) { + console.error("Flash Player >= 10.0.0 is required."); + return; + } + if (location.protocol == "file:") { + console.error( + "WARNING: web-socket-js doesn't work in file:///... URL " + + "unless you set Flash Security Settings properly. " + + "Open the page via Web server i.e. http://..."); + } + + /** + * This class represents a faux web socket. + * @param {string} url + * @param {array or string} protocols + * @param {string} proxyHost + * @param {int} proxyPort + * @param {string} headers + */ + WebSocket = function(url, protocols, proxyHost, proxyPort, headers) { + var self = this; + self.__id = WebSocket.__nextId++; + WebSocket.__instances[self.__id] = self; + self.readyState = WebSocket.CONNECTING; + self.bufferedAmount = 0; + self.__events = {}; + if (!protocols) { + protocols = []; + } else if (typeof protocols == "string") { + protocols = [protocols]; + } + // Uses setTimeout() to make sure __createFlash() runs after the caller sets ws.onopen etc. + // Otherwise, when onopen fires immediately, onopen is called before it is set. + setTimeout(function() { + WebSocket.__addTask(function() { + WebSocket.__flash.create( + self.__id, url, protocols, proxyHost || null, proxyPort || 0, headers || null); + }); + }, 0); + }; + + /** + * Send data to the web socket. + * @param {string} data The data to send to the socket. + * @return {boolean} True for success, false for failure. + */ + WebSocket.prototype.send = function(data) { + if (this.readyState == WebSocket.CONNECTING) { + throw "INVALID_STATE_ERR: Web Socket connection has not been established"; + } + // We use encodeURIComponent() here, because FABridge doesn't work if + // the argument includes some characters. We don't use escape() here + // because of this: + // https://developer.mozilla.org/en/Core_JavaScript_1.5_Guide/Functions#escape_and_unescape_Functions + // But it looks decodeURIComponent(encodeURIComponent(s)) doesn't + // preserve all Unicode characters either e.g. "\uffff" in Firefox. + // Note by wtritch: Hopefully this will not be necessary using ExternalInterface. Will require + // additional testing. + var result = WebSocket.__flash.send(this.__id, encodeURIComponent(data)); + if (result < 0) { // success + return true; + } else { + this.bufferedAmount += result; + return false; + } + }; + + /** + * Close this web socket gracefully. + */ + WebSocket.prototype.close = function() { + if (this.readyState == WebSocket.CLOSED || this.readyState == WebSocket.CLOSING) { + return; + } + this.readyState = WebSocket.CLOSING; + WebSocket.__flash.close(this.__id); + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.addEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) { + this.__events[type] = []; + } + this.__events[type].push(listener); + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {string} type + * @param {function} listener + * @param {boolean} useCapture + * @return void + */ + WebSocket.prototype.removeEventListener = function(type, listener, useCapture) { + if (!(type in this.__events)) return; + var events = this.__events[type]; + for (var i = events.length - 1; i >= 0; --i) { + if (events[i] === listener) { + events.splice(i, 1); + break; + } + } + }; + + /** + * Implementation of {@link DOM 2 EventTarget Interface} + * + * @param {Event} event + * @return void + */ + WebSocket.prototype.dispatchEvent = function(event) { + var events = this.__events[event.type] || []; + for (var i = 0; i < events.length; ++i) { + events[i](event); + } + var handler = this["on" + event.type]; + if (handler) handler(event); + }; + + /** + * Handles an event from Flash. + * @param {Object} flashEvent + */ + WebSocket.prototype.__handleEvent = function(flashEvent) { + if ("readyState" in flashEvent) { + this.readyState = flashEvent.readyState; + } + if ("protocol" in flashEvent) { + this.protocol = flashEvent.protocol; + } + + var jsEvent; + if (flashEvent.type == "open" || flashEvent.type == "error") { + jsEvent = this.__createSimpleEvent(flashEvent.type); + } else if (flashEvent.type == "close") { + // TODO implement jsEvent.wasClean + jsEvent = this.__createSimpleEvent("close"); + } else if (flashEvent.type == "message") { + var data = decodeURIComponent(flashEvent.message); + jsEvent = this.__createMessageEvent("message", data); + } else { + throw "unknown event type: " + flashEvent.type; + } + + this.dispatchEvent(jsEvent); + }; + + WebSocket.prototype.__createSimpleEvent = function(type) { + if (document.createEvent && window.Event) { + var event = document.createEvent("Event"); + event.initEvent(type, false, false); + return event; + } else { + return {type: type, bubbles: false, cancelable: false}; + } + }; + + WebSocket.prototype.__createMessageEvent = function(type, data) { + if (document.createEvent && window.MessageEvent && !window.opera) { + var event = document.createEvent("MessageEvent"); + event.initMessageEvent("message", false, false, data, null, null, window, null); + return event; + } else { + // IE and Opera, the latter one truncates the data parameter after any 0x00 bytes. + return {type: type, data: data, bubbles: false, cancelable: false}; + } + }; + + /** + * Define the WebSocket readyState enumeration. + */ + WebSocket.CONNECTING = 0; + WebSocket.OPEN = 1; + WebSocket.CLOSING = 2; + WebSocket.CLOSED = 3; + + WebSocket.__flash = null; + WebSocket.__instances = {}; + WebSocket.__tasks = []; + WebSocket.__nextId = 0; + + /** + * Load a new flash security policy file. + * @param {string} url + */ + WebSocket.loadFlashPolicyFile = function(url){ + WebSocket.__addTask(function() { + WebSocket.__flash.loadManualPolicyFile(url); + }); + }; + + /** + * Loads WebSocketMain.swf and creates WebSocketMain object in Flash. + */ + WebSocket.__initialize = function() { + if (WebSocket.__flash) return; + + if (WebSocket.__swfLocation) { + // For backword compatibility. + window.WEB_SOCKET_SWF_LOCATION = WebSocket.__swfLocation; + } + if (!window.WEB_SOCKET_SWF_LOCATION) { + console.error("[WebSocket] set WEB_SOCKET_SWF_LOCATION to location of WebSocketMain.swf"); + return; + } + var container = document.createElement("div"); + container.id = "webSocketContainer"; + // Hides Flash box. We cannot use display: none or visibility: hidden because it prevents + // Flash from loading at least in IE. So we move it out of the screen at (-100, -100). + // But this even doesn't work with Flash Lite (e.g. in Droid Incredible). So with Flash + // Lite, we put it at (0, 0). This shows 1x1 box visible at left-top corner but this is + // the best we can do as far as we know now. + container.style.position = "absolute"; + if (WebSocket.__isFlashLite()) { + container.style.left = "0px"; + container.style.top = "0px"; + } else { + container.style.left = "-100px"; + container.style.top = "-100px"; + } + var holder = document.createElement("div"); + holder.id = "webSocketFlash"; + container.appendChild(holder); + document.body.appendChild(container); + // See this article for hasPriority: + // http://help.adobe.com/en_US/as3/mobile/WS4bebcd66a74275c36cfb8137124318eebc6-7ffd.html + swfobject.embedSWF( + WEB_SOCKET_SWF_LOCATION, + "webSocketFlash", + "1" /* width */, + "1" /* height */, + "10.0.0" /* SWF version */, + null, + null, + {hasPriority: true, swliveconnect : true, allowScriptAccess: "always"}, + null, + function(e) { + if (!e.success) { + console.error("[WebSocket] swfobject.embedSWF failed"); + } + }); + }; + + /** + * Called by Flash to notify JS that it's fully loaded and ready + * for communication. + */ + WebSocket.__onFlashInitialized = function() { + // We need to set a timeout here to avoid round-trip calls + // to flash during the initialization process. + setTimeout(function() { + WebSocket.__flash = document.getElementById("webSocketFlash"); + WebSocket.__flash.setCallerUrl(location.href); + WebSocket.__flash.setDebug(!!window.WEB_SOCKET_DEBUG); + for (var i = 0; i < WebSocket.__tasks.length; ++i) { + WebSocket.__tasks[i](); + } + WebSocket.__tasks = []; + }, 0); + }; + + /** + * Called by Flash to notify WebSockets events are fired. + */ + WebSocket.__onFlashEvent = function() { + setTimeout(function() { + try { + // Gets events using receiveEvents() instead of getting it from event object + // of Flash event. This is to make sure to keep message order. + // It seems sometimes Flash events don't arrive in the same order as they are sent. + var events = WebSocket.__flash.receiveEvents(); + for (var i = 0; i < events.length; ++i) { + WebSocket.__instances[events[i].webSocketId].__handleEvent(events[i]); + } + } catch (e) { + console.error(e); + } + }, 0); + return true; + }; + + // Called by Flash. + WebSocket.__log = function(message) { + console.log(decodeURIComponent(message)); + }; + + // Called by Flash. + WebSocket.__error = function(message) { + console.error(decodeURIComponent(message)); + }; + + WebSocket.__addTask = function(task) { + if (WebSocket.__flash) { + task(); + } else { + WebSocket.__tasks.push(task); + } + }; + + /** + * Test if the browser is running flash lite. + * @return {boolean} True if flash lite is running, false otherwise. + */ + WebSocket.__isFlashLite = function() { + if (!window.navigator || !window.navigator.mimeTypes) { + return false; + } + var mimeType = window.navigator.mimeTypes["application/x-shockwave-flash"]; + if (!mimeType || !mimeType.enabledPlugin || !mimeType.enabledPlugin.filename) { + return false; + } + return mimeType.enabledPlugin.filename.match(/flashlite/i) ? true : false; + }; + + if (!window.WEB_SOCKET_DISABLE_AUTO_INITIALIZATION) { + if (window.addEventListener) { + window.addEventListener("load", function(){ + WebSocket.__initialize(); + }, false); + } else { + window.attachEvent("onload", function(){ + WebSocket.__initialize(); + }); + } + } + +})(); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + + /** + * Expose constructor. + * + * @api public + */ + + exports.XHR = XHR; + + /** + * XHR constructor + * + * @costructor + * @api public + */ + + function XHR (socket) { + if (!socket) return; + + io.Transport.apply(this, arguments); + this.sendBuffer = []; + }; + + /** + * Inherits from Transport. + */ + + io.util.inherit(XHR, io.Transport); + + /** + * Establish a connection + * + * @returns {Transport} + * @api public + */ + + XHR.prototype.open = function () { + this.socket.setBuffer(false); + this.onOpen(); + this.get(); + + // we need to make sure the request succeeds since we have no indication + // whether the request opened or not until it succeeded. + this.setCloseTimeout(); + + return this; + }; + + /** + * Check if we need to send data to the Socket.IO server, if we have data in our + * buffer we encode it and forward it to the `post` method. + * + * @api private + */ + + XHR.prototype.payload = function (payload) { + var msgs = []; + + for (var i = 0, l = payload.length; i < l; i++) { + msgs.push(io.parser.encodePacket(payload[i])); + } + + this.send(io.parser.encodePayload(msgs)); + }; + + /** + * Send data to the Socket.IO server. + * + * @param data The message + * @returns {Transport} + * @api public + */ + + XHR.prototype.send = function (data) { + this.post(data); + return this; + }; + + /** + * Posts a encoded message to the Socket.IO server. + * + * @param {String} data A encoded message. + * @api private + */ + + function empty () { }; + + XHR.prototype.post = function (data) { + var self = this; + this.socket.setBuffer(true); + + function stateChange () { + if (this.readyState == 4) { + this.onreadystatechange = empty; + self.posting = false; + + if (this.status == 200){ + self.socket.setBuffer(false); + } else { + self.onClose(); + } + } + } + + function onload () { + this.onload = empty; + self.socket.setBuffer(false); + }; + + this.sendXHR = this.request('POST'); + + if (global.XDomainRequest && this.sendXHR instanceof XDomainRequest) { + this.sendXHR.onload = this.sendXHR.onerror = onload; + } else { + this.sendXHR.onreadystatechange = stateChange; + } + + this.sendXHR.send(data); + }; + + /** + * Disconnects the established `XHR` connection. + * + * @returns {Transport} + * @api public + */ + + XHR.prototype.close = function () { + this.onClose(); + return this; + }; + + /** + * Generates a configured XHR request + * + * @param {String} url The url that needs to be requested. + * @param {String} method The method the request should use. + * @returns {XMLHttpRequest} + * @api private + */ + + XHR.prototype.request = function (method) { + var req = io.util.request(this.socket.isXDomain()) + , query = io.util.query(this.socket.options.query, 't=' + +new Date); + + req.open(method || 'GET', this.prepareUrl() + query, true); + + if (method == 'POST') { + try { + if (req.setRequestHeader) { + req.setRequestHeader('Content-type', 'text/plain;charset=UTF-8'); + } else { + // XDomainRequest + req.contentType = 'text/plain'; + } + } catch (e) {} + } + + return req; + }; + + /** + * Returns the scheme to use for the transport URLs. + * + * @api private + */ + + XHR.prototype.scheme = function () { + return this.socket.options.secure ? 'https' : 'http'; + }; + + /** + * Check if the XHR transports are supported + * + * @param {Boolean} xdomain Check if we support cross domain requests. + * @returns {Boolean} + * @api public + */ + + XHR.check = function (socket, xdomain) { + try { + var request = io.util.request(xdomain), + usesXDomReq = (global.XDomainRequest && request instanceof XDomainRequest), + socketProtocol = (socket && socket.options && socket.options.secure ? 'https:' : 'http:'), + isXProtocol = (global.location && socketProtocol != global.location.protocol); + if (request && !(usesXDomReq && isXProtocol)) { + return true; + } + } catch(e) {} + + return false; + }; + + /** + * Check if the XHR transport supports cross domain requests. + * + * @returns {Boolean} + * @api public + */ + + XHR.xdomainCheck = function (socket) { + return XHR.check(socket, true); + }; + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports + , this +); +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io) { + + /** + * Expose constructor. + */ + + exports.htmlfile = HTMLFile; + + /** + * The HTMLFile transport creates a `forever iframe` based transport + * for Internet Explorer. Regular forever iframe implementations will + * continuously trigger the browsers buzy indicators. If the forever iframe + * is created inside a `htmlfile` these indicators will not be trigged. + * + * @constructor + * @extends {io.Transport.XHR} + * @api public + */ + + function HTMLFile (socket) { + io.Transport.XHR.apply(this, arguments); + }; + + /** + * Inherits from XHR transport. + */ + + io.util.inherit(HTMLFile, io.Transport.XHR); + + /** + * Transport name + * + * @api public + */ + + HTMLFile.prototype.name = 'htmlfile'; + + /** + * Creates a new Ac...eX `htmlfile` with a forever loading iframe + * that can be used to listen to messages. Inside the generated + * `htmlfile` a reference will be made to the HTMLFile transport. + * + * @api private + */ + + HTMLFile.prototype.get = function () { + this.doc = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); + this.doc.open(); + this.doc.write(''); + this.doc.close(); + this.doc.parentWindow.s = this; + + var iframeC = this.doc.createElement('div'); + iframeC.className = 'socketio'; + + this.doc.body.appendChild(iframeC); + this.iframe = this.doc.createElement('iframe'); + + iframeC.appendChild(this.iframe); + + var self = this + , query = io.util.query(this.socket.options.query, 't='+ +new Date); + + this.iframe.src = this.prepareUrl() + query; + + io.util.on(window, 'unload', function () { + self.destroy(); + }); + }; + + /** + * The Socket.IO server will write script tags inside the forever + * iframe, this function will be used as callback for the incoming + * information. + * + * @param {String} data The message + * @param {document} doc Reference to the context + * @api private + */ + + HTMLFile.prototype._ = function (data, doc) { + this.onData(data); + try { + var script = doc.getElementsByTagName('script')[0]; + script.parentNode.removeChild(script); + } catch (e) { } + }; + + /** + * Destroy the established connection, iframe and `htmlfile`. + * And calls the `CollectGarbage` function of Internet Explorer + * to release the memory. + * + * @api private + */ + + HTMLFile.prototype.destroy = function () { + if (this.iframe){ + try { + this.iframe.src = 'about:blank'; + } catch(e){} + + this.doc = null; + this.iframe.parentNode.removeChild(this.iframe); + this.iframe = null; + + CollectGarbage(); + } + }; + + /** + * Disconnects the established connection. + * + * @returns {Transport} Chaining. + * @api public + */ + + HTMLFile.prototype.close = function () { + this.destroy(); + return io.Transport.XHR.prototype.close.call(this); + }; + + /** + * Checks if the browser supports this transport. The browser + * must have an `Ac...eXObject` implementation. + * + * @return {Boolean} + * @api public + */ + + HTMLFile.check = function (socket) { + if (typeof window != "undefined" && (['Active'].concat('Object').join('X')) in window){ + try { + var a = new window[(['Active'].concat('Object').join('X'))]('htmlfile'); + return a && io.Transport.XHR.check(socket); + } catch(e){} + } + return false; + }; + + /** + * Check if cross domain requests are supported. + * + * @returns {Boolean} + * @api public + */ + + HTMLFile.xdomainCheck = function () { + // we can probably do handling for sub-domains, we should + // test that it's cross domain but a subdomain here + return false; + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('htmlfile'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + + /** + * Expose constructor. + */ + + exports['xhr-polling'] = XHRPolling; + + /** + * The XHR-polling transport uses long polling XHR requests to create a + * "persistent" connection with the server. + * + * @constructor + * @api public + */ + + function XHRPolling () { + io.Transport.XHR.apply(this, arguments); + }; + + /** + * Inherits from XHR transport. + */ + + io.util.inherit(XHRPolling, io.Transport.XHR); + + /** + * Merge the properties from XHR transport + */ + + io.util.merge(XHRPolling, io.Transport.XHR); + + /** + * Transport name + * + * @api public + */ + + XHRPolling.prototype.name = 'xhr-polling'; + + /** + * Indicates whether heartbeats is enabled for this transport + * + * @api private + */ + + XHRPolling.prototype.heartbeats = function () { + return false; + }; + + /** + * Establish a connection, for iPhone and Android this will be done once the page + * is loaded. + * + * @returns {Transport} Chaining. + * @api public + */ + + XHRPolling.prototype.open = function () { + var self = this; + + io.Transport.XHR.prototype.open.call(self); + return false; + }; + + /** + * Starts a XHR request to wait for incoming messages. + * + * @api private + */ + + function empty () {}; + + XHRPolling.prototype.get = function () { + if (!this.isOpen) return; + + var self = this; + + function stateChange () { + if (this.readyState == 4) { + this.onreadystatechange = empty; + + if (this.status == 200) { + self.onData(this.responseText); + self.get(); + } else { + self.onClose(); + } + } + }; + + function onload () { + this.onload = empty; + this.onerror = empty; + self.retryCounter = 1; + self.onData(this.responseText); + self.get(); + }; + + function onerror () { + self.retryCounter ++; + if(!self.retryCounter || self.retryCounter > 3) { + self.onClose(); + } else { + self.get(); + } + }; + + this.xhr = this.request(); + + if (global.XDomainRequest && this.xhr instanceof XDomainRequest) { + this.xhr.onload = onload; + this.xhr.onerror = onerror; + } else { + this.xhr.onreadystatechange = stateChange; + } + + this.xhr.send(null); + }; + + /** + * Handle the unclean close behavior. + * + * @api private + */ + + XHRPolling.prototype.onClose = function () { + io.Transport.XHR.prototype.onClose.call(this); + + if (this.xhr) { + this.xhr.onreadystatechange = this.xhr.onload = this.xhr.onerror = empty; + try { + this.xhr.abort(); + } catch(e){} + this.xhr = null; + } + }; + + /** + * Webkit based browsers show a infinit spinner when you start a XHR request + * before the browsers onload event is called so we need to defer opening of + * the transport until the onload event is called. Wrapping the cb in our + * defer method solve this. + * + * @param {Socket} socket The socket instance that needs a transport + * @param {Function} fn The callback + * @api private + */ + + XHRPolling.prototype.ready = function (socket, fn) { + var self = this; + + io.util.defer(function () { + fn.call(self); + }); + }; + + /** + * Add the transport to your public io.transports array. + * + * @api private + */ + + io.transports.push('xhr-polling'); + +})( + 'undefined' != typeof io ? io.Transport : module.exports + , 'undefined' != typeof io ? io : module.parent.exports + , this +); + +/** + * socket.io + * Copyright(c) 2011 LearnBoost + * MIT Licensed + */ + +(function (exports, io, global) { + /** + * There is a way to hide the loading indicator in Firefox. If you create and + * remove a iframe it will stop showing the current loading indicator. + * Unfortunately we can't feature detect that and UA sniffing is evil. + * + * @api private + */ + + var indicator = global.document && "MozAppearance" in + global.document.documentElement.style; + + /** + * Expose constructor. + */ + + exports['jsonp-polling'] = JSONPPolling; + + /** + * The JSONP transport creates an persistent connection by dynamically + * inserting a script tag in the page. This script tag will receive the + * information of the Socket.IO server. When new information is received + * it creates a new script tag for the new data stream. + * + * @constructor + * @extends {io.Transport.xhr-polling} + * @api public + */ + + function JSONPPolling (socket) { + io.Transport['xhr-polling'].apply(this, arguments); + + this.index = io.j.length; + + var self = this; + + io.j.push(function (msg) { + self._(msg); + }); + }; + + /** + * Inherits from XHR polling transport. + */ + + io.util.inherit(JSONPPolling, io.Transport['xhr-polling']); + + /** + * Transport name + * + * @api public + */ + + JSONPPolling.prototype.name = 'jsonp-polling'; + + /** + * Posts a encoded message to the Socket.IO server using an iframe. + * The iframe is used because script tags can create POST based requests. + * The iframe is positioned outside of the view so the user does not + * notice it's existence. + * + * @param {String} data A encoded message. + * @api private + */ + + JSONPPolling.prototype.post = function (data) { + var self = this + , query = io.util.query( + this.socket.options.query + , 't='+ (+new Date) + '&i=' + this.index + ); + + if (!this.form) { + var form = document.createElement('form') + , area = document.createElement('textarea') + , id = this.iframeId = 'socketio_iframe_' + this.index + , iframe; + + form.className = 'socketio'; + form.style.position = 'absolute'; + form.style.top = '0px'; + form.style.left = '0px'; + form.style.display = 'none'; + form.target = id; + form.method = 'POST'; + form.setAttribute('accept-charset', 'utf-8'); + area.name = 'd'; + form.appendChild(area); + document.body.appendChild(form); + + this.form = form; + this.area = area; + } + + this.form.action = this.prepareUrl() + query; + + function complete () { + initIframe(); + self.socket.setBuffer(false); + }; + + function initIframe () { + if (self.iframe) { + self.form.removeChild(self.iframe); + } + + try { + // ie6 dynamic iframes with target="" support (thanks Chris Lambacher) + iframe = document.createElement('