From c82e4f08fcf83e17d537a2dd49d588dc1690bced Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Fri, 18 Dec 2015 11:12:35 -0500 Subject: [PATCH 01/33] Updating dependencies for 3.0 --- CHANGELOG.md | 629 --------------------------------------------------- README.md | 2 +- package.json | 80 +++---- 3 files changed, 42 insertions(+), 669 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 40e1c480..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,629 +0,0 @@ -# Change Log - -## [2.0.1](https://github.com/avoidwork/tenso/tree/2.0.1) (2015-06-17) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/2.0.0...2.0.1) - -## [2.0.0](https://github.com/avoidwork/tenso/tree/2.0.0) (2015-06-15) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.6.1...2.0.0) - -**Implemented enhancements:** - -- Hypermedia should not generate links past leafs [\#46](https://github.com/avoidwork/tenso/issues/46) - -- Admin dashboard [\#44](https://github.com/avoidwork/tenso/issues/44) - -- Hypermedia generation should be a little closer to a faux spec [\#43](https://github.com/avoidwork/tenso/issues/43) - -**Fixed bugs:** - -- Rate limiting should run a head of route validation [\#49](https://github.com/avoidwork/tenso/issues/49) - -- Erroneous 403 response when 405 is expected [\#48](https://github.com/avoidwork/tenso/issues/48) - -**Merged pull requests:** - -- 2.0 [\#45](https://github.com/avoidwork/tenso/pull/45) ([avoidwork](https://github.com/avoidwork)) - -## [1.6.1](https://github.com/avoidwork/tenso/tree/1.6.1) (2015-06-07) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.6.0...1.6.1) - -## [1.6.0](https://github.com/avoidwork/tenso/tree/1.6.0) (2015-06-04) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.5.2...1.6.0) - -## [1.5.2](https://github.com/avoidwork/tenso/tree/1.5.2) (2015-06-02) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.5.1...1.5.2) - -## [1.5.1](https://github.com/avoidwork/tenso/tree/1.5.1) (2015-05-25) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.5.0...1.5.1) - -**Fixed bugs:** - -- An empty request body should be allowed in the browsable interface [\#42](https://github.com/avoidwork/tenso/issues/42) - -## [1.5.0](https://github.com/avoidwork/tenso/tree/1.5.0) (2015-05-25) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.9...1.5.0) - -**Fixed bugs:** - -- `hypermedia\(\)` paginates POST responses [\#41](https://github.com/avoidwork/tenso/issues/41) - -## [1.4.9](https://github.com/avoidwork/tenso/tree/1.4.9) (2015-04-19) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.8...1.4.9) - -## [1.4.8](https://github.com/avoidwork/tenso/tree/1.4.8) (2015-04-15) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.7...1.4.8) - -## [1.4.7](https://github.com/avoidwork/tenso/tree/1.4.7) (2015-04-14) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.6...1.4.7) - -## [1.4.6](https://github.com/avoidwork/tenso/tree/1.4.6) (2015-04-14) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.5...1.4.6) - -## [1.4.5](https://github.com/avoidwork/tenso/tree/1.4.5) (2015-04-14) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.4...1.4.5) - -## [1.4.4](https://github.com/avoidwork/tenso/tree/1.4.4) (2015-04-14) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.3...1.4.4) - -**Fixed bugs:** - -- Browsable API `prepare\(\)` doesn't handle meta characters properly [\#40](https://github.com/avoidwork/tenso/issues/40) - -## [1.4.3](https://github.com/avoidwork/tenso/tree/1.4.3) (2015-04-10) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.2...1.4.3) - -## [1.4.2](https://github.com/avoidwork/tenso/tree/1.4.2) (2015-04-09) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.1...1.4.2) - -**Implemented enhancements:** - -- Needs pass through support of non-JSON friendly response bodies [\#39](https://github.com/avoidwork/tenso/issues/39) - -- Need custom parsers [\#32](https://github.com/avoidwork/tenso/issues/32) - -## [1.4.1](https://github.com/avoidwork/tenso/tree/1.4.1) (2015-04-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.4.0...1.4.1) - -## [1.4.0](https://github.com/avoidwork/tenso/tree/1.4.0) (2015-04-02) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.21...1.4.0) - -**Implemented enhancements:** - -- Needs a CSV renderer [\#38](https://github.com/avoidwork/tenso/issues/38) - -- Browsable API "send" button doesn't work on an iPad [\#34](https://github.com/avoidwork/tenso/issues/34) - -- Need custom renderers [\#31](https://github.com/avoidwork/tenso/issues/31) - -## [1.3.21](https://github.com/avoidwork/tenso/tree/1.3.21) (2015-04-01) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.20...1.3.21) - -**Fixed bugs:** - -- Media queries need to be tweaked [\#37](https://github.com/avoidwork/tenso/issues/37) - -- Cannot read property 'x-csrf-token' of undefined [\#36](https://github.com/avoidwork/tenso/issues/36) - -## [1.3.20](https://github.com/avoidwork/tenso/tree/1.3.20) (2015-04-01) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.19...1.3.20) - -## [1.3.19](https://github.com/avoidwork/tenso/tree/1.3.19) (2015-04-01) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.18...1.3.19) - -**Fixed bugs:** - -- Browsable API response error handling is brittle [\#35](https://github.com/avoidwork/tenso/issues/35) - -## [1.3.18](https://github.com/avoidwork/tenso/tree/1.3.18) (2015-03-31) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.17...1.3.18) - -**Implemented enhancements:** - -- Browsable API needs implement other methods [\#24](https://github.com/avoidwork/tenso/issues/24) - -## [1.3.17](https://github.com/avoidwork/tenso/tree/1.3.17) (2015-03-31) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.16...1.3.17) - -## [1.3.16](https://github.com/avoidwork/tenso/tree/1.3.16) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.15...1.3.16) - -## [1.3.15](https://github.com/avoidwork/tenso/tree/1.3.15) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.14...1.3.15) - -## [1.3.14](https://github.com/avoidwork/tenso/tree/1.3.14) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.13...1.3.14) - -## [1.3.13](https://github.com/avoidwork/tenso/tree/1.3.13) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.12...1.3.13) - -## [1.3.12](https://github.com/avoidwork/tenso/tree/1.3.12) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.11...1.3.12) - -## [1.3.11](https://github.com/avoidwork/tenso/tree/1.3.11) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.10...1.3.11) - -## [1.3.10](https://github.com/avoidwork/tenso/tree/1.3.10) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.9...1.3.10) - -## [1.3.9](https://github.com/avoidwork/tenso/tree/1.3.9) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.8...1.3.9) - -## [1.3.8](https://github.com/avoidwork/tenso/tree/1.3.8) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.7...1.3.8) - -**Fixed bugs:** - -- Browsable API interface is not responsive [\#28](https://github.com/avoidwork/tenso/issues/28) - -## [1.3.7](https://github.com/avoidwork/tenso/tree/1.3.7) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.6...1.3.7) - -**Implemented enhancements:** - -- Browsable API headers not in alphabetical order [\#30](https://github.com/avoidwork/tenso/issues/30) - -## [1.3.6](https://github.com/avoidwork/tenso/tree/1.3.6) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.5...1.3.6) - -## [1.3.5](https://github.com/avoidwork/tenso/tree/1.3.5) (2015-03-30) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.4...1.3.5) - -**Fixed bugs:** - -- Response should not be cachable if `x-ratelimit-limit` is set [\#29](https://github.com/avoidwork/tenso/issues/29) - -## [1.3.4](https://github.com/avoidwork/tenso/tree/1.3.4) (2015-03-29) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.3...1.3.4) - -**Implemented enhancements:** - -- Browsable API is missing 'already decorated' response headers [\#27](https://github.com/avoidwork/tenso/issues/27) - -**Fixed bugs:** - -- Browsable API is missing 'already decorated' response headers [\#27](https://github.com/avoidwork/tenso/issues/27) - -## [1.3.3](https://github.com/avoidwork/tenso/tree/1.3.3) (2015-03-29) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.2...1.3.3) - -**Fixed bugs:** - -- Browsable API URL header has wrong protocol [\#26](https://github.com/avoidwork/tenso/issues/26) - -## [1.3.2](https://github.com/avoidwork/tenso/tree/1.3.2) (2015-03-29) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.1...1.3.2) - -**Implemented enhancements:** - -- Browsable API needs to support HTTP/HTTPS [\#25](https://github.com/avoidwork/tenso/issues/25) - -**Fixed bugs:** - -- Browsable API needs to support HTTP/HTTPS [\#25](https://github.com/avoidwork/tenso/issues/25) - -## [1.3.1](https://github.com/avoidwork/tenso/tree/1.3.1) (2015-03-29) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.3.0...1.3.1) - -## [1.3.0](https://github.com/avoidwork/tenso/tree/1.3.0) (2015-03-29) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.2.6...1.3.0) - -## [1.2.6](https://github.com/avoidwork/tenso/tree/1.2.6) (2015-03-26) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.2.5...1.2.6) - -## [1.2.5](https://github.com/avoidwork/tenso/tree/1.2.5) (2015-03-20) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.2.4...1.2.5) - -## [1.2.4](https://github.com/avoidwork/tenso/tree/1.2.4) (2015-03-19) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.2.3...1.2.4) - -**Implemented enhancements:** - -- Remove redundant caching [\#23](https://github.com/avoidwork/tenso/issues/23) - -## [1.2.3](https://github.com/avoidwork/tenso/tree/1.2.3) (2015-03-13) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.2.2...1.2.3) - -## [1.2.2](https://github.com/avoidwork/tenso/tree/1.2.2) (2015-02-25) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.2.1...1.2.2) - -## [1.2.1](https://github.com/avoidwork/tenso/tree/1.2.1) (2015-02-13) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.2.0...1.2.1) - -## [1.2.0](https://github.com/avoidwork/tenso/tree/1.2.0) (2015-02-04) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.1.1...1.2.0) - -## [1.1.1](https://github.com/avoidwork/tenso/tree/1.1.1) (2015-02-01) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.1.0...1.1.1) - -## [1.1.0](https://github.com/avoidwork/tenso/tree/1.1.0) (2015-01-11) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.0.1...1.1.0) - -**Implemented enhancements:** - -- Audit CSRF implementation [\#22](https://github.com/avoidwork/tenso/issues/22) - -**Merged pull requests:** - -- Add a Gitter chat badge to README.md [\#21](https://github.com/avoidwork/tenso/pull/21) ([gitter-badger](https://github.com/gitter-badger)) - -## [1.0.1](https://github.com/avoidwork/tenso/tree/1.0.1) (2014-12-14) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/1.0.0...1.0.1) - -## [1.0.0](https://github.com/avoidwork/tenso/tree/1.0.0) (2014-12-01) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.27...1.0.0) - -## [0.9.27](https://github.com/avoidwork/tenso/tree/0.9.27) (2014-11-25) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.26...0.9.27) - -## [0.9.26](https://github.com/avoidwork/tenso/tree/0.9.26) (2014-11-22) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.25...0.9.26) - -## [0.9.25](https://github.com/avoidwork/tenso/tree/0.9.25) (2014-11-22) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.24...0.9.25) - -## [0.9.24](https://github.com/avoidwork/tenso/tree/0.9.24) (2014-11-19) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.23...0.9.24) - -## [0.9.23](https://github.com/avoidwork/tenso/tree/0.9.23) (2014-11-07) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.22...0.9.23) - -## [0.9.22](https://github.com/avoidwork/tenso/tree/0.9.22) (2014-11-06) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.21...0.9.22) - -## [0.9.21](https://github.com/avoidwork/tenso/tree/0.9.21) (2014-11-06) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.20...0.9.21) - -## [0.9.20](https://github.com/avoidwork/tenso/tree/0.9.20) (2014-11-06) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.19...0.9.20) - -## [0.9.19](https://github.com/avoidwork/tenso/tree/0.9.19) (2014-11-05) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.18...0.9.19) - -## [0.9.18](https://github.com/avoidwork/tenso/tree/0.9.18) (2014-11-05) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.17...0.9.18) - -## [0.9.17](https://github.com/avoidwork/tenso/tree/0.9.17) (2014-11-05) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.16...0.9.17) - -**Implemented enhancements:** - -- Needs rate limiting tiers [\#19](https://github.com/avoidwork/tenso/issues/19) - -## [0.9.16](https://github.com/avoidwork/tenso/tree/0.9.16) (2014-11-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.15...0.9.16) - -**Implemented enhancements:** - -- Add `X-CSRFToken` to CORS 'allowed' header [\#16](https://github.com/avoidwork/tenso/issues/16) - -**Fixed bugs:** - -- Implement `X-Forwarded-Proto` header in `hypermedia\(\)` [\#18](https://github.com/avoidwork/tenso/issues/18) - -- Rate limiting count is off by 1 when returning with a valid session [\#17](https://github.com/avoidwork/tenso/issues/17) - -## [0.9.15](https://github.com/avoidwork/tenso/tree/0.9.15) (2014-10-24) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.14...0.9.15) - -## [0.9.14](https://github.com/avoidwork/tenso/tree/0.9.14) (2014-10-23) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.13...0.9.14) - -**Implemented enhancements:** - -- Needs kerberos support [\#11](https://github.com/avoidwork/tenso/issues/11) - -## [0.9.13](https://github.com/avoidwork/tenso/tree/0.9.13) (2014-10-20) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.12...0.9.13) - -## [0.9.12](https://github.com/avoidwork/tenso/tree/0.9.12) (2014-10-16) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.11...0.9.12) - -## [0.9.11](https://github.com/avoidwork/tenso/tree/0.9.11) (2014-10-09) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.10...0.9.11) - -## [0.9.10](https://github.com/avoidwork/tenso/tree/0.9.10) (2014-10-07) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.9...0.9.10) - -## [0.9.9](https://github.com/avoidwork/tenso/tree/0.9.9) (2014-10-04) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.8...0.9.9) - -## [0.9.8](https://github.com/avoidwork/tenso/tree/0.9.8) (2014-10-04) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.7...0.9.8) - -## [0.9.7](https://github.com/avoidwork/tenso/tree/0.9.7) (2014-10-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.6...0.9.7) - -## [0.9.6](https://github.com/avoidwork/tenso/tree/0.9.6) (2014-10-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.5...0.9.6) - -## [0.9.5](https://github.com/avoidwork/tenso/tree/0.9.5) (2014-09-20) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.4...0.9.5) - -## [0.9.4](https://github.com/avoidwork/tenso/tree/0.9.4) (2014-09-18) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.3...0.9.4) - -## [0.9.3](https://github.com/avoidwork/tenso/tree/0.9.3) (2014-09-18) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.2...0.9.3) - -## [0.9.2](https://github.com/avoidwork/tenso/tree/0.9.2) (2014-09-12) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.1...0.9.2) - -## [0.9.1](https://github.com/avoidwork/tenso/tree/0.9.1) (2014-09-09) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.9.0...0.9.1) - -## [0.9.0](https://github.com/avoidwork/tenso/tree/0.9.0) (2014-09-06) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.8.3...0.9.0) - -## [0.8.3](https://github.com/avoidwork/tenso/tree/0.8.3) (2014-09-06) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.8.2...0.8.3) - -## [0.8.2](https://github.com/avoidwork/tenso/tree/0.8.2) (2014-09-06) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.8.1...0.8.2) - -## [0.8.1](https://github.com/avoidwork/tenso/tree/0.8.1) (2014-09-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.8.0...0.8.1) - -## [0.8.0](https://github.com/avoidwork/tenso/tree/0.8.0) (2014-09-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.7.2...0.8.0) - -**Closed issues:** - -- "/auth/linkedin" Does not redirect to linkedin. [\#15](https://github.com/avoidwork/tenso/issues/15) - -## [0.7.2](https://github.com/avoidwork/tenso/tree/0.7.2) (2014-09-01) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.7.1...0.7.2) - -## [0.7.1](https://github.com/avoidwork/tenso/tree/0.7.1) (2014-08-28) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.7.0...0.7.1) - -## [0.7.0](https://github.com/avoidwork/tenso/tree/0.7.0) (2014-08-28) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.6.1...0.7.0) - -**Implemented enhancements:** - -- Callback URIs for auth strategies should all for custom redirects [\#14](https://github.com/avoidwork/tenso/issues/14) - -- Refactor `auth\(\)` to support a mix of 'stateless' & 'stateful' strategies [\#13](https://github.com/avoidwork/tenso/issues/13) - -- Needs SAML support [\#12](https://github.com/avoidwork/tenso/issues/12) - -- Needs generic OAuth2 support [\#10](https://github.com/avoidwork/tenso/issues/10) - -## [0.6.1](https://github.com/avoidwork/tenso/tree/0.6.1) (2014-08-25) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.6.0...0.6.1) - -**Closed issues:** - -- Each auth strategy contain unique paths to protect [\#9](https://github.com/avoidwork/tenso/issues/9) - -## [0.6.0](https://github.com/avoidwork/tenso/tree/0.6.0) (2014-08-24) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.5.2...0.6.0) - -**Closed issues:** - -- This is not an issue but a comment [\#8](https://github.com/avoidwork/tenso/issues/8) - -## [0.5.2](https://github.com/avoidwork/tenso/tree/0.5.2) (2014-08-21) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.5.1...0.5.2) - -## [0.5.1](https://github.com/avoidwork/tenso/tree/0.5.1) (2014-08-21) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.5.0...0.5.1) - -## [0.5.0](https://github.com/avoidwork/tenso/tree/0.5.0) (2014-08-21) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.4.4...0.5.0) - -## [0.4.4](https://github.com/avoidwork/tenso/tree/0.4.4) (2014-08-19) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.4.3...0.4.4) - -**Closed issues:** - -- Passport middleware isn't blacklisted in `auth\(\)` [\#7](https://github.com/avoidwork/tenso/issues/7) - -- keymaster\(\) has a logical flaw when handling HEAD or OPTIONS requests [\#6](https://github.com/avoidwork/tenso/issues/6) - -- 404 on OPTIONS [\#5](https://github.com/avoidwork/tenso/issues/5) - -## [0.4.3](https://github.com/avoidwork/tenso/tree/0.4.3) (2014-08-19) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.4.2...0.4.3) - -## [0.4.2](https://github.com/avoidwork/tenso/tree/0.4.2) (2014-08-16) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.4.1...0.4.2) - -## [0.4.1](https://github.com/avoidwork/tenso/tree/0.4.1) (2014-08-16) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.4.0...0.4.1) - -## [0.4.0](https://github.com/avoidwork/tenso/tree/0.4.0) (2014-08-15) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.3.6...0.4.0) - -## [0.3.6](https://github.com/avoidwork/tenso/tree/0.3.6) (2014-08-08) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.3.5...0.3.6) - -## [0.3.5](https://github.com/avoidwork/tenso/tree/0.3.5) (2014-08-07) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.3.4...0.3.5) - -**Fixed bugs:** - -- Middleware that executes `this.respond\(\)` sends erroneous response [\#3](https://github.com/avoidwork/tenso/issues/3) - -## [0.3.4](https://github.com/avoidwork/tenso/tree/0.3.4) (2014-08-07) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.3.3...0.3.4) - -**Implemented enhancements:** - -- Pretty JSON output should be customizable via the `Accept` header [\#4](https://github.com/avoidwork/tenso/issues/4) - -## [0.3.3](https://github.com/avoidwork/tenso/tree/0.3.3) (2014-08-07) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.3.2...0.3.3) - -## [0.3.2](https://github.com/avoidwork/tenso/tree/0.3.2) (2014-08-07) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.3.1...0.3.2) - -**Implemented enhancements:** - -- Compression should be disable if SSL is enabled [\#2](https://github.com/avoidwork/tenso/issues/2) - -**Fixed bugs:** - -- Routes with wildcards/\* do not get handled properly [\#1](https://github.com/avoidwork/tenso/issues/1) - -## [0.3.1](https://github.com/avoidwork/tenso/tree/0.3.1) (2014-08-06) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.3.0...0.3.1) - -## [0.3.0](https://github.com/avoidwork/tenso/tree/0.3.0) (2014-08-05) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.2.0...0.3.0) - -## [0.2.0](https://github.com/avoidwork/tenso/tree/0.2.0) (2014-08-05) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.1.1...0.2.0) - -## [0.1.1](https://github.com/avoidwork/tenso/tree/0.1.1) (2014-08-04) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.1.0...0.1.1) - -## [0.1.0](https://github.com/avoidwork/tenso/tree/0.1.0) (2014-08-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.10...0.1.0) - -## [0.0.10](https://github.com/avoidwork/tenso/tree/0.0.10) (2014-08-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.9...0.0.10) - -## [0.0.9](https://github.com/avoidwork/tenso/tree/0.0.9) (2014-08-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.8...0.0.9) - -## [0.0.8](https://github.com/avoidwork/tenso/tree/0.0.8) (2014-08-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.7...0.0.8) - -## [0.0.7](https://github.com/avoidwork/tenso/tree/0.0.7) (2014-08-03) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.6...0.0.7) - -## [0.0.6](https://github.com/avoidwork/tenso/tree/0.0.6) (2014-08-02) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.5...0.0.6) - -## [0.0.5](https://github.com/avoidwork/tenso/tree/0.0.5) (2014-08-02) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.4...0.0.5) - -## [0.0.4](https://github.com/avoidwork/tenso/tree/0.0.4) (2014-08-02) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.3...0.0.4) - -## [0.0.3](https://github.com/avoidwork/tenso/tree/0.0.3) (2014-08-02) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.2...0.0.3) - -## [0.0.2](https://github.com/avoidwork/tenso/tree/0.0.2) (2014-08-02) - -[Full Changelog](https://github.com/avoidwork/tenso/compare/0.0.1...0.0.2) - -## [0.0.1](https://github.com/avoidwork/tenso/tree/0.0.1) (2014-08-02) - - - -\* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* \ No newline at end of file diff --git a/README.md b/README.md index 5bb0b10a..08b5259a 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ Tensō [![build status](https://secure.travis-ci.org/avoidwork/tenso.svg)](http://travis-ci.org/avoidwork/tenso) [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/avoidwork/tenso?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Tensō is a REST API facade for node.js, designed to simplify the implementation of APIs. +Tensō is a REST API gateway for node.js, designed to simplify the implementation of APIs. Tensō will handle the serialization & creation of hypermedia links, all you have to do is give it `Arrays` or `Objects`. diff --git a/package.json b/package.json index b401c526..46cb41b0 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,9 @@ { "name": "tenso", - "description": "Tensō is a REST API facade for node.js, designed to simplify the implementation of APIs.", - "version": "2.0.7", + "description": "Tensō is a REST API gateway for node.js, designed to simplify the implementation of APIs.", + "version": "3.0.0", "homepage": "http://avoidwork.github.io/tenso", - "author": { - "name": "Jason Mulligan", - "email": "jason.mulligan@avoidwork.com" - }, + "author": "Jason Mulligan ", "repository": { "type": "git", "url": "git://github.com/avoidwork/tenso.git" @@ -17,53 +14,58 @@ "license": "BSD-3-Clause", "main": "lib/tenso", "engines": { - "node": ">=0.10.0" + "node": ">=4.0.0" }, "scripts": { "test": "grunt test" }, "dependencies": { - "connect-redis": "~2.2.0", - "cookie-parser": "~1.3.4", - "express-session": "~1.10.3", + "connect-redis": "^3.0.1", + "cookie-parser": "^1.4.0", + "express-session": "^1.12.1", "keigai": "~1.3.19", - "lusca": "~1.0.2", - "passport": "~0.2.1", - "passport-facebook": "~1.0.3", - "passport-google": "~0.3.0", - "passport-http": "~0.2.2", - "passport-http-bearer": "~1.0.1", - "passport-linkedin": "~0.1.3", - "passport-local": "~1.0.0", - "passport-oauth2": "~1.1.2", - "passport-saml": "~0.9.1", - "passport-twitter": "~1.0.2", + "lusca": "^1.3.0", + "passport": "^0.3.2", + "passport-facebook": "^2.0.0", + "passport-google": "^0.3.0", + "passport-http": "^0.3.0", + "passport-http-bearer": "^1.0.1", + "passport-linkedin": "^1.0.0", + "passport-local": "^1.0.0", + "passport-oauth2": "^1.1.2", + "passport-saml": "^0.14.0", + "passport-twitter": "^1.0.3", "retsu": "^2.0.0", - "tiny-defer": "^1.0.3", - "turtle.io": "~4.1.5", - "yamljs": "^0.2.1" + "tiny-defer": "^1.0.4", + "turtle.io": "^5.0.3", + "yamljs": "^0.2.4" }, "devDependencies": { - "babel-eslint": "^3.1.15", - "grunt": "~0.4.5", - "grunt-babel": "^5.0.0", - "grunt-cli": "~0.1.13", - "grunt-contrib-concat": "~0.5.0", - "grunt-contrib-sass": "~0.8.1", - "grunt-contrib-watch": "~0.6.1", - "grunt-eslint": "^14.0.0", + "babel-eslint": "^4.1.4", + "babel-preset-es2015": "^6.1.2", + "grunt": "^0.4.5", + "grunt-babel": "^6.0.0", + "grunt-cli": "^0.1.13", + "grunt-contrib-concat": "^0.1.3", + "grunt-contrib-sass": "^0.9.2", + "grunt-contrib-watch": "^0.2.0", + "grunt-eslint": "^17.3.1", "grunt-jsdoc": "~0.5.6", - "grunt-mocha-test": "^0.11.0", - "grunt-nsp-package": "^0.0.5", - "grunt-sed": "~0.1.1", - "hippie": "~0.3.0", - "ink-docstrap": "~0.4.12" + "grunt-mocha-test": "^0.12.7", + "grunt-nsp": "^2.1.2", + "hippie": "^0.4.0", + "ink-docstrap": "~0.4.12", + "mocha": "^2.3.2" }, "keywords": [ "REST", "API", - "facade", + "gateway", "server", - "hypermedia" + "hypermedia", + "elastic", + "api", + "app", + "application" ] } From 36404f68bbd2e1a3c84719631330bebb0c680b4b Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Fri, 18 Dec 2015 11:13:58 -0500 Subject: [PATCH 02/33] Updating travis-ci.org config --- .travis.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9cdf9778..e37f2fd7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,10 @@ language: node_js node_js: + - "5.3" + - "5.2" + - "5.1" + - "5.0" - "4.2" - - "4.1" - - "4.0" - - "iojs" - - "0.12" - - "0.10" branches: only: From 27b2c41c8f835b148c4b45e16b6342611a8aa1a7 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Fri, 18 Dec 2015 14:33:04 -0500 Subject: [PATCH 03/33] Initial commit of restructuring code --- Gruntfile.js | 3 + package.json | 1 + src/bootstrap.js | 102 ------- src/clone.js | 10 - src/error.js | 9 - src/hypermedia.js | 149 ---------- src/{factory.js => index.js} | 23 +- src/keymaster.js | 59 ---- src/middleware.js | 74 +++++ src/outro.js | 1 - src/prepare.js | 17 -- src/rate.js | 26 -- src/regex.js | 9 +- src/renderers.js | 23 +- src/response.js | 24 -- src/sanitize.js | 18 -- src/serializers.js | 15 + src/{constructor.js => tenso.js} | 85 +----- src/{auth.js => utility.js} | 463 ++++++++++++++++++++++++++++++- src/xml.js | 45 +++ 20 files changed, 639 insertions(+), 517 deletions(-) delete mode 100644 src/bootstrap.js delete mode 100644 src/clone.js delete mode 100644 src/error.js delete mode 100644 src/hypermedia.js rename src/{factory.js => index.js} (50%) delete mode 100644 src/keymaster.js create mode 100644 src/middleware.js delete mode 100644 src/outro.js delete mode 100644 src/prepare.js delete mode 100644 src/rate.js delete mode 100644 src/response.js delete mode 100644 src/sanitize.js create mode 100644 src/serializers.js rename src/{constructor.js => tenso.js} (60%) rename src/{auth.js => utility.js} (54%) create mode 100644 src/xml.js diff --git a/Gruntfile.js b/Gruntfile.js index 58fd0bbb..75641fee 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -69,6 +69,9 @@ module.exports = function (grunt) { src : ["test/*_test.js"] } }, + nsp: { + package: grunt.file.readJSON("package.json") + }, sass: { dist: { options : { diff --git a/package.json b/package.json index 46cb41b0..ab20eb0d 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "passport-twitter": "^1.0.3", "retsu": "^2.0.0", "tiny-defer": "^1.0.4", + "tiny-uuid4": "^1.0.0", "turtle.io": "^5.0.3", "yamljs": "^0.2.4" }, diff --git a/src/bootstrap.js b/src/bootstrap.js deleted file mode 100644 index a7b2fde6..00000000 --- a/src/bootstrap.js +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Bootstraps an instance of Tenso - * - * @method bootstrap - * @param {Object} obj Tenso instance - * @param {Object} config Application configuration - * @return {Object} Tenso instance - */ -function bootstrap (obj, config) { - let notify = false; - - function decorate (req, res, next) { - res.error = function (status, body) { - return obj.error(req, res, status, body); - }; - - res.redirect = function (uri) { - return obj.redirect(req, res, uri); - }; - - res.respond = function (body, status, headers) { - return obj.respond(req, res, body, status, headers); - }; - - res.send = function (body, status, headers) { - return obj.respond(req, res, body, status, headers); - }; - - next(); - } - - function parse (req, res, next) { - let args, type; - - if (REGEX.body.test(req.method) && req.body !== undefined) { - type = req.headers["content-type"]; - - if (REGEX.encode_form.test(type)) { - args = req.body ? array.chunk(req.body.split(REGEX.body_split), 2) : []; - req.body = {}; - - array.each(args, function (i) { - req.body[i[0]] = coerce(i[1]); - }); - } - - if (REGEX.encode_json.test(type)) { - req.body = json.decode(req.body, true) || req.body; - } - } - - next(); - } - - obj.server.use(decorate).blacklist(decorate); - obj.server.use(parse).blacklist(parse); - - // Bootstrapping configuration - auth(obj, config); - config.headers = config.headers || {}; - config.headers.server = SERVER; - - // Creating status > message map - iterate(obj.server.codes, function (value, key) { - obj.messages[value] = obj.server.messages[key]; - }); - - // Setting routes - iterate(config.routes, function (routes, method) { - iterate(routes, function (arg, route) { - if (typeof arg === "function") { - obj.server[method](route, function (...args) { - arg.apply(obj, args); - }); - } else { - obj.server[method](route, function (req, res) { - obj.respond(req, res, arg); - }); - } - }); - }); - - // Disabling compression over SSL due to BREACH - if (config.ssl.cert && config.ssl.key) { - config.compress = false; - notify = true; - } - - // Starting API server - obj.server.start(config, function (req, res, status, msg) { - let stat = status instanceof Error ? parseInt(status.message, 10) : status, - err = msg instanceof Error ? msg : new Error(msg || obj.messages[stat]); - - error(obj, req, res, stat, err, obj); - }); - - if (notify) { - obj.server.log("Compression over SSL is disabled for your protection", "debug"); - } - - return obj; -} diff --git a/src/clone.js b/src/clone.js deleted file mode 100644 index d479d87e..00000000 --- a/src/clone.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Shallow clones an Object - * - * @method clone - * @param {Mixed} arg To be cloned - * @returns {Mixed} Clone of `arg` - */ -function clone (arg) { - return JSON.parse(JSON.stringify(arg)); -} diff --git a/src/error.js b/src/error.js deleted file mode 100644 index 8d2cda71..00000000 --- a/src/error.js +++ /dev/null @@ -1,9 +0,0 @@ -/** - * Route error handler - * - * @method error - * @return {Undefined} undefined - */ -function error (server, req, res, status, err) { - server.respond(req, res, err, status); -} diff --git a/src/hypermedia.js b/src/hypermedia.js deleted file mode 100644 index adaabcde..00000000 --- a/src/hypermedia.js +++ /dev/null @@ -1,149 +0,0 @@ -/** - * Decorates the `rep` with hypermedia links - * - * Arrays of results are automatically paginated, Objects - * will be parsed and have keys 'lifted' into the 'link' - * Array if a pattern is matched, e.g. "user_(guid|uuid|id|uri|url)" - * will map to "/users/$1" - * - * @method hypermedia - * @param {Object} server TurtleIO instance - * @param {Object} req Client request - * @param {Object} rep Serialized representation - * @param {Object} headers HTTP response headers - * @return {Object} HTTP response body - */ -function hypermedia (server, req, rep, headers) { - let seen = {}, - collection = req.parsed.pathname, - query, page, page_size, nth, root, parent; - - // Parsing the object for hypermedia properties - function parse (obj, rel, item_collection) { - let keys = array.keys(obj), - lrel = rel || "related", - result; - - if (keys.length === 0) { - result = null; - } else { - array.each(keys, function (i) { - let lcollection, uri; - - // If ID like keys are found, and are not URIs, they are assumed to be root collections - if (REGEX.id.test(i) || REGEX.hypermedia.test(i)) { - if (!REGEX.id.test(i)) { - lcollection = i.replace(REGEX.trailing, "").replace(REGEX.trailing_s, "").replace(REGEX.trailing_y, "ie") + "s"; - lrel = "related"; - } else { - lcollection = item_collection; - lrel = "item"; - } - - uri = REGEX.scheme.test(obj[i]) ? obj[i] : ("/" + lcollection + "/" + obj[i]); - - if (uri !== root && !seen[uri]) { - seen[uri] = 1; - - if (server.allowed("get", uri, req.vhost)) { - rep.links.push({uri: uri, rel: lrel}); - } - } - } - }); - - result = obj; - } - - return result; - } - - if (rep.status >= 200 && rep.status <= 206) { - query = req.parsed.query; - page = query.page || 1; - page_size = query.page_size || server.config.pageSize || 5; - root = req.parsed.pathname; - - if (req.parsed.pathname !== "/") { - rep.links.push({ - uri: root.replace(REGEX.trailing_slash, "").replace(REGEX.collection, "$1") || "/", - rel: "collection" - }); - } - - if (rep.data instanceof Array) { - if (req.method === "GET") { - if (isNaN(page) || page <= 0) { - page = 1; - } - - nth = Math.ceil(rep.data.length / page_size); - - if (nth > 1) { - rep.data = array.limit(rep.data, (page - 1) * page_size, page_size); - query.page = 0; - query.page_size = page_size; - - root += "?" + array.keys(query).map(function (i) { - return i + "=" + encodeURIComponent(query[i]); - }).join("&"); - - if (page > 1) { - rep.links.push({uri: root.replace("page=0", "page=1"), rel: "first"}); - } - - if (page - 1 > 1 && page <= nth) { - rep.links.push({uri: root.replace("page=0", "page=" + (page - 1)), rel: "prev"}); - } - - if (page + 1 < nth) { - rep.links.push({uri: root.replace("page=0", "page=" + (page + 1)), rel: "next"}); - } - - if (nth > 0 && page !== nth) { - rep.links.push({uri: root.replace("page=0", "page=" + nth), rel: "last"}); - } - } else { - root += "?" + array.keys(query).map(function (i) { - return i + "=" + encodeURIComponent(query[i]); - }).join("&"); - } - } - - array.each(rep.data, function (i) { - var li = i.toString(), - uri; - - if (li !== collection) { - uri = li.indexOf("//") > -1 || li.indexOf("/") === 0 ? li : (collection + "/" + li).replace(/^\/\//, "/"); - - if (server.allowed("get", uri, req.vhost)) { - rep.links.push({uri: uri, rel: "item"}); - } - } - - if (i instanceof Object) { - parse(i, "item", req.parsed.pathname.replace(REGEX.trailing_slash, "").replace(REGEX.leading, "")); - } - }); - } else if (rep.data instanceof Object) { - parent = req.parsed.pathname.split("/").filter(function (i) { - return i !== ""; - }); - - if (parent.length > 1) { - parent.pop(); - } - - rep.data = parse(rep.data, undefined, array.last(parent)); - } - - if (rep.links.length > 0) { - headers.link = array.keySort(rep.links, "rel, uri").map(function (i) { - return "<" + i.uri + ">; rel=\"" + i.rel + "\""; - }).join(", "); - } - } - - return rep; -} diff --git a/src/factory.js b/src/index.js similarity index 50% rename from src/factory.js rename to src/index.js index 4b3bff2f..51cea8fe 100644 --- a/src/factory.js +++ b/src/index.js @@ -1,14 +1,13 @@ -/** - * Tenso factory - * - * @method factory - * @param {Object} arg [Optional] Configuration - * @return {Object} Tenso instance - */ +const path = require("path"), + root = path.join(__dirname, ".."), + cfg = require(path.join(root, "config.json")), + Tenso = require(path.join(__dirname, "tenso.js")); + utility = require(path.join(__dirname, "utility.js")); + function factory (arg) { let hostname = arg ? arg.hostname || "localhost" : "localhost", vhosts = {}, - config = arg ? merge(clone(CONFIG), arg) : CONFIG, + config = arg ? utility.merge(utility.clone(cfg), arg) : utility.clone(cfg), obj; if (!config.port) { @@ -17,12 +16,16 @@ function factory (arg) { } vhosts[hostname] = "www"; - config.root = path.join(__dirname, ".."); + config.root = root; config.vhosts = vhosts; config.default = hostname; config.template = fs.readFileSync(path.join(config.root, "template.html"), {encoding: "utf8"}); obj = new Tenso(); obj.hostname = hostname; - return bootstrap(obj, config); + return utility.bootstrap(obj, config); } + +factory.version = "{{VERSION}}"; + +module.exports = factory; diff --git a/src/keymaster.js b/src/keymaster.js deleted file mode 100644 index 717eeb6f..00000000 --- a/src/keymaster.js +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Keymaster for the request - * - * @method keymaster - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Function} next Next middleware - * @return {Undefined} undefined - */ -function keymaster (req, res, next) { - let obj = req.server.tenso, - method, result, routes, uri; - - // No authentication, or it's already happened - if (!req.protect || !req.protectAsync || (req.session && req.isAuthenticated())) { - method = REGEX.get_rewrite.test(req.method) ? "get" : req.method.toLowerCase(); - routes = req.server.config.routes[method] || {}; - uri = req.parsed.pathname; - - rate(obj, req, res, function () { - if (uri in routes) { - result = routes[uri]; - - if (typeof result === "function") { - result.call(obj, req, res); - } else { - obj.respond(req, res, result).then(function () { - next(); - }, function (e) { - next(e); - }); - } - } else { - iterate(routes, function (value, key) { - if (new RegExp("^" + key + "$", "i").test(uri)) { - return !(result = value); - } - }); - - if (result) { - if (typeof result === "function") { - result.call(obj, req, res); - next(); - } else { - obj.respond(req, res, result).then(function () { - next(); - }, function (e) { - next(e); - }); - } - } else { - obj.error(req, res, 404); - } - } - }); - } else { - rate(obj, req, res, next); - } -} diff --git a/src/middleware.js b/src/middleware.js new file mode 100644 index 00000000..5613a499 --- /dev/null +++ b/src/middleware.js @@ -0,0 +1,74 @@ +const path = require("path"), + regex = require(path.join(__dirname, "regex.js")), + utility = require(path.join(__dirname, "utility.js")); + +const rateHeaders = [ + "x-ratelimit-limit", + "x-ratelimit-remaining", + "x-ratelimit-reset" +]; + +function rate (req, res, next) { + let server = req.server, + obj = server.tenso, + config = server.config.rate, + results = obj.rate(req, config.override), + valid = results.shift(); + + rateHeaders.forEach(function (i, idx) { + res.setHeader(i, results[idx]); + }); + + if (valid) { + next(); + } else { + obj.error(req, res, config.status || 429, config.message || "Too Many Requests"); + } +} + +function keymaster (req, res, next) { + let obj = req.server.tenso, + method, result, routes, uri; + + // No authentication, or it's already happened + if (!req.protect || !req.protectAsync || (req.session && req.isAuthenticated())) { + method = regex.get_rewrite.test(req.method) ? "get" : req.method.toLowerCase(); + routes = req.server.config.routes[method] || {}; + uri = req.parsed.pathname; + + if (uri in routes) { + result = routes[uri]; + + if (typeof result === "function") { + result.call(obj, req, res); + next(); + } else { + obj.respond(req, res, result).then(next, next); + } + } else { + utility.iterate(routes, function (value, key) { + if (new RegExp("^" + key + "$", "i").test(uri)) { + return !(result = value); + } + }); + + if (result) { + if (typeof result === "function") { + result.call(obj, req, res); + next(); + } else { + obj.respond(req, res, result).then(next, next); + } + } else { + obj.error(req, res, 404); + } + } + } else { + obj.error(req, res, 401); + } +} + +module.exports = { + keymaster: keymaster, + rate: rate +}; diff --git a/src/outro.js b/src/outro.js deleted file mode 100644 index 98acf744..00000000 --- a/src/outro.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = factory; diff --git a/src/prepare.js b/src/prepare.js deleted file mode 100644 index 42a31ec1..00000000 --- a/src/prepare.js +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Prepares a response body - * - * @method prepare - * @param {Mixed} arg [Optional] Response body "data" - * @param {Object} error [Optional] Error instance - * @param {Number} status HTTP status code - * @return {Object} Standardized response body - */ -function prepare (arg, err, status) { - return { - data: arg ? clone(arg) : null, - error: !arg ? (err.message || err || "Something went wrong") : null, - links: [], - status: status || 200 - }; -} diff --git a/src/rate.js b/src/rate.js deleted file mode 100644 index 18d5bc69..00000000 --- a/src/rate.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Rate limiting middleware - * - * @method rate - * @param {Object} obj Tenso instance - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Function} next Next middleware - * @return {Undefined} undefined - */ -function rate (obj, req, res, next) { - let headers = ["x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset"], - config = obj.server.config.rate, - results = obj.rate(req, config.override), - valid = results.shift(); - - array.each(headers, function (i, idx) { - res.setHeader(i, results[idx]); - }); - - if (valid) { - next(); - } else { - obj.error(req, res, config.status || 429, config.message || "Too Many Requests"); - } -} diff --git a/src/regex.js b/src/regex.js index d84b88d8..a8631859 100644 --- a/src/regex.js +++ b/src/regex.js @@ -1,9 +1,4 @@ -/** - * RegExp cache - * - * @type Object - */ -const REGEX = { +const regex = { body: /POST|PUT|PATCH/i, body_split: /&|=/, collection: /(.*)(\/.*)$/, @@ -20,3 +15,5 @@ const REGEX = { trailing_slash: /\/$/, trailing_y: /y$/ }; + +modules.export = regex; diff --git a/src/renderers.js b/src/renderers.js index ed9db6cd..7e81946a 100644 --- a/src/renderers.js +++ b/src/renderers.js @@ -1,8 +1,19 @@ -/** - * Renderers - * - * @type {Object} - */ +const array = require("retsu"), + xml = require("tiny-xml"), + yaml = require("yamljs"); + +function sanitize (arg) { + let output = arg; + + if (typeof arg === "string") { + [["<", "<"], [">", ">"]].forEach(function (i) { + output = output.replace(new RegExp(i[0], "g"), i[1]); + }); + } + + return output; +} + let renderers = { csv: { fn: function (arg, req) { @@ -56,3 +67,5 @@ let renderers = { header: "application/xml" } }; + +modules.export = renderers; diff --git a/src/response.js b/src/response.js deleted file mode 100644 index 59fcea31..00000000 --- a/src/response.js +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Creates a response - * - * @method response - * @param {Mixed} arg Unserialized response body - * @param {Number} status HTTP status, default is `200` - * @return {Object} Response body - */ -function response (arg, status) { - let err = arg instanceof Error, - result; - - if (err) { - if (status === undefined) { - throw new Error("Invalid arguments"); - } - - result = prepare(null, arg, status); - } else { - result = prepare(arg, null, status); - } - - return result; -} diff --git a/src/sanitize.js b/src/sanitize.js deleted file mode 100644 index d0720cf2..00000000 --- a/src/sanitize.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Sanitizes outbound Strings to avoid XSS issues - * - * @method sanitize - * @param {String} arg String to sanitize - * @return {String} Sanitized String - */ -function sanitize (arg) { - let output = arg; - - if (typeof arg === "string") { - array.each([["<", "<"], [">", ">"]], function (i) { - output = output.replace(new RegExp(i[0], "g"), i[1]); - }); - } - - return output; -} diff --git a/src/serializers.js b/src/serializers.js new file mode 100644 index 00000000..a14b02e1 --- /dev/null +++ b/src/serializers.js @@ -0,0 +1,15 @@ +function tenso (arg, err, status) { + return { + data: arg !== null ? arg : null, + error: arg === null ? (err.message || err || "Something went wrong") : null, + links: [], + status: status || 200 + }; +} + +let serializers = { + default: "tenso", + tenso: tenso +}; + +modules.export = serializers; diff --git a/src/constructor.js b/src/tenso.js similarity index 60% rename from src/constructor.js rename to src/tenso.js index 445468ff..2baf191a 100644 --- a/src/constructor.js +++ b/src/tenso.js @@ -1,9 +1,4 @@ class Tenso { - /** - * Tenso - * - * @constructor - */ constructor () { this.hostname = ""; this.messages = {}; @@ -13,31 +8,12 @@ class Tenso { this.version = VERSION; } - /** - * Sends an Error to the Client - * - * @method redirect - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Number} status Response status - * @param {Object} arg Response body - */ error (req, res, status, arg) { this.server.error(req, res, status, arg); return this; } - /** - * Returns rate limit information for Client request - * - * @method rate - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} fn [Optional] Override default rate limit - * @return {Array} Array of rate limit information `[valid, total, remaining, reset]` - */ rate (req, fn) { let config = this.server.config.rate, id = req.sessionID || req.ip, @@ -76,36 +52,16 @@ class Tenso { return [valid, limit, remaining, reset]; } - /** - * Redirects the Client - * - * @method redirect - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Mixed} uri Target URI - * @return {Object} {@link Tenso} - */ - redirect (req, res, uri) { - this.server.respond(req, res, this.server.messages.NO_CONTENT, this.server.codes.FOUND, {location: uri}); + redirect (req, res, uri, perm = false) { + this.server.respond(req, res, this.server.messages.NO_CONTENT, this.server.codes[!perm ? "FOUND" : "MOVED"], {location: uri}); return this; } - /** - * Renders a response body, defaults to JSON - * - * @method render - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} arg HTTP response body - * @param {Object} headers HTTP response headers - * @return {String} HTTP response body - */ render (req, arg, headers) { - let accept = req.parsed.query.format || req.headers.accept || "application/json"; - let accepts = string.explode(accept, ";"); - let format = "json"; + let accept = req.parsed.query.format || req.headers.accept || "application/json", + accepts = string.explode(accept, ";"), + format = "json"; array.each(this.server.config.renderers || [], function (i) { let found = false; @@ -113,8 +69,7 @@ class Tenso { array.each(accepts, function (x) { if (x.indexOf(i) > -1) { format = i; - found = true; - return false; + return !(found = true); } }); @@ -128,16 +83,6 @@ class Tenso { return renderers[format].fn(arg, req, headers, format === "html" ? this.server.config.template : undefined); } - /** - * Registers a renderer - * - * @method renderer - * @memberOf Tenso - * @param {String} name Name of the renderer, e.g. "html" - * @param {Function} fn Function accepts `arg, req, headers, template` - * @param {String} mimetype Content-Type value - * @return {Object} {@link Tenso} - */ renderer (name, fn, mimetype) { renderers[name] = {fn: fn, header: mimetype}; array.add(this.server.config.renderers, name); @@ -145,18 +90,6 @@ class Tenso { return this; } - /** - * Sends a response to the Client - * - * @method respond - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Mixed} arg Response body - * @param {Number} status Response status - * @param {Object} headers Response headers - * @return {Object} Defer - */ respond (req, res, arg, status, headers) { let resStatus = status || 200, defer = deferred(), @@ -196,3 +129,9 @@ class Tenso { return defer.promise; } } + +module.exports = Tenso; + +/*function error (server, req, res, status, err) { + server.respond(req, res, err, status); +}*/ diff --git a/src/auth.js b/src/utility.js similarity index 54% rename from src/auth.js rename to src/utility.js index e686568a..af302cfa 100644 --- a/src/auth.js +++ b/src/utility.js @@ -1,11 +1,419 @@ -/** - * Setups up authentication - * - * @method auth - * @param {Object} obj Tenso instance - * @param {Object} config Tenso configuration - * @return {Object} Updated Tenso configuration - */ +const path = require("path"), + array = require("retsu"), + regex = require(path.join(__dirname, "regex.js")), + url = require("url"), + session = require("express-session"), + cookie = require("cookie-parser"), + lusca = require("lusca"), + passport = require("passport"), + BasicStrategy = require("passport-http").BasicStrategy, + BearerStrategy = require("passport-http-bearer").Strategy, + FacebookStrategy = require("passport-facebook").Strategy, + GoogleStrategy = require("passport-google").Strategy, + LinkedInStrategy = require("passport-linkedin").Strategy, + LocalStrategy = require("passport-local").Strategy, + OAuth2Strategy = require("passport-oauth2").Strategy, + SAMLStrategy = require("passport-saml").Strategy, + TwitterStrategy = require("passport-twitter").Strategy, + RedisStore = require("connect-redis")(session), + serializers = require(path.join(__dirname, "serializers.js")), + serializer = serializers[serializers.default]; + +function trim (obj) { + return obj.replace(/^(\s+|\t+|\n+)|(\s+|\t+|\n+)$/g, ""); +} + +function explode (obj, arg = ",") { + return trim(obj).split(new RegExp("\\s*" + arg + "\\s*")); +} + +function escape (arg) { + return arg.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&"); +} + +function capitalize (obj, all = false) { + let result; + + if (all) { + result = explode(obj, " ").map(capitalize).join(" "); + } else { + result = obj.charAt(0).toUpperCase() + obj.slice(1); + } + + return result; +} + +function clone (arg) { + return JSON.parse(JSON.stringify(arg)); +} + +function coerce (value) { + let tmp; + + if (value === null || value === undefined) { + return undefined; + } else if (value === "true") { + return true; + } else if (value === "false") { + return false; + } else if (value === "null") { + return null; + } else if (value === "undefined") { + return undefined; + } else if (value === "") { + return value; + } else if (!isNaN(tmp = Number(value))) { + return tmp; + } else if (regex.json_wrap.test(value)) { + return JSON.parse(value); + } else { + return value; + } +} + +function contains (haystack, needle) { + return haystack.indexOf(needle) > -1; +} + +function bootstrap (obj, config) { + let notify = false; + + function decorate (req, res, next) { + res.error = function (status, body) { + return obj.error(req, res, status, body); + }; + + res.redirect = function (uri, perm = true) { + return obj.redirect(req, res, uri, undefined, perm); + }; + + res.respond = function (body, status, headers) { + return obj.respond(req, res, body, status, headers); + }; + + res.send = function (body, status, headers) { + return obj.respond(req, res, body, status, headers); + }; + + next(); + } + + function parse (req, res, next) { + let args, type; + + if (REGEX.body.test(req.method) && req.body !== undefined) { + type = req.headers["content-type"]; + + if (REGEX.encode_form.test(type)) { + args = req.body ? array.chunk(req.body.split(REGEX.body_split), 2) : []; + req.body = {}; + + array.each(args, function (i) { + req.body[i[0]] = coerce(i[1]); + }); + } + + if (REGEX.encode_json.test(type)) { + req.body = json.decode(req.body, true) || req.body; + } + } + + next(); + } + + obj.server.use(decorate).blacklist(decorate); + obj.server.use(parse).blacklist(parse); + + // Bootstrapping configuration + auth(obj, config); + config.headers = config.headers || {}; + config.headers.server = "tenso/{{VERSION}}"; + + // Creating status > message map + iterate(obj.server.codes, function (value, key) { + obj.messages[value] = obj.server.messages[key]; + }); + + // Setting routes + iterate(config.routes, function (routes, method) { + iterate(routes, function (arg, route) { + if (typeof arg === "function") { + obj.server[method](route, function (...args) { + arg.apply(obj, args); + }); + } else { + obj.server[method](route, function (req, res) { + obj.respond(req, res, arg); + }); + } + }); + }); + + // Disabling compression over SSL due to BREACH + if (config.ssl.cert && config.ssl.key) { + config.compress = false; + notify = true; + } + + // Starting API server + obj.server.start(config, function (req, res, status, msg) { + let stat = status instanceof Error ? parseInt(status.message, 10) : status, + err = msg instanceof Error ? msg : new Error(msg || obj.messages[stat]); + + error(obj, req, res, stat, err, obj); + }); + + if (notify) { + obj.server.log("Compression over SSL is disabled for your protection", "debug"); + } + + return obj; +} + +function hypermedia (server, req, rep, headers) { + let seen = {}, + collection = req.parsed.pathname, + query, page, page_size, nth, root, parent; + + // Parsing the object for hypermedia properties + function parse (obj, rel, item_collection) { + let keys = array.keys(obj), + lrel = rel || "related", + result; + + if (keys.length === 0) { + result = null; + } else { + array.each(keys, function (i) { + let lcollection, uri; + + // If ID like keys are found, and are not URIs, they are assumed to be root collections + if (REGEX.id.test(i) || REGEX.hypermedia.test(i)) { + if (!REGEX.id.test(i)) { + lcollection = i.replace(REGEX.trailing, "").replace(REGEX.trailing_s, "").replace(REGEX.trailing_y, "ie") + "s"; + lrel = "related"; + } else { + lcollection = item_collection; + lrel = "item"; + } + + uri = REGEX.scheme.test(obj[i]) ? obj[i] : ("/" + lcollection + "/" + obj[i]); + + if (uri !== root && !seen[uri]) { + seen[uri] = 1; + + if (server.allowed("get", uri, req.vhost)) { + rep.links.push({uri: uri, rel: lrel}); + } + } + } + }); + + result = obj; + } + + return result; + } + + if (rep.status >= 200 && rep.status <= 206) { + query = req.parsed.query; + page = query.page || 1; + page_size = query.page_size || server.config.pageSize || 5; + root = req.parsed.pathname; + + if (req.parsed.pathname !== "/") { + rep.links.push({ + uri: root.replace(REGEX.trailing_slash, "").replace(REGEX.collection, "$1") || "/", + rel: "collection" + }); + } + + if (rep.data instanceof Array) { + if (req.method === "GET") { + if (isNaN(page) || page <= 0) { + page = 1; + } + + nth = Math.ceil(rep.data.length / page_size); + + if (nth > 1) { + rep.data = array.limit(rep.data, (page - 1) * page_size, page_size); + query.page = 0; + query.page_size = page_size; + + root += "?" + array.keys(query).map(function (i) { + return i + "=" + encodeURIComponent(query[i]); + }).join("&"); + + if (page > 1) { + rep.links.push({uri: root.replace("page=0", "page=1"), rel: "first"}); + } + + if (page - 1 > 1 && page <= nth) { + rep.links.push({uri: root.replace("page=0", "page=" + (page - 1)), rel: "prev"}); + } + + if (page + 1 < nth) { + rep.links.push({uri: root.replace("page=0", "page=" + (page + 1)), rel: "next"}); + } + + if (nth > 0 && page !== nth) { + rep.links.push({uri: root.replace("page=0", "page=" + nth), rel: "last"}); + } + } else { + root += "?" + array.keys(query).map(function (i) { + return i + "=" + encodeURIComponent(query[i]); + }).join("&"); + } + } + + array.each(rep.data, function (i) { + var li = i.toString(), + uri; + + if (li !== collection) { + uri = li.indexOf("//") > -1 || li.indexOf("/") === 0 ? li : (collection + "/" + li).replace(/^\/\//, "/"); + + if (server.allowed("get", uri, req.vhost)) { + rep.links.push({uri: uri, rel: "item"}); + } + } + + if (i instanceof Object) { + parse(i, "item", req.parsed.pathname.replace(REGEX.trailing_slash, "").replace(REGEX.leading, "")); + } + }); + } else if (rep.data instanceof Object) { + parent = req.parsed.pathname.split("/").filter(function (i) { + return i !== ""; + }); + + if (parent.length > 1) { + parent.pop(); + } + + rep.data = parse(rep.data, undefined, array.last(parent)); + } + + if (rep.links.length > 0) { + headers.link = array.keySort(rep.links, "rel, uri").map(function (i) { + return "<" + i.uri + ">; rel=\"" + i.rel + "\""; + }).join(", "); + } + } + + return rep; +} + +function isEmpty (obj) { + return trim(obj) === ""; +} + +function iterate (obj, fn) { + if (obj instanceof Object) { + Object.keys(obj).forEach(function (i) { + fn.call(obj, obj[i], i); + }); + } else { + obj.forEach(fn); + } +} + +function merge (a, b) { + if (a instanceof Object && b instanceof Object) { + Object.keys(b).forEach(function (i) { + if (a[i] instanceof Object && b[i] instanceof Object) { + a[i] = merge(a[i], b[i]); + } else if (a[i] instanceof Array && b[i] instanceof Array) { + a[i] = a[i].concat(b[i]); + } else { + a[i] = b[i]; + } + }); + } else if (a instanceof Array && b instanceof Array) { + a = a.concat(b); + } else { + a = b; + } + + return a; +} + +function queryString (qstring = "") { + let obj = {}; + let aresult = qstring.split("?"); + let result; + + if (aresult.length > 1) { + aresult.shift(); + } + + result = aresult.join("?"); + result.split("&").forEach(function (prop) { + let aitem = prop.replace(/\+/g, " ").split("="); + let item; + + if (aitem.length > 2) { + item = [aitem.shift(), aitem.join("=")]; + } else { + item = aitem; + } + + if (isEmpty(item[0])) { + return; + } + + if (item[1] === undefined) { + item[1] = ""; + } else { + item[1] = coerce(decodeURIComponent(item[1])); + } + + if (obj[item[0]] === undefined) { + obj[item[0]] = item[1]; + } else if (obj[item[0]] instanceof Array === false) { + obj[item[0]] = [obj[item[0]]]; + obj[item[0]].push(item[1]); + } else { + obj[item[0]].push(item[1]); + } + }); + + return obj; +} + +function parse (uri) { + let luri = uri; + let idxAscii, idxQ, parsed; + + if (luri === undefined || luri === null) { + luri = ""; + } else { + idxAscii = luri.indexOf("%3F"); + idxQ = luri.indexOf("?"); + + switch (true) { + case idxQ === -1 && idxAscii > -1: + case idxAscii < idxQ: + luri = luri.replace("%3F", "?"); + break; + default: + void 0; + } + } + + parsed = url.parse(luri); + parsed.query = parsed.search ? queryString(parsed.search) : {}; + + iterate(parsed, function (v, k) { + if (v === null) { + parsed[k] = ""; + } + }); + + return parsed; +} + function auth (obj, config) { let ssl = config.ssl.cert && config.ssl.key, proto = "http" + (ssl ? "s" : ""), @@ -466,3 +874,42 @@ function auth (obj, config) { return config; } + +function response (arg, status) { + let err = arg instanceof Error, + result; + + if (err) { + if (status === undefined) { + throw new Error("Invalid arguments"); + } + + result = serializer(null, arg, status); + } else { + result = serializer(arg, null, status); + } + + return result; +} + + + +module.exports = { + auth: auth, + bootstrap: bootstrap, + capitalize: capitalize, + clone: clone, + coerce: coerce, + contains: contains, + explode: explode, + escape: escape, + hypermedia: hypermedia, + isEmpty: isEmpty, + iterate: iterate, + merge: merge, + queryString: queryString, + parse: parse, + response: response, + trim: trim, + xml: xml +}; diff --git a/src/xml.js b/src/xml.js new file mode 100644 index 00000000..73041bf1 --- /dev/null +++ b/src/xml.js @@ -0,0 +1,45 @@ +const xml = { + decode: function (arg) { + return new DOMParser().parseFromString(arg, "text/xml"); + }, + encode: function (arg, wrap = true, top = true, key = "xml") { + let x = wrap ? "<" + key + ">" : ""; + + if (arg !== null && arg.xml) { + arg = arg.xml; + } + + if (arg instanceof Document) { + arg = new XMLSerializer().serializeToString(arg); + } + + if (regex.boolean_number_string.test(typeof arg)) { + x += xml.node(isNaN(key) ? key : "item", arg); + } else if (arg === null || arg === undefined) { + x += "null"; + } else if (arg instanceof Array) { + arg.forEach(function (v) { + x += xml.encode(v, typeof v === "object", false, "item"); + }); + } else if (arg instanceof Object) { + utility.iterate(arg, function (v, k) { + x += xml.encode(v, typeof v === "object", false, k); + }); + } + + x += wrap ? "" : ""; + + if (top) { + x = "" + x; + } + + return x; + }, + node: function (name, value) { + return "v".replace("v", regex.cdata.test(value) ? "" : value).replace(/<(\/)?n>/g, "<$1" + name + ">"); + }, + valid: function (arg) { + return xml.decode(arg).getElementsByTagName("parsererror").length === 0; + } +}; +modules.export = xml; From 5b4e43245b0aa3b4f48565420dc5e70af73d0513 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Fri, 18 Dec 2015 14:34:04 -0500 Subject: [PATCH 04/33] Adding tiny-xml to the party --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index ab20eb0d..44e5d30f 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,7 @@ "retsu": "^2.0.0", "tiny-defer": "^1.0.4", "tiny-uuid4": "^1.0.0", + "tiny-xml": "^1.0.2", "turtle.io": "^5.0.3", "yamljs": "^0.2.4" }, From f56ad4495d54e722039928cfb51d599b61a61a79 Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Fri, 18 Dec 2015 17:48:57 -0500 Subject: [PATCH 05/33] Initial build passing lint --- .eslintrc | 18 +- Gruntfile.js | 80 +-- config.json | 5 +- lib/index.js | 34 ++ lib/middleware.js | 225 ++++++++ lib/regex.js | 21 + lib/renderers.js | 65 +++ lib/serializers.js | 17 + lib/tenso.es6.js | 1279 -------------------------------------------- lib/tenso.js | 1240 +++--------------------------------------- lib/utility.js | 835 +++++++++++++++++++++++++++++ package.json | 6 +- src/index.js | 5 +- src/intro.js | 35 -- src/middleware.js | 174 +++++- src/regex.js | 2 +- src/renderers.js | 8 +- src/serializers.js | 6 +- src/tenso.js | 68 ++- src/utility.js | 741 +++++++++++-------------- src/xml.js | 45 -- src/zuul.js | 29 - 22 files changed, 1836 insertions(+), 3102 deletions(-) create mode 100644 lib/index.js create mode 100644 lib/middleware.js create mode 100644 lib/regex.js create mode 100644 lib/renderers.js create mode 100644 lib/serializers.js delete mode 100644 lib/tenso.es6.js create mode 100644 lib/utility.js delete mode 100644 src/intro.js delete mode 100644 src/xml.js delete mode 100644 src/zuul.js diff --git a/.eslintrc b/.eslintrc index 04fe66df..97386c13 100644 --- a/.eslintrc +++ b/.eslintrc @@ -6,7 +6,8 @@ "env": { "browser": true, "node": true, - "es6": true + "es6": true, + "amd": true }, "ecmaFeatures": { "jsx": true, @@ -32,7 +33,7 @@ "func-names": [0], "func-style": [0, "declaration"], "generator-star-spacing": [2, "after"], - "global-strict": [2, "always"], + "strict": [2, "always"], "guard-for-in": [0], "handle-callback-err": [0], "key-spacing": [2, { "beforeColon": false, "afterColon": true }], @@ -63,7 +64,6 @@ "no-duplicate-case": [2], "no-else-return": [0], "no-empty": [2], - "no-empty-class": [2], "no-empty-label": [2], "no-eq-null": [0], "no-eval": [2], @@ -73,7 +73,7 @@ "no-extra-boolean-cast": [2], "no-extra-parens": [0], "no-extra-semi": [1], - "no-extra-strict": [2], + "no-empty-character-class": [2], "no-fallthrough": [2], "no-floating-decimal": [2], "no-func-assign": [2], @@ -93,7 +93,7 @@ "no-multi-spaces": [2], "no-multi-str": [2], "no-multiple-empty-lines": [2, { "max": 2 }], - "no-native-reassign": [1], + "no-native-reassign": [0], "no-negated-in-lhs": [2], "no-nested-ternary": [0], "no-new": [2], @@ -104,7 +104,7 @@ "no-obj-calls": [2], "no-octal": [2], "no-octal-escape": [2], - "no-param-reassign": [2], + "no-param-reassign": [0], "no-path-concat": [0], "no-plusplus": [0], "no-process-env": [0], @@ -120,7 +120,7 @@ "no-sequences": [2], "no-shadow": [2], "no-shadow-restricted-names": [2], - "no-space-before-semi": [2], + "semi-spacing": [2], "no-spaced-func": [2], "no-sparse-arrays": [2], "no-sync": [0], @@ -138,7 +138,7 @@ "no-void": [0], "no-warning-comments": [0, { "terms": ["todo", "fixme", "xxx"], "location": "start" }], "no-with": [2], - "no-wrap-func": [2], + "no-extra-parens": [2], "one-var": [0], "operator-assignment": [0, "always"], "operator-linebreak": [2, "after"], @@ -173,4 +173,4 @@ "wrap-regex": [2], "yoda": [2, "never", { "exceptRange": true }] } -} \ No newline at end of file +} diff --git a/Gruntfile.js b/Gruntfile.js index 75641fee..d86b74a0 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,65 +1,22 @@ module.exports = function (grunt) { grunt.initConfig({ - pkg : grunt.file.readJSON("package.json"), - concat : { - options : { - banner : "/**\n" + - " * <%= pkg.description %>\n" + - " *\n" + - " * @author <%= pkg.author.name %> <<%= pkg.author.email %>>\n" + - " * @copyright <%= grunt.template.today('yyyy') %> <%= pkg.author.name %>\n" + - " * @license <%= pkg.license %>\n" + - " * @link <%= pkg.homepage %>\n" + - " * @module <%= pkg.name %>\n" + - " * @version <%= pkg.version %>\n" + - " */\n" - }, - dist : { - src : [ - "src/intro.js", - "src/regex.js", - "src/sanitize.js", - "src/renderers.js", - "src/error.js", - "src/zuul.js", - "src/auth.js", - "src/bootstrap.js", - "src/clone.js", - "src/hypermedia.js", - "src/keymaster.js", - "src/prepare.js", - "src/rate.js", - "src/response.js", - "src/constructor.js", - "src/factory.js", - "src/outro.js" - ], - dest : "lib/<%= pkg.name %>.es6.js" - } - }, babel: { options: { - sourceMap: false + sourceMap: false, + presets: ["babel-preset-es2015"] }, dist: { - files: { - "lib/<%= pkg.name %>.js": "lib/<%= pkg.name %>.es6.js" - } + files: [{ + expand: true, + cwd: 'src', + src: ['*.js'], + dest: 'lib', + ext: '.js' + }] } }, eslint: { - target: ["lib/<%= pkg.name %>.es6.js"] - }, - jsdoc : { - dist : { - src: ["lib/<%= pkg.name %>.js", "README.md"], - options: { - destination : "doc", - template : "node_modules/ink-docstrap/template", - configure : "docstrap.json", - "private" : false - } - } + target: ["src/*.js"] }, mochaTest : { options: { @@ -110,20 +67,15 @@ module.exports = function (grunt) { }); // tasks + grunt.loadNpmTasks("grunt-babel"); + grunt.loadNpmTasks("grunt-eslint"); + grunt.loadNpmTasks("grunt-mocha-test"); + grunt.loadNpmTasks("grunt-nsp"); grunt.loadNpmTasks("grunt-sed"); - grunt.loadNpmTasks("grunt-jsdoc"); - grunt.loadNpmTasks("grunt-contrib-concat"); grunt.loadNpmTasks("grunt-contrib-watch"); grunt.loadNpmTasks("grunt-contrib-sass"); - grunt.loadNpmTasks("grunt-mocha-test"); - grunt.loadNpmTasks("grunt-nsp-package"); - grunt.loadNpmTasks("grunt-babel"); - grunt.loadNpmTasks("grunt-eslint"); // aliases - grunt.registerTask("build", ["concat", "sed", "sass", "babel"]); - grunt.registerTask("test", ["eslint", "mochaTest"]); - grunt.registerTask("default", ["build", "test"]); - grunt.registerTask("validate", "validate-package"); - grunt.registerTask("package", ["validate", "default", "jsdoc"]); + grunt.registerTask("test", ["eslint", "mochaTest", "nsp"]); + grunt.registerTask("default", ["sass", "eslint", "babel", "mochaTest", "nsp"]); }; diff --git a/config.json b/config.json index 61765c20..b9f57be7 100644 --- a/config.json +++ b/config.json @@ -77,6 +77,9 @@ "hsts": null, "xssProtection": null }, + "serializers": [ + "tenso" + ], "maxBytes": 1048576, "port": 8000, "routes": {}, @@ -101,4 +104,4 @@ "override": null }, "title": "Tensō Browsable API" -} \ No newline at end of file +} diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 00000000..8b52f541 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,34 @@ +"use strict"; + +var fs = require("fs"), + path = require("path"), + root = path.join(__dirname, ".."), + cfg = require(path.join(root, "config.json")), + Tenso = require(path.join(__dirname, "tenso.js")), + utility = require(path.join(__dirname, "utility.js")); + +function factory(arg) { + var hostname = arg ? arg.hostname || "localhost" : "localhost", + vhosts = {}, + config = arg ? utility.merge(utility.clone(cfg), arg) : utility.clone(cfg), + obj = undefined; + + if (!config.port) { + console.error("Invalid configuration"); + process.exit(1); + } + + vhosts[hostname] = "www"; + config.root = root; + config.vhosts = vhosts; + config.default = hostname; + config.template = fs.readFileSync(path.join(config.root, "template.html"), { encoding: "utf8" }); + obj = new Tenso(); + obj.hostname = hostname; + + return utility.bootstrap(obj, config); +} + +factory.version = "{{VERSION}}"; + +module.exports = factory; diff --git a/lib/middleware.js b/lib/middleware.js new file mode 100644 index 00000000..c80af7e1 --- /dev/null +++ b/lib/middleware.js @@ -0,0 +1,225 @@ +"use strict"; + +var path = require("path"), + array = require("retsu"), + regex = require(path.join(__dirname, "regex.js")), + utility = require(path.join(__dirname, "utility.js")); + +var cookie = undefined, + session = undefined, + luscaCsp = undefined, + luscaCsrf = undefined, + luscaXframe = undefined, + luscaP3p = undefined, + luscaHsts = undefined, + luscaXssProtection = undefined, + passportAuth = undefined, + passportInit = undefined, + passportSession = undefined; + +var rateHeaders = ["x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset"]; + +function decorate(req, res, next) { + var obj = req.server.tenso; + + req.protect = false; + req.protectAsync = false; + req.unprotect = false; + + res.error = function (status, body) { + return obj.error(req, res, status, body); + }; + + res.redirect = function (uri) { + var perm = arguments.length <= 1 || arguments[1] === undefined ? true : arguments[1]; + + return obj.redirect(req, res, uri, undefined, perm); + }; + + res.respond = function (body, status, headers) { + return obj.respond(req, res, body, status, headers); + }; + + res.send = function (body, status, headers) { + return obj.respond(req, res, body, status, headers); + }; + + next(); +} + +function asyncFlag(req, res, next) { + req.protectAsync = true; + next(); +} + +function bypass(req, res, next) { + var pass = req.server.tenso.config.auth.unprotect.filter(function (i) { + return i.test(req.url); + }).length > 0; + + if (pass) { + req.unprotect = true; + } + + next(); +} + +function csrfWrapper(req, res, next) { + if (req.unprotect) { + next(); + } else { + luscaCsrf(req, res, next); + } +} + +function guard(req, res, next) { + if (req.parsed.url === "/login" || req.isAuthenticated()) { + next(); + } else { + res.redirect("/login"); + } +} + +function parse(req, res, next) { + var args = undefined, + type = undefined; + + if (regex.body.test(req.method) && req.body !== undefined) { + type = req.headers["content-type"]; + + if (regex.encode_form.test(type)) { + args = req.body ? array.chunk(req.body.split(regex.body_split), 2) : []; + req.body = {}; + + array.each(args, function (i) { + req.body[i[0]] = utility.coerce(i[1]); + }); + } + + if (regex.encode_json.test(type)) { + try { + req.body = JSON.parse(req.body); + } catch (e) { + console.warn(e.message); + } + } + } + + next(); +} + +function keymaster(req, res, next) { + var obj = req.server.tenso, + authd = req.session && req.isAuthenticated(), + method = undefined, + result = undefined, + routes = undefined, + uri = undefined; + + // No authentication, or it's already happened + if (!req.protect || !req.protectAsync || authd) { + method = regex.get_rewrite.test(req.method) ? "get" : req.method.toLowerCase(); + routes = req.server.config.routes[method] || {}; + uri = req.parsed.pathname; + + if (uri in routes) { + result = routes[uri]; + + if (typeof result === "function") { + result.call(obj, req, res); + next(); + } else { + res.send(result).then(next, next); + } + } else { + utility.iterate(routes, function (value, key) { + if (new RegExp("^" + key + "$", "i").test(uri)) { + return !(result = value); + } + }); + + if (result) { + if (typeof result === "function") { + result.call(obj, req, res); + next(); + } else { + res.send(result).then(next, next); + } + } else { + next(new Error(404)); + } + } + } else { + next(new Error(401)); + } +} + +function zuul(req, res, next) { + var uri = req.parsed.path, + protectd = false; + + array.each(req.server.tenso.protect, function (r) { + if (r.test(uri)) { + return !(protectd = true); + } + }); + + // Setting state so the connection can be terminated properly + req.protect = protectd; + req.protectAsync = false; + + if (protectd && next) { + next(); + } else { + keymaster(req, res, next); + } +} + +function rate(req, res, next) { + var server = req.server, + obj = server.tenso, + config = server.config.rate, + results = obj.rate(req, config.override), + good = results.shift(); + + rateHeaders.forEach(function (i, idx) { + res.setHeader(i, results[idx]); + }); + + if (good) { + next(); + } else { + next(new Error(config.status || 429)); + } +} + +function valid(req, res, next) { + if (req.allow.indexOf(req.method) > -1) { + next(); + } else { + next(new Error(405)); + } +} + +module.exports = { + asyncFlag: asyncFlag, + bypass: bypass, + cookie: cookie, + csrfWrapper: csrfWrapper, + decorate: decorate, + guard: guard, + luscaCsp: luscaCsp, + luscaCsrf: luscaCsrf, + luscaXframe: luscaXframe, + luscaP3p: luscaP3p, + luscaHsts: luscaHsts, + luscaXssProtection: luscaXssProtection, + passportAuth: passportAuth, + passportInit: passportInit, + passportSession: passportSession, + parse: parse, + rate: rate, + session: session, + valid: valid, + zuul: zuul +}; diff --git a/lib/regex.js b/lib/regex.js new file mode 100644 index 00000000..3c96e02b --- /dev/null +++ b/lib/regex.js @@ -0,0 +1,21 @@ +"use strict"; + +var regex = { + body: /POST|PUT|PATCH/i, + body_split: /&|=/, + collection: /(.*)(\/.*)$/, + encode_form: /application\/x-www-form-urlencoded/, + encode_json: /application\/json/, + get_rewrite: /HEAD|OPTIONS/i, + hypermedia: /[a-zA-Z]+_(guid|uuid|id|url|uri)$/, + id: /^(_id|id)$/i, + leading: /.*\//, + modify: /DELETE|PATCH|POST|PUT/, + scheme: /^(\w+\:\/\/)|\//, + trailing: /_.*$/, + trailing_s: /s$/, + trailing_slash: /\/$/, + trailing_y: /y$/ +}; + +module.exports = regex; diff --git a/lib/renderers.js b/lib/renderers.js new file mode 100644 index 00000000..9b68ed6c --- /dev/null +++ b/lib/renderers.js @@ -0,0 +1,65 @@ +"use strict"; + +var array = require("retsu"), + xml = require("tiny-xml"), + yaml = require("yamljs"), + path = require("path"), + utility = require(path.join(__dirname, "utility.js")); + +function sanitize(arg) { + var output = arg; + + if (typeof arg === "string") { + [["<", "<"], [">", ">"]].forEach(function (i) { + output = output.replace(new RegExp(i[0], "g"), i[1]); + }); + } + + return output; +} + +var renderers = { + csv: { + fn: function fn(arg, req) { + req.headers.accept = "text/csv"; + return arg.data.result; + }, + header: "text/csv" + }, + html: { + fn: function fn(arg, req, headers, tpl) { + var protocol = req.headers["x-forwarded-proto"] ? req.headers["x-forwarded-proto"] + ":" : req.parsed.protocol; + + return (tpl || "").replace(/\{\{title\}\}/g, req.server.config.title).replace("{{url}}", req.parsed.href.replace(req.parsed.protocol, protocol)).replace("{{headers}}", Object.keys(headers).sort(array.sort).map(function (i) { + return "" + i + "" + sanitize(headers[i]) + ""; + }).join("\n")).replace("{{formats}}", req.server.config.renderers.map(function (i) { + return ""; + }).join("\n")).replace("{{body}}", JSON.stringify(arg, null, 2)).replace("{{year}}", new Date().getFullYear()).replace("{{version}}", "{{VERSION}}").replace("{{allow}}", headers.allow).replace("{{methods}}", utility.explode(headers.allow.replace("GET, HEAD, OPTIONS", "")).filter(function (i) { + return i !== ""; + }).map(function (i) { + return ""; + }).join("\n")).replace("{{csrf}}", headers["x-csrf-token"] || ""); + }, + header: "text/html" + }, + json: { + fn: function fn(arg) { + return arg; + }, + header: "application/json" + }, + yaml: { + fn: function fn(arg) { + return yaml.stringify(arg, 4); + }, + header: "application/yaml" + }, + xml: { + fn: function fn(arg) { + return xml.encode(arg); + }, + header: "application/xml" + } +}; + +module.exports = renderers; diff --git a/lib/serializers.js b/lib/serializers.js new file mode 100644 index 00000000..7c9243e7 --- /dev/null +++ b/lib/serializers.js @@ -0,0 +1,17 @@ +"use strict"; + +function tenso(arg, err, status) { + return { + data: arg !== null ? arg : null, + error: arg === null ? err.message || err || "Something went wrong" : null, + links: [], + status: status || 200 + }; +} + +var serializers = { + "application/json": tenso, + tenso: tenso +}; + +module.exports = serializers; diff --git a/lib/tenso.es6.js b/lib/tenso.es6.js deleted file mode 100644 index bdea8de1..00000000 --- a/lib/tenso.es6.js +++ /dev/null @@ -1,1279 +0,0 @@ -/** - * Tensō is a REST API facade for node.js, designed to simplify the implementation of APIs. - * - * @author Jason Mulligan - * @copyright 2015 Jason Mulligan - * @license BSD-3-Clause - * @link http://avoidwork.github.io/tenso - * @module tenso - * @version 2.0.7 - */ -"use strict"; - -const CONFIG = require(__dirname + "/../config.json"); -const VERSION = "2.0.7"; -const SERVER = "tenso/" + VERSION; - -let keigai = require("keigai"), - deferred = require("tiny-defer"), - util = keigai.util, - array = util.array, - coerce = util.coerce, - iterate = util.iterate, - json = util.json, - merge = util.merge, - string = util.string, - uuid = util.uuid, - xml = util.xml, - fs = require("fs"), - path = require("path"), - yaml = require("yamljs"), - turtleio = require("turtle.io"), - session = require("express-session"), - cookie = require("cookie-parser"), - lusca = require("lusca"), - passport = require("passport"), - BasicStrategy = require("passport-http").BasicStrategy, - BearerStrategy = require("passport-http-bearer").Strategy, - FacebookStrategy = require("passport-facebook").Strategy, - GoogleStrategy = require("passport-google").Strategy, - LinkedInStrategy = require("passport-linkedin").Strategy, - LocalStrategy = require("passport-local").Strategy, - OAuth2Strategy = require("passport-oauth2").Strategy, - SAMLStrategy = require("passport-saml").Strategy, - TwitterStrategy = require("passport-twitter").Strategy, - RedisStore = require("connect-redis")(session); - -/** - * RegExp cache - * - * @type Object - */ -const REGEX = { - body: /POST|PUT|PATCH/i, - body_split: /&|=/, - collection: /(.*)(\/.*)$/, - encode_form: /application\/x-www-form-urlencoded/, - encode_json: /application\/json/, - get_rewrite: /HEAD|OPTIONS/i, - hypermedia: /[a-zA-Z]+_(guid|uuid|id|url|uri)$/, - id: /^(_id|id)$/i, - leading: /.*\//, - modify: /DELETE|PATCH|POST|PUT/, - scheme: /^(\w+\:\/\/)|\//, - trailing: /_.*$/, - trailing_s: /s$/, - trailing_slash: /\/$/, - trailing_y: /y$/ -}; - -/** - * Sanitizes outbound Strings to avoid XSS issues - * - * @method sanitize - * @param {String} arg String to sanitize - * @return {String} Sanitized String - */ -function sanitize (arg) { - let output = arg; - - if (typeof arg === "string") { - array.each([["<", "<"], [">", ">"]], function (i) { - output = output.replace(new RegExp(i[0], "g"), i[1]); - }); - } - - return output; -} - -/** - * Renderers - * - * @type {Object} - */ -let renderers = { - csv: { - fn: function (arg, req) { - req.headers.accept = "text/csv"; - return arg.data.result; - }, - header: "text/csv" - }, - html: { - fn: function (arg, req, headers, tpl) { - let protocol = req.headers["x-forwarded-proto"] ? req.headers["x-forwarded-proto"] + ":" : req.parsed.protocol; - - return (tpl || "") - .replace(/\{\{title\}\}/g, req.server.config.title) - .replace("{{url}}", req.parsed.href.replace(req.parsed.protocol, protocol)) - .replace("{{headers}}", Object.keys(headers).sort(array.sort).map(function (i) { - return "" + i + "" + sanitize(headers[i]) + ""; - }).join("\n")) - .replace("{{formats}}", req.server.config.renderers.map(function (i) { - return ""; - }).join("\n")) - .replace("{{body}}", JSON.stringify(arg, null, 2)) - .replace("{{year}}", new Date().getFullYear()) - .replace("{{version}}", "2.0.7") - .replace("{{allow}}", headers.allow) - .replace("{{methods}}", string.explode(headers.allow.replace("GET, HEAD, OPTIONS", "")).filter(function (i) { - return i !== ""; - }).map(function (i) { - return ""; - }).join("\n")) - .replace("{{csrf}}", headers["x-csrf-token"] || ""); - }, - header: "text/html" - }, - json: { - fn: function (arg) { - return arg; - }, - header: "application/json" - }, - yaml: { - fn: function (arg) { - return yaml.stringify(arg, 4); - }, - header: "application/yaml" - }, - xml: { - fn: function (arg) { - return xml.encode(arg); - }, - header: "application/xml" - } -}; - -/** - * Route error handler - * - * @method error - * @return {Undefined} undefined - */ -function error (server, req, res, status, err) { - server.respond(req, res, err, status); -} - -/** - * Returns middleware to determine if a route is protected - * - * @method zuul - * @param {Array} protect Array of routes - * @return {Function} Middleware - */ -function zuul (protect) { - return function (req, res, next) { - let uri = req.parsed.path, - protectd = false; - - array.each(protect, function (r) { - if (r.test(uri)) { - return !(protectd = true); - } - }); - - // Setting state so the connection can be terminated properly - req.protect = protectd; - req.protectAsync = false; - - if (protectd && next) { - next(); - } else { - keymaster(req, res, next); - } - }; -} - -/** - * Setups up authentication - * - * @method auth - * @param {Object} obj Tenso instance - * @param {Object} config Tenso configuration - * @return {Object} Updated Tenso configuration - */ -function auth (obj, config) { - let ssl = config.ssl.cert && config.ssl.key, - proto = "http" + (ssl ? "s" : ""), - realm = proto + "://" + (config.hostname === "localhost" ? "127.0.0.1" : config.hostname) + (config.port !== 80 && config.port !== 443 ? ":" + config.port : ""), - async = (config.auth.facebook.enabled || config.auth.google.enabled || config.auth.linkedin.enabled || config.auth.twitter.enabled), - stateless = (config.auth.basic.enabled || config.auth.bearer.enabled), - stateful = (async || config.auth.local.enabled || config.security.csrf), - authMap = {}, - authUris = [], - keys, sesh, fnCookie, fnSesh, luscaCsrf, luscaCsp, luscaXframe, luscaP3p, luscaHsts, luscaXssProtection, protection, passportAuth, passportInit, passportSession; - - function asyncFlag (req, res, next) { - req.protectAsync = true; - next(); - } - - function bypass (req, res, next) { - if (config.auth.unprotect.filter(function (i) { - return i.test(req.url); - }).length > 0) { - req.protect = false; - req.unprotect = true; - } - - next(); - } - - function csrfWrapper (req, res, next) { - if (req.unprotect) { - next(); - } else { - luscaCsrf(req, res, next); - } - } - - function init (sess) { - passportInit = passport.initialize(); - obj.server.use(passportInit).blacklist(passportInit); - - if (sess) { - passportSession = passport.session(); - obj.server.use(passportSession).blacklist(passportSession); - } - } - - function guard (req, res, next) { - if (req.url === "/login" || req.isAuthenticated()) { - rate(obj, req, res, next); - } else { - res.redirect("/login"); - } - } - - function redirect (req, res) { - res.redirect(config.auth.redirect); - } - - function valid (req, res, next) { - if (req.allow.indexOf(req.method) > -1) { - next(); - } else { - next(new Error(405)); - } - } - - obj.server.blacklist(asyncFlag); - - config.auth.protect = (config.auth.protect || []).map(function (i) { - return new RegExp("^" + i !== "/login" ? i.replace(/\.\*/g, "*").replace(/\*/g, ".*") : "$", "i"); - }); - - config.auth.unprotect = (config.auth.unprotect || []).map(function (i) { - return new RegExp("^" + i !== "/login" ? i.replace(/\.\*/g, "*").replace(/\*/g, ".*") : "$", "i"); - }); - - if (async) { - iterate(config.auth, function (v, k) { - if (v.enabled) { - authMap[k + "_uri"] = "/auth/" + k; - config.auth.protect.push(new RegExp("^/auth/" + k)); - } - }); - } - - authUris = array.keys(authMap); - - if (config.auth.local.enabled) { - authUris.push(config.auth.redirect); - authUris.push("/login"); - } - - obj.server.use(valid).blacklist(valid); - - if (stateful) { - sesh = { - secret: config.session.secret || uuid(), - saveUninitialized: true, - rolling: true, - resave: true - }; - - if (config.session.store === "redis") { - sesh.store = new RedisStore(config.session.redis); - } - - fnCookie = cookie(); - fnSesh = session(sesh); - - obj.server.use(fnSesh).blacklist(fnSesh); - obj.server.use(fnCookie).blacklist(fnCookie); - obj.server.use(bypass).blacklist(bypass); - - if (config.security.csrf) { - luscaCsrf = lusca.csrf({key: config.security.key, secret: config.security.secret}); - obj.server.use(csrfWrapper).blacklist(csrfWrapper); - } - } - - if (config.security.csp instanceof Object) { - luscaCsp = lusca.csp(config.security.csp); - obj.server.use(luscaCsp).blacklist(luscaCsp); - } - - if (!string.isEmpty(config.security.xframe || "")) { - luscaXframe = lusca.xframe(config.security.xframe); - obj.server.use(luscaXframe).blacklist(luscaXframe); - } - - if (!string.isEmpty(config.security.p3p || "")) { - luscaP3p = lusca.p3p(config.security.p3p); - obj.server.use(luscaP3p).blacklist(luscaP3p); - } - - if (config.security.hsts instanceof Object) { - luscaHsts = lusca.hsts(config.security.hsts); - obj.server.use(luscaHsts).blacklist(luscaHsts); - } - - if (config.security.xssProtection instanceof Object) { - luscaXssProtection = lusca.xssProtection(config.security.xssProtection); - obj.server.use(luscaXssProtection).blacklist(luscaXssProtection); - } - - protection = zuul(config.auth.protect); - obj.server.use(protection).blacklist(protection); - - if (stateless && !stateful) { - init(false); - } else { - init(true); - - passport.serializeUser(function (user, done) { - done(null, user); - }); - - passport.deserializeUser(function (arg, done) { - done(null, arg); - }); - - if (authUris.length > 0) { - keys = array.keys(authMap).length > 0; - - if (keys) { - config.routes.get["/auth"] = authMap; - } - - (function () { - let r = "(?!/auth/("; - - array.each(authUris, function (i) { - r += i.replace("_uri", "") + "|"; - }); - - r = r.replace(/\|$/, "") + ")).*$"; - - obj.server.use(r, guard).blacklist(guard); - }()); - - config.routes.get["/login"] = config.auth.local.enabled ? (keys ? { - login_uri: "/auth", - instruction: "POST 'username' & 'password' to authenticate" - } : {instruction: "POST 'username' & 'password' to authenticate"}) : {login_uri: "/auth"}; - } else if (config.auth.local.enabled) { - config.routes.get["/login"] = {instruction: "POST 'username' & 'password' to authenticate"}; - } - - config.routes.get["/logout"] = function (req, res) { - if (req.session) { - req.session.destroy(); - } - - res.redirect(config.auth.redirect); - }; - } - - if (config.auth.basic.enabled) { - (function () { - let x = {}; - - function validate (arg, cb) { - if (x[arg]) { - cb(null, x[arg]); - } else { - cb(new Error("Unauthorized"), null); - } - } - - array.each(config.auth.basic.list || [], function (i) { - let args = i.split(":"); - - if (args.length > 0) { - x[args[0]] = {password: args[1]}; - } - }); - - passport.use(new BasicStrategy(function (username, password, done) { - validate(username, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - if (!user || user.password !== password) { - return done(null, false); - } - - return done(null, user); - }); - })); - - passportAuth = passport.authenticate("basic", {session: stateful}); - - if (async || config.auth.local.enabled) { - obj.server.get("/auth/basic", passportAuth).blacklist(passportAuth); - obj.server.get("/auth/basic", redirect); - } else { - obj.server.use(passportAuth).blacklist(passportAuth); - } - }()); - } - - if (config.auth.bearer.enabled) { - (function () { - let x = config.auth.bearer.tokens || []; - - function validate (arg, cb) { - if (array.contains(x, arg)) { - cb(null, arg); - } else { - cb(new Error("Unauthorized"), null); - } - } - - passport.use(new BearerStrategy(function (token, done) { - validate(token, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - if (!user) { - return done(null, false); - } - - return done(null, user, {scope: "read"}); - }); - })); - - passportAuth = passport.authenticate("bearer", {session: stateful}); - - if (async || config.auth.local.enabled) { - obj.server.get("/auth/bearer", passportAuth).blacklist(passportAuth); - obj.server.get("/auth/bearer", redirect); - } else { - obj.server.use(passportAuth).blacklist(passportAuth); - } - }()); - } - - if (config.auth.facebook.enabled) { - passport.use(new FacebookStrategy({ - clientID: config.auth.facebook.client_id, - clientSecret: config.auth.facebook.client_secret, - callbackURL: realm + "/auth/facebook/callback" - }, function (accessToken, refreshToken, profile, done) { - config.auth.facebook.auth(accessToken, refreshToken, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/facebook", asyncFlag); - obj.server.get("/auth/facebook", passport.authenticate("facebook")); - obj.server.get("/auth/facebook/callback", asyncFlag); - obj.server.get("/auth/facebook/callback", passport.authenticate("facebook", {failureRedirect: "/login"})); - obj.server.get("/auth/facebook/callback", redirect); - } - - if (config.auth.google.enabled) { - passport.use(new GoogleStrategy({ - returnURL: realm + "/auth/google/callback", - realm: realm - }, function (identifier, profile, done) { - config.auth.google.auth.call(obj, identifier, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/google", asyncFlag); - obj.server.get("/auth/google", passport.authenticate("google")); - obj.server.get("/auth/google/callback", asyncFlag); - obj.server.get("/auth/google/callback", passport.authenticate("google", {failureRedirect: "/login"})); - obj.server.get("/auth/google/callback", redirect); - } - - if (config.auth.linkedin.enabled) { - passport.use(new LinkedInStrategy({ - consumerKey: config.auth.linkedin.client_id, - consumerSecret: config.auth.linkedin.client_secret, - callbackURL: realm + "/auth/linkedin/callback" - }, function (token, tokenSecret, profile, done) { - config.auth.linkedin.auth(token, tokenSecret, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/linkedin", asyncFlag); - obj.server.get("/auth/linkedin", passport.authenticate("linkedin", {"scope": config.auth.linkedin.scope || ["r_basicprofile", "r_emailaddress"]})); - obj.server.get("/auth/linkedin/callback", asyncFlag); - obj.server.get("/auth/linkedin/callback", passport.authenticate("linkedin", {failureRedirect: "/login"})); - obj.server.get("/auth/linkedin/callback", redirect); - } - - if (config.auth.local.enabled) { - passport.use(new LocalStrategy(function (username, password, done) { - config.auth.local.auth(username, password, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - config.routes.post = config.routes.post || {}; - config.routes.post["/login"] = function (req, res) { - function final () { - passport.authenticate("local")(req, res, function (e) { - if (e) { - res.error(401, "Unauthorized"); - } else if (req.cors && req.headers["x-requested-with"] && req.headers["x-requested-with"] === "XMLHttpRequest") { - res.respond("Success"); - } else { - res.redirect(config.auth.redirect); - } - }); - } - - function mid () { - passportSession(req, res, final); - } - - passportInit(req, res, mid); - }; - } - - if (config.auth.oauth2.enabled) { - passport.use(new OAuth2Strategy({ - authorizationURL: config.auth.oauth2.auth_url, - tokenURL: config.auth.oauth2.token_url, - clientID: config.auth.oauth2.client_id, - clientSecret: config.auth.oauth2.client_secret, - callbackURL: realm + "/auth/oauth2/callback" - }, function (accessToken, refreshToken, profile, done) { - config.auth.oauth2.auth(accessToken, refreshToken, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/oauth2", asyncFlag); - obj.server.get("/auth/oauth2", passport.authenticate("oauth2")); - obj.server.get("/auth/oauth2/callback", asyncFlag); - obj.server.get("/auth/oauth2/callback", passport.authenticate("oauth2", {failureRedirect: "/login"})); - obj.server.get("/auth/oauth2/callback", redirect); - } - - if (config.auth.saml.enabled) { - (function () { - let arg = config.auth.saml; - - arg.callbackURL = realm + "/auth/saml/callback"; - delete arg.enabled; - delete arg.path; - - passport.use(new SAMLStrategy(arg, function (profile, done) { - config.auth.saml.auth(profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - }()); - - obj.server.get("/auth/saml", asyncFlag); - obj.server.get("/auth/saml", passport.authenticate("saml")); - obj.server.get("/auth/saml/callback", asyncFlag); - obj.server.get("/auth/saml/callback", passport.authenticate("saml", {failureRedirect: "/login"})); - obj.server.get("/auth/saml/callback", redirect); - } - - if (config.auth.twitter.enabled) { - passport.use(new TwitterStrategy({ - consumerKey: config.auth.twitter.consumer_key, - consumerSecret: config.auth.twitter.consumer_secret, - callbackURL: realm + "/auth/twitter/callback" - }, function (token, tokenSecret, profile, done) { - config.auth.twitter.auth(token, tokenSecret, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/twitter", asyncFlag); - obj.server.get("/auth/twitter", passport.authenticate("twitter")); - obj.server.get("/auth/twitter/callback", asyncFlag); - obj.server.get("/auth/twitter/callback", passport.authenticate("twitter", { - successRedirect: config.auth.redirect, - failureRedirect: "/login" - })); - } - - return config; -} - -/** - * Bootstraps an instance of Tenso - * - * @method bootstrap - * @param {Object} obj Tenso instance - * @param {Object} config Application configuration - * @return {Object} Tenso instance - */ -function bootstrap (obj, config) { - let notify = false; - - function decorate (req, res, next) { - res.error = function (status, body) { - return obj.error(req, res, status, body); - }; - - res.redirect = function (uri) { - return obj.redirect(req, res, uri); - }; - - res.respond = function (body, status, headers) { - return obj.respond(req, res, body, status, headers); - }; - - res.send = function (body, status, headers) { - return obj.respond(req, res, body, status, headers); - }; - - next(); - } - - function parse (req, res, next) { - let args, type; - - if (REGEX.body.test(req.method) && req.body !== undefined) { - type = req.headers["content-type"]; - - if (REGEX.encode_form.test(type)) { - args = req.body ? array.chunk(req.body.split(REGEX.body_split), 2) : []; - req.body = {}; - - array.each(args, function (i) { - req.body[i[0]] = coerce(i[1]); - }); - } - - if (REGEX.encode_json.test(type)) { - req.body = json.decode(req.body, true) || req.body; - } - } - - next(); - } - - obj.server.use(decorate).blacklist(decorate); - obj.server.use(parse).blacklist(parse); - - // Bootstrapping configuration - auth(obj, config); - config.headers = config.headers || {}; - config.headers.server = SERVER; - - // Creating status > message map - iterate(obj.server.codes, function (value, key) { - obj.messages[value] = obj.server.messages[key]; - }); - - // Setting routes - iterate(config.routes, function (routes, method) { - iterate(routes, function (arg, route) { - if (typeof arg === "function") { - obj.server[method](route, function (...args) { - arg.apply(obj, args); - }); - } else { - obj.server[method](route, function (req, res) { - obj.respond(req, res, arg); - }); - } - }); - }); - - // Disabling compression over SSL due to BREACH - if (config.ssl.cert && config.ssl.key) { - config.compress = false; - notify = true; - } - - // Starting API server - obj.server.start(config, function (req, res, status, msg) { - let stat = status instanceof Error ? parseInt(status.message, 10) : status, - err = msg instanceof Error ? msg : new Error(msg || obj.messages[stat]); - - error(obj, req, res, stat, err, obj); - }); - - if (notify) { - obj.server.log("Compression over SSL is disabled for your protection", "debug"); - } - - return obj; -} - -/** - * Shallow clones an Object - * - * @method clone - * @param {Mixed} arg To be cloned - * @returns {Mixed} Clone of `arg` - */ -function clone (arg) { - return JSON.parse(JSON.stringify(arg)); -} - -/** - * Decorates the `rep` with hypermedia links - * - * Arrays of results are automatically paginated, Objects - * will be parsed and have keys 'lifted' into the 'link' - * Array if a pattern is matched, e.g. "user_(guid|uuid|id|uri|url)" - * will map to "/users/$1" - * - * @method hypermedia - * @param {Object} server TurtleIO instance - * @param {Object} req Client request - * @param {Object} rep Serialized representation - * @param {Object} headers HTTP response headers - * @return {Object} HTTP response body - */ -function hypermedia (server, req, rep, headers) { - let seen = {}, - collection = req.parsed.pathname, - query, page, page_size, nth, root, parent; - - // Parsing the object for hypermedia properties - function parse (obj, rel, item_collection) { - let keys = array.keys(obj), - lrel = rel || "related", - result; - - if (keys.length === 0) { - result = null; - } else { - array.each(keys, function (i) { - let lcollection, uri; - - // If ID like keys are found, and are not URIs, they are assumed to be root collections - if (REGEX.id.test(i) || REGEX.hypermedia.test(i)) { - if (!REGEX.id.test(i)) { - lcollection = i.replace(REGEX.trailing, "").replace(REGEX.trailing_s, "").replace(REGEX.trailing_y, "ie") + "s"; - lrel = "related"; - } else { - lcollection = item_collection; - lrel = "item"; - } - - uri = REGEX.scheme.test(obj[i]) ? obj[i] : ("/" + lcollection + "/" + obj[i]); - - if (uri !== root && !seen[uri]) { - seen[uri] = 1; - - if (server.allowed("get", uri, req.vhost)) { - rep.links.push({uri: uri, rel: lrel}); - } - } - } - }); - - result = obj; - } - - return result; - } - - if (rep.status >= 200 && rep.status <= 206) { - query = req.parsed.query; - page = query.page || 1; - page_size = query.page_size || server.config.pageSize || 5; - root = req.parsed.pathname; - - if (req.parsed.pathname !== "/") { - rep.links.push({ - uri: root.replace(REGEX.trailing_slash, "").replace(REGEX.collection, "$1") || "/", - rel: "collection" - }); - } - - if (rep.data instanceof Array) { - if (req.method === "GET") { - if (isNaN(page) || page <= 0) { - page = 1; - } - - nth = Math.ceil(rep.data.length / page_size); - - if (nth > 1) { - rep.data = array.limit(rep.data, (page - 1) * page_size, page_size); - query.page = 0; - query.page_size = page_size; - - root += "?" + array.keys(query).map(function (i) { - return i + "=" + encodeURIComponent(query[i]); - }).join("&"); - - if (page > 1) { - rep.links.push({uri: root.replace("page=0", "page=1"), rel: "first"}); - } - - if (page - 1 > 1 && page <= nth) { - rep.links.push({uri: root.replace("page=0", "page=" + (page - 1)), rel: "prev"}); - } - - if (page + 1 < nth) { - rep.links.push({uri: root.replace("page=0", "page=" + (page + 1)), rel: "next"}); - } - - if (nth > 0 && page !== nth) { - rep.links.push({uri: root.replace("page=0", "page=" + nth), rel: "last"}); - } - } else { - root += "?" + array.keys(query).map(function (i) { - return i + "=" + encodeURIComponent(query[i]); - }).join("&"); - } - } - - array.each(rep.data, function (i) { - var li = i.toString(), - uri; - - if (li !== collection) { - uri = li.indexOf("//") > -1 || li.indexOf("/") === 0 ? li : (collection + "/" + li).replace(/^\/\//, "/"); - - if (server.allowed("get", uri, req.vhost)) { - rep.links.push({uri: uri, rel: "item"}); - } - } - - if (i instanceof Object) { - parse(i, "item", req.parsed.pathname.replace(REGEX.trailing_slash, "").replace(REGEX.leading, "")); - } - }); - } else if (rep.data instanceof Object) { - parent = req.parsed.pathname.split("/").filter(function (i) { - return i !== ""; - }); - - if (parent.length > 1) { - parent.pop(); - } - - rep.data = parse(rep.data, undefined, array.last(parent)); - } - - if (rep.links.length > 0) { - headers.link = array.keySort(rep.links, "rel, uri").map(function (i) { - return "<" + i.uri + ">; rel=\"" + i.rel + "\""; - }).join(", "); - } - } - - return rep; -} - -/** - * Keymaster for the request - * - * @method keymaster - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Function} next Next middleware - * @return {Undefined} undefined - */ -function keymaster (req, res, next) { - let obj = req.server.tenso, - method, result, routes, uri; - - // No authentication, or it's already happened - if (!req.protect || !req.protectAsync || (req.session && req.isAuthenticated())) { - method = REGEX.get_rewrite.test(req.method) ? "get" : req.method.toLowerCase(); - routes = req.server.config.routes[method] || {}; - uri = req.parsed.pathname; - - rate(obj, req, res, function () { - if (uri in routes) { - result = routes[uri]; - - if (typeof result === "function") { - result.call(obj, req, res); - } else { - obj.respond(req, res, result).then(function () { - next(); - }, function (e) { - next(e); - }); - } - } else { - iterate(routes, function (value, key) { - if (new RegExp("^" + key + "$", "i").test(uri)) { - return !(result = value); - } - }); - - if (result) { - if (typeof result === "function") { - result.call(obj, req, res); - next(); - } else { - obj.respond(req, res, result).then(function () { - next(); - }, function (e) { - next(e); - }); - } - } else { - obj.error(req, res, 404); - } - } - }); - } else { - rate(obj, req, res, next); - } -} - -/** - * Prepares a response body - * - * @method prepare - * @param {Mixed} arg [Optional] Response body "data" - * @param {Object} error [Optional] Error instance - * @param {Number} status HTTP status code - * @return {Object} Standardized response body - */ -function prepare (arg, err, status) { - return { - data: arg ? clone(arg) : null, - error: !arg ? (err.message || err || "Something went wrong") : null, - links: [], - status: status || 200 - }; -} - -/** - * Rate limiting middleware - * - * @method rate - * @param {Object} obj Tenso instance - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Function} next Next middleware - * @return {Undefined} undefined - */ -function rate (obj, req, res, next) { - let headers = ["x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset"], - config = obj.server.config.rate, - results = obj.rate(req, config.override), - valid = results.shift(); - - array.each(headers, function (i, idx) { - res.setHeader(i, results[idx]); - }); - - if (valid) { - next(); - } else { - obj.error(req, res, config.status || 429, config.message || "Too Many Requests"); - } -} - -/** - * Creates a response - * - * @method response - * @param {Mixed} arg Unserialized response body - * @param {Number} status HTTP status, default is `200` - * @return {Object} Response body - */ -function response (arg, status) { - let err = arg instanceof Error, - result; - - if (err) { - if (status === undefined) { - throw new Error("Invalid arguments"); - } - - result = prepare(null, arg, status); - } else { - result = prepare(arg, null, status); - } - - return result; -} - -class Tenso { - /** - * Tenso - * - * @constructor - */ - constructor () { - this.hostname = ""; - this.messages = {}; - this.rates = {}; - this.server = turtleio(); - this.server.tenso = this; - this.version = VERSION; - } - - /** - * Sends an Error to the Client - * - * @method redirect - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Number} status Response status - * @param {Object} arg Response body - */ - error (req, res, status, arg) { - this.server.error(req, res, status, arg); - - return this; - } - - /** - * Returns rate limit information for Client request - * - * @method rate - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} fn [Optional] Override default rate limit - * @return {Array} Array of rate limit information `[valid, total, remaining, reset]` - */ - rate (req, fn) { - let config = this.server.config.rate, - id = req.sessionID || req.ip, - valid = true, - seconds = parseInt(new Date().getTime() / 1000, 10), - limit, remaining, reset, state; - - if (!this.rates[id]) { - this.rates[id] = { - limit: config.limit, - remaining: config.limit, - reset: seconds + config.reset, - time_reset: config.reset - }; - } - - if (typeof fn === "function") { - this.rates[id] = fn(req, this.rates[id]); - } - - state = this.rates[id]; - limit = state.limit; - remaining = state.remaining; - reset = state.reset; - - if (seconds >= reset) { - reset = state.reset = (seconds + config.reset); - remaining = state.remaining = limit - 1; - } else if (remaining > 0) { - state.remaining--; - remaining = state.remaining; - } else { - valid = false; - } - - return [valid, limit, remaining, reset]; - } - - /** - * Redirects the Client - * - * @method redirect - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Mixed} uri Target URI - * @return {Object} {@link Tenso} - */ - redirect (req, res, uri) { - this.server.respond(req, res, this.server.messages.NO_CONTENT, this.server.codes.FOUND, {location: uri}); - - return this; - } - - /** - * Renders a response body, defaults to JSON - * - * @method render - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} arg HTTP response body - * @param {Object} headers HTTP response headers - * @return {String} HTTP response body - */ - render (req, arg, headers) { - let accept = req.parsed.query.format || req.headers.accept || "application/json"; - let accepts = string.explode(accept, ";"); - let format = "json"; - - array.each(this.server.config.renderers || [], function (i) { - let found = false; - - array.each(accepts, function (x) { - if (x.indexOf(i) > -1) { - format = i; - found = true; - return false; - } - }); - - if (found) { - return false; - } - }); - - headers["content-type"] = renderers[format].header; - - return renderers[format].fn(arg, req, headers, format === "html" ? this.server.config.template : undefined); - } - - /** - * Registers a renderer - * - * @method renderer - * @memberOf Tenso - * @param {String} name Name of the renderer, e.g. "html" - * @param {Function} fn Function accepts `arg, req, headers, template` - * @param {String} mimetype Content-Type value - * @return {Object} {@link Tenso} - */ - renderer (name, fn, mimetype) { - renderers[name] = {fn: fn, header: mimetype}; - array.add(this.server.config.renderers, name); - - return this; - } - - /** - * Sends a response to the Client - * - * @method respond - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Mixed} arg Response body - * @param {Number} status Response status - * @param {Object} headers Response headers - * @return {Object} Defer - */ - respond (req, res, arg, status, headers) { - let resStatus = status || 200, - defer = deferred(), - ref; - - if (!res._header) { - ref = [headers || {}]; - - if (res._headers) { - merge(ref[0], res._headers); - } - - if (req.protect) { - if (ref[0]["cache-control"] === undefined && this.server.config.headers["cache-control"]) { - ref[0]["cache-control"] = clone(this.server.config.headers["cache-control"]); - } - - if (ref[0]["cache-control"] !== undefined && ref[0]["cache-control"].indexOf("private ") === -1) { - ref[0]["cache-control"] = "private " + ref[0]["cache-control"]; - } - } - - if (!REGEX.modify.test(req.method) && REGEX.modify.test(req.allow) && this.server.config.security.csrf && res.locals[this.server.config.security.key]) { - ref[0][this.server.config.security.key] = res.locals[this.server.config.security.key]; - } - - ref[0] = this.server.headers(req, ref[0], resStatus); - this.server.respond(req, res, this.render(req, hypermedia(this.server, req, response(arg, resStatus), ref[0]), ref[0]), resStatus, ref[0]).then(function () { - defer.resolve(true); - }, function (e) { - defer.reject(e); - }); - } else { - defer.resolve(true); - } - - return defer.promise; - } -} - -/** - * Tenso factory - * - * @method factory - * @param {Object} arg [Optional] Configuration - * @return {Object} Tenso instance - */ -function factory (arg) { - let hostname = arg ? arg.hostname || "localhost" : "localhost", - vhosts = {}, - config = arg ? merge(clone(CONFIG), arg) : CONFIG, - obj; - - if (!config.port) { - console.error("Invalid configuration"); - process.exit(1); - } - - vhosts[hostname] = "www"; - config.root = path.join(__dirname, ".."); - config.vhosts = vhosts; - config.default = hostname; - config.template = fs.readFileSync(path.join(config.root, "template.html"), {encoding: "utf8"}); - obj = new Tenso(); - obj.hostname = hostname; - - return bootstrap(obj, config); -} - -module.exports = factory; diff --git a/lib/tenso.js b/lib/tenso.js index 5a9fd9d2..ae390ff1 100644 --- a/lib/tenso.js +++ b/lib/tenso.js @@ -1,1081 +1,20 @@ -/** - * Tensō is a REST API facade for node.js, designed to simplify the implementation of APIs. - * - * @author Jason Mulligan - * @copyright 2015 Jason Mulligan - * @license BSD-3-Clause - * @link http://avoidwork.github.io/tenso - * @module tenso - * @version 2.0.7 - */ "use strict"; var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } -var CONFIG = require(__dirname + "/../config.json"); -var VERSION = "2.0.7"; -var SERVER = "tenso/" + VERSION; - -var keigai = require("keigai"), - deferred = require("tiny-defer"), - util = keigai.util, - array = util.array, - coerce = util.coerce, - iterate = util.iterate, - json = util.json, - merge = util.merge, - string = util.string, - uuid = util.uuid, - xml = util.xml, - fs = require("fs"), - path = require("path"), - yaml = require("yamljs"), +var path = require("path"), + array = require("retsu"), turtleio = require("turtle.io"), - session = require("express-session"), - cookie = require("cookie-parser"), - lusca = require("lusca"), - passport = require("passport"), - BasicStrategy = require("passport-http").BasicStrategy, - BearerStrategy = require("passport-http-bearer").Strategy, - FacebookStrategy = require("passport-facebook").Strategy, - GoogleStrategy = require("passport-google").Strategy, - LinkedInStrategy = require("passport-linkedin").Strategy, - LocalStrategy = require("passport-local").Strategy, - OAuth2Strategy = require("passport-oauth2").Strategy, - SAMLStrategy = require("passport-saml").Strategy, - TwitterStrategy = require("passport-twitter").Strategy, - RedisStore = require("connect-redis")(session); - -/** - * RegExp cache - * - * @type Object - */ -var REGEX = { - body: /POST|PUT|PATCH/i, - body_split: /&|=/, - collection: /(.*)(\/.*)$/, - encode_form: /application\/x-www-form-urlencoded/, - encode_json: /application\/json/, - get_rewrite: /HEAD|OPTIONS/i, - hypermedia: /[a-zA-Z]+_(guid|uuid|id|url|uri)$/, - id: /^(_id|id)$/i, - leading: /.*\//, - modify: /DELETE|PATCH|POST|PUT/, - scheme: /^(\w+\:\/\/)|\//, - trailing: /_.*$/, - trailing_s: /s$/, - trailing_slash: /\/$/, - trailing_y: /y$/ -}; - -/** - * Sanitizes outbound Strings to avoid XSS issues - * - * @method sanitize - * @param {String} arg String to sanitize - * @return {String} Sanitized String - */ -function sanitize(arg) { - var output = arg; - - if (typeof arg === "string") { - array.each([["<", "<"], [">", ">"]], function (i) { - output = output.replace(new RegExp(i[0], "g"), i[1]); - }); - } - - return output; -} - -/** - * Renderers - * - * @type {Object} - */ -var renderers = { - csv: { - fn: function fn(arg, req) { - req.headers.accept = "text/csv"; - return arg.data.result; - }, - header: "text/csv" - }, - html: { - fn: function fn(arg, req, headers, tpl) { - var protocol = req.headers["x-forwarded-proto"] ? req.headers["x-forwarded-proto"] + ":" : req.parsed.protocol; - - return (tpl || "").replace(/\{\{title\}\}/g, req.server.config.title).replace("{{url}}", req.parsed.href.replace(req.parsed.protocol, protocol)).replace("{{headers}}", Object.keys(headers).sort(array.sort).map(function (i) { - return "" + i + "" + sanitize(headers[i]) + ""; - }).join("\n")).replace("{{formats}}", req.server.config.renderers.map(function (i) { - return ""; - }).join("\n")).replace("{{body}}", JSON.stringify(arg, null, 2)).replace("{{year}}", new Date().getFullYear()).replace("{{version}}", "2.0.7").replace("{{allow}}", headers.allow).replace("{{methods}}", string.explode(headers.allow.replace("GET, HEAD, OPTIONS", "")).filter(function (i) { - return i !== ""; - }).map(function (i) { - return ""; - }).join("\n")).replace("{{csrf}}", headers["x-csrf-token"] || ""); - }, - header: "text/html" - }, - json: { - fn: function fn(arg) { - return arg; - }, - header: "application/json" - }, - yaml: { - fn: function fn(arg) { - return yaml.stringify(arg, 4); - }, - header: "application/yaml" - }, - xml: { - fn: function fn(arg) { - return xml.encode(arg); - }, - header: "application/xml" - } -}; - -/** - * Route error handler - * - * @method error - * @return {Undefined} undefined - */ -function error(server, req, res, status, err) { - server.respond(req, res, err, status); -} - -/** - * Returns middleware to determine if a route is protected - * - * @method zuul - * @param {Array} protect Array of routes - * @return {Function} Middleware - */ -function zuul(protect) { - return function (req, res, next) { - var uri = req.parsed.path, - protectd = false; - - array.each(protect, function (r) { - if (r.test(uri)) { - return !(protectd = true); - } - }); - - // Setting state so the connection can be terminated properly - req.protect = protectd; - req.protectAsync = false; - - if (protectd && next) { - next(); - } else { - keymaster(req, res, next); - } - }; -} - -/** - * Setups up authentication - * - * @method auth - * @param {Object} obj Tenso instance - * @param {Object} config Tenso configuration - * @return {Object} Updated Tenso configuration - */ -function auth(obj, config) { - var ssl = config.ssl.cert && config.ssl.key, - proto = "http" + (ssl ? "s" : ""), - realm = proto + "://" + (config.hostname === "localhost" ? "127.0.0.1" : config.hostname) + (config.port !== 80 && config.port !== 443 ? ":" + config.port : ""), - async = config.auth.facebook.enabled || config.auth.google.enabled || config.auth.linkedin.enabled || config.auth.twitter.enabled, - stateless = config.auth.basic.enabled || config.auth.bearer.enabled, - stateful = async || config.auth.local.enabled || config.security.csrf, - authMap = {}, - authUris = [], - keys = undefined, - sesh = undefined, - fnCookie = undefined, - fnSesh = undefined, - luscaCsrf = undefined, - luscaCsp = undefined, - luscaXframe = undefined, - luscaP3p = undefined, - luscaHsts = undefined, - luscaXssProtection = undefined, - protection = undefined, - passportAuth = undefined, - passportInit = undefined, - passportSession = undefined; - - function asyncFlag(req, res, next) { - req.protectAsync = true; - next(); - } - - function bypass(req, res, next) { - if (config.auth.unprotect.filter(function (i) { - return i.test(req.url); - }).length > 0) { - req.protect = false; - req.unprotect = true; - } - - next(); - } - - function csrfWrapper(req, res, next) { - if (req.unprotect) { - next(); - } else { - luscaCsrf(req, res, next); - } - } - - function init(sess) { - passportInit = passport.initialize(); - obj.server.use(passportInit).blacklist(passportInit); - - if (sess) { - passportSession = passport.session(); - obj.server.use(passportSession).blacklist(passportSession); - } - } - - function guard(req, res, next) { - if (req.url === "/login" || req.isAuthenticated()) { - rate(obj, req, res, next); - } else { - res.redirect("/login"); - } - } - - function redirect(req, res) { - res.redirect(config.auth.redirect); - } - - function valid(req, res, next) { - if (req.allow.indexOf(req.method) > -1) { - next(); - } else { - next(new Error(405)); - } - } - - obj.server.blacklist(asyncFlag); - - config.auth.protect = (config.auth.protect || []).map(function (i) { - return new RegExp("^" + i !== "/login" ? i.replace(/\.\*/g, "*").replace(/\*/g, ".*") : "$", "i"); - }); - - config.auth.unprotect = (config.auth.unprotect || []).map(function (i) { - return new RegExp("^" + i !== "/login" ? i.replace(/\.\*/g, "*").replace(/\*/g, ".*") : "$", "i"); - }); - - if (async) { - iterate(config.auth, function (v, k) { - if (v.enabled) { - authMap[k + "_uri"] = "/auth/" + k; - config.auth.protect.push(new RegExp("^/auth/" + k)); - } - }); - } - - authUris = array.keys(authMap); - - if (config.auth.local.enabled) { - authUris.push(config.auth.redirect); - authUris.push("/login"); - } - - obj.server.use(valid).blacklist(valid); - - if (stateful) { - sesh = { - secret: config.session.secret || uuid(), - saveUninitialized: true, - rolling: true, - resave: true - }; - - if (config.session.store === "redis") { - sesh.store = new RedisStore(config.session.redis); - } - - fnCookie = cookie(); - fnSesh = session(sesh); - - obj.server.use(fnSesh).blacklist(fnSesh); - obj.server.use(fnCookie).blacklist(fnCookie); - obj.server.use(bypass).blacklist(bypass); - - if (config.security.csrf) { - luscaCsrf = lusca.csrf({ key: config.security.key, secret: config.security.secret }); - obj.server.use(csrfWrapper).blacklist(csrfWrapper); - } - } - - if (config.security.csp instanceof Object) { - luscaCsp = lusca.csp(config.security.csp); - obj.server.use(luscaCsp).blacklist(luscaCsp); - } - - if (!string.isEmpty(config.security.xframe || "")) { - luscaXframe = lusca.xframe(config.security.xframe); - obj.server.use(luscaXframe).blacklist(luscaXframe); - } - - if (!string.isEmpty(config.security.p3p || "")) { - luscaP3p = lusca.p3p(config.security.p3p); - obj.server.use(luscaP3p).blacklist(luscaP3p); - } - - if (config.security.hsts instanceof Object) { - luscaHsts = lusca.hsts(config.security.hsts); - obj.server.use(luscaHsts).blacklist(luscaHsts); - } - - if (config.security.xssProtection instanceof Object) { - luscaXssProtection = lusca.xssProtection(config.security.xssProtection); - obj.server.use(luscaXssProtection).blacklist(luscaXssProtection); - } - - protection = zuul(config.auth.protect); - obj.server.use(protection).blacklist(protection); - - if (stateless && !stateful) { - init(false); - } else { - init(true); - - passport.serializeUser(function (user, done) { - done(null, user); - }); - - passport.deserializeUser(function (arg, done) { - done(null, arg); - }); - - if (authUris.length > 0) { - keys = array.keys(authMap).length > 0; - - if (keys) { - config.routes.get["/auth"] = authMap; - } - - (function () { - var r = "(?!/auth/("; - - array.each(authUris, function (i) { - r += i.replace("_uri", "") + "|"; - }); - - r = r.replace(/\|$/, "") + ")).*$"; - - obj.server.use(r, guard).blacklist(guard); - })(); - - config.routes.get["/login"] = config.auth.local.enabled ? keys ? { - login_uri: "/auth", - instruction: "POST 'username' & 'password' to authenticate" - } : { instruction: "POST 'username' & 'password' to authenticate" } : { login_uri: "/auth" }; - } else if (config.auth.local.enabled) { - config.routes.get["/login"] = { instruction: "POST 'username' & 'password' to authenticate" }; - } - - config.routes.get["/logout"] = function (req, res) { - if (req.session) { - req.session.destroy(); - } - - res.redirect(config.auth.redirect); - }; - } - - if (config.auth.basic.enabled) { - (function () { - var x = {}; - - function validate(arg, cb) { - if (x[arg]) { - cb(null, x[arg]); - } else { - cb(new Error("Unauthorized"), null); - } - } - - array.each(config.auth.basic.list || [], function (i) { - var args = i.split(":"); - - if (args.length > 0) { - x[args[0]] = { password: args[1] }; - } - }); - - passport.use(new BasicStrategy(function (username, password, done) { - validate(username, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - if (!user || user.password !== password) { - return done(null, false); - } - - return done(null, user); - }); - })); - - passportAuth = passport.authenticate("basic", { session: stateful }); - - if (async || config.auth.local.enabled) { - obj.server.get("/auth/basic", passportAuth).blacklist(passportAuth); - obj.server.get("/auth/basic", redirect); - } else { - obj.server.use(passportAuth).blacklist(passportAuth); - } - })(); - } - - if (config.auth.bearer.enabled) { - (function () { - var x = config.auth.bearer.tokens || []; - - function validate(arg, cb) { - if (array.contains(x, arg)) { - cb(null, arg); - } else { - cb(new Error("Unauthorized"), null); - } - } - - passport.use(new BearerStrategy(function (token, done) { - validate(token, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - if (!user) { - return done(null, false); - } - - return done(null, user, { scope: "read" }); - }); - })); - - passportAuth = passport.authenticate("bearer", { session: stateful }); - - if (async || config.auth.local.enabled) { - obj.server.get("/auth/bearer", passportAuth).blacklist(passportAuth); - obj.server.get("/auth/bearer", redirect); - } else { - obj.server.use(passportAuth).blacklist(passportAuth); - } - })(); - } - - if (config.auth.facebook.enabled) { - passport.use(new FacebookStrategy({ - clientID: config.auth.facebook.client_id, - clientSecret: config.auth.facebook.client_secret, - callbackURL: realm + "/auth/facebook/callback" - }, function (accessToken, refreshToken, profile, done) { - config.auth.facebook.auth(accessToken, refreshToken, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/facebook", asyncFlag); - obj.server.get("/auth/facebook", passport.authenticate("facebook")); - obj.server.get("/auth/facebook/callback", asyncFlag); - obj.server.get("/auth/facebook/callback", passport.authenticate("facebook", { failureRedirect: "/login" })); - obj.server.get("/auth/facebook/callback", redirect); - } - - if (config.auth.google.enabled) { - passport.use(new GoogleStrategy({ - returnURL: realm + "/auth/google/callback", - realm: realm - }, function (identifier, profile, done) { - config.auth.google.auth.call(obj, identifier, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/google", asyncFlag); - obj.server.get("/auth/google", passport.authenticate("google")); - obj.server.get("/auth/google/callback", asyncFlag); - obj.server.get("/auth/google/callback", passport.authenticate("google", { failureRedirect: "/login" })); - obj.server.get("/auth/google/callback", redirect); - } - - if (config.auth.linkedin.enabled) { - passport.use(new LinkedInStrategy({ - consumerKey: config.auth.linkedin.client_id, - consumerSecret: config.auth.linkedin.client_secret, - callbackURL: realm + "/auth/linkedin/callback" - }, function (token, tokenSecret, profile, done) { - config.auth.linkedin.auth(token, tokenSecret, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/linkedin", asyncFlag); - obj.server.get("/auth/linkedin", passport.authenticate("linkedin", { "scope": config.auth.linkedin.scope || ["r_basicprofile", "r_emailaddress"] })); - obj.server.get("/auth/linkedin/callback", asyncFlag); - obj.server.get("/auth/linkedin/callback", passport.authenticate("linkedin", { failureRedirect: "/login" })); - obj.server.get("/auth/linkedin/callback", redirect); - } - - if (config.auth.local.enabled) { - passport.use(new LocalStrategy(function (username, password, done) { - config.auth.local.auth(username, password, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - config.routes.post = config.routes.post || {}; - config.routes.post["/login"] = function (req, res) { - function final() { - passport.authenticate("local")(req, res, function (e) { - if (e) { - res.error(401, "Unauthorized"); - } else if (req.cors && req.headers["x-requested-with"] && req.headers["x-requested-with"] === "XMLHttpRequest") { - res.respond("Success"); - } else { - res.redirect(config.auth.redirect); - } - }); - } - - function mid() { - passportSession(req, res, final); - } - - passportInit(req, res, mid); - }; - } - - if (config.auth.oauth2.enabled) { - passport.use(new OAuth2Strategy({ - authorizationURL: config.auth.oauth2.auth_url, - tokenURL: config.auth.oauth2.token_url, - clientID: config.auth.oauth2.client_id, - clientSecret: config.auth.oauth2.client_secret, - callbackURL: realm + "/auth/oauth2/callback" - }, function (accessToken, refreshToken, profile, done) { - config.auth.oauth2.auth(accessToken, refreshToken, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/oauth2", asyncFlag); - obj.server.get("/auth/oauth2", passport.authenticate("oauth2")); - obj.server.get("/auth/oauth2/callback", asyncFlag); - obj.server.get("/auth/oauth2/callback", passport.authenticate("oauth2", { failureRedirect: "/login" })); - obj.server.get("/auth/oauth2/callback", redirect); - } - - if (config.auth.saml.enabled) { - (function () { - var arg = config.auth.saml; - - arg.callbackURL = realm + "/auth/saml/callback"; - delete arg.enabled; - delete arg.path; - - passport.use(new SAMLStrategy(arg, function (profile, done) { - config.auth.saml.auth(profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - })(); - - obj.server.get("/auth/saml", asyncFlag); - obj.server.get("/auth/saml", passport.authenticate("saml")); - obj.server.get("/auth/saml/callback", asyncFlag); - obj.server.get("/auth/saml/callback", passport.authenticate("saml", { failureRedirect: "/login" })); - obj.server.get("/auth/saml/callback", redirect); - } - - if (config.auth.twitter.enabled) { - passport.use(new TwitterStrategy({ - consumerKey: config.auth.twitter.consumer_key, - consumerSecret: config.auth.twitter.consumer_secret, - callbackURL: realm + "/auth/twitter/callback" - }, function (token, tokenSecret, profile, done) { - config.auth.twitter.auth(token, tokenSecret, profile, function (err, user) { - if (err) { - delete err.stack; - return done(err); - } - - done(null, user); - }); - })); - - obj.server.get("/auth/twitter", asyncFlag); - obj.server.get("/auth/twitter", passport.authenticate("twitter")); - obj.server.get("/auth/twitter/callback", asyncFlag); - obj.server.get("/auth/twitter/callback", passport.authenticate("twitter", { - successRedirect: config.auth.redirect, - failureRedirect: "/login" - })); - } - - return config; -} - -/** - * Bootstraps an instance of Tenso - * - * @method bootstrap - * @param {Object} obj Tenso instance - * @param {Object} config Application configuration - * @return {Object} Tenso instance - */ -function bootstrap(obj, config) { - var notify = false; - - function decorate(req, res, next) { - res.error = function (status, body) { - return obj.error(req, res, status, body); - }; - - res.redirect = function (uri) { - return obj.redirect(req, res, uri); - }; - - res.respond = function (body, status, headers) { - return obj.respond(req, res, body, status, headers); - }; - - res.send = function (body, status, headers) { - return obj.respond(req, res, body, status, headers); - }; - - next(); - } - - function parse(req, res, next) { - var args = undefined, - type = undefined; - - if (REGEX.body.test(req.method) && req.body !== undefined) { - type = req.headers["content-type"]; - - if (REGEX.encode_form.test(type)) { - args = req.body ? array.chunk(req.body.split(REGEX.body_split), 2) : []; - req.body = {}; - - array.each(args, function (i) { - req.body[i[0]] = coerce(i[1]); - }); - } - - if (REGEX.encode_json.test(type)) { - req.body = json.decode(req.body, true) || req.body; - } - } - - next(); - } - - obj.server.use(decorate).blacklist(decorate); - obj.server.use(parse).blacklist(parse); - - // Bootstrapping configuration - auth(obj, config); - config.headers = config.headers || {}; - config.headers.server = SERVER; - - // Creating status > message map - iterate(obj.server.codes, function (value, key) { - obj.messages[value] = obj.server.messages[key]; - }); - - // Setting routes - iterate(config.routes, function (routes, method) { - iterate(routes, function (arg, route) { - if (typeof arg === "function") { - obj.server[method](route, function () { - for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { - args[_key] = arguments[_key]; - } - - arg.apply(obj, args); - }); - } else { - obj.server[method](route, function (req, res) { - obj.respond(req, res, arg); - }); - } - }); - }); - - // Disabling compression over SSL due to BREACH - if (config.ssl.cert && config.ssl.key) { - config.compress = false; - notify = true; - } - - // Starting API server - obj.server.start(config, function (req, res, status, msg) { - var stat = status instanceof Error ? parseInt(status.message, 10) : status, - err = msg instanceof Error ? msg : new Error(msg || obj.messages[stat]); - - error(obj, req, res, stat, err, obj); - }); - - if (notify) { - obj.server.log("Compression over SSL is disabled for your protection", "debug"); - } - - return obj; -} - -/** - * Shallow clones an Object - * - * @method clone - * @param {Mixed} arg To be cloned - * @returns {Mixed} Clone of `arg` - */ -function clone(arg) { - return JSON.parse(JSON.stringify(arg)); -} - -/** - * Decorates the `rep` with hypermedia links - * - * Arrays of results are automatically paginated, Objects - * will be parsed and have keys 'lifted' into the 'link' - * Array if a pattern is matched, e.g. "user_(guid|uuid|id|uri|url)" - * will map to "/users/$1" - * - * @method hypermedia - * @param {Object} server TurtleIO instance - * @param {Object} req Client request - * @param {Object} rep Serialized representation - * @param {Object} headers HTTP response headers - * @return {Object} HTTP response body - */ -function hypermedia(server, req, rep, headers) { - var seen = {}, - collection = req.parsed.pathname, - query = undefined, - page = undefined, - page_size = undefined, - nth = undefined, - root = undefined, - parent = undefined; - - // Parsing the object for hypermedia properties - function parse(obj, rel, item_collection) { - var keys = array.keys(obj), - lrel = rel || "related", - result = undefined; - - if (keys.length === 0) { - result = null; - } else { - array.each(keys, function (i) { - var lcollection = undefined, - uri = undefined; - - // If ID like keys are found, and are not URIs, they are assumed to be root collections - if (REGEX.id.test(i) || REGEX.hypermedia.test(i)) { - if (!REGEX.id.test(i)) { - lcollection = i.replace(REGEX.trailing, "").replace(REGEX.trailing_s, "").replace(REGEX.trailing_y, "ie") + "s"; - lrel = "related"; - } else { - lcollection = item_collection; - lrel = "item"; - } - - uri = REGEX.scheme.test(obj[i]) ? obj[i] : "/" + lcollection + "/" + obj[i]; - - if (uri !== root && !seen[uri]) { - seen[uri] = 1; - - if (server.allowed("get", uri, req.vhost)) { - rep.links.push({ uri: uri, rel: lrel }); - } - } - } - }); - - result = obj; - } - - return result; - } - - if (rep.status >= 200 && rep.status <= 206) { - query = req.parsed.query; - page = query.page || 1; - page_size = query.page_size || server.config.pageSize || 5; - root = req.parsed.pathname; - - if (req.parsed.pathname !== "/") { - rep.links.push({ - uri: root.replace(REGEX.trailing_slash, "").replace(REGEX.collection, "$1") || "/", - rel: "collection" - }); - } - - if (rep.data instanceof Array) { - if (req.method === "GET") { - if (isNaN(page) || page <= 0) { - page = 1; - } - - nth = Math.ceil(rep.data.length / page_size); - - if (nth > 1) { - rep.data = array.limit(rep.data, (page - 1) * page_size, page_size); - query.page = 0; - query.page_size = page_size; - - root += "?" + array.keys(query).map(function (i) { - return i + "=" + encodeURIComponent(query[i]); - }).join("&"); - - if (page > 1) { - rep.links.push({ uri: root.replace("page=0", "page=1"), rel: "first" }); - } - - if (page - 1 > 1 && page <= nth) { - rep.links.push({ uri: root.replace("page=0", "page=" + (page - 1)), rel: "prev" }); - } - - if (page + 1 < nth) { - rep.links.push({ uri: root.replace("page=0", "page=" + (page + 1)), rel: "next" }); - } - - if (nth > 0 && page !== nth) { - rep.links.push({ uri: root.replace("page=0", "page=" + nth), rel: "last" }); - } - } else { - root += "?" + array.keys(query).map(function (i) { - return i + "=" + encodeURIComponent(query[i]); - }).join("&"); - } - } - - array.each(rep.data, function (i) { - var li = i.toString(), - uri; - - if (li !== collection) { - uri = li.indexOf("//") > -1 || li.indexOf("/") === 0 ? li : (collection + "/" + li).replace(/^\/\//, "/"); - - if (server.allowed("get", uri, req.vhost)) { - rep.links.push({ uri: uri, rel: "item" }); - } - } - - if (i instanceof Object) { - parse(i, "item", req.parsed.pathname.replace(REGEX.trailing_slash, "").replace(REGEX.leading, "")); - } - }); - } else if (rep.data instanceof Object) { - parent = req.parsed.pathname.split("/").filter(function (i) { - return i !== ""; - }); - - if (parent.length > 1) { - parent.pop(); - } - - rep.data = parse(rep.data, undefined, array.last(parent)); - } - - if (rep.links.length > 0) { - headers.link = array.keySort(rep.links, "rel, uri").map(function (i) { - return "<" + i.uri + ">; rel=\"" + i.rel + "\""; - }).join(", "); - } - } - - return rep; -} - -/** - * Keymaster for the request - * - * @method keymaster - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Function} next Next middleware - * @return {Undefined} undefined - */ -function keymaster(req, res, next) { - var obj = req.server.tenso, - method = undefined, - result = undefined, - routes = undefined, - uri = undefined; - - // No authentication, or it's already happened - if (!req.protect || !req.protectAsync || req.session && req.isAuthenticated()) { - method = REGEX.get_rewrite.test(req.method) ? "get" : req.method.toLowerCase(); - routes = req.server.config.routes[method] || {}; - uri = req.parsed.pathname; - - rate(obj, req, res, function () { - if (uri in routes) { - result = routes[uri]; - - if (typeof result === "function") { - result.call(obj, req, res); - } else { - obj.respond(req, res, result).then(function () { - next(); - }, function (e) { - next(e); - }); - } - } else { - iterate(routes, function (value, key) { - if (new RegExp("^" + key + "$", "i").test(uri)) { - return !(result = value); - } - }); - - if (result) { - if (typeof result === "function") { - result.call(obj, req, res); - next(); - } else { - obj.respond(req, res, result).then(function () { - next(); - }, function (e) { - next(e); - }); - } - } else { - obj.error(req, res, 404); - } - } - }); - } else { - rate(obj, req, res, next); - } -} - -/** - * Prepares a response body - * - * @method prepare - * @param {Mixed} arg [Optional] Response body "data" - * @param {Object} error [Optional] Error instance - * @param {Number} status HTTP status code - * @return {Object} Standardized response body - */ -function prepare(arg, err, status) { - return { - data: arg ? clone(arg) : null, - error: !arg ? err.message || err || "Something went wrong" : null, - links: [], - status: status || 200 - }; -} - -/** - * Rate limiting middleware - * - * @method rate - * @param {Object} obj Tenso instance - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Function} next Next middleware - * @return {Undefined} undefined - */ -function rate(obj, req, res, next) { - var headers = ["x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset"], - config = obj.server.config.rate, - results = obj.rate(req, config.override), - valid = results.shift(); - - array.each(headers, function (i, idx) { - res.setHeader(i, results[idx]); - }); - - if (valid) { - next(); - } else { - obj.error(req, res, config.status || 429, config.message || "Too Many Requests"); - } -} - -/** - * Creates a response - * - * @method response - * @param {Mixed} arg Unserialized response body - * @param {Number} status HTTP status, default is `200` - * @return {Object} Response body - */ -function response(arg, status) { - var err = arg instanceof Error, - result = undefined; - - if (err) { - if (status === undefined) { - throw new Error("Invalid arguments"); - } - - result = prepare(null, arg, status); - } else { - result = prepare(arg, null, status); - } + deferred = require("tiny-defer"), + regex = require(path.join(__dirname, "regex")), + utility = require(path.join(__dirname, "utility")); - return result; -} +var renderers = require(path.join(__dirname, "renderers")), + serializers = require(path.join(__dirname, "serializers")); var Tenso = (function () { - /** - * Tenso - * - * @constructor - */ - function Tenso() { _classCallCheck(this, Tenso); @@ -1084,28 +23,9 @@ var Tenso = (function () { this.rates = {}; this.server = turtleio(); this.server.tenso = this; - this.version = VERSION; + this.version = "{{VERSION}}"; } - /** - * Tenso factory - * - * @method factory - * @param {Object} arg [Optional] Configuration - * @return {Object} Tenso instance - */ - - /** - * Sends an Error to the Client - * - * @method redirect - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Number} status Response status - * @param {Object} arg Response body - */ - _createClass(Tenso, [{ key: "error", value: function error(req, res, status, arg) { @@ -1113,16 +33,6 @@ var Tenso = (function () { return this; } - - /** - * Returns rate limit information for Client request - * - * @method rate - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} fn [Optional] Override default rate limit - * @return {Array} Array of rate limit information `[valid, total, remaining, reset]` - */ }, { key: "rate", value: function rate(req, fn) { @@ -1165,41 +75,21 @@ var Tenso = (function () { return [valid, limit, remaining, reset]; } - - /** - * Redirects the Client - * - * @method redirect - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Mixed} uri Target URI - * @return {Object} {@link Tenso} - */ }, { key: "redirect", value: function redirect(req, res, uri) { - this.server.respond(req, res, this.server.messages.NO_CONTENT, this.server.codes.FOUND, { location: uri }); + var perm = arguments.length <= 3 || arguments[3] === undefined ? false : arguments[3]; + + this.server.respond(req, res, this.server.messages.NO_CONTENT, this.server.codes[!perm ? "FOUND" : "MOVED"], { location: uri }); return this; } - - /** - * Renders a response body, defaults to JSON - * - * @method render - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} arg HTTP response body - * @param {Object} headers HTTP response headers - * @return {String} HTTP response body - */ }, { key: "render", value: function render(req, arg, headers) { - var accept = req.parsed.query.format || req.headers.accept || "application/json"; - var accepts = string.explode(accept, ";"); - var format = "json"; + var accept = req.parsed.query.format || req.headers.accept || "application/json", + accepts = utility.explode(accept, ";"), + format = "json"; array.each(this.server.config.renderers || [], function (i) { var found = false; @@ -1207,8 +97,7 @@ var Tenso = (function () { array.each(accepts, function (x) { if (x.indexOf(i) > -1) { format = i; - found = true; - return false; + return !(found = true); } }); @@ -1221,17 +110,6 @@ var Tenso = (function () { return renderers[format].fn(arg, req, headers, format === "html" ? this.server.config.template : undefined); } - - /** - * Registers a renderer - * - * @method renderer - * @memberOf Tenso - * @param {String} name Name of the renderer, e.g. "html" - * @param {Function} fn Function accepts `arg, req, headers, template` - * @param {String} mimetype Content-Type value - * @return {Object} {@link Tenso} - */ }, { key: "renderer", value: function renderer(name, fn, mimetype) { @@ -1240,19 +118,6 @@ var Tenso = (function () { return this; } - - /** - * Sends a response to the Client - * - * @method respond - * @memberOf Tenso - * @param {Object} req Client request - * @param {Object} res Client response - * @param {Mixed} arg Response body - * @param {Number} status Response status - * @param {Object} headers Response headers - * @return {Object} Defer - */ }, { key: "respond", value: function respond(req, res, arg, status, headers) { @@ -1264,12 +129,12 @@ var Tenso = (function () { ref = [headers || {}]; if (res._headers) { - merge(ref[0], res._headers); + utility.merge(ref[0], res._headers); } if (req.protect) { if (ref[0]["cache-control"] === undefined && this.server.config.headers["cache-control"]) { - ref[0]["cache-control"] = clone(this.server.config.headers["cache-control"]); + ref[0]["cache-control"] = utility.clone(this.server.config.headers["cache-control"]); } if (ref[0]["cache-control"] !== undefined && ref[0]["cache-control"].indexOf("private ") === -1) { @@ -1277,12 +142,12 @@ var Tenso = (function () { } } - if (!REGEX.modify.test(req.method) && REGEX.modify.test(req.allow) && this.server.config.security.csrf && res.locals[this.server.config.security.key]) { + if (!regex.modify.test(req.method) && regex.modify.test(req.allow) && this.server.config.security.csrf && res.locals[this.server.config.security.key]) { ref[0][this.server.config.security.key] = res.locals[this.server.config.security.key]; } ref[0] = this.server.headers(req, ref[0], resStatus); - this.server.respond(req, res, this.render(req, hypermedia(this.server, req, response(arg, resStatus), ref[0]), ref[0]), resStatus, ref[0]).then(function () { + this.server.respond(req, res, this.render(req, utility.hypermedia(this.server, req, this.serialize(req, arg, resStatus), ref[0]), ref[0]), resStatus, ref[0]).then(function () { defer.resolve(true); }, function (e) { defer.reject(e); @@ -1293,31 +158,54 @@ var Tenso = (function () { return defer.promise; } - }]); + }, { + key: "serialize", + value: function serialize(req, arg) { + var status = arguments.length <= 2 || arguments[2] === undefined ? 200 : arguments[2]; + + var format = "application/json", + accept = req.parsed.query.format || req.headers.accept || format, + accepts = utility.explode(accept, ";"), + errz = arg instanceof Error, + result = undefined, + serializer = undefined; + + array.each(this.server.config.serializers || [], function (i) { + var found = false; - return Tenso; -})(); + array.each(accepts, function (x) { + if (x.indexOf(i) > -1) { + format = i; + return !(found = true); + } + }); -function factory(arg) { - var hostname = arg ? arg.hostname || "localhost" : "localhost", - vhosts = {}, - config = arg ? merge(clone(CONFIG), arg) : CONFIG, - obj = undefined; + if (found) { + return false; + } + }); - if (!config.port) { - console.error("Invalid configuration"); - process.exit(1); - } + serializer = serializers[format] || serializers.tenso; - vhosts[hostname] = "www"; - config.root = path.join(__dirname, ".."); - config.vhosts = vhosts; - config["default"] = hostname; - config.template = fs.readFileSync(path.join(config.root, "template.html"), { encoding: "utf8" }); - obj = new Tenso(); - obj.hostname = hostname; + if (errz) { + result = serializer(null, arg, status < 400 ? 500 : status); + } else { + result = serializer(arg, null, status); + } - return bootstrap(obj, config); -} + return result; + } + }, { + key: "serializer", + value: function serializer(mime, fn) { + serializers[mime] = fn; + array.add(this.server.config.serializers, mime); + + return this; + } + }]); + + return Tenso; +})(); -module.exports = factory; +module.exports = Tenso; diff --git a/lib/utility.js b/lib/utility.js new file mode 100644 index 00000000..e50c0d2a --- /dev/null +++ b/lib/utility.js @@ -0,0 +1,835 @@ +"use strict"; + +var path = require("path"), + array = require("retsu"), + url = require("url"), + session = require("express-session"), + cookie = require("cookie-parser"), + lusca = require("lusca"), + uuid = require("tiny-uuid4"), + middleware = require(path.join(__dirname, "middleware.js")), + regex = require(path.join(__dirname, "regex.js")), + passport = require("passport"), + BasicStrategy = require("passport-http").BasicStrategy, + BearerStrategy = require("passport-http-bearer").Strategy, + FacebookStrategy = require("passport-facebook").Strategy, + GoogleStrategy = require("passport-google").Strategy, + LinkedInStrategy = require("passport-linkedin").Strategy, + LocalStrategy = require("passport-local").Strategy, + OAuth2Strategy = require("passport-oauth2").Strategy, + SAMLStrategy = require("passport-saml").Strategy, + TwitterStrategy = require("passport-twitter").Strategy, + RedisStore = require("connect-redis")(session); + +function trim(obj) { + return obj.replace(/^(\s+|\t+|\n+)|(\s+|\t+|\n+)$/g, ""); +} + +function explode(obj) { + var arg = arguments.length <= 1 || arguments[1] === undefined ? "," : arguments[1]; + + return trim(obj).split(new RegExp("\\s*" + arg + "\\s*")); +} + +function escape(arg) { + return arg.replace(/[-[\]{}()*+?.,\\^$|#]/g, "\\$&"); +} + +function capitalize(obj) { + var all = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1]; + + var result = undefined; + + if (all) { + result = explode(obj, " ").map(capitalize).join(" "); + } else { + result = obj.charAt(0).toUpperCase() + obj.slice(1); + } + + return result; +} + +function clone(arg) { + return JSON.parse(JSON.stringify(arg)); +} + +function coerce(value) { + var tmp = undefined; + + if (value === null || value === undefined) { + return undefined; + } else if (value === "true") { + return true; + } else if (value === "false") { + return false; + } else if (value === "null") { + return null; + } else if (value === "undefined") { + return undefined; + } else if (value === "") { + return value; + } else if (!isNaN(tmp = Number(value))) { + return tmp; + } else if (regex.json_wrap.test(value)) { + return JSON.parse(value); + } else { + return value; + } +} + +function contains(haystack, needle) { + return haystack.indexOf(needle) > -1; +} + +function isEmpty(obj) { + return trim(obj) === ""; +} + +function iterate(obj, fn) { + if (obj instanceof Object) { + Object.keys(obj).forEach(function (i) { + fn.call(obj, obj[i], i); + }); + } else { + obj.forEach(fn); + } +} + +function merge(a, b) { + if (a instanceof Object && b instanceof Object) { + Object.keys(b).forEach(function (i) { + if (a[i] instanceof Object && b[i] instanceof Object) { + a[i] = merge(a[i], b[i]); + } else if (a[i] instanceof Array && b[i] instanceof Array) { + a[i] = a[i].concat(b[i]); + } else { + a[i] = b[i]; + } + }); + } else if (a instanceof Array && b instanceof Array) { + a = a.concat(b); + } else { + a = b; + } + + return a; +} + +function auth(obj, config) { + var ssl = config.ssl.cert && config.ssl.key, + proto = "http" + (ssl ? "s" : ""), + realm = proto + "://" + (config.hostname === "localhost" ? "127.0.0.1" : config.hostname) + (config.port !== 80 && config.port !== 443 ? ":" + config.port : ""), + async = (config.auth.facebook.enabled || config.auth.google.enabled || config.auth.linkedin.enabled || config.auth.twitter.enabled) !== false, + stateless = (config.auth.basic.enabled || config.auth.bearer.enabled) !== false, + stateful = (async || config.auth.local.enabled || config.security.csrf) !== false, + authMap = {}, + authUris = [], + keys = undefined, + sesh = undefined; + + function init(sess) { + middleware.passportInit = passport.initialize(); + obj.server.use(middleware.passportInit).blacklist(middleware.passportInit); + + if (sess) { + middleware.passportSession = passport.session(); + obj.server.use(middleware.passportSession).blacklist(middleware.passportSession); + } + } + + function redirect(req, res) { + res.redirect(config.auth.redirect); + } + + obj.server.use(middleware.decorate).blacklist(middleware.decorate); + obj.server.use(middleware.parse).blacklist(middleware.parse); + + obj.server.blacklist(middleware.asyncFlag); + + config.auth.protect = (config.auth.protect || []).map(function (i) { + return new RegExp("^" + i !== "/login" ? i.replace(/\.\*/g, "*").replace(/\*/g, ".*") : "$", "i"); + }); + + config.auth.unprotect = (config.auth.unprotect || []).map(function (i) { + return new RegExp("^" + i !== "/login" ? i.replace(/\.\*/g, "*").replace(/\*/g, ".*") : "$", "i"); + }); + + if (async) { + iterate(config.auth, function (v, k) { + if (v.enabled) { + authMap[k + "_uri"] = "/auth/" + k; + config.auth.protect.push(new RegExp("^/auth/" + k)); + } + }); + } + + authUris = array.keys(authMap); + + if (config.auth.local.enabled) { + authUris.push(config.auth.redirect); + authUris.push("/login"); + } + + obj.server.use(middleware.valid).blacklist(middleware.valid); + + if (stateful) { + sesh = { + secret: config.session.secret || uuid(), + saveUninitialized: true, + rolling: true, + resave: true + }; + + if (config.session.store === "redis") { + sesh.store = new RedisStore(config.session.redis); + } + + middleware.cookie = cookie(); + middleware.session = session(sesh); + + obj.server.use(middleware.session).blacklist(middleware.session); + obj.server.use(middleware.cookie).blacklist(middleware.cookie); + obj.server.use(middleware.bypass).blacklist(middleware.bypass); + + if (config.security.csrf) { + middleware.luscaCsrf = lusca.csrf({ key: config.security.key, secret: config.security.secret }); + obj.server.use(middleware.csrfWrapper).blacklist(middleware.csrfWrapper); + } + } + + if (config.security.csp instanceof Object) { + middleware.luscaCsp = lusca.csp(config.security.csp); + obj.server.use(middleware.luscaCsp).blacklist(middleware.luscaCsp); + } + + if (!isEmpty(config.security.xframe || "")) { + middleware.luscaXframe = lusca.xframe(config.security.xframe); + obj.server.use(middleware.luscaXframe).blacklist(middleware.luscaXframe); + } + + if (!isEmpty(config.security.p3p || "")) { + middleware.luscaP3p = lusca.p3p(config.security.p3p); + obj.server.use(middleware.luscaP3p).blacklist(middleware.luscaP3p); + } + + if (config.security.hsts instanceof Object) { + middleware.luscaHsts = lusca.hsts(config.security.hsts); + obj.server.use(middleware.luscaHsts).blacklist(middleware.luscaHsts); + } + + if (config.security.xssProtection instanceof Object) { + middleware.luscaXssProtection = lusca.xssProtection(config.security.xssProtection); + obj.server.use(middleware.luscaXssProtection).blacklist(middleware.luscaXssProtection); + } + + // Can fork to `middleware.keymaster()` + obj.server.use(middleware.zuul).blacklist(middleware.zuul); + + if (stateless && !stateful) { + init(false); + } else { + init(true); + + passport.serializeUser(function (user, done) { + done(null, user); + }); + + passport.deserializeUser(function (arg, done) { + done(null, arg); + }); + + if (authUris.length > 0) { + keys = array.keys(authMap).length > 0; + + if (keys) { + config.routes.get["/auth"] = authMap; + } + + (function () { + var r = "(?!/auth/("; + + array.each(authUris, function (i) { + r += i.replace("_uri", "") + "|"; + }); + + r = r.replace(/\|$/, "") + ")).*$"; + + obj.server.use(r, middleware.rate).blacklist(middleware.rate); + obj.server.use(r, middleware.guard).blacklist(middleware.guard); + })(); + + config.routes.get["/login"] = config.auth.local.enabled ? keys ? { + login_uri: "/auth", + instruction: "POST 'username' & 'password' to authenticate" + } : { instruction: "POST 'username' & 'password' to authenticate" } : { login_uri: "/auth" }; + } else if (config.auth.local.enabled) { + config.routes.get["/login"] = { instruction: "POST 'username' & 'password' to authenticate" }; + } + + config.routes.get["/logout"] = function (req, res) { + if (req.session) { + req.session.destroy(); + } + + res.redirect(config.auth.redirect); + }; + } + + if (config.auth.basic.enabled) { + (function () { + var x = {}; + + function validate(arg, cb) { + if (x[arg]) { + cb(null, x[arg]); + } else { + cb(new Error("Unauthorized"), null); + } + } + + array.each(config.auth.basic.list || [], function (i) { + var args = i.split(":"); + + if (args.length > 0) { + x[args[0]] = { password: args[1] }; + } + }); + + passport.use(new BasicStrategy(function (username, password, done) { + validate(username, function (err, user) { + if (err) { + delete err.stack; + return done(err); + } + + if (!user || user.password !== password) { + return done(null, false); + } + + return done(null, user); + }); + })); + + middleware.passportAuth = passport.authenticate("basic", { session: stateful }); + + if (async || config.auth.local.enabled) { + obj.server.get("/auth/basic", middleware.passportAuth).blacklist(middleware.passportAuth); + obj.server.get("/auth/basic", redirect); + } else { + obj.server.use(middleware.passportAuth).blacklist(middleware.passportAuth); + } + })(); + } + + if (config.auth.bearer.enabled) { + (function () { + var x = config.auth.bearer.tokens || []; + + function validate(arg, cb) { + if (array.contains(x, arg)) { + cb(null, arg); + } else { + cb(new Error("Unauthorized"), null); + } + } + + passport.use(new BearerStrategy(function (token, done) { + validate(token, function (err, user) { + if (err) { + delete err.stack; + return done(err); + } + + if (!user) { + return done(null, false); + } + + return done(null, user, { scope: "read" }); + }); + })); + + middleware.passportAuth = passport.authenticate("bearer", { session: stateful }); + + if (async || config.auth.local.enabled) { + obj.server.get("/auth/bearer", middleware.passportAuth).blacklist(middleware.passportAuth); + obj.server.get("/auth/bearer", redirect); + } else { + obj.server.use(middleware.passportAuth).blacklist(middleware.passportAuth); + } + })(); + } + + if (config.auth.facebook.enabled) { + passport.use(new FacebookStrategy({ + clientID: config.auth.facebook.client_id, + clientSecret: config.auth.facebook.client_secret, + callbackURL: realm + "/auth/facebook/callback" + }, function (accessToken, refreshToken, profile, done) { + config.auth.facebook.auth(accessToken, refreshToken, profile, function (err, user) { + if (err) { + delete err.stack; + return done(err); + } + + done(null, user); + }); + })); + + obj.server.get("/auth/facebook", middleware.asyncFlag); + obj.server.get("/auth/facebook", passport.authenticate("facebook")); + obj.server.get("/auth/facebook/callback", middleware.asyncFlag); + obj.server.get("/auth/facebook/callback", passport.authenticate("facebook", { failureRedirect: "/login" })); + obj.server.get("/auth/facebook/callback", redirect); + } + + if (config.auth.google.enabled) { + passport.use(new GoogleStrategy({ + returnURL: realm + "/auth/google/callback", + realm: realm + }, function (identifier, profile, done) { + config.auth.google.auth.call(obj, identifier, profile, function (err, user) { + if (err) { + delete err.stack; + return done(err); + } + + done(null, user); + }); + })); + + obj.server.get("/auth/google", middleware.asyncFlag); + obj.server.get("/auth/google", passport.authenticate("google")); + obj.server.get("/auth/google/callback", middleware.asyncFlag); + obj.server.get("/auth/google/callback", passport.authenticate("google", { failureRedirect: "/login" })); + obj.server.get("/auth/google/callback", redirect); + } + + if (config.auth.linkedin.enabled) { + passport.use(new LinkedInStrategy({ + consumerKey: config.auth.linkedin.client_id, + consumerSecret: config.auth.linkedin.client_secret, + callbackURL: realm + "/auth/linkedin/callback" + }, function (token, tokenSecret, profile, done) { + config.auth.linkedin.auth(token, tokenSecret, profile, function (err, user) { + if (err) { + delete err.stack; + return done(err); + } + + done(null, user); + }); + })); + + obj.server.get("/auth/linkedin", middleware.asyncFlag); + obj.server.get("/auth/linkedin", passport.authenticate("linkedin", { "scope": config.auth.linkedin.scope || ["r_basicprofile", "r_emailaddress"] })); + obj.server.get("/auth/linkedin/callback", middleware.asyncFlag); + obj.server.get("/auth/linkedin/callback", passport.authenticate("linkedin", { failureRedirect: "/login" })); + obj.server.get("/auth/linkedin/callback", redirect); + } + + if (config.auth.local.enabled) { + passport.use(new LocalStrategy(function (username, password, done) { + config.auth.local.auth(username, password, function (err, user) { + if (err) { + delete err.stack; + return done(err); + } + + done(null, user); + }); + })); + + config.routes.post = config.routes.post || {}; + config.routes.post["/login"] = function (req, res) { + function final() { + passport.authenticate("local")(req, res, function (e) { + if (e) { + res.error(401, "Unauthorized"); + } else if (req.cors && req.headers["x-requested-with"] && req.headers["x-requested-with"] === "XMLHttpRequest") { + res.respond("Success"); + } else { + res.redirect(config.auth.redirect); + } + }); + } + + function mid() { + middleware.passportSession(req, res, final); + } + + middleware.passportInit(req, res, mid); + }; + } + + if (config.auth.oauth2.enabled) { + passport.use(new OAuth2Strategy({ + authorizationURL: config.auth.oauth2.auth_url, + tokenURL: config.auth.oauth2.token_url, + clientID: config.auth.oauth2.client_id, + clientSecret: config.auth.oauth2.client_secret, + callbackURL: realm + "/auth/oauth2/callback" + }, function (accessToken, refreshToken, profile, done) { + config.auth.oauth2.auth(accessToken, refreshToken, profile, function (err, user) { + if (err) { + delete err.stack; + return done(err); + } + + done(null, user); + }); + })); + + obj.server.get("/auth/oauth2", middleware.asyncFlag); + obj.server.get("/auth/oauth2", passport.authenticate("oauth2")); + obj.server.get("/auth/oauth2/callback", middleware.asyncFlag); + obj.server.get("/auth/oauth2/callback", passport.authenticate("oauth2", { failureRedirect: "/login" })); + obj.server.get("/auth/oauth2/callback", redirect); + } + + if (config.auth.saml.enabled) { + (function () { + var arg = config.auth.saml; + + arg.callbackURL = realm + "/auth/saml/callback"; + delete arg.enabled; + delete arg.path; + + passport.use(new SAMLStrategy(arg, function (profile, done) { + config.auth.saml.auth(profile, function (err, user) { + if (err) { + delete err.stack; + return done(err); + } + + done(null, user); + }); + })); + })(); + + obj.server.get("/auth/saml", middleware.asyncFlag); + obj.server.get("/auth/saml", passport.authenticate("saml")); + obj.server.get("/auth/saml/callback", middleware.asyncFlag); + obj.server.get("/auth/saml/callback", passport.authenticate("saml", { failureRedirect: "/login" })); + obj.server.get("/auth/saml/callback", redirect); + } + + if (config.auth.twitter.enabled) { + passport.use(new TwitterStrategy({ + consumerKey: config.auth.twitter.consumer_key, + consumerSecret: config.auth.twitter.consumer_secret, + callbackURL: realm + "/auth/twitter/callback" + }, function (token, tokenSecret, profile, done) { + config.auth.twitter.auth(token, tokenSecret, profile, function (err, user) { + if (err) { + delete err.stack; + return done(err); + } + + done(null, user); + }); + })); + + obj.server.get("/auth/twitter", middleware.asyncFlag); + obj.server.get("/auth/twitter", passport.authenticate("twitter")); + obj.server.get("/auth/twitter/callback", middleware.asyncFlag); + obj.server.get("/auth/twitter/callback", passport.authenticate("twitter", { + successRedirect: config.auth.redirect, + failureRedirect: "/login" + })); + } + + return config; +} + +function bootstrap(obj, config) { + var notify = false; + + // Bootstrapping configuration + auth(obj, config); + + // Setting headers + config.headers = config.headers || {}; + config.headers.server = "tenso/{{VERSION}}"; + + // Creating status > message map + iterate(obj.server.codes, function (value, key) { + obj.messages[value] = obj.server.messages[key]; + }); + + // Setting routes + iterate(config.routes, function (routes, method) { + iterate(routes, function (arg, route) { + if (typeof arg === "function") { + obj.server[method](route, function () { + for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { + args[_key] = arguments[_key]; + } + + arg.apply(obj, args); + }); + } else { + obj.server[method](route, function (req, res) { + obj.respond(req, res, arg); + }); + } + }); + }); + + // Disabling compression over SSL due to BREACH + if (config.ssl.cert && config.ssl.key) { + config.compress = false; + notify = true; + } + + // Starting API server + obj.server.start(config, function (req, res, status, msg) { + var stat = status instanceof Error ? parseInt(status.message, 10) : status, + err = msg instanceof Error ? msg : new Error(msg || obj.messages[stat]); + + obj.error(req, res, stat, err); + }); + + if (notify) { + obj.server.log("Compression over SSL is disabled for your protection", "debug"); + } + + return obj; +} + +function queryString() { + var qstring = arguments.length <= 0 || arguments[0] === undefined ? "" : arguments[0]; + + var obj = {}; + var aresult = qstring.split("?"); + var result = undefined; + + if (aresult.length > 1) { + aresult.shift(); + } + + result = aresult.join("?"); + result.split("&").forEach(function (prop) { + var aitem = prop.replace(/\+/g, " ").split("="); + var item = undefined; + + if (aitem.length > 2) { + item = [aitem.shift(), aitem.join("=")]; + } else { + item = aitem; + } + + if (isEmpty(item[0])) { + return; + } + + if (item[1] === undefined) { + item[1] = ""; + } else { + item[1] = coerce(decodeURIComponent(item[1])); + } + + if (obj[item[0]] === undefined) { + obj[item[0]] = item[1]; + } else if (obj[item[0]] instanceof Array === false) { + obj[item[0]] = [obj[item[0]]]; + obj[item[0]].push(item[1]); + } else { + obj[item[0]].push(item[1]); + } + }); + + return obj; +} + +function parse(uri) { + var luri = uri; + var idxAscii = undefined, + idxQ = undefined, + parsed = undefined; + + if (luri === undefined || luri === null) { + luri = ""; + } else { + idxAscii = luri.indexOf("%3F"); + idxQ = luri.indexOf("?"); + + switch (true) { + case idxQ === -1 && idxAscii > -1: + case idxAscii < idxQ: + luri = luri.replace("%3F", "?"); + break; + default: + void 0; + } + } + + parsed = url.parse(luri); + parsed.query = parsed.search ? queryString(parsed.search) : {}; + + iterate(parsed, function (v, k) { + if (v === null) { + parsed[k] = ""; + } + }); + + return parsed; +} + +function hypermedia(server, req, rep, headers) { + var seen = {}, + collection = req.parsed.pathname, + query = undefined, + page = undefined, + page_size = undefined, + nth = undefined, + root = undefined, + parent = undefined; + + // Parsing the object for hypermedia properties + function marshal(obj, rel, item_collection) { + var keys = array.keys(obj), + lrel = rel || "related", + result = undefined; + + if (keys.length === 0) { + result = null; + } else { + array.each(keys, function (i) { + var lcollection = undefined, + uri = undefined; + + // If ID like keys are found, and are not URIs, they are assumed to be root collections + if (regex.id.test(i) || regex.hypermedia.test(i)) { + if (!regex.id.test(i)) { + lcollection = i.replace(regex.trailing, "").replace(regex.trailing_s, "").replace(regex.trailing_y, "ie") + "s"; + lrel = "related"; + } else { + lcollection = item_collection; + lrel = "item"; + } + + uri = regex.scheme.test(obj[i]) ? obj[i] : "/" + lcollection + "/" + obj[i]; + + if (uri !== root && !seen[uri]) { + seen[uri] = 1; + + if (server.allowed("get", uri, req.vhost)) { + rep.links.push({ uri: uri, rel: lrel }); + } + } + } + }); + + result = obj; + } + + return result; + } + + if (rep.status >= 200 && rep.status <= 206) { + query = req.parsed.query; + page = query.page || 1; + page_size = query.page_size || server.config.pageSize || 5; + root = req.parsed.pathname; + + if (req.parsed.pathname !== "/") { + rep.links.push({ + uri: root.replace(regex.trailing_slash, "").replace(regex.collection, "$1") || "/", + rel: "collection" + }); + } + + if (rep.data instanceof Array) { + if (req.method === "GET") { + if (isNaN(page) || page <= 0) { + page = 1; + } + + nth = Math.ceil(rep.data.length / page_size); + + if (nth > 1) { + rep.data = array.limit(rep.data, (page - 1) * page_size, page_size); + query.page = 0; + query.page_size = page_size; + + root += "?" + array.keys(query).map(function (i) { + return i + "=" + encodeURIComponent(query[i]); + }).join("&"); + + if (page > 1) { + rep.links.push({ uri: root.replace("page=0", "page=1"), rel: "first" }); + } + + if (page - 1 > 1 && page <= nth) { + rep.links.push({ uri: root.replace("page=0", "page=" + (page - 1)), rel: "prev" }); + } + + if (page + 1 < nth) { + rep.links.push({ uri: root.replace("page=0", "page=" + (page + 1)), rel: "next" }); + } + + if (nth > 0 && page !== nth) { + rep.links.push({ uri: root.replace("page=0", "page=" + nth), rel: "last" }); + } + } else { + root += "?" + array.keys(query).map(function (i) { + return i + "=" + encodeURIComponent(query[i]); + }).join("&"); + } + } + + array.each(rep.data, function (i) { + var li = i.toString(), + uri; + + if (li !== collection) { + uri = li.indexOf("//") > -1 || li.indexOf("/") === 0 ? li : (collection + "/" + li).replace(/^\/\//, "/"); + + if (server.allowed("get", uri, req.vhost)) { + rep.links.push({ uri: uri, rel: "item" }); + } + } + + if (i instanceof Object) { + parse(i, "item", req.parsed.pathname.replace(regex.trailing_slash, "").replace(regex.leading, "")); + } + }); + } else if (rep.data instanceof Object) { + parent = req.parsed.pathname.split("/").filter(function (i) { + return i !== ""; + }); + + if (parent.length > 1) { + parent.pop(); + } + + rep.data = marshal(rep.data, undefined, array.last(parent)); + } + + if (rep.links.length > 0) { + headers.link = array.keySort(rep.links, "rel, uri").map(function (i) { + return "<" + i.uri + ">; rel=\"" + i.rel + "\""; + }).join(", "); + } + } + + return rep; +} + +module.exports = { + auth: auth, + bootstrap: bootstrap, + capitalize: capitalize, + clone: clone, + coerce: coerce, + contains: contains, + explode: explode, + escape: escape, + hypermedia: hypermedia, + isEmpty: isEmpty, + iterate: iterate, + merge: merge, + queryString: queryString, + parse: parse, + trim: trim +}; diff --git a/package.json b/package.json index 44e5d30f..c648e876 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "connect-redis": "^3.0.1", "cookie-parser": "^1.4.0", "express-session": "^1.12.1", - "keigai": "~1.3.19", "lusca": "^1.3.0", "passport": "^0.3.2", "passport-facebook": "^2.0.0", @@ -47,16 +46,13 @@ "babel-preset-es2015": "^6.1.2", "grunt": "^0.4.5", "grunt-babel": "^6.0.0", - "grunt-cli": "^0.1.13", - "grunt-contrib-concat": "^0.1.3", "grunt-contrib-sass": "^0.9.2", "grunt-contrib-watch": "^0.2.0", "grunt-eslint": "^17.3.1", - "grunt-jsdoc": "~0.5.6", "grunt-mocha-test": "^0.12.7", "grunt-nsp": "^2.1.2", + "grunt-sed": "^0.1.1", "hippie": "^0.4.0", - "ink-docstrap": "~0.4.12", "mocha": "^2.3.2" }, "keywords": [ diff --git a/src/index.js b/src/index.js index 51cea8fe..bc3ccf5c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,8 @@ -const path = require("path"), +const fs = require("fs"), + path = require("path"), root = path.join(__dirname, ".."), cfg = require(path.join(root, "config.json")), - Tenso = require(path.join(__dirname, "tenso.js")); + Tenso = require(path.join(__dirname, "tenso.js")), utility = require(path.join(__dirname, "utility.js")); function factory (arg) { diff --git a/src/intro.js b/src/intro.js deleted file mode 100644 index 169d90af..00000000 --- a/src/intro.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; - -const CONFIG = require(__dirname + "/../config.json"); -const VERSION = "{{VERSION}}"; -const SERVER = "tenso/" + VERSION; - -let keigai = require("keigai"), - deferred = require("tiny-defer"), - util = keigai.util, - array = util.array, - coerce = util.coerce, - iterate = util.iterate, - json = util.json, - merge = util.merge, - string = util.string, - uuid = util.uuid, - xml = util.xml, - fs = require("fs"), - path = require("path"), - yaml = require("yamljs"), - turtleio = require("turtle.io"), - session = require("express-session"), - cookie = require("cookie-parser"), - lusca = require("lusca"), - passport = require("passport"), - BasicStrategy = require("passport-http").BasicStrategy, - BearerStrategy = require("passport-http-bearer").Strategy, - FacebookStrategy = require("passport-facebook").Strategy, - GoogleStrategy = require("passport-google").Strategy, - LinkedInStrategy = require("passport-linkedin").Strategy, - LocalStrategy = require("passport-local").Strategy, - OAuth2Strategy = require("passport-oauth2").Strategy, - SAMLStrategy = require("passport-saml").Strategy, - TwitterStrategy = require("passport-twitter").Strategy, - RedisStore = require("connect-redis")(session); diff --git a/src/middleware.js b/src/middleware.js index 5613a499..284c9c6b 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -1,37 +1,110 @@ const path = require("path"), + array = require("retsu"), regex = require(path.join(__dirname, "regex.js")), utility = require(path.join(__dirname, "utility.js")); +let cookie, session, luscaCsp, luscaCsrf, luscaXframe, luscaP3p, luscaHsts, luscaXssProtection, passportAuth, + passportInit, passportSession; + const rateHeaders = [ "x-ratelimit-limit", "x-ratelimit-remaining", "x-ratelimit-reset" ]; -function rate (req, res, next) { - let server = req.server, - obj = server.tenso, - config = server.config.rate, - results = obj.rate(req, config.override), - valid = results.shift(); +function decorate (req, res, next) { + var obj = req.server.tenso; - rateHeaders.forEach(function (i, idx) { - res.setHeader(i, results[idx]); - }); + req.protect = false; + req.protectAsync = false; + req.unprotect = false; + + res.error = function (status, body) { + return obj.error(req, res, status, body); + }; + + res.redirect = function (uri, perm = true) { + return obj.redirect(req, res, uri, undefined, perm); + }; + + res.respond = function (body, status, headers) { + return obj.respond(req, res, body, status, headers); + }; + + res.send = function (body, status, headers) { + return obj.respond(req, res, body, status, headers); + }; + + next(); +} + +function asyncFlag (req, res, next) { + req.protectAsync = true; + next(); +} + +function bypass (req, res, next) { + let pass = req.server.tenso.config.auth.unprotect.filter(function (i) { + return i.test(req.url); + }).length > 0; - if (valid) { + if (pass) { + req.unprotect = true; + } + + next(); +} + +function csrfWrapper (req, res, next) { + if (req.unprotect) { + next(); + } else { + luscaCsrf(req, res, next); + } +} + +function guard (req, res, next) { + if (req.parsed.url === "/login" || req.isAuthenticated()) { next(); } else { - obj.error(req, res, config.status || 429, config.message || "Too Many Requests"); + res.redirect("/login"); } } +function parse (req, res, next) { + let args, type; + + if (regex.body.test(req.method) && req.body !== undefined) { + type = req.headers["content-type"]; + + if (regex.encode_form.test(type)) { + args = req.body ? array.chunk(req.body.split(regex.body_split), 2) : []; + req.body = {}; + + array.each(args, function (i) { + req.body[i[0]] = utility.coerce(i[1]); + }); + } + + if (regex.encode_json.test(type)) { + try { + req.body = JSON.parse(req.body); + } catch (e) { + console.warn(e.message); + } + } + } + + next(); +} + function keymaster (req, res, next) { let obj = req.server.tenso, + authd = req.session && req.isAuthenticated(), method, result, routes, uri; // No authentication, or it's already happened - if (!req.protect || !req.protectAsync || (req.session && req.isAuthenticated())) { + if (!req.protect || !req.protectAsync || authd) { method = regex.get_rewrite.test(req.method) ? "get" : req.method.toLowerCase(); routes = req.server.config.routes[method] || {}; uri = req.parsed.pathname; @@ -43,7 +116,7 @@ function keymaster (req, res, next) { result.call(obj, req, res); next(); } else { - obj.respond(req, res, result).then(next, next); + res.send(result).then(next, next); } } else { utility.iterate(routes, function (value, key) { @@ -57,18 +130,83 @@ function keymaster (req, res, next) { result.call(obj, req, res); next(); } else { - obj.respond(req, res, result).then(next, next); + res.send(result).then(next, next); } } else { - obj.error(req, res, 404); + next(new Error(404)); } } } else { - obj.error(req, res, 401); + next(new Error(401)); + } +} + +function zuul (req, res, next) { + let uri = req.parsed.path, + protectd = false; + + array.each(req.server.tenso.protect, function (r) { + if (r.test(uri)) { + return !(protectd = true); + } + }); + + // Setting state so the connection can be terminated properly + req.protect = protectd; + req.protectAsync = false; + + if (protectd && next) { + next(); + } else { + keymaster(req, res, next); + } +} + +function rate (req, res, next) { + let server = req.server, + obj = server.tenso, + config = server.config.rate, + results = obj.rate(req, config.override), + good = results.shift(); + + rateHeaders.forEach(function (i, idx) { + res.setHeader(i, results[idx]); + }); + + if (good) { + next(); + } else { + next(new Error(config.status || 429)); + } +} + +function valid (req, res, next) { + if (req.allow.indexOf(req.method) > -1) { + next(); + } else { + next(new Error(405)); } } module.exports = { - keymaster: keymaster, - rate: rate + asyncFlag: asyncFlag, + bypass: bypass, + cookie: cookie, + csrfWrapper: csrfWrapper, + decorate: decorate, + guard: guard, + luscaCsp: luscaCsp, + luscaCsrf: luscaCsrf, + luscaXframe: luscaXframe, + luscaP3p: luscaP3p, + luscaHsts: luscaHsts, + luscaXssProtection: luscaXssProtection, + passportAuth: passportAuth, + passportInit: passportInit, + passportSession: passportSession, + parse: parse, + rate: rate, + session: session, + valid: valid, + zuul: zuul }; diff --git a/src/regex.js b/src/regex.js index a8631859..39ae7877 100644 --- a/src/regex.js +++ b/src/regex.js @@ -16,4 +16,4 @@ const regex = { trailing_y: /y$/ }; -modules.export = regex; +module.exports = regex; diff --git a/src/renderers.js b/src/renderers.js index 7e81946a..636d2541 100644 --- a/src/renderers.js +++ b/src/renderers.js @@ -1,6 +1,8 @@ const array = require("retsu"), xml = require("tiny-xml"), - yaml = require("yamljs"); + yaml = require("yamljs"), + path = require("path"), + utility = require(path.join(__dirname, "utility.js")); function sanitize (arg) { let output = arg; @@ -39,7 +41,7 @@ let renderers = { .replace("{{year}}", new Date().getFullYear()) .replace("{{version}}", "{{VERSION}}") .replace("{{allow}}", headers.allow) - .replace("{{methods}}", string.explode(headers.allow.replace("GET, HEAD, OPTIONS", "")).filter(function (i) { + .replace("{{methods}}", utility.explode(headers.allow.replace("GET, HEAD, OPTIONS", "")).filter(function (i) { return i !== ""; }).map(function (i) { return ""; @@ -68,4 +70,4 @@ let renderers = { } }; -modules.export = renderers; +module.exports = renderers; diff --git a/src/serializers.js b/src/serializers.js index a14b02e1..ce3641ba 100644 --- a/src/serializers.js +++ b/src/serializers.js @@ -1,15 +1,15 @@ function tenso (arg, err, status) { return { data: arg !== null ? arg : null, - error: arg === null ? (err.message || err || "Something went wrong") : null, + error: arg === null ? err.message || err || "Something went wrong" : null, links: [], status: status || 200 }; } let serializers = { - default: "tenso", + "application/json": tenso, tenso: tenso }; -modules.export = serializers; +module.exports = serializers; diff --git a/src/tenso.js b/src/tenso.js index 2baf191a..ab7dd210 100644 --- a/src/tenso.js +++ b/src/tenso.js @@ -1,3 +1,13 @@ +const path = require("path"), + array = require("retsu"), + turtleio = require("turtle.io"), + deferred = require("tiny-defer"), + regex = require(path.join(__dirname, "regex")), + utility = require(path.join(__dirname, "utility")); + +let renderers = require(path.join(__dirname, "renderers")), + serializers = require(path.join(__dirname, "serializers")); + class Tenso { constructor () { this.hostname = ""; @@ -5,7 +15,7 @@ class Tenso { this.rates = {}; this.server = turtleio(); this.server.tenso = this; - this.version = VERSION; + this.version = "{{VERSION}}"; } error (req, res, status, arg) { @@ -40,7 +50,7 @@ class Tenso { reset = state.reset; if (seconds >= reset) { - reset = state.reset = (seconds + config.reset); + reset = state.reset = seconds + config.reset; remaining = state.remaining = limit - 1; } else if (remaining > 0) { state.remaining--; @@ -60,7 +70,7 @@ class Tenso { render (req, arg, headers) { let accept = req.parsed.query.format || req.headers.accept || "application/json", - accepts = string.explode(accept, ";"), + accepts = utility.explode(accept, ";"), format = "json"; array.each(this.server.config.renderers || [], function (i) { @@ -99,12 +109,12 @@ class Tenso { ref = [headers || {}]; if (res._headers) { - merge(ref[0], res._headers); + utility.merge(ref[0], res._headers); } if (req.protect) { if (ref[0]["cache-control"] === undefined && this.server.config.headers["cache-control"]) { - ref[0]["cache-control"] = clone(this.server.config.headers["cache-control"]); + ref[0]["cache-control"] = utility.clone(this.server.config.headers["cache-control"]); } if (ref[0]["cache-control"] !== undefined && ref[0]["cache-control"].indexOf("private ") === -1) { @@ -112,12 +122,12 @@ class Tenso { } } - if (!REGEX.modify.test(req.method) && REGEX.modify.test(req.allow) && this.server.config.security.csrf && res.locals[this.server.config.security.key]) { + if (!regex.modify.test(req.method) && regex.modify.test(req.allow) && this.server.config.security.csrf && res.locals[this.server.config.security.key]) { ref[0][this.server.config.security.key] = res.locals[this.server.config.security.key]; } ref[0] = this.server.headers(req, ref[0], resStatus); - this.server.respond(req, res, this.render(req, hypermedia(this.server, req, response(arg, resStatus), ref[0]), ref[0]), resStatus, ref[0]).then(function () { + this.server.respond(req, res, this.render(req, utility.hypermedia(this.server, req, this.serialize(req, arg, resStatus), ref[0]), ref[0]), resStatus, ref[0]).then(function () { defer.resolve(true); }, function (e) { defer.reject(e); @@ -128,10 +138,46 @@ class Tenso { return defer.promise; } + + serialize (req, arg, status = 200) { + let format = "application/json", + accept = req.parsed.query.format || req.headers.accept || format, + accepts = utility.explode(accept, ";"), + errz = arg instanceof Error, + result, serializer; + + array.each(this.server.config.serializers || [], function (i) { + let found = false; + + array.each(accepts, function (x) { + if (x.indexOf(i) > -1) { + format = i; + return !(found = true); + } + }); + + if (found) { + return false; + } + }); + + serializer = serializers[format] || serializers.tenso; + + if (errz) { + result = serializer(null, arg, status < 400 ? 500 : status); + } else { + result = serializer(arg, null, status); + } + + return result; + } + + serializer (mime, fn) { + serializers[mime] = fn; + array.add(this.server.config.serializers, mime); + + return this; + } } module.exports = Tenso; - -/*function error (server, req, res, status, err) { - server.respond(req, res, err, status); -}*/ diff --git a/src/utility.js b/src/utility.js index af302cfa..04814130 100644 --- a/src/utility.js +++ b/src/utility.js @@ -1,10 +1,12 @@ const path = require("path"), array = require("retsu"), - regex = require(path.join(__dirname, "regex.js")), url = require("url"), session = require("express-session"), cookie = require("cookie-parser"), lusca = require("lusca"), + uuid = require("tiny-uuid4"), + middleware = require(path.join(__dirname, "middleware.js")), + regex = require(path.join(__dirname, "regex.js")), passport = require("passport"), BasicStrategy = require("passport-http").BasicStrategy, BearerStrategy = require("passport-http-bearer").Strategy, @@ -15,9 +17,7 @@ const path = require("path"), OAuth2Strategy = require("passport-oauth2").Strategy, SAMLStrategy = require("passport-saml").Strategy, TwitterStrategy = require("passport-twitter").Strategy, - RedisStore = require("connect-redis")(session), - serializers = require(path.join(__dirname, "serializers.js")), - serializer = serializers[serializers.default]; + RedisStore = require("connect-redis")(session); function trim (obj) { return obj.replace(/^(\s+|\t+|\n+)|(\s+|\t+|\n+)$/g, ""); @@ -75,236 +75,6 @@ function contains (haystack, needle) { return haystack.indexOf(needle) > -1; } -function bootstrap (obj, config) { - let notify = false; - - function decorate (req, res, next) { - res.error = function (status, body) { - return obj.error(req, res, status, body); - }; - - res.redirect = function (uri, perm = true) { - return obj.redirect(req, res, uri, undefined, perm); - }; - - res.respond = function (body, status, headers) { - return obj.respond(req, res, body, status, headers); - }; - - res.send = function (body, status, headers) { - return obj.respond(req, res, body, status, headers); - }; - - next(); - } - - function parse (req, res, next) { - let args, type; - - if (REGEX.body.test(req.method) && req.body !== undefined) { - type = req.headers["content-type"]; - - if (REGEX.encode_form.test(type)) { - args = req.body ? array.chunk(req.body.split(REGEX.body_split), 2) : []; - req.body = {}; - - array.each(args, function (i) { - req.body[i[0]] = coerce(i[1]); - }); - } - - if (REGEX.encode_json.test(type)) { - req.body = json.decode(req.body, true) || req.body; - } - } - - next(); - } - - obj.server.use(decorate).blacklist(decorate); - obj.server.use(parse).blacklist(parse); - - // Bootstrapping configuration - auth(obj, config); - config.headers = config.headers || {}; - config.headers.server = "tenso/{{VERSION}}"; - - // Creating status > message map - iterate(obj.server.codes, function (value, key) { - obj.messages[value] = obj.server.messages[key]; - }); - - // Setting routes - iterate(config.routes, function (routes, method) { - iterate(routes, function (arg, route) { - if (typeof arg === "function") { - obj.server[method](route, function (...args) { - arg.apply(obj, args); - }); - } else { - obj.server[method](route, function (req, res) { - obj.respond(req, res, arg); - }); - } - }); - }); - - // Disabling compression over SSL due to BREACH - if (config.ssl.cert && config.ssl.key) { - config.compress = false; - notify = true; - } - - // Starting API server - obj.server.start(config, function (req, res, status, msg) { - let stat = status instanceof Error ? parseInt(status.message, 10) : status, - err = msg instanceof Error ? msg : new Error(msg || obj.messages[stat]); - - error(obj, req, res, stat, err, obj); - }); - - if (notify) { - obj.server.log("Compression over SSL is disabled for your protection", "debug"); - } - - return obj; -} - -function hypermedia (server, req, rep, headers) { - let seen = {}, - collection = req.parsed.pathname, - query, page, page_size, nth, root, parent; - - // Parsing the object for hypermedia properties - function parse (obj, rel, item_collection) { - let keys = array.keys(obj), - lrel = rel || "related", - result; - - if (keys.length === 0) { - result = null; - } else { - array.each(keys, function (i) { - let lcollection, uri; - - // If ID like keys are found, and are not URIs, they are assumed to be root collections - if (REGEX.id.test(i) || REGEX.hypermedia.test(i)) { - if (!REGEX.id.test(i)) { - lcollection = i.replace(REGEX.trailing, "").replace(REGEX.trailing_s, "").replace(REGEX.trailing_y, "ie") + "s"; - lrel = "related"; - } else { - lcollection = item_collection; - lrel = "item"; - } - - uri = REGEX.scheme.test(obj[i]) ? obj[i] : ("/" + lcollection + "/" + obj[i]); - - if (uri !== root && !seen[uri]) { - seen[uri] = 1; - - if (server.allowed("get", uri, req.vhost)) { - rep.links.push({uri: uri, rel: lrel}); - } - } - } - }); - - result = obj; - } - - return result; - } - - if (rep.status >= 200 && rep.status <= 206) { - query = req.parsed.query; - page = query.page || 1; - page_size = query.page_size || server.config.pageSize || 5; - root = req.parsed.pathname; - - if (req.parsed.pathname !== "/") { - rep.links.push({ - uri: root.replace(REGEX.trailing_slash, "").replace(REGEX.collection, "$1") || "/", - rel: "collection" - }); - } - - if (rep.data instanceof Array) { - if (req.method === "GET") { - if (isNaN(page) || page <= 0) { - page = 1; - } - - nth = Math.ceil(rep.data.length / page_size); - - if (nth > 1) { - rep.data = array.limit(rep.data, (page - 1) * page_size, page_size); - query.page = 0; - query.page_size = page_size; - - root += "?" + array.keys(query).map(function (i) { - return i + "=" + encodeURIComponent(query[i]); - }).join("&"); - - if (page > 1) { - rep.links.push({uri: root.replace("page=0", "page=1"), rel: "first"}); - } - - if (page - 1 > 1 && page <= nth) { - rep.links.push({uri: root.replace("page=0", "page=" + (page - 1)), rel: "prev"}); - } - - if (page + 1 < nth) { - rep.links.push({uri: root.replace("page=0", "page=" + (page + 1)), rel: "next"}); - } - - if (nth > 0 && page !== nth) { - rep.links.push({uri: root.replace("page=0", "page=" + nth), rel: "last"}); - } - } else { - root += "?" + array.keys(query).map(function (i) { - return i + "=" + encodeURIComponent(query[i]); - }).join("&"); - } - } - - array.each(rep.data, function (i) { - var li = i.toString(), - uri; - - if (li !== collection) { - uri = li.indexOf("//") > -1 || li.indexOf("/") === 0 ? li : (collection + "/" + li).replace(/^\/\//, "/"); - - if (server.allowed("get", uri, req.vhost)) { - rep.links.push({uri: uri, rel: "item"}); - } - } - - if (i instanceof Object) { - parse(i, "item", req.parsed.pathname.replace(REGEX.trailing_slash, "").replace(REGEX.leading, "")); - } - }); - } else if (rep.data instanceof Object) { - parent = req.parsed.pathname.split("/").filter(function (i) { - return i !== ""; - }); - - if (parent.length > 1) { - parent.pop(); - } - - rep.data = parse(rep.data, undefined, array.last(parent)); - } - - if (rep.links.length > 0) { - headers.link = array.keySort(rep.links, "rel, uri").map(function (i) { - return "<" + i.uri + ">; rel=\"" + i.rel + "\""; - }).join(", "); - } - } - - return rep; -} - function isEmpty (obj) { return trim(obj) === ""; } @@ -339,131 +109,24 @@ function merge (a, b) { return a; } -function queryString (qstring = "") { - let obj = {}; - let aresult = qstring.split("?"); - let result; - - if (aresult.length > 1) { - aresult.shift(); - } - - result = aresult.join("?"); - result.split("&").forEach(function (prop) { - let aitem = prop.replace(/\+/g, " ").split("="); - let item; - - if (aitem.length > 2) { - item = [aitem.shift(), aitem.join("=")]; - } else { - item = aitem; - } - - if (isEmpty(item[0])) { - return; - } - - if (item[1] === undefined) { - item[1] = ""; - } else { - item[1] = coerce(decodeURIComponent(item[1])); - } - - if (obj[item[0]] === undefined) { - obj[item[0]] = item[1]; - } else if (obj[item[0]] instanceof Array === false) { - obj[item[0]] = [obj[item[0]]]; - obj[item[0]].push(item[1]); - } else { - obj[item[0]].push(item[1]); - } - }); - - return obj; -} - -function parse (uri) { - let luri = uri; - let idxAscii, idxQ, parsed; - - if (luri === undefined || luri === null) { - luri = ""; - } else { - idxAscii = luri.indexOf("%3F"); - idxQ = luri.indexOf("?"); - - switch (true) { - case idxQ === -1 && idxAscii > -1: - case idxAscii < idxQ: - luri = luri.replace("%3F", "?"); - break; - default: - void 0; - } - } - - parsed = url.parse(luri); - parsed.query = parsed.search ? queryString(parsed.search) : {}; - - iterate(parsed, function (v, k) { - if (v === null) { - parsed[k] = ""; - } - }); - - return parsed; -} - -function auth (obj, config) { - let ssl = config.ssl.cert && config.ssl.key, - proto = "http" + (ssl ? "s" : ""), - realm = proto + "://" + (config.hostname === "localhost" ? "127.0.0.1" : config.hostname) + (config.port !== 80 && config.port !== 443 ? ":" + config.port : ""), - async = (config.auth.facebook.enabled || config.auth.google.enabled || config.auth.linkedin.enabled || config.auth.twitter.enabled), - stateless = (config.auth.basic.enabled || config.auth.bearer.enabled), - stateful = (async || config.auth.local.enabled || config.security.csrf), - authMap = {}, - authUris = [], - keys, sesh, fnCookie, fnSesh, luscaCsrf, luscaCsp, luscaXframe, luscaP3p, luscaHsts, luscaXssProtection, protection, passportAuth, passportInit, passportSession; - - function asyncFlag (req, res, next) { - req.protectAsync = true; - next(); - } - - function bypass (req, res, next) { - if (config.auth.unprotect.filter(function (i) { - return i.test(req.url); - }).length > 0) { - req.protect = false; - req.unprotect = true; - } - - next(); - } - - function csrfWrapper (req, res, next) { - if (req.unprotect) { - next(); - } else { - luscaCsrf(req, res, next); - } - } - +function auth (obj, config) { + let ssl = config.ssl.cert && config.ssl.key, + proto = "http" + (ssl ? "s" : ""), + realm = proto + "://" + (config.hostname === "localhost" ? "127.0.0.1" : config.hostname) + (config.port !== 80 && config.port !== 443 ? ":" + config.port : ""), + async = (config.auth.facebook.enabled || config.auth.google.enabled || config.auth.linkedin.enabled || config.auth.twitter.enabled) !== false, + stateless = (config.auth.basic.enabled || config.auth.bearer.enabled) !== false, + stateful = (async || config.auth.local.enabled || config.security.csrf) !== false, + authMap = {}, + authUris = [], + keys, sesh; + function init (sess) { - passportInit = passport.initialize(); - obj.server.use(passportInit).blacklist(passportInit); + middleware.passportInit = passport.initialize(); + obj.server.use(middleware.passportInit).blacklist(middleware.passportInit); if (sess) { - passportSession = passport.session(); - obj.server.use(passportSession).blacklist(passportSession); - } - } - - function guard (req, res, next) { - if (req.url === "/login" || req.isAuthenticated()) { - rate(obj, req, res, next); - } else { - res.redirect("/login"); + middleware.passportSession = passport.session(); + obj.server.use(middleware.passportSession).blacklist(middleware.passportSession); } } @@ -471,15 +134,10 @@ function auth (obj, config) { res.redirect(config.auth.redirect); } - function valid (req, res, next) { - if (req.allow.indexOf(req.method) > -1) { - next(); - } else { - next(new Error(405)); - } - } + obj.server.use(middleware.decorate).blacklist(middleware.decorate); + obj.server.use(middleware.parse).blacklist(middleware.parse); - obj.server.blacklist(asyncFlag); + obj.server.blacklist(middleware.asyncFlag); config.auth.protect = (config.auth.protect || []).map(function (i) { return new RegExp("^" + i !== "/login" ? i.replace(/\.\*/g, "*").replace(/\*/g, ".*") : "$", "i"); @@ -505,7 +163,7 @@ function auth (obj, config) { authUris.push("/login"); } - obj.server.use(valid).blacklist(valid); + obj.server.use(middleware.valid).blacklist(middleware.valid); if (stateful) { sesh = { @@ -519,46 +177,46 @@ function auth (obj, config) { sesh.store = new RedisStore(config.session.redis); } - fnCookie = cookie(); - fnSesh = session(sesh); + middleware.cookie = cookie(); + middleware.session = session(sesh); - obj.server.use(fnSesh).blacklist(fnSesh); - obj.server.use(fnCookie).blacklist(fnCookie); - obj.server.use(bypass).blacklist(bypass); + obj.server.use(middleware.session).blacklist(middleware.session); + obj.server.use(middleware.cookie).blacklist(middleware.cookie); + obj.server.use(middleware.bypass).blacklist(middleware.bypass); if (config.security.csrf) { - luscaCsrf = lusca.csrf({key: config.security.key, secret: config.security.secret}); - obj.server.use(csrfWrapper).blacklist(csrfWrapper); + middleware.luscaCsrf = lusca.csrf({key: config.security.key, secret: config.security.secret}); + obj.server.use(middleware.csrfWrapper).blacklist(middleware.csrfWrapper); } } if (config.security.csp instanceof Object) { - luscaCsp = lusca.csp(config.security.csp); - obj.server.use(luscaCsp).blacklist(luscaCsp); + middleware.luscaCsp = lusca.csp(config.security.csp); + obj.server.use(middleware.luscaCsp).blacklist(middleware.luscaCsp); } - if (!string.isEmpty(config.security.xframe || "")) { - luscaXframe = lusca.xframe(config.security.xframe); - obj.server.use(luscaXframe).blacklist(luscaXframe); + if (!isEmpty(config.security.xframe || "")) { + middleware.luscaXframe = lusca.xframe(config.security.xframe); + obj.server.use(middleware.luscaXframe).blacklist(middleware.luscaXframe); } - if (!string.isEmpty(config.security.p3p || "")) { - luscaP3p = lusca.p3p(config.security.p3p); - obj.server.use(luscaP3p).blacklist(luscaP3p); + if (!isEmpty(config.security.p3p || "")) { + middleware.luscaP3p = lusca.p3p(config.security.p3p); + obj.server.use(middleware.luscaP3p).blacklist(middleware.luscaP3p); } if (config.security.hsts instanceof Object) { - luscaHsts = lusca.hsts(config.security.hsts); - obj.server.use(luscaHsts).blacklist(luscaHsts); + middleware.luscaHsts = lusca.hsts(config.security.hsts); + obj.server.use(middleware.luscaHsts).blacklist(middleware.luscaHsts); } if (config.security.xssProtection instanceof Object) { - luscaXssProtection = lusca.xssProtection(config.security.xssProtection); - obj.server.use(luscaXssProtection).blacklist(luscaXssProtection); + middleware.luscaXssProtection = lusca.xssProtection(config.security.xssProtection); + obj.server.use(middleware.luscaXssProtection).blacklist(middleware.luscaXssProtection); } - protection = zuul(config.auth.protect); - obj.server.use(protection).blacklist(protection); + // Can fork to `middleware.keymaster()` + obj.server.use(middleware.zuul).blacklist(middleware.zuul); if (stateless && !stateful) { init(false); @@ -589,13 +247,14 @@ function auth (obj, config) { r = r.replace(/\|$/, "") + ")).*$"; - obj.server.use(r, guard).blacklist(guard); + obj.server.use(r, middleware.rate).blacklist(middleware.rate); + obj.server.use(r, middleware.guard).blacklist(middleware.guard); }()); - config.routes.get["/login"] = config.auth.local.enabled ? (keys ? { + config.routes.get["/login"] = config.auth.local.enabled ? keys ? { login_uri: "/auth", instruction: "POST 'username' & 'password' to authenticate" - } : {instruction: "POST 'username' & 'password' to authenticate"}) : {login_uri: "/auth"}; + } : {instruction: "POST 'username' & 'password' to authenticate"} : {login_uri: "/auth"}; } else if (config.auth.local.enabled) { config.routes.get["/login"] = {instruction: "POST 'username' & 'password' to authenticate"}; } @@ -644,13 +303,13 @@ function auth (obj, config) { }); })); - passportAuth = passport.authenticate("basic", {session: stateful}); + middleware.passportAuth = passport.authenticate("basic", {session: stateful}); if (async || config.auth.local.enabled) { - obj.server.get("/auth/basic", passportAuth).blacklist(passportAuth); + obj.server.get("/auth/basic", middleware.passportAuth).blacklist(middleware.passportAuth); obj.server.get("/auth/basic", redirect); } else { - obj.server.use(passportAuth).blacklist(passportAuth); + obj.server.use(middleware.passportAuth).blacklist(middleware.passportAuth); } }()); } @@ -682,13 +341,13 @@ function auth (obj, config) { }); })); - passportAuth = passport.authenticate("bearer", {session: stateful}); + middleware.passportAuth = passport.authenticate("bearer", {session: stateful}); if (async || config.auth.local.enabled) { - obj.server.get("/auth/bearer", passportAuth).blacklist(passportAuth); + obj.server.get("/auth/bearer", middleware.passportAuth).blacklist(middleware.passportAuth); obj.server.get("/auth/bearer", redirect); } else { - obj.server.use(passportAuth).blacklist(passportAuth); + obj.server.use(middleware.passportAuth).blacklist(middleware.passportAuth); } }()); } @@ -709,9 +368,9 @@ function auth (obj, config) { }); })); - obj.server.get("/auth/facebook", asyncFlag); + obj.server.get("/auth/facebook", middleware.asyncFlag); obj.server.get("/auth/facebook", passport.authenticate("facebook")); - obj.server.get("/auth/facebook/callback", asyncFlag); + obj.server.get("/auth/facebook/callback", middleware.asyncFlag); obj.server.get("/auth/facebook/callback", passport.authenticate("facebook", {failureRedirect: "/login"})); obj.server.get("/auth/facebook/callback", redirect); } @@ -731,9 +390,9 @@ function auth (obj, config) { }); })); - obj.server.get("/auth/google", asyncFlag); + obj.server.get("/auth/google", middleware.asyncFlag); obj.server.get("/auth/google", passport.authenticate("google")); - obj.server.get("/auth/google/callback", asyncFlag); + obj.server.get("/auth/google/callback", middleware.asyncFlag); obj.server.get("/auth/google/callback", passport.authenticate("google", {failureRedirect: "/login"})); obj.server.get("/auth/google/callback", redirect); } @@ -754,9 +413,9 @@ function auth (obj, config) { }); })); - obj.server.get("/auth/linkedin", asyncFlag); + obj.server.get("/auth/linkedin", middleware.asyncFlag); obj.server.get("/auth/linkedin", passport.authenticate("linkedin", {"scope": config.auth.linkedin.scope || ["r_basicprofile", "r_emailaddress"]})); - obj.server.get("/auth/linkedin/callback", asyncFlag); + obj.server.get("/auth/linkedin/callback", middleware.asyncFlag); obj.server.get("/auth/linkedin/callback", passport.authenticate("linkedin", {failureRedirect: "/login"})); obj.server.get("/auth/linkedin/callback", redirect); } @@ -788,10 +447,10 @@ function auth (obj, config) { } function mid () { - passportSession(req, res, final); + middleware.passportSession(req, res, final); } - passportInit(req, res, mid); + middleware.passportInit(req, res, mid); }; } @@ -813,9 +472,9 @@ function auth (obj, config) { }); })); - obj.server.get("/auth/oauth2", asyncFlag); + obj.server.get("/auth/oauth2", middleware.asyncFlag); obj.server.get("/auth/oauth2", passport.authenticate("oauth2")); - obj.server.get("/auth/oauth2/callback", asyncFlag); + obj.server.get("/auth/oauth2/callback", middleware.asyncFlag); obj.server.get("/auth/oauth2/callback", passport.authenticate("oauth2", {failureRedirect: "/login"})); obj.server.get("/auth/oauth2/callback", redirect); } @@ -840,9 +499,9 @@ function auth (obj, config) { })); }()); - obj.server.get("/auth/saml", asyncFlag); + obj.server.get("/auth/saml", middleware.asyncFlag); obj.server.get("/auth/saml", passport.authenticate("saml")); - obj.server.get("/auth/saml/callback", asyncFlag); + obj.server.get("/auth/saml/callback", middleware.asyncFlag); obj.server.get("/auth/saml/callback", passport.authenticate("saml", {failureRedirect: "/login"})); obj.server.get("/auth/saml/callback", redirect); } @@ -863,9 +522,9 @@ function auth (obj, config) { }); })); - obj.server.get("/auth/twitter", asyncFlag); + obj.server.get("/auth/twitter", middleware.asyncFlag); obj.server.get("/auth/twitter", passport.authenticate("twitter")); - obj.server.get("/auth/twitter/callback", asyncFlag); + obj.server.get("/auth/twitter/callback", middleware.asyncFlag); obj.server.get("/auth/twitter/callback", passport.authenticate("twitter", { successRedirect: config.auth.redirect, failureRedirect: "/login" @@ -875,24 +534,266 @@ function auth (obj, config) { return config; } -function response (arg, status) { - let err = arg instanceof Error, - result; +function bootstrap (obj, config) { + let notify = false; + + // Bootstrapping configuration + auth(obj, config); + + // Setting headers + config.headers = config.headers || {}; + config.headers.server = "tenso/{{VERSION}}"; + + // Creating status > message map + iterate(obj.server.codes, function (value, key) { + obj.messages[value] = obj.server.messages[key]; + }); + + // Setting routes + iterate(config.routes, function (routes, method) { + iterate(routes, function (arg, route) { + if (typeof arg === "function") { + obj.server[method](route, function (...args) { + arg.apply(obj, args); + }); + } else { + obj.server[method](route, function (req, res) { + obj.respond(req, res, arg); + }); + } + }); + }); + + // Disabling compression over SSL due to BREACH + if (config.ssl.cert && config.ssl.key) { + config.compress = false; + notify = true; + } + + // Starting API server + obj.server.start(config, function (req, res, status, msg) { + let stat = status instanceof Error ? parseInt(status.message, 10) : status, + err = msg instanceof Error ? msg : new Error(msg || obj.messages[stat]); + + obj.error(req, res, stat, err); + }); + + if (notify) { + obj.server.log("Compression over SSL is disabled for your protection", "debug"); + } + + return obj; +} + +function queryString (qstring = "") { + let obj = {}; + let aresult = qstring.split("?"); + let result; + + if (aresult.length > 1) { + aresult.shift(); + } + + result = aresult.join("?"); + result.split("&").forEach(function (prop) { + let aitem = prop.replace(/\+/g, " ").split("="); + let item; + + if (aitem.length > 2) { + item = [aitem.shift(), aitem.join("=")]; + } else { + item = aitem; + } + + if (isEmpty(item[0])) { + return; + } + + if (item[1] === undefined) { + item[1] = ""; + } else { + item[1] = coerce(decodeURIComponent(item[1])); + } - if (err) { - if (status === undefined) { - throw new Error("Invalid arguments"); + if (obj[item[0]] === undefined) { + obj[item[0]] = item[1]; + } else if (obj[item[0]] instanceof Array === false) { + obj[item[0]] = [obj[item[0]]]; + obj[item[0]].push(item[1]); + } else { + obj[item[0]].push(item[1]); } + }); + + return obj; +} + +function parse (uri) { + let luri = uri; + let idxAscii, idxQ, parsed; - result = serializer(null, arg, status); + if (luri === undefined || luri === null) { + luri = ""; } else { - result = serializer(arg, null, status); + idxAscii = luri.indexOf("%3F"); + idxQ = luri.indexOf("?"); + + switch (true) { + case idxQ === -1 && idxAscii > -1: + case idxAscii < idxQ: + luri = luri.replace("%3F", "?"); + break; + default: + void 0; + } } - return result; + parsed = url.parse(luri); + parsed.query = parsed.search ? queryString(parsed.search) : {}; + + iterate(parsed, function (v, k) { + if (v === null) { + parsed[k] = ""; + } + }); + + return parsed; } +function hypermedia (server, req, rep, headers) { + let seen = {}, + collection = req.parsed.pathname, + query, page, page_size, nth, root, parent; + + // Parsing the object for hypermedia properties + function marshal (obj, rel, item_collection) { + let keys = array.keys(obj), + lrel = rel || "related", + result; + + if (keys.length === 0) { + result = null; + } else { + array.each(keys, function (i) { + let lcollection, uri; + + // If ID like keys are found, and are not URIs, they are assumed to be root collections + if (regex.id.test(i) || regex.hypermedia.test(i)) { + if (!regex.id.test(i)) { + lcollection = i.replace(regex.trailing, "").replace(regex.trailing_s, "").replace(regex.trailing_y, "ie") + "s"; + lrel = "related"; + } else { + lcollection = item_collection; + lrel = "item"; + } + + uri = regex.scheme.test(obj[i]) ? obj[i] : "/" + lcollection + "/" + obj[i]; + + if (uri !== root && !seen[uri]) { + seen[uri] = 1; + + if (server.allowed("get", uri, req.vhost)) { + rep.links.push({uri: uri, rel: lrel}); + } + } + } + }); + + result = obj; + } + + return result; + } + + if (rep.status >= 200 && rep.status <= 206) { + query = req.parsed.query; + page = query.page || 1; + page_size = query.page_size || server.config.pageSize || 5; + root = req.parsed.pathname; + + if (req.parsed.pathname !== "/") { + rep.links.push({ + uri: root.replace(regex.trailing_slash, "").replace(regex.collection, "$1") || "/", + rel: "collection" + }); + } + + if (rep.data instanceof Array) { + if (req.method === "GET") { + if (isNaN(page) || page <= 0) { + page = 1; + } + + nth = Math.ceil(rep.data.length / page_size); + + if (nth > 1) { + rep.data = array.limit(rep.data, (page - 1) * page_size, page_size); + query.page = 0; + query.page_size = page_size; + + root += "?" + array.keys(query).map(function (i) { + return i + "=" + encodeURIComponent(query[i]); + }).join("&"); + + if (page > 1) { + rep.links.push({uri: root.replace("page=0", "page=1"), rel: "first"}); + } + + if (page - 1 > 1 && page <= nth) { + rep.links.push({uri: root.replace("page=0", "page=" + (page - 1)), rel: "prev"}); + } + + if (page + 1 < nth) { + rep.links.push({uri: root.replace("page=0", "page=" + (page + 1)), rel: "next"}); + } + + if (nth > 0 && page !== nth) { + rep.links.push({uri: root.replace("page=0", "page=" + nth), rel: "last"}); + } + } else { + root += "?" + array.keys(query).map(function (i) { + return i + "=" + encodeURIComponent(query[i]); + }).join("&"); + } + } + + array.each(rep.data, function (i) { + var li = i.toString(), + uri; + + if (li !== collection) { + uri = li.indexOf("//") > -1 || li.indexOf("/") === 0 ? li : (collection + "/" + li).replace(/^\/\//, "/"); + + if (server.allowed("get", uri, req.vhost)) { + rep.links.push({uri: uri, rel: "item"}); + } + } + + if (i instanceof Object) { + parse(i, "item", req.parsed.pathname.replace(regex.trailing_slash, "").replace(regex.leading, "")); + } + }); + } else if (rep.data instanceof Object) { + parent = req.parsed.pathname.split("/").filter(function (i) { + return i !== ""; + }); + + if (parent.length > 1) { + parent.pop(); + } + + rep.data = marshal(rep.data, undefined, array.last(parent)); + } + + if (rep.links.length > 0) { + headers.link = array.keySort(rep.links, "rel, uri").map(function (i) { + return "<" + i.uri + ">; rel=\"" + i.rel + "\""; + }).join(", "); + } + } + return rep; +} module.exports = { auth: auth, @@ -909,7 +810,5 @@ module.exports = { merge: merge, queryString: queryString, parse: parse, - response: response, - trim: trim, - xml: xml + trim: trim }; diff --git a/src/xml.js b/src/xml.js deleted file mode 100644 index 73041bf1..00000000 --- a/src/xml.js +++ /dev/null @@ -1,45 +0,0 @@ -const xml = { - decode: function (arg) { - return new DOMParser().parseFromString(arg, "text/xml"); - }, - encode: function (arg, wrap = true, top = true, key = "xml") { - let x = wrap ? "<" + key + ">" : ""; - - if (arg !== null && arg.xml) { - arg = arg.xml; - } - - if (arg instanceof Document) { - arg = new XMLSerializer().serializeToString(arg); - } - - if (regex.boolean_number_string.test(typeof arg)) { - x += xml.node(isNaN(key) ? key : "item", arg); - } else if (arg === null || arg === undefined) { - x += "null"; - } else if (arg instanceof Array) { - arg.forEach(function (v) { - x += xml.encode(v, typeof v === "object", false, "item"); - }); - } else if (arg instanceof Object) { - utility.iterate(arg, function (v, k) { - x += xml.encode(v, typeof v === "object", false, k); - }); - } - - x += wrap ? "" : ""; - - if (top) { - x = "" + x; - } - - return x; - }, - node: function (name, value) { - return "v".replace("v", regex.cdata.test(value) ? "" : value).replace(/<(\/)?n>/g, "<$1" + name + ">"); - }, - valid: function (arg) { - return xml.decode(arg).getElementsByTagName("parsererror").length === 0; - } -}; -modules.export = xml; diff --git a/src/zuul.js b/src/zuul.js deleted file mode 100644 index 2c78d974..00000000 --- a/src/zuul.js +++ /dev/null @@ -1,29 +0,0 @@ -/** - * Returns middleware to determine if a route is protected - * - * @method zuul - * @param {Array} protect Array of routes - * @return {Function} Middleware - */ -function zuul (protect) { - return function (req, res, next) { - let uri = req.parsed.path, - protectd = false; - - array.each(protect, function (r) { - if (r.test(uri)) { - return !(protectd = true); - } - }); - - // Setting state so the connection can be terminated properly - req.protect = protectd; - req.protectAsync = false; - - if (protectd && next) { - next(); - } else { - keymaster(req, res, next); - } - }; -} From fe34730b9af65dc9970caa7e20a75c8622399e3d Mon Sep 17 00:00:00 2001 From: Jason Mulligan Date: Fri, 18 Dec 2015 17:49:52 -0500 Subject: [PATCH 06/33] Deleting generated docs --- doc/global.html | 2197 ------- doc/img/glyphicons-halflings-white.png | Bin 8777 -> 0 bytes doc/img/glyphicons-halflings.png | Bin 12799 -> 0 bytes doc/index.html | 489 -- doc/module-tenso.html | 439 -- doc/modules.list.html | 283 - doc/scripts/URI.js | 1429 ----- doc/scripts/bootstrap-dropdown.js | 169 - doc/scripts/bootstrap-tab.js | 144 - doc/scripts/docstrap.lib.js | 9 - doc/scripts/prettify/Apache-License-2.0.txt | 202 - doc/scripts/prettify/jquery.min.js | 6 - doc/scripts/prettify/lang-css.js | 21 - doc/scripts/prettify/prettify.js | 496 -- doc/scripts/sunlight.js | 1157 ---- doc/scripts/toc.js | 100 - doc/styles/darkstrap.css | 960 --- doc/styles/prettify-tomorrow.css | 132 - doc/styles/site.amelia.css | 6369 ------------------- doc/styles/site.cerulean.css | 5701 ----------------- doc/styles/site.cosmo.css | 5944 ----------------- doc/styles/site.cyborg.css | 6151 ------------------ doc/styles/site.darkstrap.css | 5638 ---------------- doc/styles/site.flatly.css | 5993 ----------------- doc/styles/site.journal.css | 5745 ----------------- doc/styles/site.readable.css | 5433 ---------------- doc/styles/site.simplex.css | 5758 ----------------- doc/styles/site.slate.css | 6204 ------------------ doc/styles/site.spacelab.css | 5796 ----------------- doc/styles/site.spruce.css | 5938 ----------------- doc/styles/site.superhero.css | 6106 ------------------ doc/styles/site.united.css | 5545 ---------------- doc/styles/sunlight.dark.css | 345 - doc/styles/sunlight.default.css | 344 - 34 files changed, 91243 deletions(-) delete mode 100644 doc/global.html delete mode 100644 doc/img/glyphicons-halflings-white.png delete mode 100644 doc/img/glyphicons-halflings.png delete mode 100644 doc/index.html delete mode 100644 doc/module-tenso.html delete mode 100644 doc/modules.list.html delete mode 100644 doc/scripts/URI.js delete mode 100644 doc/scripts/bootstrap-dropdown.js delete mode 100644 doc/scripts/bootstrap-tab.js delete mode 100644 doc/scripts/docstrap.lib.js delete mode 100644 doc/scripts/prettify/Apache-License-2.0.txt delete mode 100644 doc/scripts/prettify/jquery.min.js delete mode 100644 doc/scripts/prettify/lang-css.js delete mode 100644 doc/scripts/prettify/prettify.js delete mode 100644 doc/scripts/sunlight.js delete mode 100644 doc/scripts/toc.js delete mode 100644 doc/styles/darkstrap.css delete mode 100644 doc/styles/prettify-tomorrow.css delete mode 100644 doc/styles/site.amelia.css delete mode 100644 doc/styles/site.cerulean.css delete mode 100644 doc/styles/site.cosmo.css delete mode 100644 doc/styles/site.cyborg.css delete mode 100644 doc/styles/site.darkstrap.css delete mode 100644 doc/styles/site.flatly.css delete mode 100644 doc/styles/site.journal.css delete mode 100644 doc/styles/site.readable.css delete mode 100644 doc/styles/site.simplex.css delete mode 100644 doc/styles/site.slate.css delete mode 100644 doc/styles/site.spacelab.css delete mode 100644 doc/styles/site.spruce.css delete mode 100644 doc/styles/site.superhero.css delete mode 100644 doc/styles/site.united.css delete mode 100644 doc/styles/sunlight.dark.css delete mode 100644 doc/styles/sunlight.default.css diff --git a/doc/global.html b/doc/global.html deleted file mode 100644 index c23073ea..00000000 --- a/doc/global.html +++ /dev/null @@ -1,2197 +0,0 @@ - - - - - - Tensō Global - - - - - - - - - -
- - -
- - -
- -
- - - -

