Skip to content

Commit

Permalink
Fixing merge conflict.
Browse files Browse the repository at this point in the history
  • Loading branch information
mikeal committed Jan 18, 2013
2 parents 53c1508 + ee6cc6e commit b23f985
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 36 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,26 @@ form.append('my_file', fs.createReadStream(path.join(__dirname, 'doodle.png'))
form.append('remote_file', request('http://google.com/doodle.png'))
```
## HTTP Authentication
```javascript
request.auth('username', 'password', false).get('http://some.server.com/');
// or
request.get('http://some.server.com/', {
'auth': {
'user': 'username',
'pass': 'password',
'sendImmediately': false
}
});
```
If passed as an option, `auth` should be a hash containing values `user` || `username`, `password` || `pass`, and `sendImmediately` (optional). The method form takes parameters `auth(username, password, sendImmediately)`.
`sendImmediately` defaults to true, which will cause a basic authentication header to be sent. If `sendImmediately` is `false`, then `request` will retry with a proper authentication header after receiving a 401 response from the server (which must contain a `WWW-Authenticate` header indicating the required authentication method).
Digest authentication is supported, but it only works with `sendImmediately` set to `false` (otherwise `request` will send basic authentication on the initial request, which will probably cause the request to fail).
## OAuth Signing
```javascript
Expand Down Expand Up @@ -173,6 +193,7 @@ The first argument can be either a url or an options object. The only required o
* `headers` - http headers, defaults to {}
* `body` - entity body for POST and PUT requests. Must be buffer or string.
* `form` - when passed an object this will set `body` but to a querystring representation of value and adds `Content-type: application/x-www-form-urlencoded; charset=utf-8` header. When passed no option a FormData instance is returned that will be piped to request.
* `auth` - A hash containing values `user` || `username`, `password` || `pass`, and `sendImmediately` (optional). See documentation above.
* `json` - sets `body` but to JSON representation of value and adds `Content-type: application/json` header. Additionally, parses the response body as json.
* `multipart` - (experimental) array of objects which contains their own headers and `body` attribute. Sends `multipart/related` request. See example below.
* `followRedirect` - follow HTTP 3xx responses as redirects. defaults to true.
Expand Down
170 changes: 138 additions & 32 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ var http = require('http')
, util = require('util')
, stream = require('stream')
, qs = require('querystring')
, crypto = require('crypto')
, oauth = require('./oauth')
, uuid = require('./uuid')
, ForeverAgent = require('./forever')
Expand Down Expand Up @@ -48,6 +49,10 @@ function toBase64 (str) {
return (new Buffer(str || "", "ascii")).toString("base64")
}

function md5 (str) {
return crypto.createHash('md5').update(str).digest('hex')
}

// Hacky fix for pre-0.4.4 https
if (https && !https.Agent) {
https.Agent = function (options) {
Expand Down Expand Up @@ -109,9 +114,9 @@ Request.prototype.init = function (options) {
var self = this

if (!options) options = {}
if (process.env.NODE_DEBUG && /request/.test(process.env.NODE_DEBUG)) console.error('REQUEST', options)
if (request.debug) console.error('REQUEST', options)
if (!self.pool && self.pool !== false) self.pool = globalPool
self.dests = []
self.dests = self.dests || []
self.__isRequestRequest = true

// Protect against double callback
Expand All @@ -138,6 +143,7 @@ Request.prototype.init = function (options) {
} else {
if (typeof self.uri == "string") self.uri = url.parse(self.uri)
}

if (self.proxy) {
if (typeof self.proxy == 'string') self.proxy = url.parse(self.proxy)

Expand All @@ -148,7 +154,9 @@ Request.prototype.init = function (options) {

var tunnelOptions = { proxy: { host: self.proxy.hostname
, port: +self.proxy.port
, proxyAuth: self.proxy.auth }
, proxyAuth: self.proxy.auth
, headers: { Host: self.uri.hostname + ':' +
(self.uri.port || self.uri.protocol === 'https:' ? 443 : 80) }}
, ca: this.ca }

self.agent = tunnelFn(tunnelOptions)
Expand Down Expand Up @@ -240,6 +248,17 @@ Request.prototype.init = function (options) {
if (options.form) {
self.form(options.form)
}

if (options.qs) self.qs(options.qs)

if (self.uri.path) {
self.path = self.uri.path
} else {
self.path = self.uri.pathname + (self.uri.search || "")
}

if (self.path.length === 0) self.path = '/'


if (options.oauth) {
self.oauth(options.oauth)
Expand All @@ -249,23 +268,22 @@ Request.prototype.init = function (options) {
self.aws(options.aws)
}

if (options.auth) {
self.auth(
options.auth.user || options.auth.username,
options.auth.pass || options.auth.password,
options.auth.sendImmediately)
}

if (self.uri.auth && !self.headers.authorization) {
self.headers.authorization = "Basic " + toBase64(self.uri.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
var authPieces = self.uri.auth.split(':').map(function(item){ return qs.unescape(item) })
self.auth(authPieces[0], authPieces[1], true)
}
if (self.proxy && self.proxy.auth && !self.headers['proxy-authorization'] && !self.tunnel) {
self.headers['proxy-authorization'] = "Basic " + toBase64(self.proxy.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
}

if (options.qs) self.qs(options.qs)

if (self.uri.path) {
self.path = self.uri.path
} else {
self.path = self.uri.pathname + (self.uri.search || "")
}

if (self.path.length === 0) self.path = '/'


if (self.proxy && !self.tunnel) self.path = (self.uri.protocol + '//' + self.uri.host + self.path)

if (options.json) {
Expand Down Expand Up @@ -403,7 +421,7 @@ Request.prototype._updateProtocol = function () {
var tunnelFn = self.proxy.protocol === 'http:'
? tunnel.httpsOverHttp : tunnel.httpsOverHttps
var tunnelOptions = { proxy: { host: self.proxy.hostname
, post: +self.proxy.port
, port: +self.proxy.port
, proxyAuth: self.proxy.auth }
, ca: self.ca }
self.agent = tunnelFn(tunnelOptions)
Expand Down Expand Up @@ -516,7 +534,13 @@ Request.prototype.start = function () {
if (self._aws) {
self.aws(self._aws, true)
}
self.req = self.httpModule.request(self, function (response) {

// We have a method named auth, which is completely different from the http.request
// auth option. If we don't remove it, we're gonna have a bad time.
var reqOptions = copy(self)
delete reqOptions.auth

self.req = self.httpModule.request(reqOptions, function (response) {
if (response.connection.listeners('error').indexOf(self._parserErrorHandler) === -1) {
response.connection.once('error', self._parserErrorHandler)
}
Expand Down Expand Up @@ -551,22 +575,86 @@ Request.prototype.start = function () {
else addCookie(response.headers['set-cookie'])
}

if (response.statusCode >= 300 && response.statusCode < 400 &&
(self.followAllRedirects ||
(self.followRedirect && (self.method !== 'PUT' && self.method !== 'POST' && self.method !== 'DELETE'))) &&
response.headers.location) {
var redirectTo = null
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
if (self.followAllRedirects) {
redirectTo = response.headers.location
} else if (self.followRedirect) {
switch (self.method) {
case 'PUT':
case 'POST':
case 'DELETE':
// Do not follow redirects
break
default:
redirectTo = response.headers.location
break
}
}
} else if (response.statusCode == 401 && self._hasAuth && !self._sentAuth) {
var authHeader = response.headers['www-authenticate']
var authVerb = authHeader && authHeader.split(' ')[0]
switch (authVerb) {
case 'Basic':
self.auth(self._user, self._pass, true)
redirectTo = self.uri
break

case 'Digest':
// TODO: More complete implementation of RFC 2617. For reference:
// http://tools.ietf.org/html/rfc2617#section-3
// https://github.com/bagder/curl/blob/master/lib/http_digest.c

var matches = authHeader.match(/([a-z0-9_-]+)="([^"]+)"/gi)
var challenge = {}

for (var i = 0; i < matches.length; i++) {
var eqPos = matches[i].indexOf('=')
var key = matches[i].substring(0, eqPos)
var quotedValue = matches[i].substring(eqPos + 1)
challenge[key] = quotedValue.substring(1, quotedValue.length - 1)
}

var ha1 = md5(self._user + ':' + challenge.realm + ':' + self._pass)
var ha2 = md5(self.method + ':' + self.uri.path)
var digestResponse = md5(ha1 + ':' + challenge.nonce + ':1::auth:' + ha2)
var authValues = {
username: self._user,
realm: challenge.realm,
nonce: challenge.nonce,
uri: self.uri.path,
qop: challenge.qop,
response: digestResponse,
nc: 1,
cnonce: ''
}

authHeader = []
for (var k in authValues) {
authHeader.push(k + '="' + authValues[k] + '"')
}
authHeader = 'Digest ' + authHeader.join(', ')
self.setHeader('authorization', authHeader)
self._sentAuth = true

redirectTo = self.uri
break
}
}

if (redirectTo) {
if (self._redirectsFollowed >= self.maxRedirects) {
self.emit('error', new Error("Exceeded maxRedirects. Probably stuck in a redirect loop "+self.uri.href))
return
}
self._redirectsFollowed += 1

if (!isUrl.test(response.headers.location)) {
response.headers.location = url.resolve(self.uri.href, response.headers.location)
if (!isUrl.test(redirectTo)) {
redirectTo = url.resolve(self.uri.href, redirectTo)
}

var uriPrev = self.uri
self.uri = url.parse(response.headers.location)
self.uri = url.parse(redirectTo)

// handle the case where we change protocol from https to http or vice versa
if (self.uri.protocol !== uriPrev.protocol) {
Expand All @@ -575,23 +663,25 @@ Request.prototype.start = function () {

self.redirects.push(
{ statusCode : response.statusCode
, redirectUri: response.headers.location
, redirectUri: redirectTo
}
)
if (self.followAllRedirects) self.method = 'GET'
if (self.followAllRedirects && response.statusCode != 401) self.method = 'GET'
// self.method = 'GET' // Force all redirects to use GET || commented out fixes #215
delete self.src
delete self.req
delete self.agent
delete self._started
delete self.body
delete self._form
if (response.statusCode != 401) {
delete self.body
delete self._form
}
if (self.headers) {
delete self.headers.host
delete self.headers['content-type']
delete self.headers['content-length']
}
if (log) log('Redirect to %uri', self)
if (log) log('Redirect to %uri due to status %status', {uri: self.uri, status: response.statusCode})
self.init()
return // Ignore the rest of the response
} else {
Expand Down Expand Up @@ -817,6 +907,19 @@ function getHeader(name, headers) {
})
return result
}
Request.prototype.auth = function (user, pass, sendImmediately) {
if (typeof user !== 'string' || typeof pass !== 'string') {
throw new Error('auth() received invalid user or password')
}
this._user = user
this._pass = pass
this._hasAuth = true
if (sendImmediately || typeof sendImmediately == 'undefined') {
this.setHeader('authorization', 'Basic ' + toBase64(user + ':' + pass))
this._sentAuth = true
}
return this
}
Request.prototype.aws = function (opts, now) {
if (!now) {
this._aws = opts
Expand Down Expand Up @@ -873,7 +976,8 @@ Request.prototype.oauth = function (_oauth) {
delete oa.oauth_consumer_secret
var token_secret = oa.oauth_token_secret
delete oa.oauth_token_secret

var timestamp = oa.oauth_timestamp

var baseurl = this.uri.protocol + '//' + this.uri.host + this.uri.pathname
var signature = oauth.hmacsign(this.method, baseurl, oa, consumer_secret, token_secret)

Expand All @@ -886,7 +990,8 @@ Request.prototype.oauth = function (_oauth) {
if (i !== 'x_auth_mode') delete oa[i]
}
}
this.headers.Authorization =
oa.oauth_timestamp = timestamp
this.headers.Authorization =
'OAuth '+Object.keys(oa).sort().map(function (i) {return i+'="'+oauth.rfc3986(oa[i])+'"'}).join(',')
this.headers.Authorization += ',oauth_signature="' + oauth.rfc3986(signature) + '"'
return this
Expand Down Expand Up @@ -998,6 +1103,8 @@ function request (uri, options, callback) {

module.exports = request

request.debug = process.env.NODE_DEBUG && /request/.test(process.env.NODE_DEBUG)

request.initParams = initParams

request.defaults = function (options, requester) {
Expand Down Expand Up @@ -1116,8 +1223,7 @@ function getSafe (self, uuid) {
}

function toJSON () {
return getSafe(this, (((1+Math.random())*0x10000)|0).toString(16))
return getSafe(this, '__' + (((1+Math.random())*0x10000)|0).toString(16))
}

Request.prototype.toJSON = toJSON

20 changes: 17 additions & 3 deletions tests/run.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
var spawn = require('child_process').spawn
, exitCode = 0
, timeout = 10000
;

var tests = [
'test-body.js'
'test-basic-auth.js'
, 'test-body.js'
, 'test-cookie.js'
, 'test-cookiejar.js'
, 'test-defaults.js'
, 'test-digest-auth.js'
, 'test-errors.js'
, 'test-form.js'
, 'test-follow-all-303.js'
Expand Down Expand Up @@ -35,11 +38,22 @@ var next = function () {
var file = tests.shift()
console.log(file)
var proc = spawn('node', [ 'tests/' + file ])

var killed = false
var t = setTimeout(function () {
proc.kill()
exitCode += 1
console.error(file + ' timeout')
killed = true
}, timeout)

proc.stdout.pipe(process.stdout)
proc.stderr.pipe(process.stderr)
proc.on('exit', function (code) {
exitCode += code || 0
next()
if (code && !killed) console.error(file + ' failed')
exitCode += code || 0
clearTimeout(t)
next()
})
}
next()
Loading

0 comments on commit b23f985

Please sign in to comment.