{"action":"created","comment":{"url":"https://api.github.com/repos/RackHD/on-http/pulls/comments/50217644","id":50217644,"diff_hunk":"@@ -0,0 +1,521 @@\n+// Copyright 2016, EMC, Inc.\n+/* jshint node:true */\n+\n+'use strict';\n+\n+var ws = require('ws');\n+\n+describe('Auth.Service', function () {\n+    var server;\n+    var sandbox = sinon.sandbox.create();\n+    var jwtStrategy = require('passport-jwt').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 token = '';\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/j8c6YGlG7iA0PB3P9ojbmANGhDlcSBE0iOTIsYsGbtSsbqP4wvsVcw==')\n+            .set('authPasswordSalt', 'zlxkgxjvcFwm0M8sWaGojh25qNYO8tuNWUMN4xKPH93PidwkCAvaX2JItLA3p7BSCWIzkw4GwWuezoMvKf3UXg==')\n+            .set('authTokenExpireIn', 86400)\n+    }\n+\n+    helper.before(function () {\n+        return [\n+            dihelper.simpleWrapper(require('express')(), 'express-app'),\n+            dihelper.simpleWrapper(ws.Server, 'WebSocketServer'),\n+            dihelper.simpleWrapper({}, 'Task.Services.OBM'),\n+            dihelper.simpleWrapper({}, 'ipmi-obm-service'),\n+            helper.require('/lib/services/http-service'),\n+            helper.requireGlob('/lib/**/*.js')\n+        ];\n+    });\n+\n+    before('allow self signed certs', function () {\n+        process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';\n+    });\n+\n+    helper.after();\n+\n+    after('disallow self signed certs', function () {\n+        process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1';\n+    });\n+\n+    describe('Auth.Service', function () {\n+        before('start http and https server with auth enabled', function () {\n+            startServer(endpoint);\n+        });\n+\n+        it('should return a token from /login', 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+                    token = res.body.token;\n+                    console.log(SUCCESS_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should able to access with correct token in query string', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config?auth_token=' + token)\n+                .expect(SUCCESS_STATUS)\n+                .expect(function (res) {\n+                    expect(res.body).to.be.a('object');\n+                    expect(res.body.apiServerPort).to.equal(8080);\n+                    console.log(SUCCESS_STATUS);\n+                })\n+        });\n+\n+        it('should fail with wrong token in query string', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config?auth_token=' + token + '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 signature');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should fail with empty token in query string', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config?auth_token=')\n+                .expect(UNAUTHORIZED_STATUS)\n+                .expect(function (res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('No auth token');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should fail with wrong token key in query string', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config?auth_tokennnnnnnn=')\n+                .expect(UNAUTHORIZED_STATUS)\n+                .expect(function (res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('No auth token');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should able to access with correct token in query header', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config')\n+                .set(\"authorization\", 'JWT ' + token)\n+                .send()\n+                .expect(SUCCESS_STATUS)\n+                .expect(function (res) {\n+                    expect(res.body).to.be.a('object');\n+                    expect(res.body.apiServerPort).to.equal(8080);\n+                    console.log(SUCCESS_STATUS);\n+                })\n+        });\n+\n+        it('should fail with wrong token in query header', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config')\n+                .set(\"authorization\", 'JWT ' + token + '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 signature');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should fail with empty token in query header', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config')\n+                .set(\"authorization\", '')\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('No auth token');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should fail with wrong token key in query header', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config')\n+                .set(\"authorization_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('No auth token');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should able to access with correct token in query body', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config')\n+                .send({auth_token: token})\n+                .expect(SUCCESS_STATUS)\n+                .expect(function (res) {\n+                    expect(res.body).to.be.a('object');\n+                    expect(res.body.apiServerPort).to.equal(8080);\n+                    console.log(SUCCESS_STATUS);\n+                })\n+        });\n+\n+        it('should fail with wrong token in query body', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config')\n+                .send({auth_token: token + '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 signature');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should fail with empty token in query body', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config')\n+                .send({auth_token: ''})\n+                .expect(UNAUTHORIZED_STATUS)\n+                .expect(function (res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('No auth token');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should fail with wrong token key in query body', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config')\n+                .send({auth_tokennnnnnnnn: token})\n+                .expect(UNAUTHORIZED_STATUS)\n+                .expect(function (res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('No auth token');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        it('should fail with no token at all', function () {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config')\n+                .expect(UNAUTHORIZED_STATUS)\n+                .expect(function (res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('No auth token');\n+                    console.log(UNAUTHORIZED_STATUS, res.body)\n+                })\n+        });\n+\n+        after('Clean up', function () {\n+            cleanUp();\n+        });\n+    });\n+\n+    describe('Should return internal server error with auth error callback', function () {\n+        before('start HTTPs server', function () {\n+            sandbox.stub(jwtStrategy.prototype, 'authenticate', function(req, options) {\n+                return this.error('something');\n+            });\n+            startServer(endpoint);\n+        });\n+\n+        it('should fail with auth', function() {\n+            return helper.request('https://localhost:8443')\n+                .get('/api/1.1/config?auth_token=' + token)\n+                .expect(ERROR_STATUS)\n+                .expect(function(res) {\n+                    expect(res.body.message).to.be.a('string');\n+                    expect(res.body.message).to.equal('Internal server error');\n+                    console.log(ERROR_STATUS, res.body);\n+                })\n+        });\n+\n+        after('stop server, restore mock and configure',function () {\n+            sandbox.restore();\n+            cleanUp();\n+        });\n+    });\n+\n+    describe('Corrupted hash from config', function () {\n+        before('Mock configure settings', function () {\n+            this.timeout(5000);\n+            helper.injector.get('Services.Configuration')\n+                .set('authPasswordHash', 'aaa');\n+        });\n+\n+        it('Should throw exception with wrong length of hash from config', function() {\n+            var authService = helper.injector.get('Auth.Services');\n+            expect(function () {\n+                authService.init();\n+            }).to.throw(Error);\n+        });\n+\n+        after('stop server, restore mock and configure',function () {\n+            restoreConfig();\n+        });\n+    });\n+\n+    describe('Corrupted salt from config', function () {\n+        before('Mock configure settings', function () {\n+            this.timeout(5000);\n+\n+            helper.injector.get('Services.Configuration')\n+                .set('authPasswordSalt', 'aaa');\n+        });\n+\n+        it('Should throw exception with wrong length of salt from config', function() {\n+            var authService = helper.injector.get('Auth.Services');\n+            expect(function () {\n+                authService.init();\n+            }).to.throw(Error);\n+        });\n+\n+        after('stop server, restore mock and configure',function () {\n+            restoreConfig();\n+        });\n+    });\n+\n+    describe('Token expiration from config is not a number', function () {\n+        before('Mock configure settings', function () {\n+            this.timeout(5000);\n+\n+            helper.injector.get('Services.Configuration')\n+                .set('authTokenExpireIn', 'aaa');\n+        });\n+\n+        it('Should throw exception with wrong length of salt from config', function() {\n+            var authService = helper.injector.get('Auth.Services');\n+            expect(function () {\n+                authService.init();\n+            }).to.throw(Error);\n+        });\n+\n+        after('stop server, restore mock and configure',function () {\n+            restoreConfig();\n+        });\n+    });\n+\n+    describe('Token should expire as expected', function () {\n+        before('start http and https server with auth enabled', function () {\n+            this.timeout(10000);\n+\n+            helper.injector.get('Services.Configuration')\n+                .set('authTokenExpireIn', 1);\n+            startServer(endpoint);\n+        });\n+\n+        it('should return a token from /login', 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+                    token = res.body.token;\n+                    console.log(SUCCESS_STATUS, res.body)\n+                })","path":"spec/lib/services/auth/auth-service-spec.js","position":352,"original_position":352,"commit_id":"5b57a4c214969492accb19bd4de51afc1c5aa80a","original_commit_id":"5b57a4c214969492accb19bd4de51afc1c5aa80a","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-20T06:03:08Z","updated_at":"2016-01-20T06:03:08Z","html_url":"https://github.com/RackHD/on-http/pull/93#discussion_r50217644","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/50217644"},"html":{"href":"https://github.com/RackHD/on-http/pull/93#discussion_r50217644"},"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":"# Note\r\nThis is still **W.I.P.**. please **DO NOT MERGE** until everthing finalized.  \r\n\r\nTo do: \r\n\r\n1. Rebase master\r\n2. Fix unit test error\r\n3. Add comments\r\n\r\n\r\n# 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","created_at":"2016-01-19T10:21:39Z","updated_at":"2016-01-20T06:03:08Z","closed_at":null,"merged_at":null,"merge_commit_sha":"4bb6a0ba1f8a1539d093b5817e0a9add840ba5d3","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/5b57a4c214969492accb19bd4de51afc1c5aa80a","head":{"label":"cgx027:Feature/RackHD_V1.1_API_Authentication","ref":"Feature/RackHD_V1.1_API_Authentication","sha":"5b57a4c214969492accb19bd4de51afc1c5aa80a","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}","created_at":"2015-12-11T03:43:07Z","updated_at":"2016-01-20T03:19:07Z","pushed_at":"2016-01-20T06:02: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":1493,"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":"2e469fdd14f22d512b4d721feaeab99e98ac8183","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}","created_at":"2015-08-31T17:29:08Z","updated_at":"2016-01-13T05:43:37Z","pushed_at":"2016-01-20T06:02:55Z","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":1754,"stargazers_count":6,"watchers_count":6,"language":"JavaScript","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":29,"mirror_url":null,"open_issues_count":1,"forks":29,"open_issues":1,"watchers":6,"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/5b57a4c214969492accb19bd4de51afc1c5aa80a"}}}}