Global

-
- -
-

- -

- -
- -
-
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - - - - - - - - - - - -

Methods

- -
- -
-

auth(obj, config) → {Object}

- - -
-
- - -
-

Setups up authentication

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
obj - - -Object - - - -

Tenso instance

config - - -Object - - - -

Tenso configuration

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

Updated Tenso configuration

-
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - -
- - - -
-

bootstrap(obj, config) → {Object}

- - -
-
- - -
-

Bootstraps an instance of Tenso

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
obj - - -Object - - - -

Tenso instance

config - - -Object - - - -

Application configuration

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

Tenso instance

-
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - -
- - - -
-

clone(arg) → {Mixed}

- - -
-
- - -
-

Shallow clones an Object

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
arg - - -Mixed - - - -

To be cloned

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

Clone of arg

-
- - - -
-
- Type -
-
- -Mixed - - -
-
- - - - - -
- - - -
-

error() → {Undefined}

- - -
-
- - -
-

Route error handler

-
- - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

undefined

-
- - - -
-
- Type -
-
- -Undefined - - -
-
- - - - - -
- - - -
-

factory(arg) → {Object}

- - -
-
- - -
-

Tenso factory

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
arg - - -Object - - - -

[Optional] Configuration

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

Tenso instance

-
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - -
- - - -
-

