{"action":"created","comment":{"url":"https://api.github.com/repos/RackHD/on-http/pulls/comments/50814249","id":50814249,"diff_hunk":"@@ -0,0 +1,376 @@\n+// Copyright 2016, EMC, Inc.\n+/* jshint node:true */\n+\n+'use strict';\n+\n+var ws = require('ws');\n+\n+describe('Http.Api.Login', function () {\n+    var server;\n+    var sandbox = sinon.sandbox.create();\n+    var localStrategy = require('passport-local').Strategy;\n+\n+    var SUCCESS_STATUS = 200;\n+    var BAD_REQUEST_STATUS = 400;\n+    var UNAUTHORIZED_STATUS = 401;\n+    var NOT_FOUND_STATUS = 404;\n+    var ERROR_STATUS = 500;\n+\n+    var endpoint = {\n+        \"address\": \"0.0.0.0\",\n+        \"port\": 8443,\n+        \"httpsEnabled\": true,\n+        \"httpsCert\": \"data/dev-cert.pem\",\n+        \"httpsKey\": \"data/dev-key.pem\",\n+        \"httpsPfx\": null,\n+        \"proxiesEnabled\": false,\n+        \"authEnabled\": true,\n+        \"routers\": \"northbound-api-router\"\n+    };\n+\n+    function startServer(endpoint){\n+        var Server = helper.injector.get('Http.Server');\n+        server = new Server(endpoint);\n+        server.start();\n+    }\n+\n+    function cleanUp(){\n+        server.stop();\n+        sandbox.restore();\n+        restoreConfig();\n+\n+    };\n+\n+    function restoreConfig(){\n+        helper.injector.get('Services.Configuration')\n+            .set('authPasswordHash', 'KcBN9YobNV0wdux8h0fKNqi4uoKCgGl/j8c6YGlG7iA' +\n+                                     '0PB3P9ojbmANGhDlcSBE0iOTIsYsGbtSsbqP4wvsVcw==')\n+            .set('authPasswordSalt', 'zlxkgxjvcFwm0M8sWaGojh25qNYO8tuNWUMN4xKPH93' +\n+                                     'PidwkCAvaX2JItLA3p7BSCWIzkw4GwWuezoMvKf3UXg==')\n+            .set('authTokenExpireIn', 86400)\n+    }\n+\n+    helper.before(function () {\n+        return [\n+            dihelper.simpleWrapper(require('swagger-express-mw'), 'swagger'),\n+            dihelper.simpleWrapper(ws.Server, 'WebSocketServer'),\n+            dihelper.simpleWrapper({}, 'Task.Services.OBM'),\n+            dihelper.simpleWrapper({}, 'ipmi-obm-service'),\n+            dihelper.requireWrapper('rimraf', 'rimraf'),\n+            dihelper.requireWrapper('os-tmpdir', 'osTmpdir'),\n+            helper.require('/lib/services/http-service'),\n+            helper.requireGlob('/lib/api/1.1/**/*.js'),\n+            helper.requireGlob('/lib/services/**/*.js'),\n+            helper.requireGlob('/lib/serializables/**/*.js')\n+        ];\n+    });\n+\n+    helper.after();\n+\n+    before('allow self signed certs', function () {\n+        process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';\n+    });\n+\n+    after('disallow self signed certs', function () {\n+        process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';\n+    });\n+\n+    describe('test with authentication enabled', function () {\n+        before('start HTTPs server', function () {\n+            this.timeout(5000);\n+            startServer(endpoint);\n+        });\n+\n+        it('should return a token with correct credential in request body', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .send({username: \"admin\", password: \"admin123\"})\n+                .expect(SUCCESS_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.token).to.be.a('string');\n+                });\n+        });\n+\n+        it('should fail with wrong username in request body', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .send({username: \"balabalabala\", password: \"admin123\"})\n+                .expect(UNAUTHORIZED_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Invalid username or password');\n+                });\n+        });\n+\n+        it('should fail with wrong password in request body', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .send({username: \"admin\", password: \"balabalabala\"})\n+                .expect(UNAUTHORIZED_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Invalid username or password');\n+                });\n+        });\n+\n+        it('should fail with empty username in request body', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .send({username: \"\", password: \"admin123\"})\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        it('should fail with empty password in request body', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .send({username: \"admin\", password: \"\"})\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        it('should fail with no username key in request body', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .send({password: \"admin123\"})\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        it('should fail with no password key in request body', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .send({username: \"admin\"})\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        it('should return a token with correct credential in query string', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login?username=admin&password=admin123')\n+                .send()\n+                .expect(SUCCESS_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.token).to.be.a('string');\n+                });\n+        });\n+\n+        it('should fail with wrong username in query string', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login?username=balabalabala&password=admin123')\n+                .send()\n+                .expect(UNAUTHORIZED_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Invalid username or password');\n+                });\n+        });\n+\n+        it('should fail with wrong password in query string', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login?username=admin&password=balabalabala')\n+                .send()\n+                .expect(UNAUTHORIZED_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Invalid username or password');\n+                });\n+        });\n+\n+        it('should fail with empty username in query string', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login?username=&password=admin123')\n+                .send()\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        it('should fail with empty password in query string', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login?username=admin&password=')\n+                .send()\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        it('should fail with no username parameter in query string', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login?password=admin123')\n+                .send()\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        it('should fail with no password parameter in query string', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login?username=admin')\n+                .send()\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        //passport-local middleware we choose does not support authentication\n+        // with credential in the header. Following test will fail if auth header\n+        // is supported in the future, thus people will get alerted.\n+        it('should fail with credential in request header', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .set('username', 'admin')\n+                .set('password', 'admin123')\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        it('should fail no credential at all - https', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        after('stop server, restore mock and configure',function () {\n+            cleanUp();\n+        });\n+    });\n+\n+    describe('test with authentication enabled', function () {\n+        before('start HTTPs server', function () {\n+            this.timeout(5000);\n+            var endpoint_http = {\n+                \"address\": \"0.0.0.0\",\n+                \"port\": 8089,\n+                \"httpsEnabled\": false,\n+                \"authEnabled\": true,\n+                \"routers\": \"northbound-api-router\"\n+            };\n+            startServer(endpoint_http);\n+        });\n+\n+        //give a shoot on http instead of https.\n+        it('should success auth with http instead of https', function() {\n+            return helper.request('http://localhost:8089')\n+                .post('/api/1.1/login')\n+                .send({username: \"admin\", password: \"admin123\"})\n+                .expect(SUCCESS_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.token).to.be.a('string');\n+                });\n+        });\n+\n+        it('should fail no credential at all - http', function() {\n+            return helper.request('http://localhost:8089')\n+                .post('/api/1.1/login')\n+                .expect(BAD_REQUEST_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Missing credentials');\n+                });\n+        });\n+\n+        after('stop server, restore mock and configure',function () {\n+            cleanUp();\n+        });\n+    });\n+\n+    describe('test with authentication disabled', function () {\n+        before('start HTTPs server', function () {\n+            this.timeout(5000);\n+            endpoint.authEnabled = false;\n+            startServer(endpoint);\n+            //restore endpoint\n+            endpoint.authEnabled = true;\n+        });\n+\n+        it('should fail with auth disabled', function() {\n+            return helper.request('https://localhost:8443')\n+                .post('/api/1.1/login')\n+                .send({username: \"admin\", password: \"admin123\"})\n+                .expect(NOT_FOUND_STATUS)","path":"spec/lib/api/1.1/login-spec.js","position":318,"original_position":318,"commit_id":"a62608e1953ce2a82d948cb466a8c27c25a09cf7","original_commit_id":"a62608e1953ce2a82d948cb466a8c27c25a09cf7","user":{"login":"houndci","id":6697940,"avatar_url":"https://avatars.githubusercontent.com/u/6697940?v=3","gravatar_id":"","url":"https://api.github.com/users/houndci","html_url":"https://github.com/houndci","followers_url":"https://api.github.com/users/houndci/followers","following_url":"https://api.github.com/users/houndci/following{/other_user}","gists_url":"https://api.github.com/users/houndci/gists{/gist_id}","starred_url":"https://api.github.com/users/houndci/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/houndci/subscriptions","organizations_url":"https://api.github.com/users/houndci/orgs","repos_url":"https://api.github.com/users/houndci/repos","events_url":"https://api.github.com/users/houndci/events{/privacy}","received_events_url":"https://api.github.com/users/houndci/received_events","type":"User","site_admin":false},"body":"Missing semicolon.","created_at":"2016-01-26T09:57:21Z","updated_at":"2016-01-26T09:57:21Z","html_url":"https://github.com/RackHD/on-http/pull/93#discussion_r50814249","pull_request_url":"https://api.github.com/repos/RackHD/on-http/pulls/93","_links":{"self":{"href":"https://api.github.com/repos/RackHD/on-http/pulls/comments/50814249"},"html":{"href":"https://github.com/RackHD/on-http/pull/93#discussion_r50814249"},"pull_request":{"href":"https://api.github.com/repos/RackHD/on-http/pulls/93"}}},"pull_request":{"url":"https://api.github.com/repos/RackHD/on-http/pulls/93","id":56446064,"html_url":"https://github.com/RackHD/on-http/pull/93","diff_url":"https://github.com/RackHD/on-http/pull/93.diff","patch_url":"https://github.com/RackHD/on-http/pull/93.patch","issue_url":"https://api.github.com/repos/RackHD/on-http/issues/93","number":93,"state":"open","locked":false,"title":"Feature/rack hd v1.1 api authentication","user":{"login":"cgx027","id":2181868,"avatar_url":"https://avatars.githubusercontent.com/u/2181868?v=3","gravatar_id":"","url":"https://api.github.com/users/cgx027","html_url":"https://github.com/cgx027","followers_url":"https://api.github.com/users/cgx027/followers","following_url":"https://api.github.com/users/cgx027/following{/other_user}","gists_url":"https://api.github.com/users/cgx027/gists{/gist_id}","starred_url":"https://api.github.com/users/cgx027/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cgx027/subscriptions","organizations_url":"https://api.github.com/users/cgx027/orgs","repos_url":"https://api.github.com/users/cgx027/repos","events_url":"https://api.github.com/users/cgx027/events{/privacy}","received_events_url":"https://api.github.com/users/cgx027/received_events","type":"User","site_admin":false},"body":"# What does the code do:\r\n\r\nThis code will shows how we setup **v1.1** authentication and also how we split northbound and southbound API\r\n\r\n# Reference \r\n1. Spec: \r\n  * https://github.com/RackHD/specs/blob/master/v1.1_api_authentication.md\r\n2. Google group discussion:\r\n  * [RackHD v1.1 API authentication use case and plan review](https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!topic/rackhd/Hw88shMffLw)\r\n  * [v1.1 API authentication --split southbound and northbound APIs](https://groups.google.com/forum/?utm_medium=email&utm_source=footer#!searchin/rackhd/split/rackhd/PmP2hosgJr8/eyvuBsVlBAAJ)\r\n  * [Separate North/South API](https://groups.google.com/forum/#!topic/rackhd/n18-mWelhVs)\r\n\r\n# Design Notes\r\n* Authentication\r\n    * Passport framework used for authentication\r\n    * Passport-local middleware used for username/password authn when accessing /login, a token will be returned after successful authz. \r\n    * crypto.pbkdf2 used to generate the hash for the default password with a default salt generated using crypto.randomBytes. The default hash and salt are stored in the configure file (/opt/onrack/etc/monorail.json). Password posted from user is hashed and then compared to the hash read from configure file.\r\n    * Passport-jwt middleware used for token verification. jsonwebtoken is used used to sign and verify the token. \r\n* Split northbound and southbound api \r\n    * Multiple http/https endpoints (defined in config file) design see [Separate North/South API](https://groups.google.com/forum/#!topic/rackhd/n18-mWelhVs).\r\n        * Refer to [monorail.json](https://github.com/WangWinson/RackHD/commit/0c37f1bc4932cd9c5f91c954ec4324e08cd8a48f) for configure file change.\r\n    * Separate API routers to northbound and southbound. Leverage 2 endpoints to host north/south API. \r\n\r\n### This PR should be merged together with https://github.com/RackHD/RackHD/pull/83 ","created_at":"2016-01-19T10:21:39Z","updated_at":"2016-01-26T09:57:21Z","closed_at":null,"merged_at":null,"merge_commit_sha":"24057862e63b05b93eddaf54f0c9d6dc7204c7d4","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/RackHD/on-http/pulls/93/commits","review_comments_url":"https://api.github.com/repos/RackHD/on-http/pulls/93/comments","review_comment_url":"https://api.github.com/repos/RackHD/on-http/pulls/comments{/number}","comments_url":"https://api.github.com/repos/RackHD/on-http/issues/93/comments","statuses_url":"https://api.github.com/repos/RackHD/on-http/statuses/a62608e1953ce2a82d948cb466a8c27c25a09cf7","head":{"label":"cgx027:Feature/RackHD_V1.1_API_Authentication","ref":"Feature/RackHD_V1.1_API_Authentication","sha":"a62608e1953ce2a82d948cb466a8c27c25a09cf7","user":{"login":"cgx027","id":2181868,"avatar_url":"https://avatars.githubusercontent.com/u/2181868?v=3","gravatar_id":"","url":"https://api.github.com/users/cgx027","html_url":"https://github.com/cgx027","followers_url":"https://api.github.com/users/cgx027/followers","following_url":"https://api.github.com/users/cgx027/following{/other_user}","gists_url":"https://api.github.com/users/cgx027/gists{/gist_id}","starred_url":"https://api.github.com/users/cgx027/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cgx027/subscriptions","organizations_url":"https://api.github.com/users/cgx027/orgs","repos_url":"https://api.github.com/users/cgx027/repos","events_url":"https://api.github.com/users/cgx027/events{/privacy}","received_events_url":"https://api.github.com/users/cgx027/received_events","type":"User","site_admin":false},"repo":{"id":47803371,"name":"on-http","full_name":"cgx027/on-http","owner":{"login":"cgx027","id":2181868,"avatar_url":"https://avatars.githubusercontent.com/u/2181868?v=3","gravatar_id":"","url":"https://api.github.com/users/cgx027","html_url":"https://github.com/cgx027","followers_url":"https://api.github.com/users/cgx027/followers","following_url":"https://api.github.com/users/cgx027/following{/other_user}","gists_url":"https://api.github.com/users/cgx027/gists{/gist_id}","starred_url":"https://api.github.com/users/cgx027/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cgx027/subscriptions","organizations_url":"https://api.github.com/users/cgx027/orgs","repos_url":"https://api.github.com/users/cgx027/repos","events_url":"https://api.github.com/users/cgx027/events{/privacy}","received_events_url":"https://api.github.com/users/cgx027/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/cgx027/on-http","description":"","fork":true,"url":"https://api.github.com/repos/cgx027/on-http","forks_url":"https://api.github.com/repos/cgx027/on-http/forks","keys_url":"https://api.github.com/repos/cgx027/on-http/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cgx027/on-http/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cgx027/on-http/teams","hooks_url":"https://api.github.com/repos/cgx027/on-http/hooks","issue_events_url":"https://api.github.com/repos/cgx027/on-http/issues/events{/number}","events_url":"https://api.github.com/repos/cgx027/on-http/events","assignees_url":"https://api.github.com/repos/cgx027/on-http/assignees{/user}","branches_url":"https://api.github.com/repos/cgx027/on-http/branches{/branch}","tags_url":"https://api.github.com/repos/cgx027/on-http/tags","blobs_url":"https://api.github.com/repos/cgx027/on-http/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cgx027/on-http/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cgx027/on-http/git/refs{/sha}","trees_url":"https://api.github.com/repos/cgx027/on-http/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cgx027/on-http/statuses/{sha}","languages_url":"https://api.github.com/repos/cgx027/on-http/languages","stargazers_url":"https://api.github.com/repos/cgx027/on-http/stargazers","contributors_url":"https://api.github.com/repos/cgx027/on-http/contributors","subscribers_url":"https://api.github.com/repos/cgx027/on-http/subscribers","subscription_url":"https://api.github.com/repos/cgx027/on-http/subscription","commits_url":"https://api.github.com/repos/cgx027/on-http/commits{/sha}","git_commits_url":"https://api.github.com/repos/cgx027/on-http/git/commits{/sha}","comments_url":"https://api.github.com/repos/cgx027/on-http/comments{/number}","issue_comment_url":"https://api.github.com/repos/cgx027/on-http/issues/comments{/number}","contents_url":"https://api.github.com/repos/cgx027/on-http/contents/{+path}","compare_url":"https://api.github.com/repos/cgx027/on-http/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cgx027/on-http/merges","archive_url":"https://api.github.com/repos/cgx027/on-http/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cgx027/on-http/downloads","issues_url":"https://api.github.com/repos/cgx027/on-http/issues{/number}","pulls_url":"https://api.github.com/repos/cgx027/on-http/pulls{/number}","milestones_url":"https://api.github.com/repos/cgx027/on-http/milestones{/number}","notifications_url":"https://api.github.com/repos/cgx027/on-http/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cgx027/on-http/labels{/name}","releases_url":"https://api.github.com/repos/cgx027/on-http/releases{/id}","deployments_url":"https://api.github.com/repos/cgx027/on-http/deployments","created_at":"2015-12-11T03:43:07Z","updated_at":"2016-01-20T03:19:07Z","pushed_at":"2016-01-26T09:56:54Z","git_url":"git://github.com/cgx027/on-http.git","ssh_url":"git@github.com:cgx027/on-http.git","clone_url":"https://github.com/cgx027/on-http.git","svn_url":"https://github.com/cgx027/on-http","homepage":null,"size":1532,"stargazers_count":0,"watchers_count":0,"language":"JavaScript","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"master"}},"base":{"label":"RackHD:master","ref":"master","sha":"6802636411e9b4380b6b8e2348bf14f11cf39d5a","user":{"login":"RackHD","id":13982662,"avatar_url":"https://avatars.githubusercontent.com/u/13982662?v=3","gravatar_id":"","url":"https://api.github.com/users/RackHD","html_url":"https://github.com/RackHD","followers_url":"https://api.github.com/users/RackHD/followers","following_url":"https://api.github.com/users/RackHD/following{/other_user}","gists_url":"https://api.github.com/users/RackHD/gists{/gist_id}","starred_url":"https://api.github.com/users/RackHD/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/RackHD/subscriptions","organizations_url":"https://api.github.com/users/RackHD/orgs","repos_url":"https://api.github.com/users/RackHD/repos","events_url":"https://api.github.com/users/RackHD/events{/privacy}","received_events_url":"https://api.github.com/users/RackHD/received_events","type":"Organization","site_admin":false},"repo":{"id":41690081,"name":"on-http","full_name":"RackHD/on-http","owner":{"login":"RackHD","id":13982662,"avatar_url":"https://avatars.githubusercontent.com/u/13982662?v=3","gravatar_id":"","url":"https://api.github.com/users/RackHD","html_url":"https://github.com/RackHD","followers_url":"https://api.github.com/users/RackHD/followers","following_url":"https://api.github.com/users/RackHD/following{/other_user}","gists_url":"https://api.github.com/users/RackHD/gists{/gist_id}","starred_url":"https://api.github.com/users/RackHD/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/RackHD/subscriptions","organizations_url":"https://api.github.com/users/RackHD/orgs","repos_url":"https://api.github.com/users/RackHD/repos","events_url":"https://api.github.com/users/RackHD/events{/privacy}","received_events_url":"https://api.github.com/users/RackHD/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/RackHD/on-http","description":"","fork":false,"url":"https://api.github.com/repos/RackHD/on-http","forks_url":"https://api.github.com/repos/RackHD/on-http/forks","keys_url":"https://api.github.com/repos/RackHD/on-http/keys{/key_id}","collaborators_url":"https://api.github.com/repos/RackHD/on-http/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/RackHD/on-http/teams","hooks_url":"https://api.github.com/repos/RackHD/on-http/hooks","issue_events_url":"https://api.github.com/repos/RackHD/on-http/issues/events{/number}","events_url":"https://api.github.com/repos/RackHD/on-http/events","assignees_url":"https://api.github.com/repos/RackHD/on-http/assignees{/user}","branches_url":"https://api.github.com/repos/RackHD/on-http/branches{/branch}","tags_url":"https://api.github.com/repos/RackHD/on-http/tags","blobs_url":"https://api.github.com/repos/RackHD/on-http/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/RackHD/on-http/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/RackHD/on-http/git/refs{/sha}","trees_url":"https://api.github.com/repos/RackHD/on-http/git/trees{/sha}","statuses_url":"https://api.github.com/repos/RackHD/on-http/statuses/{sha}","languages_url":"https://api.github.com/repos/RackHD/on-http/languages","stargazers_url":"https://api.github.com/repos/RackHD/on-http/stargazers","contributors_url":"https://api.github.com/repos/RackHD/on-http/contributors","subscribers_url":"https://api.github.com/repos/RackHD/on-http/subscribers","subscription_url":"https://api.github.com/repos/RackHD/on-http/subscription","commits_url":"https://api.github.com/repos/RackHD/on-http/commits{/sha}","git_commits_url":"https://api.github.com/repos/RackHD/on-http/git/commits{/sha}","comments_url":"https://api.github.com/repos/RackHD/on-http/comments{/number}","issue_comment_url":"https://api.github.com/repos/RackHD/on-http/issues/comments{/number}","contents_url":"https://api.github.com/repos/RackHD/on-http/contents/{+path}","compare_url":"https://api.github.com/repos/RackHD/on-http/compare/{base}...{head}","merges_url":"https://api.github.com/repos/RackHD/on-http/merges","archive_url":"https://api.github.com/repos/RackHD/on-http/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/RackHD/on-http/downloads","issues_url":"https://api.github.com/repos/RackHD/on-http/issues{/number}","pulls_url":"https://api.github.com/repos/RackHD/on-http/pulls{/number}","milestones_url":"https://api.github.com/repos/RackHD/on-http/milestones{/number}","notifications_url":"https://api.github.com/repos/RackHD/on-http/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/RackHD/on-http/labels{/name}","releases_url":"https://api.github.com/repos/RackHD/on-http/releases{/id}","deployments_url":"https://api.github.com/repos/RackHD/on-http/deployments","created_at":"2015-08-31T17:29:08Z","updated_at":"2016-01-20T12:14:40Z","pushed_at":"2016-01-26T09:56:54Z","git_url":"git://github.com/RackHD/on-http.git","ssh_url":"git@github.com:RackHD/on-http.git","clone_url":"https://github.com/RackHD/on-http.git","svn_url":"https://github.com/RackHD/on-http","homepage":null,"size":1792,"stargazers_count":7,"watchers_count":7,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":30,"mirror_url":null,"open_issues_count":2,"forks":30,"open_issues":2,"watchers":7,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/RackHD/on-http/pulls/93"},"html":{"href":"https://github.com/RackHD/on-http/pull/93"},"issue":{"href":"https://api.github.com/repos/RackHD/on-http/issues/93"},"comments":{"href":"https://api.github.com/repos/RackHD/on-http/issues/93/comments"},"review_comments":{"href":"https://api.github.com/repos/RackHD/on-http/pulls/93/comments"},"review_comment":{"href":"https://api.github.com/repos/RackHD/on-http/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/RackHD/on-http/pulls/93/commits"},"statuses":{"href":"https://api.github.com/repos/RackHD/on-http/statuses/a62608e1953ce2a82d948cb466a8c27c25a09cf7"}}}}