hypermedia(server, req, rep, headers) → {Object}

- - -
-
- - -
-

Decorates the rep with hypermedia links

-

Arrays of results are automatically paginated, Objects -will be parsed and have keys 'lifted' into the 'link' -Array if a pattern is matched, e.g. "user_(guid|uuid|id|uri|url)" -will map to "/users/$1"

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
server - - -Object - - - -

TurtleIO instance

req - - -Object - - - -

Client request

rep - - -Object - - - -

Serialized representation

headers - - -Object - - - -

HTTP response headers

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

HTTP response body

-
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - -
- - - -
-

keymaster(req, res, next) → {Undefined}

- - -
-
- - -
-

Keymaster for the request

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
req - - -Object - - - -

Client request

res - - -Object - - - -

Client response

next - - -function - - - -

Next middleware

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

undefined

-
- - - -
-
- Type -
-
- -Undefined - - -
-
- - - - - -
- - - -
-

prepare(arg, error, status) → {Object}

- - -
-
- - -
-

Prepares a response body

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
arg - - -Mixed - - - -

[Optional] Response body "data"

error - - -Object - - - -

[Optional] Error instance

status - - -Number - - - -

HTTP status code

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

Standardized response body

-
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - -
- - - -
-

rate(obj, req, res, next) → {Undefined}

- - -
-
- - -
-

Rate limiting middleware

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
obj - - -Object - - - -

Tenso instance

req - - -Object - - - -

Client request

res - - -Object - - - -

Client response

next - - -function - - - -

Next middleware

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

undefined

-
- - - -
-
- Type -
-
- -Undefined - - -
-
- - - - - -
- - - -
-

response(arg, status) → {Object}

- - -
-
- - -
-

Creates a response

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
arg - - -Mixed - - - -

Unserialized response body

status - - -Number - - - -

HTTP status, default is 200

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

Response body

-
- - - -
-
- Type -
-
- -Object - - -
-
- - - - - -
- - - -
-

sanitize(arg) → {String}

- - -
-
- - -
-

Sanitizes outbound Strings to avoid XSS issues

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
arg - - -String - - - -

String to sanitize

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

Sanitized String

-
- - - -
-
- Type -
-
- -String - - -
-
- - - - - -
- - - -
-

zuul(protect) → {function}

- - -
-
- - -
-

Returns middleware to determine if a route is protected

-
- - - - - - - -
Parameters:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
NameTypeDescription
protect - - -Array - - - -

Array of routes

- - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - -
Returns:
- - -
-

Middleware

-
- - - -
-
- Type -
-
- -function - - -
-
- - - - - -
- -
- - - - - -
- -
- - - - -
- -
-
- - - - Copyright © 2014 Jason Mulligan - -
- - - Documentation generated by JSDoc 3.2.2 - on 2015-07-04T10:55:38-04:00 using the DocStrap template. - -
-
- - -
-
-
- -
-
- -
- - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/doc/img/glyphicons-halflings-white.png b/doc/img/glyphicons-halflings-white.png deleted file mode 100644 index 3bf6484a29d8da269f9bc874b25493a45fae3bae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8777 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 B7G?kd diff --git a/doc/img/glyphicons-halflings.png b/doc/img/glyphicons-halflings.png deleted file mode 100644 index a9969993201f9cee63cf9f49217646347297b643..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12799 zcma*OWmH^Ivn@*S;K3nSf_t!#;0f+&pm7Po8`nk}2q8f5;M%x$SdAkd9FAvlc$ zx660V9e3Ox@4WZ^?7jZ%QFGU-T~%||Ug4iK6bbQY@zBuF2$hxOw9wF=A)nUSxR_5@ zEX>HBryGrjyuOFFv$Y4<+|3H@gQfEqD<)+}a~mryD|1U9*I_FOG&F%+Ww{SJ-V2BR zjt<81Ek$}Yb*95D4RS0HCps|uLyovt;P05hchQb-u2bzLtmog&f2}1VlNhxXV);S9 zM2buBg~!q9PtF)&KGRgf3#z7B(hm5WlNClaCWFs!-P!4-u*u5+=+D|ZE9e`KvhTHT zJBnLwGM%!u&vlE%1ytJ=!xt~y_YkFLQb6bS!E+s8l7PiPGSt9xrmg?LV&&SL?J~cI zS(e9TF1?SGyh+M_p@o1dyWu7o7_6p;N6hO!;4~ z2B`I;y`;$ZdtBpvK5%oQ^p4eR2L)BH>B$FQeC*t)c`L71gXHPUa|vyu`Bnz)H$ZcXGve(}XvR!+*8a>BLV;+ryG1kt0=)ytl zNJxFUN{V7P?#|Cp85QTa@(*Q3%K-R(Pkv1N8YU*(d(Y}9?PQ(j;NzWoEVWRD-~H$=f>j9~PN^BM2okI(gY-&_&BCV6RP&I$FnSEM3d=0fCxbxA6~l>54-upTrw zYgX@%m>jsSGi`0cQt6b8cX~+02IghVlNblR7eI;0ps}mpWUcxty1yG56C5rh%ep(X z?)#2d?C<4t-KLc*EAn>>M8%HvC1TyBSoPNg(4id~H8JwO#I)Bf;N*y6ai6K9_bA`4 z_g9(-R;qyH&6I$`b42v|0V3Z8IXN*p*8g$gE98+JpXNY+jXxU0zsR^W$#V=KP z3AEFp@OL}WqwOfsV<)A^UTF4&HF1vQecz?LWE@p^Z2){=KEC_3Iopx_eS42>DeiDG zWMXGbYfG~W7C8s@@m<_?#Gqk;!&)_Key@^0xJxrJahv{B&{^!>TV7TEDZlP|$=ZCz zmX=ZWtt4QZKx**)lQQoW8y-XLiOQy#T`2t}p6l*S`68ojyH@UXJ-b~@tN`WpjF z%7%Yzv807gsO!v=!(2uR)16!&U5~VPrPHtGzUU?2w(b1Xchq}(5Ed^G|SD7IG+kvgyVksU) z(0R)SW1V(>&q2nM%Z!C9=;pTg!(8pPSc%H01urXmQI6Gi^dkYCYfu6b4^tW))b^U+ z$2K&iOgN_OU7n#GC2jgiXU{caO5hZt0(>k+c^(r><#m|#J^s?zA6pi;^#*rp&;aqL zRcZi0Q4HhVX3$ybclxo4FFJW*`IV`)Bj_L3rQe?5{wLJh168Ve1jZv+f1D}f0S$N= zm4i|9cEWz&C9~ZI3q*gwWH^<6sBWuphgy@S3Qy?MJiL>gwd|E<2h9-$3;gT9V~S6r z)cAcmE0KXOwDA5eJ02-75d~f?3;n7a9d_xPBJaO;Z)#@s7gk5$Qn(Fc^w@9c5W0zY z59is0?Mt^@Rolcn{4%)Ioat(kxQH6}hIykSA)zht=9F_W*D#<}N(k&&;k;&gKkWIL z0Of*sP=X(Uyu$Pw;?F@?j{}=>{aSHFcii#78FC^6JGrg-)!)MV4AKz>pXnhVgTgx8 z1&5Y=>|8RGA6++FrSy=__k_imx|z-EI@foKi>tK0Hq2LetjUotCgk2QFXaej!BWYL zJc{fv(&qA7UUJ|AXLc5z*_NW#yWzKtl(c8mEW{A>5Hj^gfZ^HC9lQNQ?RowXjmuCj4!!54Us1=hY z0{@-phvC}yls!PmA~_z>Y&n&IW9FQcj}9(OLO-t^NN$c0o}YksCUWt|DV(MJB%%Sr zdf}8!9ylU2TW!=T{?)g-ojAMKc>3pW;KiZ7f0;&g)k}K^#HBhE5ot)%oxq$*$W@b# zg4p<Ou`ME|Kd1WHK@8 zzLD+0(NHWa`B{em3Ye?@aVsEi>y#0XVZfaFuq#;X5C3{*ikRx7UY4FF{ZtNHNO?A_ z#Q?hwRv~D8fPEc%B5E-ZMI&TAmikl||EERumQCRh7p;)>fdZMxvKq;ky0}7IjhJph zW*uuu*(Y6)S;Od--8uR^R#sb$cmFCnPcj9PPCWhPN;n`i1Q#Qn>ii z{WR|0>8F`vf&#E(c2NsoH=I7Cd-FV|%(7a`i}gZw4N~QFFG2WtS^H%@c?%9UZ+kez z;PwGgg_r6V>Kn5n(nZ40P4qMyrCP3bDkJp@hp6&X3>gzC>=f@Hsen<%I~7W+x@}b> z0}Et*vx_50-q@PIV=(3&Tbm}}QRo*FP2@)A#XX-8jYspIhah`9ukPBr)$8>Tmtg&R z?JBoH17?+1@Y@r>anoKPQ}F8o9?vhcG79Cjv^V6ct709VOQwg{c0Q#rBSsSmK3Q;O zBpNihl3S0_IGVE)^`#94#j~$;7+u870yWiV$@={|GrBmuz4b)*bCOPkaN0{6$MvazOEBxFdKZDlbVvv{8_*kJ zfE6C`4&Kkz<5u%dEdStd85-5UHG5IOWbo8i9azgg#zw-(P1AA049hddAB*UdG3Vn0 zX`OgM+EM|<+KhJ<=k?z~WA5waVj?T9eBdfJGebVifBKS1u<$#vl^BvSg)xsnT5Aw_ZY#}v*LXO#htB>f}x3qDdDHoFeb zAq7;0CW;XJ`d&G*9V)@H&739DpfWYzdQt+Kx_E1K#Cg1EMtFa8eQRk_JuUdHD*2;W zR~XFnl!L2A?48O;_iqCVr1oxEXvOIiN_9CUVTZs3C~P+11}ebyTRLACiJuMIG#`xP zKlC|E(S@QvN+%pBc6vPiQS8KgQAUh75C0a2xcPQDD$}*bM&z~g8+=9ltmkT$;c;s z5_=8%i0H^fEAOQbHXf0;?DN5z-5+1 zDxj50yYkz4ox9p$HbZ|H?8ukAbLE^P$@h}L%i6QVcY>)i!w=hkv2zvrduut%!8>6b zcus3bh1w~L804EZ*s96?GB&F7c5?m?|t$-tp2rKMy>F*=4;w*jW}^;8v`st&8)c; z2Ct2{)?S(Z;@_mjAEjb8x=qAQvx=}S6l9?~H?PmP`-xu;ME*B8sm|!h@BX4>u(xg_ zIHmQzp4Tgf*J}Y=8STR5_s)GKcmgV!$JKTg@LO402{{Wrg>#D4-L%vjmtJ4r?p&$F!o-BOf7ej~ z6)BuK^^g1b#(E>$s`t3i13{6-mmSp7{;QkeG5v}GAN&lM2lQT$@(aQCcFP(%UyZbF z#$HLTqGT^@F#A29b0HqiJsRJAlh8kngU`BDI6 zJUE~&!cQ*&f95Ot$#mxU5+*^$qg_DWNdfu+1irglB7yDglzH()2!@#rpu)^3S8weW z_FE$=j^GTY*|5SH95O8o8W9FluYwB=2PwtbW|JG6kcV^dMVmX(wG+Otj;E$%gfu^K z!t~<3??8=()WQSycsBKy24>NjRtuZ>zxJIED;YXaUz$@0z4rl+TW zWxmvM$%4jYIpO>j5k1t1&}1VKM~s!eLsCVQ`TTjn3JRXZD~>GM z$-IT~(Y)flNqDkC%DfbxaV9?QuWCV&-U1yzrV@0jRhE;)ZO0=r-{s@W?HOFbRHDDV zq;eLo+wOW;nI|#mNf(J?RImB9{YSO2Y`9825Lz#u4(nk3)RGv3X8B(A$TsontJ8L! z9JP^eWxtKC?G8^xAZa1HECx*rp35s!^%;&@Jyk)NexVc)@U4$^X1Dag6`WKs|(HhZ#rzO2KEw3xh~-0<;|zcs0L>OcO#YYX{SN8m6`9pp+ zQG@q$I)T?aoe#AoR@%om_#z=c@ych!bj~lV13Qi-xg$i$hXEAB#l=t7QWENGbma4L zbBf*X*4oNYZUd_;1{Ln_ZeAwQv4z?n9$eoxJeI?lU9^!AB2Y~AwOSq67dT9ADZ)s@ zCRYS7W$Zpkdx$3T>7$I%3EI2ik~m!f7&$Djpt6kZqDWZJ-G{*_eXs*B8$1R4+I}Kf zqniwCI64r;>h2Lu{0c(#Atn)%E8&)=0S4BMhq9$`vu|Ct;^ur~gL`bD>J@l)P$q_A zO7b3HGOUG`vgH{}&&AgrFy%K^>? z>wf**coZ2vdSDcNYSm~dZ(vk6&m6bVKmVgrx-X<>{QzA!)2*L+HLTQz$e8UcB&Djq zl)-%s$ZtUN-R!4ZiG=L0#_P=BbUyH+YPmFl_ogkkQ$=s@T1v}rNnZ^eMaqJ|quc+6 z*ygceDOrldsL30w`H;rNu+IjlS+G~p&0SawXCA1+D zC%cZtjUkLNq%FadtHE?O(yQTP486A{1x<{krq#rpauNQaeyhM3*i0%tBpQHQo-u)x z{0{&KS`>}vf2_}b160XZO2$b)cyrHq7ZSeiSbRvaxnKUH{Q`-P(nL&^fcF2){vhN- zbX&WEjP7?b4A%0y6n_=m%l00uZ+}mCYO(!x?j$+O$*TqoD_Q5EoyDJ?w?^UIa491H zE}87(bR`X;@u#3Qy~9wWdWQIg1`cXrk$x9=ccR|RY1~%{fAJ@uq@J3e872x0v$hmv ze_KcL(wM|n0EOp;t{hKoohYyDmYO;!`7^Lx;0k=PWPGZpI>V5qYlzjSL_(%|mud50 z7#{p97s`U|Sn$WYF>-i{i4`kzlrV6a<}=72q2sAT7Zh{>P%*6B;Zl;~0xWymt10Mo zl5{bmR(wJefJpNGK=fSRP|mpCI-)Nf6?Pv==FcFmpSwF1%CTOucV{yqxSyx4Zws3O z8hr5Uyd%ezIO7?PnEO0T%af#KOiXD$e?V&OX-B|ZX-YsgSs%sv-6U+sLPuz{D4bq| zpd&|o5tNCmpT>(uIbRf?8c}d3IpOb3sn6>_dr*26R#ev<_~vi)wleW$PX|5)$_ z+_|=pi(0D(AB_sjQ;sQQSM&AWqzDO1@NHw;C9cPdXRKRI#@nUW)CgFxzQ1nyd!+h& zcjU!U=&u|>@}R(9D$%lu2TlV>@I2-n@fCr5PrZNVyKWR7hm zWjoy^p7v8m#$qN0K#8jT- zq`mSirDZDa1Jxm;Rg3rAPhC)LcI4@-RvKT+@9&KsR3b0_0zuM!Fg7u>oF>3bzOxZPU&$ab$Z9@ zY)f7pKh22I7ZykL{YsdjcqeN++=0a}elQM-4;Q)(`Ep3|VFHqnXOh14`!Bus& z9w%*EWK6AiAM{s$6~SEQS;A>ey$#`7)khZvamem{P?>k)5&7Sl&&NXKk}o!%vd;-! zpo2p-_h^b$DNBO>{h4JdGB=D>fvGIYN8v&XsfxU~VaefL?q} z3ekM?iOKkCzQHkBkhg=hD!@&(L}FcHKoa zbZ7)H1C|lHjwEb@tu=n^OvdHOo7o+W`0-y3KdP#bb~wM=Vr_gyoEq|#B?$&d$tals ziIs-&7isBpvS|CjC|7C&3I0SE?~`a%g~$PI%;au^cUp@ER3?mn-|vyu!$7MV6(uvt z+CcGuM(Ku2&G0tcRCo7#D$Dirfqef2qPOE5I)oCGzmR5G!o#Q~(k~)c=LpIfrhHQk zeAva6MilEifE7rgP1M7AyWmLOXK}i8?=z2;N=no)`IGm#y%aGE>-FN zyXCp0Sln{IsfOBuCdE*#@CQof%jzuU*jkR*Su3?5t}F(#g0BD0Zzu|1MDes8U7f9; z$JBg|mqTXt`muZ8=Z`3wx$uizZG_7>GI7tcfOHW`C2bKxNOR)XAwRkLOaHS4xwlH4 zDpU29#6wLXI;H?0Se`SRa&I_QmI{zo7p%uveBZ0KZKd9H6@U?YGArbfm)D*^5=&Rp z`k{35?Z5GbZnv>z@NmJ%+sx=1WanWg)8r}C_>EGR8mk(NR$pW<-l8OTU^_u3M@gwS z7}GGa1)`z5G|DZirw;FB@VhH7Dq*0qc=|9lLe{w2#`g+_nt>_%o<~9(VZe=zI*SSz4w43-_o>4E4`M@NPKTWZuQJs)?KXbWp1M zimd5F;?AP(LWcaI-^Sl{`~>tmxsQB9Y$Xi*{Zr#py_+I$vx7@NY`S?HFfS!hUiz$a z{>!&e1(16T!Om)m)&k1W#*d#GslD^4!TwiF2WjFBvi=Ms!ADT)ArEW6zfVuIXcXVk z>AHjPADW+mJzY`_Ieq(s?jbk4iD2Rb8*V3t6?I+E06(K8H!!xnDzO%GB;Z$N-{M|B zeT`jo%9)s%op*XZKDd6*)-^lWO{#RaIGFdBH+;XXjI(8RxpBc~azG1H^2v7c^bkFE zZCVPE+E*Q=FSe8Vm&6|^3ki{9~qafiMAf7i4APZg>b%&5>nT@pHH z%O*pOv(77?ZiT{W zBibx}Q12tRc7Py1NcZTp`Q4ey%T_nj@1WKg5Fz_Rjl4wlJQj)rtp8yL3r!Shy zvZvnmh!tH4T6Js-?vI0<-rzzl{mgT*S0d_7^AU_8gBg^03o-J=p(1o6kww2hx|!%T z-jqp}m^G*W?$!R#M%Ef?&2jYxmx+lXWZszpI4d$pUN`(S)|*c^CgdwY>Fa>> zgGBJhwe8y#Xd*q0=@SLEgPF>+Qe4?%E*v{a`||luZ~&dqMBrRfJ{SDMaJ!s_;cSJp zSqZHXIdc@@XteNySUZs^9SG7xK`8=NBNM)fRVOjw)D^)w%L2OPkTQ$Tel-J)GD3=YXy+F4in(ILy*A3m@3o73uv?JC}Q>f zrY&8SWmesiba0|3X-jmlMT3 z*ST|_U@O=i*sM_*48G)dgXqlwoFp5G6qSM3&%_f_*n!PiT>?cNI)fAUkA{qWnqdMi+aNK_yVQ&lx4UZknAc9FIzVk% zo6JmFH~c{_tK!gt4+o2>)zoP{sR}!!vfRjI=13!z5}ijMFQ4a4?QIg-BE4T6!#%?d&L;`j5=a`4is>U;%@Rd~ zXC~H7eGQhhYWhMPWf9znDbYIgwud(6$W3e>$W4$~d%qoJ z+JE`1g$qJ%>b|z*xCKenmpV$0pM=Gl-Y*LT8K+P)2X#;XYEFF4mRbc~jj?DM@(1e`nL=F4Syv)TKIePQUz)bZ?Bi3@G@HO$Aps1DvDGkYF50O$_welu^cL7;vPiMGho74$;4fDqKbE{U zd1h{;LfM#Fb|Z&uH~Rm_J)R~Vy4b;1?tW_A)Iz#S_=F|~pISaVkCnQ0&u%Yz%o#|! zS-TSg87LUfFSs{tTuM3$!06ZzH&MFtG)X-l7>3)V?Txuj2HyG*5u;EY2_5vU0ujA? zHXh5G%6e3y7v?AjhyX79pnRBVr}RmPmtrxoB7lkxEzChX^(vKd+sLh?SBic=Q)5nA zdz7Mw3_iA>;T^_Kl~?1|5t%GZ;ki_+i>Q~Q1EVdKZ)$Sh3LM@ea&D~{2HOG++7*wF zAC6jW4>fa~!Vp5+$Z{<)Qxb|{unMgCv2)@%3j=7)Zc%U<^i|SAF88s!A^+Xs!OASYT%7;Jx?olg_6NFP1475N z#0s<@E~FI}#LNQ{?B1;t+N$2k*`K$Hxb%#8tRQi*Z#No0J}Pl;HWb){l7{A8(pu#@ zfE-OTvEreoz1+p`9sUI%Y{e5L-oTP_^NkgpYhZjp&ykinnW;(fu1;ttpSsgYM8ABX4dHe_HxU+%M(D=~) zYM}XUJ5guZ;=_ZcOsC`_{CiU$zN3$+x&5C`vX-V3`8&RjlBs^rf00MNYZW+jCd~7N z%{jJuUUwY(M`8$`B>K&_48!Li682ZaRknMgQ3~dnlp8C?__!P2z@=Auv;T^$yrsNy zCARmaA@^Yo2sS%2$`031-+h9KMZsIHfB>s@}>Y(z988e!`%4=EDoAQ0kbk>+lCoK60Mx9P!~I zlq~wf7kcm_NFImt3ZYlE(b3O1K^QWiFb$V^a2Jlwvm(!XYx<`i@ZMS3UwFt{;x+-v zhx{m=m;4dgvkKp5{*lfSN3o^keSpp9{hlXj%=}e_7Ou{Yiw(J@NXuh*;pL6@$HsfB zh?v+r^cp@jQ4EspC#RqpwPY(}_SS$wZ{S959`C25777&sgtNh%XTCo9VHJC-G z;;wi9{-iv+ETiY;K9qvlEc04f;ZnUP>cUL_T*ms``EtGoP^B#Q>n2dSrbAg8a>*Lg zd0EJ^=tdW~7fbcLFsqryFEcy*-8!?;n%;F+8i{eZyCDaiYxghr z$8k>L|2&-!lhvuVdk!r-kpSFl`5F5d4DJr%M4-qOy3gdmQbqF1=aBtRM7)c_Ae?$b8 zQg4c8*KQ{XJmL)1c7#0Yn0#PTMEs4-IHPjkn0!=;JdhMXqzMLeh`yOylXROP- zl#z3+fwM9l3%VN(6R77ua*uI9%hO7l7{+Hcbr(peh;afUK?B4EC09J{-u{mv)+u#? zdKVBCPt`eU@IzL)OXA`Ebu`Xp?u0m%h&X41}FNfnJ*g1!1wcbbpo%F4x!-#R9ft!8{5`Ho}04?FI#Kg zL|k`tF1t_`ywdy8(wnTut>HND(qNnq%Sq=AvvZbXnLx|mJhi!*&lwG2g|edBdVgLy zjvVTKHAx(+&P;P#2Xobo7_RttUi)Nllc}}hX>|N?-u5g7VJ-NNdwYcaOG?NK=5)}` zMtOL;o|i0mSKm(UI_7BL_^6HnVOTkuPI6y@ZLR(H?c1cr-_ouSLp{5!bx^DiKd*Yb z{K78Ci&Twup zTKm)ioN|wcYy%Qnwb)IzbH>W!;Ah5Zdm_jRY`+VRJ2 zhkspZ9hbK3iQD91A$d!0*-1i#%x81|s+SPRmD}d~<1p6!A13(!vABP2kNgqEG z?AMgl^P+iRoIY(9@_I?n1829lGvAsRnHwS~|5vD2+Zi53j<5N4wNn0{q>>jF9*bI) zL$kMXM-awNOElF>{?Jr^tOz1glbwaD-M0OKOlTeW3C!1ZyxRbB>8JDof(O&R1bh%3x#>y2~<>OXO#IIedH0Q`(&&?eo-c~ z>*Ah#3~09unym~UC-UFqqI>{dmUD$Y4@evG#ORLI*{ZM)Jl=e1it!XzY($S3V zLG!Y6fCjE>x6r@5FG1n|8ompSZaJ>9)q6jqU;XxCQk9zV(?C9+i*>w z21+KYt1gXX&0`x3E)hS7I5}snbBzox9C@Xzcr|{B8Hw;SY1$}&BoYKXH^hpjW-RgJ z-Fb}tannKCv>y~^`r|(1Q9;+sZlYf3XPSX|^gR01UFtu$B*R;$sPZdIZShRr>|b@J z;#G{EdoY+O;REEjQ}X7_YzWLO+Ey3>a_KDe1CjSe| z6arqcEZ)CX!8r(si`dqbF$uu&pnf^Np{1f*TdJ`r2;@SaZ z#hb4xlaCA@Pwqj#LlUEe5L{I$k(Zj$d3(~)u(F%&xb8={N9hKxlZIO1ABsM{Mt|)2 zJ^t9Id;?%4PfR4&Ph9B9cFK~@tG3wlFW-0fXZS_L4U*EiAA%+`h%q2^6BCC;t0iO4V=s4Qug{M|iDV@s zC7|ef-dxiR7T&Mpre!%hiUhHM%3Qxi$Lzw6&(Tvlx9QA_7LhYq<(o~=Y>3ka-zrQa zhGpfFK@)#)rtfz61w35^sN1=IFw&Oc!Nah+8@qhJ0UEGr;JplaxOGI82OVqZHsqfX ze1}r{jy;G?&}Da}a7>SCDsFDuzuseeCKof|Dz2BPsP8? zY;a)Tkr2P~0^2BeO?wnzF_Ul-ekY=-w26VnU%U3f19Z-pj&2 z4J_a|o4Dci+MO)mPQIM>kdPG1xydiR9@#8m zh27D7GF{p|a{8({Q-Pr-;#jV{2zHR>lGoFtIfIpoMo?exuQyX_A;;l0AP4!)JEM$EwMInZkj+8*IHP4vKRd zKx_l-i*>A*C@{u%ct`y~s6MWAfO{@FPIX&sg8H{GMDc{4M3%$@c8&RAlw0-R<4DO3 trJqdc$mBpWeznn?E0M$F`|3v=`3%T2A17h;rxP7$%JLd=6(2u;`(N3pt&so# diff --git a/doc/index.html b/doc/index.html deleted file mode 100644 index 1989e7f8..00000000 --- a/doc/index.html +++ /dev/null @@ -1,489 +0,0 @@ - - - - - - Tensō Index - - - - - - - - - -
- - -
- - -
- -
- - - - - Index - - - - - - - - - - - - - - - - - - - - - -
-

Tensō

-

build status Gitter

-

Tensō is a REST API facade for node.js, designed to simplify the implementation of APIs.

-

Tensō will handle the serialization & creation of hypermedia links, all you have to do is give it Arrays or Objects.

-

Example

-

Creating an API with Tensō can be as simple as one statement.

-
require("tenso")({routes: require(__dirname+"/routes.js")});
-

Creating Routes

-

Routes are loaded as a module, with each HTTP method as an export, affording a very customizable API server.

-

Route handlers have the context of the Tensō server, i.e. this will allow you to send a response with this.respond(req, res, body[, status, headers]). You can also use res to res.respond(body[, status, headers]), res.redirect(url), or res.error(status[, Error]).

-

The following example will create GET routes that will return an empty Array at /, an Error at /reports/tps, & a version 4 UUID at /uuid.

-
var uuid = require( "keigai" ).util.uuid;
-
-module.exports.get = {
-    "/": ["reports", "uuid"],
-    "/reports": ["tps"],
-    "/reports/tps": function ( req, res ) {
-        res.error( 785, Error( "TPS Cover Sheet not attached" ) );
-    },
-    "/uuid": function ( req, res ) {
-        res.respond( uuid(), 200, {"cache-control": "no-cache"} );
-    }
-};
-

Protected Routes

-

Protected routes are routes that require authorization for access, and will redirect to authentication end points if needed.

-

Unprotected Routes

-

Unprotected routes are routes that do not require authorization for access, and will exit the authorization pipeline early to avoid rate limiting, csrf tokens, & other security measures. These routes are the DMZ of your API! You must secure these end points with alternative methods if accepting input!

-

Request Helpers

-

Tensō decorates req with "helpers" such as req.ip, & req.parsed. PATCH, PUT, & POST payloads are available as req.body. Sessions are available as req.session when using local authentication.

-

Responses

-

Responses will have a standard shape, and will be utf-8 by default. The result will be in data. Hypermedia (pagination, links, etc.) will be in links:[ {"uri": "...", "rel": "..."}, ...], & pagination will also be present via the Link HTTP header.

-
{
-  "data": "`null` or ?",
-  "error": "`null` or an `Error` stack trace / message",
-  "links": [],
-  "status": 200
-}
-

REST / Hypermedia

-

Hypermedia is a prerequisite of REST, and is best described by the Richardson Maturity Model. Tensō will automagically paginate Arrays of results, or parse Entity representations for keys that imply -relationships, and create the appropriate Objects in the link Array, as well as the Link HTTP header. Object keys that match this pattern: /_(guid|uuid|id|uri|url)$/ will be considered -hypermedia links.

-

For example, if the key user_id was found, it would be mapped to /users/:id with a link rel of related.

-

Tensō will bend the rules of REST when using authentication strategies provided by passport.js, or CSRF if is enabled, because they rely on a session. Session storage is in memory, or Redis. You have the option of a stateless or stateful API.

-

Browsable API / Renderers

-

Tensō 1.4.0 added a few common format renderers, such as CSV, HTML, YAML, & XML. The HTML interface is a browsable API! You can use it to verify requests & responses, or simply poke around your API to see how it behaves.

-

Custom renderers can be registered with server.renderer('name', fn, 'mimetype');.

-

Cache

-

Tensō has a robust multi-level cache strategy, starting at the response headers. If a response can be cached, an Etag will be sent to the Client, and registered in an Etag LRU cache which Tensō -uses along with a 'cache compressed asset to disk' strategy, allowing Tensō to stream the last known version of a resource to the next Client that supports the same compression (gzip or deflate). -Etags will lazy expire from the cache, to minimize wasted cycles.

-

Caching can be disabled by setting the cache-control header to a "private" or "no cache" directive (see the above /uuid example).

-

Configuration

-

This is a sample configuration for Tensō, without authentication or SSL. This would be ideal for development, but not production! Enabling SSL is as easy as providing file paths for the two keys.

-
{
-    "auth": {}, /* Optional, see Authentication section */
-    "cache": 1000, /* Optional, size of Etag LRU cache */
-    "compress": false, /* Optional, enabled by default, disabled with SSL */
-    "headers": {}, /* Optional, custom headers */
-    "hostname": "localhost", /* Optional, default is 'localhost' */
-    "json": 2, /* Optional, default indent for 'pretty' JSON */
-    "logs": {
-        "level": "info", /* Optional */
-        "stdout": true, /* Optional */
-        "dtrace": false, /* Optional */
-        "stack": true /* Optional */
-    },
-    "port": 8000, /* Optional */
-    "routes": require("./routes.js"), /* Required! */
-    "session": { /* Optional */
-        "secret": null,
-        "store": "memory", /* "memory" or "redis" */
-        "redis": {} /* See connect-redis for options */
-    },
-    "ssl": { /* Optional */
-        "cert": null,
-        "key": null
-    },
-    "title": "My API", /* Page title for browsable API */
-    "uid": 33 /* Optional, system account uid to drop to after starting with elevated privileges to run on a low port */
-}
-

Authentication

-

The protect Array is the endpoints that will require authentication. The redirect String is the end point users will be redirected to upon successfully authenticating, the default is /.

-

Sessions are used for non Basic or Bearer Token authentication, and will have /login, /logout, & custom routes. Redis is supported for session storage.

-

Multiple authentication strategies can be enabled at once.

-

Basic Auth

-
{
-    "auth": {
-        "basic": {
-            "enabled": true,
-            "list": ["username:password", ...],
-        },
-        "protect": ["/"]
-    }
-}
-

Facebook

-

Facebook authentication will create /auth, /auth/facebook, & /auth/facebook/callback routes. auth(accessToken, refreshToken, profile, callback) must execute callback(err, user).

-
{
-    "auth": {
-        "facebook": {
-            "enabled": true,
-            "auth": function ( ... ) { ... }, /* Authentication handler, to 'find' or 'create' a User */
-            "client_id": "", /* Get this from Facebook */
-            "client_secret": "" /* Get this from Facebook */
-        },
-        "protect": ["/private"]
-    }
-}
-

Google

-

Google authentication (OpenID) will create /auth, /auth/google, & /auth/google/callback routes. auth(identifier, profile, callback) must execute callback(err, user).

-
{
-    "auth": {
-        "google": {
-            "enabled": true,
-            "auth": function ( ... ) { ... }, /* Authentication handler, to 'find' or 'create' a User */
-        },
-        "protect": ["/private"]
-    }
-}
-

LinkedIn

-

LinkedIn authentication will create /auth, /auth/linkedin, & /auth/linkedin/callback routes. auth(token, tokenSecret, profile, callback) must execute callback(err, user).

-
{
-    "auth": {
-        "linkedin": {
-            "enabled": true,
-            "auth": function ( ... ) { ... }, /* Authentication handler, to 'find' or 'create' a User */
-            "client_id": "", /* Get this from LinkedIn */
-            "client_secret": "", /* Get this from LinkedIn */,
-            "scope": "" /* Optional, permission scope */
-        }
-        "protect": ["/private"]
-    }
-}
-

Local

-

Local authentication will create /login. auth(username, password) must execute callback(err, user).

-
{
-    "auth": {
-        "local": {
-            "enabled": true,
-            "auth": function ( ... ) { ... }, /* Authentication handler, to 'find' or 'create' a User */
-        }
-        "protect": ["/private"]
-    }
-}
-

OAuth2

-

OAuth2 authentication will create /auth, /auth/oauth2, & /auth/oauth2/callback routes. auth(accessToken, refreshToken, profile, callback) must execute callback(err, user).

-
{
-    "auth": {
-        "oauth2": {
-            "enabled": true,
-            "auth": function ( ... ) { ... }, /* Authentication handler, to 'find' or 'create' a User */
-            "auth_url": "", /* Authorization URL */
-            "token_url": "", /* Token URL */
-            "client_id": "", /* Get this from authorization server */
-            "client_secret": "" /* Get this from authorization server */
-        },
-        "protect": ["/private"]
-    }
-}
-

Oauth2 Bearer Token

-
{
-    "auth": {
-        "bearer": {
-            "enabled": true,
-            "tokens": ["abc", ...]
-        },
-        "protect": ["/"]
-    }
-}
-

SAML

-

SAML authentication will create /auth, /auth/saml, & /auth/saml/callback routes. auth(profile, callback) must execute callback(err, user).

-

Tensō uses passport-saml, for configuration options please visit it's homepage.

-
{
-    "auth": {
-        "saml": {
-            "enabled": true,
-            ...
-        },
-        "protect": ["/private"]
-    }
-}
-

Twitter

-

Twitter authentication will create /auth, /auth/twitter, & /auth/twitter/callback routes. auth(token, tokenSecret, profile, callback) must execute callback(err, user).

-
{
-    "auth": {
-        "twitter": {
-            "enabled": true,
-            "auth": function ( ... ) { ... }, /* Authentication handler, to 'find' or 'create' a User */
-            "consumer_key": "", /* Get this from Twitter */
-            "consumer_secret": "" /* Get this from Twitter */
-        },
-        "protect": ["/private"]
-    }
-}
-

Sessions

-

Sessions can use a memory (default) or redis store. Memory will limit your sessions to a single server instance, while redis will allow you to share sessions across a cluster of processes, or machines. To use redis, set the store property to "redis".

-

If the session secret is not provided, a version 4 UUID will be used.

-
{
-    "session" : {
-        "secret": "my secret",
-        "store": "redis",
-        "redis": {
-            "host": "127.0.0.1",
-            "port": 6379
-        }
-    }
-}
-

Security

-

Tensō uses lusca for security as a middleware. Please see it's documentation for how to configure it; each method & argument is a key:value pair for security.

-
{
-    "security": { ... }
-}
-

Compression

-

Compression is enabled by default, for Clients that support gzip or deflate. Compression will be disabled if SSL is enabled.

-

Rate Limiting

-

Rate limiting is controlled by configuration, and is disabled by default. Rate limiting is based on token, session, or ip, depending upon authentication method.

-

Rate limiting can be overridden by providing an override function that takes req & rate, and must return (a modified) rate.

-
{
-    "rate": {
-        "enabled": true,
-        "limit": 450, /* Maximum requests allowed before `reset` */
-        "reset": 900, /* TTL in seconds */
-        "status": 429, /* Optional HTTP status */
-        "message": "Too many requests",  /* Optional error message */
-        "override": function ( req, rate ) { ... } /* Override the default rate limiting */
-    }
-}
-

Limiting upload size

-

A 'max byte' limit can be enforced on all routes that handle PATCH, POST, & PUT requests. The default limit is 1 MB (1048576 b).

-
{
-    "maxBytes": 5242880
-}
-

Logging

-

Standard log levels are supported, and are emitted (by configuration) to stdout & stderr. Stack traces can be enabled.

-

DTrace

-

DTrace probes can be enabled by configuration (disabled by default). A shell script is available at ./dtrace.sh to observe the probes. -The last argument for each probe is the nanoseconds it took to execute.

-
"allowed",        "char *", "char *", "char *", "int"
-"allows",         "char *", "char *", "int"
-"compress",       "char *", "char *", "int"
-"compression",    "char *", "int"
-"error",          "char *", "char *", "int",    "char *", "int"
-"headers",        "int",    "int"
-"log",            "char *", "int",    "int",    "int"
-"proxy",          "char *", "char *", "char *", "char *", "int"
-"middleware",     "char *", "char *", "int"
-"request",        "char *", "int"
-"respond",        "char *", "char *", "char *", "int",    "int"
-"status",         "int",    "int",    "int",    "int",    "int"
-"write",          "char *", "char *", "char *", "char *", "int"
-

License

-

Copyright (c) 2015 Jason Mulligan
Licensed under the BSD-3 license.

-
- - - - - - - -
- -
-
- - - - Copyright © 2014 Jason Mulligan - -
- - - Documentation generated by JSDoc 3.2.2 - on 2015-07-04T10:55:38-04:00 using the DocStrap template. - -
-
- - -
-
-
- -
-
- -
- - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/doc/module-tenso.html b/doc/module-tenso.html deleted file mode 100644 index 4855946a..00000000 --- a/doc/module-tenso.html +++ /dev/null @@ -1,439 +0,0 @@ - - - - - - Tensō Module: tenso - - - - - - - - - -
- - -
- - -
- -
- - - -

Module: tenso

-
- -
-

- tenso -

- -
- -
-
- - - - -

Tensō is a REST API facade for node.js, designed to simplify the implementation of APIs.

- - - -
- - - -
Version:
-
-
    -
  • 2.0.3
  • -
-
- - - - - - - - - -
Author:
-
- -
- - - - - - - - -
License:
-
-
    -
  • BSD-3-Clause
  • -
-
- - - - - - - - - - - - - -
- - - - -
- - - - - - - - - - - - -

Members

- -
- -
-

<inner> REGEX :Object

- - -
-
- -
-

RegExp cache

-
- - - -
Type:
-
    -
  • - -Object - - -
  • -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- - - -
-

<inner> renderers :Object

- - -
-
- -
-

Renderers

-
- - - -
Type:
-
    -
  • - -Object - - -
  • -
- - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - -
- -
- - - - - - - -
- -
- - - - -
- -
-
- - - - Copyright © 2014 Jason Mulligan - -
- - - Documentation generated by JSDoc 3.2.2 - on 2015-07-04T10:55:38-04:00 using the DocStrap template. - -
-
- - -
-
-
- -
-
- -
- - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/doc/modules.list.html b/doc/modules.list.html deleted file mode 100644 index 502746a5..00000000 --- a/doc/modules.list.html +++ /dev/null @@ -1,283 +0,0 @@ - - - - - - Tensō Modules - - - - - - - - - -
- - -
- - -
- -
- - - -

Modules

-
- -
-

- -

- -
- -
-
- - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
- - - - -
- - - - - - - - - - - - - - - - - - -
- -
- - - - -
- -
-
- - - - Copyright © 2014 Jason Mulligan - -
- - - Documentation generated by JSDoc 3.2.2 - on 2015-07-04T10:55:38-04:00 using the DocStrap template. - -
-
- - -
-
-
- -
-
- -
- - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/doc/scripts/URI.js b/doc/scripts/URI.js deleted file mode 100644 index f8546c72..00000000 --- a/doc/scripts/URI.js +++ /dev/null @@ -1,1429 +0,0 @@ -/*! - * URI.js - Mutating URLs - * - * Version: 1.8.3 - * - * Author: Rodney Rehm - * Web: http://medialize.github.com/URI.js/ - * - * Licensed under - * MIT License http://www.opensource.org/licenses/mit-license - * GPL v3 http://opensource.org/licenses/GPL-3.0 - * - */ -(function(root, factory) { - // https://github.com/umdjs/umd/blob/master/returnExports.js - if (typeof exports === 'object') { - // Node - module.exports = factory(require('./punycode'), require('./IPv6'), require('./SecondLevelDomains')); - } else if (typeof define === 'function' && define.amd) { - // AMD. Register as an anonymous module. - define(['./punycode', './IPv6', './SecondLevelDomains'], factory); - } else { - // Browser globals (root is window) - root.URI = factory(root.punycode, root.IPv6, root.SecondLevelDomains); - } -}(this, function(punycode, IPv6, SLD) { - "use strict"; - - function URI(url, base) { - // Allow instantiation without the 'new' keyword - if (!(this instanceof URI)) { - return new URI(url, base); - } - if (url === undefined) { - if (typeof location !== 'undefined') { - url = location.href + ""; - } else { - url = ""; - } - } - this.href(url); - // resolve to base according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#constructor - if (base !== undefined) { - return this.absoluteTo(base); - } - return this; - }; - var p = URI.prototype; - var hasOwn = Object.prototype.hasOwnProperty; - - function escapeRegEx(string) { - // https://github.com/medialize/URI.js/commit/85ac21783c11f8ccab06106dba9735a31a86924d#commitcomment-821963 - return string.replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1'); - } - - function isArray(obj) { - return String(Object.prototype.toString.call(obj)) === "[object Array]"; - } - - function filterArrayValues(data, value) { - var lookup = {}; - var i, length; - if (isArray(value)) { - for (i = 0, length = value.length; i < length; i++) { - lookup[value[i]] = true; - } - } else { - lookup[value] = true; - } - for (i = 0, length = data.length; i < length; i++) { - if (lookup[data[i]] !== undefined) { - data.splice(i, 1); - length--; - i--; - } - } - return data; - } - URI._parts = function() { - return { - protocol: null, - username: null, - password: null, - hostname: null, - urn: null, - port: null, - path: null, - query: null, - fragment: null, - // state - duplicateQueryParameters: URI.duplicateQueryParameters - }; - }; - // state: allow duplicate query parameters (a=1&a=1) - URI.duplicateQueryParameters = false; - // static properties - URI.protocol_expression = /^[a-z][a-z0-9-+-]*$/i; - URI.idn_expression = /[^a-z0-9\.-]/i; - URI.punycode_expression = /(xn--)/i; - // well, 333.444.555.666 matches, but it sure ain't no IPv4 - do we care? - URI.ip4_expression = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; - // credits to Rich Brown - // source: http://forums.intermapper.com/viewtopic.php?p=1096#1096 - // specification: http://www.ietf.org/rfc/rfc4291.txt - URI.ip6_expression = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/; - // gruber revised expression - http://rodneyrehm.de/t/url-regex.html - URI.find_uri_expression = /\b((?:[a-z][\w-]+:(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]+|\(([^\s()<>]+|(\([^\s()<>]+\)))*\))+(?:\(([^\s()<>]+|(\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?«»“”‘’]))/ig; - // http://www.iana.org/assignments/uri-schemes.html - // http://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers#Well-known_ports - URI.defaultPorts = { - http: "80", - https: "443", - ftp: "21", - gopher: "70", - ws: "80", - wss: "443" - }; - // allowed hostname characters according to RFC 3986 - // ALPHA DIGIT "-" "." "_" "~" "!" "$" "&" "'" "(" ")" "*" "+" "," ";" "=" %encoded - // I've never seen a (non-IDN) hostname other than: ALPHA DIGIT . - - URI.invalid_hostname_characters = /[^a-zA-Z0-9\.-]/; - // encoding / decoding according to RFC3986 - - function strictEncodeURIComponent(string) { - // see https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/encodeURIComponent - return encodeURIComponent(string).replace(/[!'()*]/g, escape).replace(/\*/g, "%2A"); - } - URI.encode = strictEncodeURIComponent; - URI.decode = decodeURIComponent; - URI.iso8859 = function() { - URI.encode = escape; - URI.decode = unescape; - }; - URI.unicode = function() { - URI.encode = strictEncodeURIComponent; - URI.decode = decodeURIComponent; - }; - URI.characters = { - pathname: { - encode: { - // RFC3986 2.1: For consistency, URI producers and normalizers should - // use uppercase hexadecimal digits for all percent-encodings. - expression: /%(24|26|2B|2C|3B|3D|3A|40)/ig, - map: { - // -._~!'()* - "%24": "$", - "%26": "&", - "%2B": "+", - "%2C": ",", - "%3B": ";", - "%3D": "=", - "%3A": ":", - "%40": "@" - } - }, - decode: { - expression: /[\/\?#]/g, - map: { - "/": "%2F", - "?": "%3F", - "#": "%23" - } - } - }, - reserved: { - encode: { - // RFC3986 2.1: For consistency, URI producers and normalizers should - // use uppercase hexadecimal digits for all percent-encodings. - expression: /%(21|23|24|26|27|28|29|2A|2B|2C|2F|3A|3B|3D|3F|40|5B|5D)/ig, - map: { - // gen-delims - "%3A": ":", - "%2F": "/", - "%3F": "?", - "%23": "#", - "%5B": "[", - "%5D": "]", - "%40": "@", - // sub-delims - "%21": "!", - "%24": "$", - "%26": "&", - "%27": "'", - "%28": "(", - "%29": ")", - "%2A": "*", - "%2B": "+", - "%2C": ",", - "%3B": ";", - "%3D": "=" - } - } - } - }; - URI.encodeQuery = function(string) { - return URI.encode(string + "").replace(/%20/g, '+'); - }; - URI.decodeQuery = function(string) { - return URI.decode((string + "").replace(/\+/g, '%20')); - }; - URI.recodePath = function(string) { - var segments = (string + "").split('/'); - for (var i = 0, length = segments.length; i < length; i++) { - segments[i] = URI.encodePathSegment(URI.decode(segments[i])); - } - return segments.join('/'); - }; - URI.decodePath = function(string) { - var segments = (string + "").split('/'); - for (var i = 0, length = segments.length; i < length; i++) { - segments[i] = URI.decodePathSegment(segments[i]); - } - return segments.join('/'); - }; - // generate encode/decode path functions - var _parts = { - 'encode': 'encode', - 'decode': 'decode' - }; - var _part; - var generateAccessor = function(_group, _part) { - return function(string) { - return URI[_part](string + "").replace(URI.characters[_group][_part].expression, function(c) { - return URI.characters[_group][_part].map[c]; - }); - }; - }; - for (_part in _parts) { - URI[_part + "PathSegment"] = generateAccessor("pathname", _parts[_part]); - } - URI.encodeReserved = generateAccessor("reserved", "encode"); - URI.parse = function(string, parts) { - var pos, t; - if (!parts) { - parts = {}; - } - // [protocol"://"[username[":"password]"@"]hostname[":"port]"/"?][path]["?"querystring]["#"fragment] - // extract fragment - pos = string.indexOf('#'); - if (pos > -1) { - // escaping? - parts.fragment = string.substring(pos + 1) || null; - string = string.substring(0, pos); - } - // extract query - pos = string.indexOf('?'); - if (pos > -1) { - // escaping? - parts.query = string.substring(pos + 1) || null; - string = string.substring(0, pos); - } - // extract protocol - if (string.substring(0, 2) === '//') { - // relative-scheme - parts.protocol = ''; - string = string.substring(2); - // extract "user:pass@host:port" - string = URI.parseAuthority(string, parts); - } else { - pos = string.indexOf(':'); - if (pos > -1) { - parts.protocol = string.substring(0, pos); - if (parts.protocol && !parts.protocol.match(URI.protocol_expression)) { - // : may be within the path - parts.protocol = undefined; - } else if (parts.protocol === 'file') { - // the file scheme: does not contain an authority - string = string.substring(pos + 3); - } else if (string.substring(pos + 1, pos + 3) === '//') { - string = string.substring(pos + 3); - // extract "user:pass@host:port" - string = URI.parseAuthority(string, parts); - } else { - string = string.substring(pos + 1); - parts.urn = true; - } - } - } - // what's left must be the path - parts.path = string; - // and we're done - return parts; - }; - URI.parseHost = function(string, parts) { - // extract host:port - var pos = string.indexOf('/'); - var bracketPos; - var t; - if (pos === -1) { - pos = string.length; - } - if (string[0] === "[") { - // IPv6 host - http://tools.ietf.org/html/draft-ietf-6man-text-addr-representation-04#section-6 - // I claim most client software breaks on IPv6 anyways. To simplify things, URI only accepts - // IPv6+port in the format [2001:db8::1]:80 (for the time being) - bracketPos = string.indexOf(']'); - parts.hostname = string.substring(1, bracketPos) || null; - parts.port = string.substring(bracketPos + 2, pos) || null; - } else if (string.indexOf(':') !== string.lastIndexOf(':')) { - // IPv6 host contains multiple colons - but no port - // this notation is actually not allowed by RFC 3986, but we're a liberal parser - parts.hostname = string.substring(0, pos) || null; - parts.port = null; - } else { - t = string.substring(0, pos).split(':'); - parts.hostname = t[0] || null; - parts.port = t[1] || null; - } - if (parts.hostname && string.substring(pos)[0] !== '/') { - pos++; - string = "/" + string; - } - return string.substring(pos) || '/'; - }; - URI.parseAuthority = function(string, parts) { - string = URI.parseUserinfo(string, parts); - return URI.parseHost(string, parts); - }; - URI.parseUserinfo = function(string, parts) { - // extract username:password - var pos = string.indexOf('@'); - var firstSlash = string.indexOf('/'); - var t; - // authority@ must come before /path - if (pos > -1 && (firstSlash === -1 || pos < firstSlash)) { - t = string.substring(0, pos).split(':'); - parts.username = t[0] ? URI.decode(t[0]) : null; - t.shift(); - parts.password = t[0] ? URI.decode(t.join(':')) : null; - string = string.substring(pos + 1); - } else { - parts.username = null; - parts.password = null; - } - return string; - }; - URI.parseQuery = function(string) { - if (!string) { - return {}; - } - // throw out the funky business - "?"[name"="value"&"]+ - string = string.replace(/&+/g, '&').replace(/^\?*&*|&+$/g, ''); - if (!string) { - return {}; - } - var items = {}; - var splits = string.split('&'); - var length = splits.length; - var v, name, value; - for (var i = 0; i < length; i++) { - v = splits[i].split('='); - name = URI.decodeQuery(v.shift()); - // no "=" is null according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#collect-url-parameters - value = v.length ? URI.decodeQuery(v.join('=')) : null; - if (items[name]) { - if (typeof items[name] === "string") { - items[name] = [items[name]]; - } - items[name].push(value); - } else { - items[name] = value; - } - } - return items; - }; - URI.build = function(parts) { - var t = ""; - if (parts.protocol) { - t += parts.protocol + ":"; - } - if (!parts.urn && (t || parts.hostname)) { - t += '//'; - } - t += (URI.buildAuthority(parts) || ''); - if (typeof parts.path === "string") { - if (parts.path[0] !== '/' && typeof parts.hostname === "string") { - t += '/'; - } - t += parts.path; - } - if (typeof parts.query === "string" && parts.query) { - t += '?' + parts.query; - } - if (typeof parts.fragment === "string" && parts.fragment) { - t += '#' + parts.fragment; - } - return t; - }; - URI.buildHost = function(parts) { - var t = ""; - if (!parts.hostname) { - return ""; - } else if (URI.ip6_expression.test(parts.hostname)) { - if (parts.port) { - t += "[" + parts.hostname + "]:" + parts.port; - } else { - // don't know if we should always wrap IPv6 in [] - // the RFC explicitly says SHOULD, not MUST. - t += parts.hostname; - } - } else { - t += parts.hostname; - if (parts.port) { - t += ':' + parts.port; - } - } - return t; - }; - URI.buildAuthority = function(parts) { - return URI.buildUserinfo(parts) + URI.buildHost(parts); - }; - URI.buildUserinfo = function(parts) { - var t = ""; - if (parts.username) { - t += URI.encode(parts.username); - if (parts.password) { - t += ':' + URI.encode(parts.password); - } - t += "@"; - } - return t; - }; - URI.buildQuery = function(data, duplicates) { - // according to http://tools.ietf.org/html/rfc3986 or http://labs.apache.org/webarch/uri/rfc/rfc3986.html - // being »-._~!$&'()*+,;=:@/?« %HEX and alnum are allowed - // the RFC explicitly states ?/foo being a valid use case, no mention of parameter syntax! - // URI.js treats the query string as being application/x-www-form-urlencoded - // see http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type - var t = ""; - var unique, key, i, length; - for (key in data) { - if (hasOwn.call(data, key) && key) { - if (isArray(data[key])) { - unique = {}; - for (i = 0, length = data[key].length; i < length; i++) { - if (data[key][i] !== undefined && unique[data[key][i] + ""] === undefined) { - t += "&" + URI.buildQueryParameter(key, data[key][i]); - if (duplicates !== true) { - unique[data[key][i] + ""] = true; - } - } - } - } else if (data[key] !== undefined) { - t += '&' + URI.buildQueryParameter(key, data[key]); - } - } - } - return t.substring(1); - }; - URI.buildQueryParameter = function(name, value) { - // http://www.w3.org/TR/REC-html40/interact/forms.html#form-content-type -- application/x-www-form-urlencoded - // don't append "=" for null values, according to http://dvcs.w3.org/hg/url/raw-file/tip/Overview.html#url-parameter-serialization - return URI.encodeQuery(name) + (value !== null ? "=" + URI.encodeQuery(value) : ""); - }; - URI.addQuery = function(data, name, value) { - if (typeof name === "object") { - for (var key in name) { - if (hasOwn.call(name, key)) { - URI.addQuery(data, key, name[key]); - } - } - } else if (typeof name === "string") { - if (data[name] === undefined) { - data[name] = value; - return; - } else if (typeof data[name] === "string") { - data[name] = [data[name]]; - } - if (!isArray(value)) { - value = [value]; - } - data[name] = data[name].concat(value); - } else { - throw new TypeError("URI.addQuery() accepts an object, string as the name parameter"); - } - }; - URI.removeQuery = function(data, name, value) { - var i, length, key; - if (isArray(name)) { - for (i = 0, length = name.length; i < length; i++) { - data[name[i]] = undefined; - } - } else if (typeof name === "object") { - for (key in name) { - if (hasOwn.call(name, key)) { - URI.removeQuery(data, key, name[key]); - } - } - } else if (typeof name === "string") { - if (value !== undefined) { - if (data[name] === value) { - data[name] = undefined; - } else if (isArray(data[name])) { - data[name] = filterArrayValues(data[name], value); - } - } else { - data[name] = undefined; - } - } else { - throw new TypeError("URI.addQuery() accepts an object, string as the first parameter"); - } - }; - URI.commonPath = function(one, two) { - var length = Math.min(one.length, two.length); - var pos; - // find first non-matching character - for (pos = 0; pos < length; pos++) { - if (one[pos] !== two[pos]) { - pos--; - break; - } - } - if (pos < 1) { - return one[0] === two[0] && one[0] === '/' ? '/' : ''; - } - // revert to last / - if (one[pos] !== '/') { - pos = one.substring(0, pos).lastIndexOf('/'); - } - return one.substring(0, pos + 1); - }; - URI.withinString = function(string, callback) { - // expression used is "gruber revised" (@gruber v2) determined to be the best solution in - // a regex sprint we did a couple of ages ago at - // * http://mathiasbynens.be/demo/url-regex - // * http://rodneyrehm.de/t/url-regex.html - return string.replace(URI.find_uri_expression, callback); - }; - URI.ensureValidHostname = function(v) { - // Theoretically URIs allow percent-encoding in Hostnames (according to RFC 3986) - // they are not part of DNS and therefore ignored by URI.js - if (v.match(URI.invalid_hostname_characters)) { - // test punycode - if (!punycode) { - throw new TypeError("Hostname '" + v + "' contains characters other than [A-Z0-9.-] and Punycode.js is not available"); - } - if (punycode.toASCII(v).match(URI.invalid_hostname_characters)) { - throw new TypeError("Hostname '" + v + "' contains characters other than [A-Z0-9.-]"); - } - } - }; - p.build = function(deferBuild) { - if (deferBuild === true) { - this._deferred_build = true; - } else if (deferBuild === undefined || this._deferred_build) { - this._string = URI.build(this._parts); - this._deferred_build = false; - } - return this; - }; - p.clone = function() { - return new URI(this); - }; - p.valueOf = p.toString = function() { - return this.build(false)._string; - }; - // generate simple accessors - _parts = { - protocol: 'protocol', - username: 'username', - password: 'password', - hostname: 'hostname', - port: 'port' - }; - generateAccessor = function(_part) { - return function(v, build) { - if (v === undefined) { - return this._parts[_part] || ""; - } else { - this._parts[_part] = v; - this.build(!build); - return this; - } - }; - }; - for (_part in _parts) { - p[_part] = generateAccessor(_parts[_part]); - } - // generate accessors with optionally prefixed input - _parts = { - query: '?', - fragment: '#' - }; - generateAccessor = function(_part, _key) { - return function(v, build) { - if (v === undefined) { - return this._parts[_part] || ""; - } else { - if (v !== null) { - v = v + ""; - if (v[0] === _key) { - v = v.substring(1); - } - } - this._parts[_part] = v; - this.build(!build); - return this; - } - }; - }; - for (_part in _parts) { - p[_part] = generateAccessor(_part, _parts[_part]); - } - // generate accessors with prefixed output - _parts = { - search: ['?', 'query'], - hash: ['#', 'fragment'] - }; - generateAccessor = function(_part, _key) { - return function(v, build) { - var t = this[_part](v, build); - return typeof t === "string" && t.length ? (_key + t) : t; - }; - }; - for (_part in _parts) { - p[_part] = generateAccessor(_parts[_part][1], _parts[_part][0]); - } - p.pathname = function(v, build) { - if (v === undefined || v === true) { - var res = this._parts.path || (this._parts.urn ? '' : '/'); - return v ? URI.decodePath(res) : res; - } else { - this._parts.path = v ? URI.recodePath(v) : "/"; - this.build(!build); - return this; - } - }; - p.path = p.pathname; - p.href = function(href, build) { - var key; - if (href === undefined) { - return this.toString(); - } - this._string = ""; - this._parts = URI._parts(); - var _URI = href instanceof URI; - var _object = typeof href === "object" && (href.hostname || href.path); - // window.location is reported to be an object, but it's not the sort - // of object we're looking for: - // * location.protocol ends with a colon - // * location.query != object.search - // * location.hash != object.fragment - // simply serializing the unknown object should do the trick - // (for location, not for everything...) - if (!_URI && _object && Object.prototype.toString.call(href) !== "[object Object]") { - href = href.toString(); - } - if (typeof href === "string") { - this._parts = URI.parse(href, this._parts); - } else if (_URI || _object) { - var src = _URI ? href._parts : href; - for (key in src) { - if (hasOwn.call(this._parts, key)) { - this._parts[key] = src[key]; - } - } - } else { - throw new TypeError("invalid input"); - } - this.build(!build); - return this; - }; - // identification accessors - p.is = function(what) { - var ip = false; - var ip4 = false; - var ip6 = false; - var name = false; - var sld = false; - var idn = false; - var punycode = false; - var relative = !this._parts.urn; - if (this._parts.hostname) { - relative = false; - ip4 = URI.ip4_expression.test(this._parts.hostname); - ip6 = URI.ip6_expression.test(this._parts.hostname); - ip = ip4 || ip6; - name = !ip; - sld = name && SLD && SLD.has(this._parts.hostname); - idn = name && URI.idn_expression.test(this._parts.hostname); - punycode = name && URI.punycode_expression.test(this._parts.hostname); - } - switch (what.toLowerCase()) { - case 'relative': - return relative; - case 'absolute': - return !relative; - // hostname identification - case 'domain': - case 'name': - return name; - case 'sld': - return sld; - case 'ip': - return ip; - case 'ip4': - case 'ipv4': - case 'inet4': - return ip4; - case 'ip6': - case 'ipv6': - case 'inet6': - return ip6; - case 'idn': - return idn; - case 'url': - return !this._parts.urn; - case 'urn': - return !!this._parts.urn; - case 'punycode': - return punycode; - } - return null; - }; - // component specific input validation - var _protocol = p.protocol; - var _port = p.port; - var _hostname = p.hostname; - p.protocol = function(v, build) { - if (v !== undefined) { - if (v) { - // accept trailing :// - v = v.replace(/:(\/\/)?$/, ''); - if (v.match(/[^a-zA-z0-9\.+-]/)) { - throw new TypeError("Protocol '" + v + "' contains characters other than [A-Z0-9.+-]"); - } - } - } - return _protocol.call(this, v, build); - }; - p.scheme = p.protocol; - p.port = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (v !== undefined) { - if (v === 0) { - v = null; - } - if (v) { - v += ""; - if (v[0] === ":") { - v = v.substring(1); - } - if (v.match(/[^0-9]/)) { - throw new TypeError("Port '" + v + "' contains characters other than [0-9]"); - } - } - } - return _port.call(this, v, build); - }; - p.hostname = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (v !== undefined) { - var x = {}; - URI.parseHost(v, x); - v = x.hostname; - } - return _hostname.call(this, v, build); - }; - // compound accessors - p.host = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (v === undefined) { - return this._parts.hostname ? URI.buildHost(this._parts) : ""; - } else { - URI.parseHost(v, this._parts); - this.build(!build); - return this; - } - }; - p.authority = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (v === undefined) { - return this._parts.hostname ? URI.buildAuthority(this._parts) : ""; - } else { - URI.parseAuthority(v, this._parts); - this.build(!build); - return this; - } - }; - p.userinfo = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (v === undefined) { - if (!this._parts.username) { - return ""; - } - var t = URI.buildUserinfo(this._parts); - return t.substring(0, t.length - 1); - } else { - if (v[v.length - 1] !== '@') { - v += '@'; - } - URI.parseUserinfo(v, this._parts); - this.build(!build); - return this; - } - }; - p.resource = function(v, build) { - var parts; - if (v === undefined) { - return this.path() + this.search() + this.hash(); - } - parts = URI.parse(v); - this._parts.path = parts.path; - this._parts.query = parts.query; - this._parts.fragment = parts.fragment; - this.build(!build); - return this; - }; - // fraction accessors - p.subdomain = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - // convenience, return "www" from "www.example.org" - if (v === undefined) { - if (!this._parts.hostname || this.is('IP')) { - return ""; - } - // grab domain and add another segment - var end = this._parts.hostname.length - this.domain().length - 1; - return this._parts.hostname.substring(0, end) || ""; - } else { - var e = this._parts.hostname.length - this.domain().length; - var sub = this._parts.hostname.substring(0, e); - var replace = new RegExp('^' + escapeRegEx(sub)); - if (v && v[v.length - 1] !== '.') { - v += "."; - } - if (v) { - URI.ensureValidHostname(v); - } - this._parts.hostname = this._parts.hostname.replace(replace, v); - this.build(!build); - return this; - } - }; - p.domain = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (typeof v === 'boolean') { - build = v; - v = undefined; - } - // convenience, return "example.org" from "www.example.org" - if (v === undefined) { - if (!this._parts.hostname || this.is('IP')) { - return ""; - } - // if hostname consists of 1 or 2 segments, it must be the domain - var t = this._parts.hostname.match(/\./g); - if (t && t.length < 2) { - return this._parts.hostname; - } - // grab tld and add another segment - var end = this._parts.hostname.length - this.tld(build).length - 1; - end = this._parts.hostname.lastIndexOf('.', end - 1) + 1; - return this._parts.hostname.substring(end) || ""; - } else { - if (!v) { - throw new TypeError("cannot set domain empty"); - } - URI.ensureValidHostname(v); - if (!this._parts.hostname || this.is('IP')) { - this._parts.hostname = v; - } else { - var replace = new RegExp(escapeRegEx(this.domain()) + "$"); - this._parts.hostname = this._parts.hostname.replace(replace, v); - } - this.build(!build); - return this; - } - }; - p.tld = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (typeof v === 'boolean') { - build = v; - v = undefined; - } - // return "org" from "www.example.org" - if (v === undefined) { - if (!this._parts.hostname || this.is('IP')) { - return ""; - } - var pos = this._parts.hostname.lastIndexOf('.'); - var tld = this._parts.hostname.substring(pos + 1); - if (build !== true && SLD && SLD.list[tld.toLowerCase()]) { - return SLD.get(this._parts.hostname) || tld; - } - return tld; - } else { - var replace; - if (!v) { - throw new TypeError("cannot set TLD empty"); - } else if (v.match(/[^a-zA-Z0-9-]/)) { - if (SLD && SLD.is(v)) { - replace = new RegExp(escapeRegEx(this.tld()) + "$"); - this._parts.hostname = this._parts.hostname.replace(replace, v); - } else { - throw new TypeError("TLD '" + v + "' contains characters other than [A-Z0-9]"); - } - } else if (!this._parts.hostname || this.is('IP')) { - throw new ReferenceError("cannot set TLD on non-domain host"); - } else { - replace = new RegExp(escapeRegEx(this.tld()) + "$"); - this._parts.hostname = this._parts.hostname.replace(replace, v); - } - this.build(!build); - return this; - } - }; - p.directory = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (v === undefined || v === true) { - if (!this._parts.path && !this._parts.hostname) { - return ''; - } - if (this._parts.path === '/') { - return '/'; - } - var end = this._parts.path.length - this.filename().length - 1; - var res = this._parts.path.substring(0, end) || (this._parts.hostname ? "/" : ""); - return v ? URI.decodePath(res) : res; - } else { - var e = this._parts.path.length - this.filename().length; - var directory = this._parts.path.substring(0, e); - var replace = new RegExp('^' + escapeRegEx(directory)); - // fully qualifier directories begin with a slash - if (!this.is('relative')) { - if (!v) { - v = '/'; - } - if (v[0] !== '/') { - v = "/" + v; - } - } - // directories always end with a slash - if (v && v[v.length - 1] !== '/') { - v += '/'; - } - v = URI.recodePath(v); - this._parts.path = this._parts.path.replace(replace, v); - this.build(!build); - return this; - } - }; - p.filename = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (v === undefined || v === true) { - if (!this._parts.path || this._parts.path === '/') { - return ""; - } - var pos = this._parts.path.lastIndexOf('/'); - var res = this._parts.path.substring(pos + 1); - return v ? URI.decodePathSegment(res) : res; - } else { - var mutatedDirectory = false; - if (v[0] === '/') { - v = v.substring(1); - } - if (v.match(/\.?\//)) { - mutatedDirectory = true; - } - var replace = new RegExp(escapeRegEx(this.filename()) + "$"); - v = URI.recodePath(v); - this._parts.path = this._parts.path.replace(replace, v); - if (mutatedDirectory) { - this.normalizePath(build); - } else { - this.build(!build); - } - return this; - } - }; - p.suffix = function(v, build) { - if (this._parts.urn) { - return v === undefined ? '' : this; - } - if (v === undefined || v === true) { - if (!this._parts.path || this._parts.path === '/') { - return ""; - } - var filename = this.filename(); - var pos = filename.lastIndexOf('.'); - var s, res; - if (pos === -1) { - return ""; - } - // suffix may only contain alnum characters (yup, I made this up.) - s = filename.substring(pos + 1); - res = (/^[a-z0-9%]+$/i).test(s) ? s : ""; - return v ? URI.decodePathSegment(res) : res; - } else { - if (v[0] === '.') { - v = v.substring(1); - } - var suffix = this.suffix(); - var replace; - if (!suffix) { - if (!v) { - return this; - } - this._parts.path += '.' + URI.recodePath(v); - } else if (!v) { - replace = new RegExp(escapeRegEx("." + suffix) + "$"); - } else { - replace = new RegExp(escapeRegEx(suffix) + "$"); - } - if (replace) { - v = URI.recodePath(v); - this._parts.path = this._parts.path.replace(replace, v); - } - this.build(!build); - return this; - } - }; - p.segment = function(segment, v, build) { - var separator = this._parts.urn ? ':' : '/'; - var path = this.path(); - var absolute = path.substring(0, 1) === '/'; - var segments = path.split(separator); - if (typeof segment !== 'number') { - build = v; - v = segment; - segment = undefined; - } - if (segment !== undefined && typeof segment !== 'number') { - throw new Error("Bad segment '" + segment + "', must be 0-based integer"); - } - if (absolute) { - segments.shift(); - } - if (segment < 0) { - // allow negative indexes to address from the end - segment = Math.max(segments.length + segment, 0); - } - if (v === undefined) { - return segment === undefined ? segments : segments[segment]; - } else if (segment === null || segments[segment] === undefined) { - if (isArray(v)) { - segments = v; - } else if (v || (typeof v === "string" && v.length)) { - if (segments[segments.length - 1] === "") { - // empty trailing elements have to be overwritten - // to prefent results such as /foo//bar - segments[segments.length - 1] = v; - } else { - segments.push(v); - } - } - } else { - if (v || (typeof v === "string" && v.length)) { - segments[segment] = v; - } else { - segments.splice(segment, 1); - } - } - if (absolute) { - segments.unshift(""); - } - return this.path(segments.join(separator), build); - }; - // mutating query string - var q = p.query; - p.query = function(v, build) { - if (v === true) { - return URI.parseQuery(this._parts.query); - } else if (v !== undefined && typeof v !== "string") { - this._parts.query = URI.buildQuery(v, this._parts.duplicateQueryParameters); - this.build(!build); - return this; - } else { - return q.call(this, v, build); - } - }; - p.addQuery = function(name, value, build) { - var data = URI.parseQuery(this._parts.query); - URI.addQuery(data, name, value === undefined ? null : value); - this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters); - if (typeof name !== "string") { - build = value; - } - this.build(!build); - return this; - }; - p.removeQuery = function(name, value, build) { - var data = URI.parseQuery(this._parts.query); - URI.removeQuery(data, name, value); - this._parts.query = URI.buildQuery(data, this._parts.duplicateQueryParameters); - if (typeof name !== "string") { - build = value; - } - this.build(!build); - return this; - }; - p.addSearch = p.addQuery; - p.removeSearch = p.removeQuery; - // sanitizing URLs - p.normalize = function() { - if (this._parts.urn) { - return this.normalizeProtocol(false).normalizeQuery(false).normalizeFragment(false).build(); - } - return this.normalizeProtocol(false).normalizeHostname(false).normalizePort(false).normalizePath(false).normalizeQuery(false).normalizeFragment(false).build(); - }; - p.normalizeProtocol = function(build) { - if (typeof this._parts.protocol === "string") { - this._parts.protocol = this._parts.protocol.toLowerCase(); - this.build(!build); - } - return this; - }; - p.normalizeHostname = function(build) { - if (this._parts.hostname) { - if (this.is('IDN') && punycode) { - this._parts.hostname = punycode.toASCII(this._parts.hostname); - } else if (this.is('IPv6') && IPv6) { - this._parts.hostname = IPv6.best(this._parts.hostname); - } - this._parts.hostname = this._parts.hostname.toLowerCase(); - this.build(!build); - } - return this; - }; - p.normalizePort = function(build) { - // remove port of it's the protocol's default - if (typeof this._parts.protocol === "string" && this._parts.port === URI.defaultPorts[this._parts.protocol]) { - this._parts.port = null; - this.build(!build); - } - return this; - }; - p.normalizePath = function(build) { - if (this._parts.urn) { - return this; - } - if (!this._parts.path || this._parts.path === '/') { - return this; - } - var _was_relative; - var _was_relative_prefix; - var _path = this._parts.path; - var _parent, _pos; - // handle relative paths - if (_path[0] !== '/') { - if (_path[0] === '.') { - _was_relative_prefix = _path.substring(0, _path.indexOf('/')); - } - _was_relative = true; - _path = '/' + _path; - } - // resolve simples - _path = _path.replace(/(\/(\.\/)+)|\/{2,}/g, '/'); - // resolve parents - while (true) { - _parent = _path.indexOf('/../'); - if (_parent === -1) { - // no more ../ to resolve - break; - } else if (_parent === 0) { - // top level cannot be relative... - _path = _path.substring(3); - break; - } - _pos = _path.substring(0, _parent).lastIndexOf('/'); - if (_pos === -1) { - _pos = _parent; - } - _path = _path.substring(0, _pos) + _path.substring(_parent + 3); - } - // revert to relative - if (_was_relative && this.is('relative')) { - if (_was_relative_prefix) { - _path = _was_relative_prefix + _path; - } else { - _path = _path.substring(1); - } - } - _path = URI.recodePath(_path); - this._parts.path = _path; - this.build(!build); - return this; - }; - p.normalizePathname = p.normalizePath; - p.normalizeQuery = function(build) { - if (typeof this._parts.query === "string") { - if (!this._parts.query.length) { - this._parts.query = null; - } else { - this.query(URI.parseQuery(this._parts.query)); - } - this.build(!build); - } - return this; - }; - p.normalizeFragment = function(build) { - if (!this._parts.fragment) { - this._parts.fragment = null; - this.build(!build); - } - return this; - }; - p.normalizeSearch = p.normalizeQuery; - p.normalizeHash = p.normalizeFragment; - p.iso8859 = function() { - // expect unicode input, iso8859 output - var e = URI.encode; - var d = URI.decode; - URI.encode = escape; - URI.decode = decodeURIComponent; - this.normalize(); - URI.encode = e; - URI.decode = d; - return this; - }; - p.unicode = function() { - // expect iso8859 input, unicode output - var e = URI.encode; - var d = URI.decode; - URI.encode = strictEncodeURIComponent; - URI.decode = unescape; - this.normalize(); - URI.encode = e; - URI.decode = d; - return this; - }; - p.readable = function() { - var uri = this.clone(); - // removing username, password, because they shouldn't be displayed according to RFC 3986 - uri.username("").password("").normalize(); - var t = ''; - if (uri._parts.protocol) { - t += uri._parts.protocol + '://'; - } - if (uri._parts.hostname) { - if (uri.is('punycode') && punycode) { - t += punycode.toUnicode(uri._parts.hostname); - if (uri._parts.port) { - t += ":" + uri._parts.port; - } - } else { - t += uri.host(); - } - } - if (uri._parts.hostname && uri._parts.path && uri._parts.path[0] !== '/') { - t += '/'; - } - t += uri.path(true); - if (uri._parts.query) { - var q = ''; - for (var i = 0, qp = uri._parts.query.split('&'), l = qp.length; i < l; i++) { - var kv = (qp[i] || "").split('='); - q += '&' + URI.decodeQuery(kv[0]).replace(/&/g, '%26'); - if (kv[1] !== undefined) { - q += "=" + URI.decodeQuery(kv[1]).replace(/&/g, '%26'); - } - } - t += '?' + q.substring(1); - } - t += uri.hash(); - return t; - }; - // resolving relative and absolute URLs - p.absoluteTo = function(base) { - var resolved = this.clone(); - var properties = ['protocol', 'username', 'password', 'hostname', 'port']; - var basedir, i, p; - if (this._parts.urn) { - throw new Error('URNs do not have any generally defined hierachical components'); - } - if (this._parts.hostname) { - return resolved; - } - if (!(base instanceof URI)) { - base = new URI(base); - } - for (i = 0, p; p = properties[i]; i++) { - resolved._parts[p] = base._parts[p]; - } - properties = ['query', 'path']; - for (i = 0, p; p = properties[i]; i++) { - if (!resolved._parts[p] && base._parts[p]) { - resolved._parts[p] = base._parts[p]; - } - } - if (resolved.path()[0] !== '/') { - basedir = base.directory(); - resolved._parts.path = (basedir ? (basedir + '/') : '') + resolved._parts.path; - resolved.normalizePath(); - } - resolved.build(); - return resolved; - }; - p.relativeTo = function(base) { - var relative = this.clone(); - var properties = ['protocol', 'username', 'password', 'hostname', 'port']; - var common, _base, _this, _base_diff, _this_diff; - if (this._parts.urn) { - throw new Error('URNs do not have any generally defined hierachical components'); - } - if (!(base instanceof URI)) { - base = new URI(base); - } - if (this.path()[0] !== '/' || base.path()[0] !== '/') { - throw new Error('Cannot calculate common path from non-relative URLs'); - } - // determine common sub path - common = URI.commonPath(relative.path(), base.path()); - // no relation if there's nothing in common - if (!common || common === '/') { - return relative; - } - // relative paths don't have authority - for (var i = 0, p; p = properties[i]; i++) { - relative._parts[p] = null; - } - _base = base.directory(); - _this = this.directory(); - // base and this are on the same level - if (_base === _this) { - relative._parts.path = './' + relative.filename(); - return relative.build(); - } - _base_diff = _base.substring(common.length); - _this_diff = _this.substring(common.length); - // this is a descendant of base - if (_base + '/' === common) { - if (_this_diff) { - _this_diff += '/'; - } - relative._parts.path = './' + _this_diff + relative.filename(); - return relative.build(); - } - // this is a descendant of base - var parents = '../'; - var _common = new RegExp('^' + escapeRegEx(common)); - var _parents = _base.replace(_common, '/').match(/\//g).length - 1; - while (_parents--) { - parents += '../'; - } - relative._parts.path = relative._parts.path.replace(_common, parents); - return relative.build(); - }; - // comparing URIs - p.equals = function(uri) { - var one = this.clone(); - var two = new URI(uri); - var one_map = {}; - var two_map = {}; - var checked = {}; - var one_query, two_query, key; - one.normalize(); - two.normalize(); - // exact match - if (one.toString() === two.toString()) { - return true; - } - // extract query string - one_query = one.query(); - two_query = two.query(); - one.query(""); - two.query(""); - // definitely not equal if not even non-query parts match - if (one.toString() !== two.toString()) { - return false; - } - // query parameters have the same length, even if they're permutated - if (one_query.length !== two_query.length) { - return false; - } - one_map = URI.parseQuery(one_query); - two_map = URI.parseQuery(two_query); - for (key in one_map) { - if (hasOwn.call(one_map, key)) { - if (!isArray(one_map[key])) { - if (one_map[key] !== two_map[key]) { - return false; - } - } else { - if (!isArray(two_map[key])) { - return false; - } - // arrays can't be equal if they have different amount of content - if (one_map[key].length !== two_map[key].length) { - return false; - } - one_map[key].sort(); - two_map[key].sort(); - for (var i = 0, l = one_map[key].length; i < l; i++) { - if (one_map[key][i] !== two_map[key][i]) { - return false; - } - } - } - checked[key] = true; - } - } - for (key in two_map) { - if (hasOwn.call(two_map, key)) { - if (!checked[key]) { - // two contains a parameter not present in one - return false; - } - } - } - return true; - }; - // state - p.duplicateQueryParameters = function(v) { - this._parts.duplicateQueryParameters = !! v; - return this; - }; - return URI; -})); \ No newline at end of file diff --git a/doc/scripts/bootstrap-dropdown.js b/doc/scripts/bootstrap-dropdown.js deleted file mode 100644 index d04da5d7..00000000 --- a/doc/scripts/bootstrap-dropdown.js +++ /dev/null @@ -1,169 +0,0 @@ -/* ============================================================ - * bootstrap-dropdown.js v2.3.2 - * http://getbootstrap.com/2.3.2/javascript.html#dropdowns - * ============================================================ - * Copyright 2013 Twitter, Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ============================================================ */ - - -!function ($) { - - "use strict"; // jshint ;_; - - - /* DROPDOWN CLASS DEFINITION - * ========================= */ - - var toggle = '[data-toggle=dropdown]' - , Dropdown = function (element) { - var $el = $(element).on('click.dropdown.data-api', this.toggle) - $('html').on('click.dropdown.data-api', function () { - $el.parent().removeClass('open') - }) - } - - Dropdown.prototype = { - - constructor: Dropdown - - , toggle: function (e) { - var $this = $(this) - , $parent - , isActive - - if ($this.is('.disabled, :disabled')) return - - $parent = getParent($this) - - isActive = $parent.hasClass('open') - - clearMenus() - - if (!isActive) { - if ('ontouchstart' in document.documentElement) { - // if mobile we we use a backdrop because click events don't delegate - $('