[Pkg-javascript-commits] [SCM] simplified HTTP request client for NodeJS branch, master, updated. debian/2.9.3-1-4-gf7038bf

Jérémy Lal kapouer at melix.org
Thu Mar 8 07:41:40 UTC 2012


The following commit has been merged in the master branch:
commit 8eb43bfef8a910336bda052baa36de1e351ecc54
Author: Jérémy Lal <kapouer at melix.org>
Date:   Thu Mar 8 08:35:40 2012 +0100

    Imported Upstream version 2.9.153

diff --git a/README.md b/README.md
index 4c8b7aa..e5839b5 100644
--- a/README.md
+++ b/README.md
@@ -38,7 +38,7 @@ request('http://google.com/doodle.png').pipe(fs.createWriteStream('doodle.png'))
 You can also stream a file to a PUT or POST request. This method will also check the file extension against a mapping of file extensions to content-types, in this case `application/json`, and use the proper content-type in the PUT request if one is not already provided in the headers.
 
 ```javascript
-fs.readStream('file.json').pipe(request.put('http://mysite.com/obj.json'))
+fs.createReadStream('file.json').pipe(request.put('http://mysite.com/obj.json'))
 ```
 
 Request can also pipe to itself. When doing so the content-type and content-length will be preserved in the PUT headers.
@@ -146,12 +146,15 @@ request.post({url:url, oauth:oauth}, function (e, r, body) {
 The first argument can be either a url or an options object. The only required option is uri, all others are optional.
 
 * `uri` || `url` - fully qualified uri or a parsed url object from url.parse()
+* `qs` - object containing querystring values to be appended to the uri
 * `method` - http method, defaults to GET
 * `headers` - http headers, defaults to {}
 * `body` - entity body for POST and PUT requests. Must be buffer or string.
+* `form` - sets `body` but to querystring representation of value and adds `Content-type: application/x-www-form-urlencoded; charset=utf-8` header.
 * `json` - sets `body` but to JSON representation of value and adds `Content-type: application/json` header.
 * `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.
+* `followAllRedirects` - follow non-GET HTTP 3xx responses as redirects. defaults to false.
 * `maxRedirects` - the maximum number of redirects to follow, defaults to 10.
 * `onResponse` - If true the callback will be fired on the "response" event instead of "end". If a function it will be called on "response" and not effect the regular semantics of the main callback on "end".
 * `encoding` - Encoding to be used on `setEncoding` of response data. If set to `null`, the body is returned as a Buffer.
diff --git a/forever.js b/forever.js
new file mode 100644
index 0000000..ac853c0
--- /dev/null
+++ b/forever.js
@@ -0,0 +1,103 @@
+module.exports = ForeverAgent
+ForeverAgent.SSL = ForeverAgentSSL
+
+var util = require('util')
+  , Agent = require('http').Agent
+  , net = require('net')
+  , tls = require('tls')
+  , AgentSSL = require('https').Agent
+
+function ForeverAgent(options) {
+  var self = this
+  self.options = options || {}
+  self.requests = {}
+  self.sockets = {}
+  self.freeSockets = {}
+  self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets
+  self.minSockets = self.options.minSockets || ForeverAgent.defaultMinSockets
+  self.on('free', function(socket, host, port) {
+    var name = host + ':' + port
+    if (self.requests[name] && self.requests[name].length) {
+      self.requests[name].shift().onSocket(socket)
+    } else if (self.sockets[name].length < self.minSockets) {
+      if (!self.freeSockets[name]) self.freeSockets[name] = []
+      self.freeSockets[name].push(socket)
+      
+      // if an error happens while we don't use the socket anyway, meh, throw the socket away
+      function onIdleError() {
+        socket.destroy()
+      }
+      socket._onIdleError = onIdleError
+      socket.on('error', onIdleError)
+    } else {
+      // If there are no pending requests just destroy the
+      // socket and it will get removed from the pool. This
+      // gets us out of timeout issues and allows us to
+      // default to Connection:keep-alive.
+      socket.destroy();
+    }
+  })
+
+}
+util.inherits(ForeverAgent, Agent)
+
+ForeverAgent.defaultMinSockets = 5
+
+
+ForeverAgent.prototype.createConnection = net.createConnection
+ForeverAgent.prototype.addRequestNoreuse = Agent.prototype.addRequest
+ForeverAgent.prototype.addRequest = function(req, host, port) {
+  var name = host + ':' + port
+  if (this.freeSockets[name] && this.freeSockets[name].length > 0 && !req.useChunkedEncodingByDefault) {
+    var idleSocket = this.freeSockets[name].pop()
+    idleSocket.removeListener('error', idleSocket._onIdleError)
+    delete idleSocket._onIdleError
+    req._reusedSocket = true
+    req.onSocket(idleSocket)
+  } else {
+    this.addRequestNoreuse(req, host, port)
+  }
+}
+
+ForeverAgent.prototype.removeSocket = function(s, name, host, port) {
+  if (this.sockets[name]) {
+    var index = this.sockets[name].indexOf(s);
+    if (index !== -1) {
+      this.sockets[name].splice(index, 1);
+    }
+  } else if (this.sockets[name] && this.sockets[name].length === 0) {
+    // don't leak
+    delete this.sockets[name];
+    delete this.requests[name];
+  }
+  
+  if (this.freeSockets[name]) {
+    var index = this.freeSockets[name].indexOf(s)
+    if (index !== -1) {
+      this.freeSockets[name].splice(index, 1)
+      if (this.freeSockets[name].length === 0) {
+        delete this.freeSockets[name]
+      }
+    }
+  }
+
+  if (this.requests[name] && this.requests[name].length) {
+    // If we have pending requests and a socket gets closed a new one
+    // needs to be created to take over in the pool for the one that closed.
+    this.createSocket(name, host, port).emit('free');
+  }
+}
+
+function ForeverAgentSSL (options) {
+  ForeverAgent.call(this, options)
+}
+util.inherits(ForeverAgentSSL, ForeverAgent)
+
+ForeverAgentSSL.prototype.createConnection = createConnectionSSL
+ForeverAgentSSL.prototype.addRequestNoreuse = AgentSSL.prototype.addRequest
+
+function createConnectionSSL (port, host, options) {
+  options.port = port
+  options.host = host
+  return tls.connect(options)
+}
diff --git a/main.js b/main.js
index 8adc767..f651202 100644
--- a/main.js
+++ b/main.js
@@ -1,4 +1,4 @@
-// Copyright 2010-2011 Mikeal Rogers
+// Copyright 2010-2012 Mikeal Rogers
 //
 //    Licensed under the Apache License, Version 2.0 (the "License");
 //    you may not use this file except in compliance with the License.
@@ -22,9 +22,11 @@ var http = require('http')
   , mimetypes = require('./mimetypes')
   , oauth = require('./oauth')
   , uuid = require('./uuid')
+  , ForeverAgent = require('./forever')
   , Cookie = require('./vendor/cookie')
   , CookieJar = require('./vendor/cookie/jar')
   , cookieJar = new CookieJar
+  , tunnel = require('./tunnel')
   ;
   
 if (process.logging) {
@@ -66,7 +68,9 @@ function isReadStream (rs) {
 
 function copy (obj) {
   var o = {}
-  for (var i in obj) o[i] = obj[i]
+  Object.keys(obj).forEach(function (i) {
+    o[i] = obj[i]
+  })
   return o
 }
 
@@ -82,24 +86,31 @@ function Request (options) {
   if (typeof options === 'string') {
     options = {uri:options}
   }
-
+  
+  var reserved = Object.keys(Request.prototype)
   for (var i in options) {
-    this[i] = options[i]
+    if (reserved.indexOf(i) === -1) {
+      this[i] = options[i]
+    } else {
+      if (typeof options[i] === 'function') {
+        delete options[i]
+      }
+    }
   }
-  if (!this.pool) this.pool = globalPool
-  this.dests = []
-  this.__isRequestRequest = true
+  options = copy(options)
+  
+  this.init(options)
 }
 util.inherits(Request, stream.Stream)
-Request.prototype.getAgent = function (host, port) {
-  if (!this.pool[host+':'+port]) {
-    this.pool[host+':'+port] = new this.httpModule.Agent({host:host, port:port})
-  }
-  return this.pool[host+':'+port]
-}
-Request.prototype.request = function () {
+Request.prototype.init = function (options) {
   var self = this
-
+  
+  if (!options) options = {}
+  
+  if (!self.pool) self.pool = globalPool
+  self.dests = []
+  self.__isRequestRequest = true
+  
   // Protect against double callback
   if (!self._callback && self.callback) {
     self._callback = self.callback
@@ -123,17 +134,32 @@ Request.prototype.request = function () {
   }
   if (self.proxy) {
     if (typeof self.proxy == 'string') self.proxy = url.parse(self.proxy)
+
+    // do the HTTP CONNECT dance using koichik/node-tunnel
+    if (http.globalAgent && self.uri.protocol === "https:") {
+      self.tunnel = true
+      var tunnelFn = self.proxy.protocol === "http:"
+                   ? tunnel.httpsOverHttp : tunnel.httpsOverHttps
+
+      var tunnelOptions = { proxy: { host: self.proxy.hostname
+                                   , port: +self.proxy.port }
+                          , ca: this.ca }
+
+      self.agent = tunnelFn(tunnelOptions)
+      self.tunnel = true
+    }
   }
 
   self._redirectsFollowed = self._redirectsFollowed || 0
   self.maxRedirects = (self.maxRedirects !== undefined) ? self.maxRedirects : 10
   self.followRedirect = (self.followRedirect !== undefined) ? self.followRedirect : true
-  if (self.followRedirect)
+  self.followAllRedirects = (self.followAllRedirects !== undefined) ? self.followAllRedirects : false;
+  if (self.followRedirect || self.followAllRedirects)
     self.redirects = self.redirects || []
 
   self.headers = self.headers ? copy(self.headers) : {}
 
-  var setHost = false
+  self.setHost = false
   if (!self.headers.host) {
     self.headers.host = self.uri.hostname
     if (self.uri.port) {
@@ -141,27 +167,10 @@ Request.prototype.request = function () {
            !(self.uri.port === 443 && self.uri.protocol === 'https:') )
       self.headers.host += (':'+self.uri.port)
     }
-    setHost = true
-  }
-
-  if (self.jar === false) {
-    // disable cookies
-    var cookies = false;
-    self._disableCookies = true;
-  } else if (self.jar) {
-    // fetch cookie from the user defined cookie jar
-    var cookies = self.jar.get({ url: self.uri.href })
-  } else {
-    // fetch cookie from the global cookie jar
-    var cookies = cookieJar.get({ url: self.uri.href })
-  }
-  if (cookies) {
-    var cookieString = cookies.map(function (c) {
-      return c.name + "=" + c.value;
-    }).join("; ");
-    
-    self.headers.Cookie = cookieString;
+    self.setHost = true
   }
+  
+  self.jar(options.jar)
 
   if (!self.uri.pathname) {self.uri.pathname = '/'}
   if (!self.uri.port) {
@@ -169,7 +178,7 @@ Request.prototype.request = function () {
     else if (self.uri.protocol == 'https:') {self.uri.port = 443}
   }
 
-  if (self.proxy) {
+  if (self.proxy && !self.tunnel) {
     self.port = self.proxy.port
     self.host = self.proxy.hostname
   } else {
@@ -182,68 +191,43 @@ Request.prototype.request = function () {
     delete self.callback
   }
 
-  var clientErrorHandler = function (error) {
-    if (setHost) delete self.headers.host
-    if (self.timeout && self.timeoutTimer) clearTimeout(self.timeoutTimer)
+  self.clientErrorHandler = function (error) {
+    if (self._aborted) return
+    
+    if (self.setHost) delete self.headers.host
+    if (self.req._reusedSocket && error.code === 'ECONNRESET'
+        && self.agent.addRequestNoreuse) {
+      self.agent = { addRequest: self.agent.addRequestNoreuse.bind(self.agent) }
+      self.start()
+      self.req.end()
+      return
+    }
+    if (self.timeout && self.timeoutTimer) {
+        clearTimeout(self.timeoutTimer);
+        self.timeoutTimer = null;
+    }
     self.emit('error', error)
   }
   if (self.onResponse) self.on('error', function (e) {self.onResponse(e)})
   if (self.callback) self.on('error', function (e) {self.callback(e)})
 
-  if (self.form) {
-    self.headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
-    self.body = qs.stringify(self.form).toString('utf8')
-  }
-
-  if (self.oauth) {
-    var form
-    if (self.headers['content-type'] && 
-        self.headers['content-type'].slice(0, 'application/x-www-form-urlencoded'.length) ===
-          'application/x-www-form-urlencoded' 
-       ) {
-      form = qs.parse(self.body)
-    } 
-    if (self.uri.query) {
-      form = qs.parse(self.uri.query)
-    } 
-    if (!form) form = {}
-    var oa = {}
-    for (var i in form) oa[i] = form[i]
-    for (var i in self.oauth) oa['oauth_'+i] = self.oauth[i]
-    if (!oa.oauth_version) oa.oauth_version = '1.0'
-    if (!oa.oauth_timestamp) oa.oauth_timestamp = Math.floor( (new Date()).getTime() / 1000 ).toString()
-    if (!oa.oauth_nonce) oa.oauth_nonce = uuid().replace(/-/g, '')
-    
-    oa.oauth_signature_method = 'HMAC-SHA1'
-    
-    var consumer_secret = oa.oauth_consumer_secret
-    delete oa.oauth_consumer_secret
-    var token_secret = oa.oauth_token_secret
-    delete oa.oauth_token_secret
-    
-    var baseurl = self.uri.protocol + '//' + self.uri.host + self.uri.pathname
-    var signature = oauth.hmacsign(self.method, baseurl, oa, consumer_secret, token_secret)
-    
-    // oa.oauth_signature = signature
-    for (var i in form) {
-      if ( i.slice(0, 'oauth_') in self.oauth) {
-        // skip 
-      } else {
-        delete oa['oauth_'+i]
-      }
-    }
-    self.headers.authorization = 
-      'OAuth '+Object.keys(oa).sort().map(function (i) {return i+'="'+oauth.rfc3986(oa[i])+'"'}).join(',')
-    self.headers.authorization += ',oauth_signature="'+oauth.rfc3986(signature)+'"'  
+  if (options.form) {
+    self.form(options.form)
+  }
+
+  if (options.oauth) {
+    self.oauth(options.oauth)
   }
 
   if (self.uri.auth && !self.headers.authorization) {
     self.headers.authorization = "Basic " + toBase64(self.uri.auth.split(':').map(function(item){ return qs.unescape(item)}).join(':'))
   }
-  if (self.proxy && self.proxy.auth && !self.headers['proxy-authorization']) {
+  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 {
@@ -252,35 +236,12 @@ Request.prototype.request = function () {
 
   if (self.path.length === 0) self.path = '/'
 
-  if (self.proxy) self.path = (self.uri.protocol + '//' + self.uri.host + self.path)
-
-  if (self.json) {
-    self.headers['content-type'] = 'application/json'
-    if (typeof self.json === 'boolean') {
-      if (typeof self.body === 'object') self.body = JSON.stringify(self.body)
-    } else {
-      self.body = JSON.stringify(self.json)
-    }
+  if (self.proxy && !self.tunnel) self.path = (self.uri.protocol + '//' + self.uri.host + self.path)
 
-  } else if (self.multipart) {
-    self.body = []
-    self.headers['content-type'] = 'multipart/related;boundary="frontier"'
-    if (!self.multipart.forEach) throw new Error('Argument error, options.multipart.')
-
-    self.multipart.forEach(function (part) {
-      var body = part.body
-      if(!body) throw Error('Body attribute missing in multipart.')
-      delete part.body
-      var preamble = '--frontier\r\n'
-      Object.keys(part).forEach(function(key){
-        preamble += key + ': ' + part[key] + '\r\n'
-      })
-      preamble += '\r\n'
-      self.body.push(new Buffer(preamble))
-      self.body.push(new Buffer(body))
-      self.body.push(new Buffer('\r\n'))
-    })
-    self.body.push(new Buffer('--frontier--'))
+  if (options.json) {
+    self.json(options.json)
+  } else if (options.multipart) {
+    self.multipart(options.multipart)
   }
 
   if (self.body) {
@@ -304,7 +265,7 @@ Request.prototype.request = function () {
     }
   }
 
-  var protocol = self.proxy ? self.proxy.protocol : self.uri.protocol
+  var protocol = self.proxy && !self.tunnel ? self.proxy.protocol : self.uri.protocol
     , defaultModules = {'http:':http, 'https:':https}
     , httpModules = self.httpModules || {}
     ;
@@ -312,177 +273,34 @@ Request.prototype.request = function () {
 
   if (!self.httpModule) throw new Error("Invalid protocol")
 
+  if (options.ca) self.ca = options.ca
+
+  if (!self.agent) {
+    if (options.agentOptions) self.agentOptions = options.agentOptions
+
+    if (options.agentClass) {
+      self.agentClass = options.agentClass
+    } else if (options.forever) {
+      self.agentClass = protocol === 'http:' ? ForeverAgent : ForeverAgent.SSL
+    } else {
+      self.agentClass = self.httpModule.Agent
+    }
+  }
+
   if (self.pool === false) {
     self.agent = false
   } else {
+    self.agent = self.agent || self.getAgent()
     if (self.maxSockets) {
       // Don't use our pooling if node has the refactored client
-      self.agent = self.httpModule.globalAgent || self.getAgent(self.host, self.port)
       self.agent.maxSockets = self.maxSockets
     }
     if (self.pool.maxSockets) {
       // Don't use our pooling if node has the refactored client
-      self.agent = self.httpModule.globalAgent || self.getAgent(self.host, self.port)
       self.agent.maxSockets = self.pool.maxSockets
     }
   }
 
-  self.start = function () {
-    self._started = true
-    self.method = self.method || 'GET'
-    self.href = self.uri.href
-    if (log) log('%method %href', self)
-    self.req = self.httpModule.request(self, function (response) {
-      self.response = response
-      response.request = self
-
-      if (self.httpModule === https &&
-          self.strictSSL &&
-          !response.client.authorized) {
-        var sslErr = response.client.authorizationError
-        self.emit('error', new Error('SSL Error: '+ sslErr))
-        return
-      }
-
-      if (setHost) delete self.headers.host
-      if (self.timeout && self.timeoutTimer) clearTimeout(self.timeoutTimer)
-      
-      if (response.headers['set-cookie'] && (!self._disableCookies)) {
-        response.headers['set-cookie'].forEach(function(cookie) {
-          if (self.jar) self.jar.add(new Cookie(cookie))
-          else cookieJar.add(new Cookie(cookie))
-        })
-      }
-
-      if (response.statusCode >= 300 &&
-          response.statusCode < 400  &&
-          self.followRedirect     &&
-          self.method !== 'PUT' &&
-          self.method !== 'POST' &&
-          response.headers.location) {
-        if (self._redirectsFollowed >= self.maxRedirects) {
-          self.emit('error', new Error("Exceeded maxRedirects. Probably stuck in a redirect loop."))
-          return
-        }
-        self._redirectsFollowed += 1
-
-        if (!isUrl.test(response.headers.location)) {
-          response.headers.location = url.resolve(self.uri.href, response.headers.location)
-        }
-        self.uri = response.headers.location
-        self.redirects.push(
-          { statusCode : response.statusCode
-          , redirectUri: response.headers.location 
-          }
-        )
-        delete self.req
-        delete self.agent
-        delete self._started
-        if (self.headers) {
-          delete self.headers.host
-        }
-        if (log) log('Redirect to %uri', self)
-        request(self, self.callback)
-        return // Ignore the rest of the response
-      } else {
-        self._redirectsFollowed = self._redirectsFollowed || 0
-        // Be a good stream and emit end when the response is finished.
-        // Hack to emit end on close because of a core bug that never fires end
-        response.on('close', function () {
-          if (!self._ended) self.response.emit('end')
-        })
-
-        if (self.encoding) {
-          if (self.dests.length !== 0) {
-            console.error("Ingoring encoding parameter as this stream is being piped to another stream which makes the encoding option invalid.")
-          } else {
-            response.setEncoding(self.encoding)
-          }
-        }
-
-        self.pipeDest = function (dest) {
-          if (dest.headers) {
-            dest.headers['content-type'] = response.headers['content-type']
-            if (response.headers['content-length']) {
-              dest.headers['content-length'] = response.headers['content-length']
-            }
-          }
-          if (dest.setHeader) {
-            for (var i in response.headers) {
-              dest.setHeader(i, response.headers[i])
-            }
-            dest.statusCode = response.statusCode
-          }
-          if (self.pipefilter) self.pipefilter(response, dest)
-        }
-
-        self.dests.forEach(function (dest) {
-          self.pipeDest(dest)
-        })
-
-        response.on("data", function (chunk) {
-          self._destdata = true
-          self.emit("data", chunk)
-        })
-        response.on("end", function (chunk) {
-          self._ended = true
-          self.emit("end", chunk)
-        })
-        response.on("close", function () {self.emit("close")})
-
-        self.emit('response', response)
-
-        if (self.onResponse) {
-          self.onResponse(null, response)
-        }
-        if (self.callback) {
-          var buffer = []
-          var bodyLen = 0
-          self.on("data", function (chunk) {
-            buffer.push(chunk)
-            bodyLen += chunk.length
-          })
-          self.on("end", function () {
-            if (buffer.length && Buffer.isBuffer(buffer[0])) {
-              var body = new Buffer(bodyLen)
-              var i = 0
-              buffer.forEach(function (chunk) {
-                chunk.copy(body, i, 0, chunk.length)
-                i += chunk.length
-              })
-              if (self.encoding === null) {
-                response.body = body
-              } else {
-                response.body = body.toString()
-              }
-            } else if (buffer.length) {
-              response.body = buffer.join('')
-            }
-
-            if (self.json) {
-              try {
-                response.body = JSON.parse(response.body)
-              } catch (e) {}
-            }
-
-            self.callback(null, response, response.body)
-          })
-        }
-      }
-    })
-
-    if (self.timeout) {
-      self.timeoutTimer = setTimeout(function() {
-        self.req.abort()
-        var e = new Error("ETIMEDOUT")
-        e.code = "ETIMEDOUT"
-        self.emit("error", e)
-      }, self.timeout)
-    }
-    
-    self.req.on('error', clientErrorHandler)
-  }
-
   self.once('pipe', function (src) {
     if (self.ntick) throw new Error("You cannot pipe to this stream after the first nextTick() after creation of the request stream.")
     self.src = src
@@ -508,6 +326,8 @@ Request.prototype.request = function () {
   })
 
   process.nextTick(function () {
+    if (self._aborted) return
+    
     if (self.body) {
       if (Array.isArray(self.body)) {
         self.body.forEach(function(part) {
@@ -527,47 +347,457 @@ Request.prototype.request = function () {
     self.ntick = true
   })
 }
-Request.prototype.pipe = function (dest) {
+
+Request.prototype.getAgent = function () {
+  var Agent = this.agentClass
+  var options = {}
+  if (this.agentOptions) {
+    for (var i in this.agentOptions) {
+      options[i] = this.agentOptions[i]
+    }
+  }
+  if (this.ca) options.ca = this.ca
+
+  var poolKey = ''
+
+  // different types of agents are in different pools
+  if (Agent !== this.httpModule.Agent) {
+    poolKey += Agent.name
+  }
+
+  if (!this.httpModule.globalAgent) {
+    // node 0.4.x
+    options.host = this.host
+    options.port = this.port
+    if (poolKey) poolKey += ':'
+    poolKey += this.host + ':' + this.port
+  }
+
+  if (options.ca) {
+    if (poolKey) poolKey += ':'
+    poolKey += options.ca
+  }
+
+  if (!poolKey && Agent === this.httpModule.Agent && this.httpModule.globalAgent) {
+    // not doing anything special.  Use the globalAgent
+    return this.httpModule.globalAgent
+  }
+
+  // already generated an agent for this setting
+  if (this.pool[poolKey]) return this.pool[poolKey]
+
+  return this.pool[poolKey] = new Agent(options)
+}
+
+Request.prototype.start = function () {
+  var self = this
+  
+  if (self._aborted) return
+  
+  self._started = true
+  self.method = self.method || 'GET'
+  self.href = self.uri.href
+  if (log) log('%method %href', self)
+  self.req = self.httpModule.request(self, function (response) {
+    if (self._aborted) return
+    if (self._paused) response.pause()
+    
+    self.response = response
+    response.request = self
+
+    if (self.httpModule === https &&
+        self.strictSSL &&
+        !response.client.authorized) {
+      var sslErr = response.client.authorizationError
+      self.emit('error', new Error('SSL Error: '+ sslErr))
+      return
+    }
+
+    if (self.setHost) delete self.headers.host
+    if (self.timeout && self.timeoutTimer) {
+        clearTimeout(self.timeoutTimer);
+        self.timeoutTimer = null;
+    }  
+    
+    if (response.headers['set-cookie'] && (!self._disableCookies)) {
+      response.headers['set-cookie'].forEach(function(cookie) {
+        if (self._jar) self._jar.add(new Cookie(cookie))
+        else cookieJar.add(new Cookie(cookie))
+      })
+    }
+
+    if (response.statusCode >= 300 && response.statusCode < 400  &&
+        (self.followAllRedirects ||
+         (self.followRedirect && (self.method !== 'PUT' && self.method !== 'POST' && self.method !== 'DELETE'))) &&
+        response.headers.location) {
+      if (self._redirectsFollowed >= self.maxRedirects) {
+        self.emit('error', new Error("Exceeded maxRedirects. Probably stuck in a redirect loop."))
+        return
+      }
+      self._redirectsFollowed += 1
+
+      if (!isUrl.test(response.headers.location)) {
+        response.headers.location = url.resolve(self.uri.href, response.headers.location)
+      }
+      self.uri = response.headers.location
+      self.redirects.push(
+        { statusCode : response.statusCode
+        , redirectUri: response.headers.location 
+        }
+      )
+      self.method = 'GET'; // Force all redirects to use GET
+      delete self.req
+      delete self.agent
+      delete self._started
+      if (self.headers) {
+        delete self.headers.host
+      }
+      if (log) log('Redirect to %uri', self)
+      self.init()
+      return // Ignore the rest of the response
+    } else {
+      self._redirectsFollowed = self._redirectsFollowed || 0
+      // Be a good stream and emit end when the response is finished.
+      // Hack to emit end on close because of a core bug that never fires end
+      response.on('close', function () {
+        if (!self._ended) self.response.emit('end')
+      })
+
+      if (self.encoding) {
+        if (self.dests.length !== 0) {
+          console.error("Ingoring encoding parameter as this stream is being piped to another stream which makes the encoding option invalid.")
+        } else {
+          response.setEncoding(self.encoding)
+        }
+      }
+
+      self.dests.forEach(function (dest) {
+        self.pipeDest(dest)
+      })
+
+      response.on("data", function (chunk) {
+        self._destdata = true
+        self.emit("data", chunk)
+      })
+      response.on("end", function (chunk) {
+        self._ended = true
+        self.emit("end", chunk)
+      })
+      response.on("close", function () {self.emit("close")})
+
+      self.emit('response', response)
+
+      if (self.onResponse) {
+        self.onResponse(null, response)
+      }
+      if (self.callback) {
+        var buffer = []
+        var bodyLen = 0
+        self.on("data", function (chunk) {
+          buffer.push(chunk)
+          bodyLen += chunk.length
+        })
+        self.on("end", function () {
+          if (self._aborted) return
+          
+          if (buffer.length && Buffer.isBuffer(buffer[0])) {
+            var body = new Buffer(bodyLen)
+            var i = 0
+            buffer.forEach(function (chunk) {
+              chunk.copy(body, i, 0, chunk.length)
+              i += chunk.length
+            })
+            if (self.encoding === null) {
+              response.body = body
+            } else {
+              response.body = body.toString()
+            }
+          } else if (buffer.length) {
+            response.body = buffer.join('')
+          }
+
+          if (self._json) {
+            try {
+              response.body = JSON.parse(response.body)
+            } catch (e) {}
+          }
+
+          self.callback(null, response, response.body)
+        })
+      }
+    }
+  })
+
+  if (self.timeout && !self.timeoutTimer) {
+    self.timeoutTimer = setTimeout(function() {
+      self.req.abort()
+      var e = new Error("ETIMEDOUT")
+      e.code = "ETIMEDOUT"
+      self.emit("error", e)
+    }, self.timeout)
+    
+    // Set additional timeout on socket - in case if remote
+    // server freeze after sending headers
+    if (self.req.setTimeout) { // only works on node 0.6+
+      self.req.setTimeout(self.timeout, function(){
+          if (self.req) {
+              self.req.abort()
+              var e = new Error("ESOCKETTIMEDOUT")
+              e.code = "ESOCKETTIMEDOUT"
+              self.emit("error", e)
+          }
+      })
+    }
+  }
+  
+  self.req.on('error', self.clientErrorHandler)
+  
+  self.emit('request', self.req)
+}
+
+Request.prototype.abort = function() {
+  this._aborted = true;
+  
+  if (this.req) {
+    this.req.abort()
+  }
+  else if (this.response) {
+    this.response.abort()
+  }
+  
+  this.emit("abort")
+}
+
+Request.prototype.pipeDest = function (dest) {
+  var response = this.response
+  // Called after the response is received
+  if (dest.headers) {
+    dest.headers['content-type'] = response.headers['content-type']
+    if (response.headers['content-length']) {
+      dest.headers['content-length'] = response.headers['content-length']
+    }
+  }
+  if (dest.setHeader) {
+    for (var i in response.headers) {
+      dest.setHeader(i, response.headers[i])
+    }
+    dest.statusCode = response.statusCode
+  }
+  if (this.pipefilter) this.pipefilter(response, dest)
+}
+
+// Composable API
+Request.prototype.setHeader = function (name, value, clobber) {
+  if (clobber === undefined) clobber = true
+  if (clobber || !this.headers.hasOwnProperty(name)) this.headers[name] = value
+  else this.headers[name] += ',' + value
+  return this
+}
+Request.prototype.setHeaders = function (headers) {
+  for (i in headers) {this.setHeader(i, headers[i])}
+  return this
+}
+Request.prototype.qs = function (q, clobber) {
+  var uri = {
+  	protocol: this.uri.protocol,
+  	host: this.uri.host,
+  	pathname: this.uri.pathname,
+  	query: clobber ? q : qs.parse(this.uri.query),
+  	hash: this.uri.hash
+  };
+  if (!clobber) for (var i in q) uri.query[i] = q[i]
+
+  this.uri= url.parse(url.format(uri))
+
+  return this
+}
+Request.prototype.form = function (form) {
+  this.headers['content-type'] = 'application/x-www-form-urlencoded; charset=utf-8'
+  this.body = qs.stringify(form).toString('utf8')
+  return this
+}
+Request.prototype.multipart = function (multipart) {
+  var self = this
+  self.body = []
+
+  if (!self.headers['content-type']) {
+    self.headers['content-type'] = 'multipart/related;boundary="frontier"';
+  } else {
+    self.headers['content-type'] = self.headers['content-type'].split(';')[0] + ';boundary="frontier"';
+  }
+
+  if (!multipart.forEach) throw new Error('Argument error, options.multipart.')
+
+  multipart.forEach(function (part) {
+    var body = part.body
+    if(!body) throw Error('Body attribute missing in multipart.')
+    delete part.body
+    var preamble = '--frontier\r\n'
+    Object.keys(part).forEach(function(key){
+      preamble += key + ': ' + part[key] + '\r\n'
+    })
+    preamble += '\r\n'
+    self.body.push(new Buffer(preamble))
+    self.body.push(new Buffer(body))
+    self.body.push(new Buffer('\r\n'))
+  })
+  self.body.push(new Buffer('--frontier--'))
+  return self
+}
+Request.prototype.json = function (val) {
+  this.setHeader('content-type', 'application/json')
+  this.setHeader('accept', 'application/json')
+  this._json = true
+  if (typeof val === 'boolean') {
+    if (typeof this.body === 'object') this.body = JSON.stringify(this.body)
+  } else {
+    this.body = JSON.stringify(val)
+  }
+  return this
+}
+Request.prototype.oauth = function (_oauth) {
+  var form
+  if (this.headers['content-type'] && 
+      this.headers['content-type'].slice(0, 'application/x-www-form-urlencoded'.length) ===
+        'application/x-www-form-urlencoded' 
+     ) {
+    form = qs.parse(this.body)
+  }
+  if (this.uri.query) {
+    form = qs.parse(this.uri.query)
+  } 
+  if (!form) form = {}
+  var oa = {}
+  for (var i in form) oa[i] = form[i]
+  for (var i in _oauth) oa['oauth_'+i] = _oauth[i]
+  if (!oa.oauth_version) oa.oauth_version = '1.0'
+  if (!oa.oauth_timestamp) oa.oauth_timestamp = Math.floor( (new Date()).getTime() / 1000 ).toString()
+  if (!oa.oauth_nonce) oa.oauth_nonce = uuid().replace(/-/g, '')
+  
+  oa.oauth_signature_method = 'HMAC-SHA1'
+  
+  var consumer_secret = oa.oauth_consumer_secret
+  delete oa.oauth_consumer_secret
+  var token_secret = oa.oauth_token_secret
+  delete oa.oauth_token_secret
+  
+  var baseurl = this.uri.protocol + '//' + this.uri.host + this.uri.pathname
+  var signature = oauth.hmacsign(this.method, baseurl, oa, consumer_secret, token_secret)
+  
+  // oa.oauth_signature = signature
+  for (var i in form) {
+    if ( i.slice(0, 'oauth_') in _oauth) {
+      // skip 
+    } else {
+      delete oa['oauth_'+i]
+    }
+  }
+  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
+}
+Request.prototype.jar = function (jar) {
+  var cookies
+  
+  if (this._redirectsFollowed === 0) {
+    this.originalCookieHeader = this.headers.cookie
+  }
+  
+  if (jar === false) {
+    // disable cookies
+    cookies = false;
+    this._disableCookies = true;
+  } else if (jar) {
+    // fetch cookie from the user defined cookie jar
+    cookies = jar.get({ url: this.uri.href })
+  } else {
+    // fetch cookie from the global cookie jar
+    cookies = cookieJar.get({ url: this.uri.href })
+  }
+  
+  if (cookies && cookies.length) {
+    var cookieString = cookies.map(function (c) {
+      return c.name + "=" + c.value
+    }).join("; ")
+
+    if (this.originalCookieHeader) {
+      // Don't overwrite existing Cookie header
+      this.headers.cookie = this.originalCookieHeader + '; ' + cookieString
+    } else {
+      this.headers.cookie = cookieString
+    }
+  }
+  this._jar = jar
+  return this
+}
+
+
+// Stream API
+Request.prototype.pipe = function (dest, opts) {
   if (this.response) {
     if (this._destdata) {
       throw new Error("You cannot pipe after data has been emitted from the response.")
     } else if (this._ended) {
       throw new Error("You cannot pipe after the response has been ended.")
     } else {
-      stream.Stream.prototype.pipe.call(this, dest)
+      stream.Stream.prototype.pipe.call(this, dest, opts)
       this.pipeDest(dest)
       return dest
     }
   } else {
     this.dests.push(dest)
-    stream.Stream.prototype.pipe.call(this, dest)
+    stream.Stream.prototype.pipe.call(this, dest, opts)
     return dest
   }
 }
 Request.prototype.write = function () {
   if (!this._started) this.start()
-  if (!this.req) throw new Error("This request has been piped before http.request() was called.")
   this.req.write.apply(this.req, arguments)
 }
-Request.prototype.end = function () {
+Request.prototype.end = function (chunk) {
+  if (chunk) this.write(chunk)
   if (!this._started) this.start()
-  if (!this.req) throw new Error("This request has been piped before http.request() was called.")
-  this.req.end.apply(this.req, arguments)
+  this.req.end()
 }
 Request.prototype.pause = function () {
-  if (!this.response) throw new Error("This request has been piped before http.request() was called.")
-  this.response.pause.apply(this.response, arguments)
+  if (!this.response) this._paused = true
+  else this.response.pause.apply(this.response, arguments)
 }
 Request.prototype.resume = function () {
-  if (!this.response) throw new Error("This request has been piped before http.request() was called.")
-  this.response.resume.apply(this.response, arguments)
+  if (!this.response) this._paused = false
+  else this.response.resume.apply(this.response, arguments)
 }
+Request.prototype.destroy = function () {
+  if (!this._ended) this.end()
+}
+
+// organize params for post, put, head, del
+function initParams(uri, options, callback) {
+  if ((typeof options === 'function') && !callback) callback = options;
+  if (typeof options === 'object') {
+    options.uri = uri;
+  } else if (typeof uri === 'string') {
+    options = {uri:uri};
+  } else {
+    options = uri;
+    uri = options.uri;
+  }
+  return { uri: uri, options: options, callback: callback };
+}
+
+function request (uri, options, callback) {
+  if ((typeof options === 'function') && !callback) callback = options;
+  if (typeof options === 'object') {
+    options.uri = uri;
+  } else if (typeof uri === 'string') {
+    options = {uri:uri};
+  } else {
+    options = uri;
+  }
 
-function request (options, callback) {
-  if (typeof options === 'string') options = {uri:options}
-  if (callback) options.callback = callback
+  if (callback) options.callback = callback;
   var r = new Request(options)
-  r.request()
   return r
 }
 
@@ -575,12 +805,12 @@ module.exports = request
 
 request.defaults = function (options) {
   var def = function (method) {
-    var d = function (opts, callback) {
-      if (typeof opts === 'string') opts = {uri:opts}
+    var d = function (uri, opts, callback) {
+      var params = initParams(uri, opts, callback);
       for (var i in options) {
-        if (opts[i] === undefined) opts[i] = options[i]
+        if (params.options[i] === undefined) params.options[i] = options[i]
       }
-      return method(opts, callback)
+      return method(params.uri, params.options, params.callback)
     }
     return d
   }
@@ -595,34 +825,50 @@ request.defaults = function (options) {
   return de
 }
 
+request.forever = function (agentOptions, optionsArg) {
+  var options = {}
+  if (optionsArg) {
+    for (option in optionsArg) {
+      options[option] = optionsArg[option]
+    }
+  }
+  if (agentOptions) options.agentOptions = agentOptions
+  options.forever = true
+  return request.defaults(options)
+}
+
 request.get = request
-request.post = function (options, callback) {
-  if (typeof options === 'string') options = {uri:options}
-  options.method = 'POST'
-  return request(options, callback)
-}
-request.put = function (options, callback) {
-  if (typeof options === 'string') options = {uri:options}
-  options.method = 'PUT'
-  return request(options, callback)
-}
-request.head = function (options, callback) {
-  if (typeof options === 'string') options = {uri:options}
-  options.method = 'HEAD'
-  if (options.body || options.requestBodyStream || options.json || options.multipart) {
+request.post = function (uri, options, callback) {
+  var params = initParams(uri, options, callback);
+  params.options.method = 'POST';
+  return request(params.uri, params.options, params.callback)
+}
+request.put = function (uri, options, callback) {
+  var params = initParams(uri, options, callback);
+  params.options.method = 'PUT'
+  return request(params.uri, params.options, params.callback)
+}
+request.head = function (uri, options, callback) {
+  var params = initParams(uri, options, callback);
+  params.options.method = 'HEAD'
+  if (params.options.body || 
+      params.options.requestBodyStream || 
+      (params.options.json && typeof params.options.json !== 'boolean') || 
+      params.options.multipart) {
     throw new Error("HTTP HEAD requests MUST NOT include a request body.")
   }
-  return request(options, callback)
+  return request(params.uri, params.options, params.callback)
 }
-request.del = function (options, callback) {
-  if (typeof options === 'string') options = {uri:options}
-  options.method = 'DELETE'
-  return request(options, callback)
+request.del = function (uri, options, callback) {
+  var params = initParams(uri, options, callback);
+  params.options.method = 'DELETE'
+  return request(params.uri, params.options, params.callback)
 }
 request.jar = function () {
   return new CookieJar
 }
 request.cookie = function (str) {
+  if (str && str.uri) str = str.uri
   if (typeof str !== 'string') throw new Error("The cookie function only accepts STRING as param")
   return new Cookie(str)
 }
diff --git a/mimetypes.js b/mimetypes.js
index 8691006..59b21b4 100644
--- a/mimetypes.js
+++ b/mimetypes.js
@@ -1,5 +1,6 @@
 // from http://github.com/felixge/node-paperboy
 exports.types = {
+  "3gp":"video/3gpp",
   "aiff":"audio/x-aiff",
   "arj":"application/x-arj-compressed",
   "asf":"video/x-ms-asf",
@@ -50,17 +51,21 @@ exports.types = {
   "lzh":"application/octet-stream",
   "m":"text/plain",
   "m3u":"audio/x-mpegurl",
+  "m4v":"video/mp4",
   "man":"application/x-troff-man",
   "me":"application/x-troff-me",
   "midi":"audio/midi",
   "mif":"application/x-mif",
   "mime":"www/mime",
+  "mkv":"  video/x-matrosk",
   "movie":"video/x-sgi-movie",
-  "mustache":"text/plain",
   "mp4":"video/mp4",
+  "mp41":"video/mp4",
+  "mp42":"video/mp4",
   "mpg":"video/mpeg",
   "mpga":"audio/mpeg",
   "ms":"application/x-troff-ms",
+  "mustache":"text/plain",
   "nc":"application/x-netcdf",
   "oda":"application/oda",
   "ogm":"application/ogg",
@@ -123,6 +128,7 @@ exports.types = {
   "vrm":"x-world/x-vrml",
   "wav":"audio/x-wav",
   "wax":"audio/x-ms-wax",
+  "webm":"video/webm",
   "wma":"audio/x-ms-wma",
   "wmv":"video/x-ms-wmv",
   "wmx":"video/x-ms-wmx",
@@ -134,7 +140,7 @@ exports.types = {
   "xpm":"image/x-xpixmap",
   "xwd":"image/x-xwindowdump",
   "xyz":"chemical/x-pdb",
-  "zip":"application/zip",
+  "zip":"application/zip"
 };
 
 exports.lookup = function(ext, defaultType) {
diff --git a/oauth.js b/oauth.js
index 25db669..31b9dc6 100644
--- a/oauth.js
+++ b/oauth.js
@@ -19,7 +19,7 @@ function rfc3986 (str) {
 function hmacsign (httpMethod, base_uri, params, consumer_secret, token_secret, body) {
   // adapted from https://dev.twitter.com/docs/auth/oauth
   var base = 
-    httpMethod + "&" +
+    (httpMethod || 'GET') + "&" +
     encodeURIComponent(  base_uri ) + "&" +
     Object.keys(params).sort().map(function (i) {
       // big WTF here with the escape + encoding but it's what twitter wants
diff --git a/package.json b/package.json
index e7b899a..a4c646f 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 { "name" : "request"
 , "description" : "Simplified HTTP request client."
 , "tags" : ["http", "simple", "util", "utility"]
-, "version" : "2.9.3"
+, "version" : "2.9.153"
 , "author" : "Mikeal Rogers <mikeal.rogers at gmail.com>"
 , "repository" :
   { "type" : "git"
@@ -11,5 +11,5 @@
   { "url" : "http://github.com/mikeal/request/issues" }
 , "engines" : ["node >= 0.3.6"]
 , "main" : "./main"
-, "scripts": { "test": "bash tests/run.sh" }
+, "scripts": { "test": "node tests/run.js" }
 }
diff --git a/tests/run.js b/tests/run.js
new file mode 100644
index 0000000..6011846
--- /dev/null
+++ b/tests/run.js
@@ -0,0 +1,37 @@
+var spawn = require('child_process').spawn
+  , exitCode = 0
+  ;
+
+var tests = [
+    'test-body.js'
+  , 'test-cookie.js'
+  , 'test-cookiejar.js'
+  , 'test-defaults.js'
+  , 'test-errors.js'
+  , 'test-headers.js'
+  , 'test-httpModule.js'
+  , 'test-https.js'
+  , 'test-https-strict.js'
+  , 'test-oauth.js'
+  , 'test-pipes.js'
+  , 'test-proxy.js'
+  , 'test-qs.js'
+  , 'test-redirect.js'
+  , 'test-timeout.js'
+  , 'test-tunnel.js'
+] 
+
+var next = function () {
+  if (tests.length === 0) process.exit(exitCode);
+  
+  var file = tests.shift()
+  console.log(file)
+  var proc = spawn('node', [ 'tests/' + file ])
+  proc.stdout.pipe(process.stdout)
+  proc.stderr.pipe(process.stderr)
+  proc.on('exit', function (code) {
+  	exitCode += code || 0
+  	next()
+  })
+}
+next()
diff --git a/tests/run.sh b/tests/run.sh
deleted file mode 100755
index 57d0f64..0000000
--- a/tests/run.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-FAILS=0
-for i in tests/test-*.js; do
-  echo $i
-  node $i || let FAILS++
-done
-exit $FAILS
diff --git a/tests/server.js b/tests/server.js
index 2e1889f..921f512 100644
--- a/tests/server.js
+++ b/tests/server.js
@@ -17,12 +17,19 @@ exports.createServer =  function (port) {
   return s;
 }
 
-exports.createSSLServer = function(port) {
+exports.createSSLServer = function(port, opts) {
   port = port || 16767
 
-  var options = { 'key' : fs.readFileSync(path.join(__dirname, 'ssl', 'test.key'))
-                , 'cert': fs.readFileSync(path.join(__dirname, 'ssl', 'test.crt'))
+  var options = { 'key' : path.join(__dirname, 'ssl', 'test.key')
+                , 'cert': path.join(__dirname, 'ssl', 'test.crt')
                 }
+  if (opts) {
+    for (var i in opts) options[i] = opts[i]
+  }
+
+  for (var i in options) {
+    options[i] = fs.readFileSync(options[i])
+  }
 
   var s = https.createServer(options, function (req, resp) {
     s.emit(req.url, req, resp);
diff --git a/tests/squid.conf b/tests/squid.conf
new file mode 100644
index 0000000..0d4a3b6
--- /dev/null
+++ b/tests/squid.conf
@@ -0,0 +1,77 @@
+#
+# Recommended minimum configuration:
+#
+acl manager proto cache_object
+acl localhost src 127.0.0.1/32 ::1
+acl to_localhost dst 127.0.0.0/8 0.0.0.0/32 ::1
+
+# Example rule allowing access from your local networks.
+# Adapt to list your (internal) IP networks from where browsing
+# should be allowed
+acl localnet src 10.0.0.0/8	# RFC1918 possible internal network
+acl localnet src 172.16.0.0/12	# RFC1918 possible internal network
+acl localnet src 192.168.0.0/16	# RFC1918 possible internal network
+acl localnet src fc00::/7       # RFC 4193 local private network range
+acl localnet src fe80::/10      # RFC 4291 link-local (directly plugged) machines
+
+acl SSL_ports port 443
+acl Safe_ports port 80		# http
+acl Safe_ports port 21		# ftp
+acl Safe_ports port 443		# https
+acl Safe_ports port 70		# gopher
+acl Safe_ports port 210		# wais
+acl Safe_ports port 1025-65535	# unregistered ports
+acl Safe_ports port 280		# http-mgmt
+acl Safe_ports port 488		# gss-http
+acl Safe_ports port 591		# filemaker
+acl Safe_ports port 777		# multiling http
+acl CONNECT method CONNECT
+
+#
+# Recommended minimum Access Permission configuration:
+#
+# Only allow cachemgr access from localhost
+http_access allow manager localhost
+http_access deny manager
+
+# Deny requests to certain unsafe ports
+http_access deny !Safe_ports
+
+# Deny CONNECT to other than secure SSL ports
+#http_access deny CONNECT !SSL_ports
+
+# We strongly recommend the following be uncommented to protect innocent
+# web applications running on the proxy server who think the only
+# one who can access services on "localhost" is a local user
+#http_access deny to_localhost
+
+#
+# INSERT YOUR OWN RULE(S) HERE TO ALLOW ACCESS FROM YOUR CLIENTS
+#
+
+# Example rule allowing access from your local networks.
+# Adapt localnet in the ACL section to list your (internal) IP networks
+# from where browsing should be allowed
+http_access allow localnet
+http_access allow localhost
+
+# And finally deny all other access to this proxy
+http_access deny all
+
+# Squid normally listens to port 3128
+http_port 3128
+
+# We recommend you to use at least the following line.
+hierarchy_stoplist cgi-bin ?
+
+# Uncomment and adjust the following to add a disk cache directory.
+#cache_dir ufs /usr/local/var/cache 100 16 256
+
+# Leave coredumps in the first cache dir
+coredump_dir /usr/local/var/cache
+
+# Add any of your own refresh_pattern entries above these.
+refresh_pattern ^ftp:		1440	20%	10080
+refresh_pattern ^gopher:	1440	0%	1440
+refresh_pattern -i (/cgi-bin/|\?) 0	0%	0
+refresh_pattern .		0	20%	4320
diff --git a/tests/ssl/ca/ca.cnf b/tests/ssl/ca/ca.cnf
new file mode 100644
index 0000000..425a889
--- /dev/null
+++ b/tests/ssl/ca/ca.cnf
@@ -0,0 +1,20 @@
+[ req ]
+default_bits           = 1024
+days                   = 3650
+distinguished_name     = req_distinguished_name
+attributes             = req_attributes
+prompt                 = no
+output_password        = password
+
+[ req_distinguished_name ]
+C                      = US
+ST                     = CA
+L                      = Oakland
+O                      = request
+OU                     = request Certificate Authority
+CN                     = requestCA
+emailAddress           = mikeal at mikealrogers.com
+
+[ req_attributes ]
+challengePassword              = password challenge
+
diff --git a/tests/ssl/ca/ca.crl b/tests/ssl/ca/ca.crl
new file mode 100644
index 0000000..e69de29
diff --git a/tests/ssl/ca/ca.crt b/tests/ssl/ca/ca.crt
new file mode 100644
index 0000000..b4524e4
--- /dev/null
+++ b/tests/ssl/ca/ca.crt
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICvTCCAiYCCQDn+P/MSbDsWjANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC
+VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMRAwDgYDVQQKEwdyZXF1
+ZXN0MSYwJAYDVQQLEx1yZXF1ZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTESMBAG
+A1UEAxMJcmVxdWVzdENBMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlrZWFscm9n
+ZXJzLmNvbTAeFw0xMjAzMDEyMjUwNTZaFw0yMjAyMjcyMjUwNTZaMIGiMQswCQYD
+VQQGEwJVUzELMAkGA1UECBMCQ0ExEDAOBgNVBAcTB09ha2xhbmQxEDAOBgNVBAoT
+B3JlcXVlc3QxJjAkBgNVBAsTHXJlcXVlc3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
+MRIwEAYDVQQDEwlyZXF1ZXN0Q0ExJjAkBgkqhkiG9w0BCQEWF21pa2VhbEBtaWtl
+YWxyb2dlcnMuY29tMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC7t9pQUAK4
+5XJYTI6NrF0n3G2HZsfN+rPYSVzzL8SuVyb1tHXos+vbPm3NKI4E8X1yVAXU8CjJ
+5SqXnp4DAypAhaseho81cbhk7LXUhFz78OvAa+OD+xTAEAnNQ8tGUr4VGyplEjfD
+xsBVuqV2j8GPNTftr+drOCFlqfAgMrBn4wIDAQABMA0GCSqGSIb3DQEBBQUAA4GB
+ADVdTlVAL45R+PACNS7Gs4o81CwSclukBu4FJbxrkd4xGQmurgfRrYYKjtqiopQm
+D7ysRamS3HMN9/VKq2T7r3z1PMHPAy7zM4uoXbbaTKwlnX4j/8pGPn8Ca3qHXYlo
+88L/OOPc6Di7i7qckS3HFbXQCTiULtxWmy97oEuTwrAj
+-----END CERTIFICATE-----
diff --git a/tests/ssl/ca/ca.csr b/tests/ssl/ca/ca.csr
new file mode 100644
index 0000000..e48c56e
--- /dev/null
+++ b/tests/ssl/ca/ca.csr
@@ -0,0 +1,13 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICBjCCAW8CAQAwgaIxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEQMA4GA1UE
+BxMHT2FrbGFuZDEQMA4GA1UEChMHcmVxdWVzdDEmMCQGA1UECxMdcmVxdWVzdCBD
+ZXJ0aWZpY2F0ZSBBdXRob3JpdHkxEjAQBgNVBAMTCXJlcXVlc3RDQTEmMCQGCSqG
+SIb3DQEJARYXbWlrZWFsQG1pa2VhbHJvZ2Vycy5jb20wgZ8wDQYJKoZIhvcNAQEB
+BQADgY0AMIGJAoGBALu32lBQArjlclhMjo2sXSfcbYdmx836s9hJXPMvxK5XJvW0
+deiz69s+bc0ojgTxfXJUBdTwKMnlKpeengMDKkCFqx6GjzVxuGTstdSEXPvw68Br
+44P7FMAQCc1Dy0ZSvhUbKmUSN8PGwFW6pXaPwY81N+2v52s4IWWp8CAysGfjAgMB
+AAGgIzAhBgkqhkiG9w0BCQcxFBMScGFzc3dvcmQgY2hhbGxlbmdlMA0GCSqGSIb3
+DQEBBQUAA4GBAGJO7grHeVHXetjHEK8urIxdnvfB2qeZeObz4GPKIkqUurjr0rfj
+bA3EK1kDMR5aeQWR8RunixdM16Q6Ry0lEdLVWkdSwRN9dmirIHT9cypqnD/FYOia
+SdezZ0lUzXgmJIwRYRwB1KSMMocIf52ll/xC2bEGg7/ZAEuAyAgcZV3X
+-----END CERTIFICATE REQUEST-----
diff --git a/tests/ssl/ca/ca.key b/tests/ssl/ca/ca.key
new file mode 100644
index 0000000..a53e7f7
--- /dev/null
+++ b/tests/ssl/ca/ca.key
@@ -0,0 +1,18 @@
+-----BEGIN RSA PRIVATE KEY-----
+Proc-Type: 4,ENCRYPTED
+DEK-Info: DES-EDE3-CBC,C8B5887048377F02
+
+nyD5ZH0Wup2uWsDvurq5mKDaDrf8lvNn9w0SH/ZkVnfR1/bkwqrFriqJWvZNUG+q
+nS0iBYczsWLJnbub9a1zLOTENWUKVD5uqbC3aGHhnoUTNSa27DONgP8gHOn6JgR+
+GAKo01HCSTiVT4LjkwN337QKHnMP2fTzg+IoC/CigvMcq09hRLwU1/guq0GJKGwH
+gTxYNuYmQC4Tjh8vdS4liF+Ve/P3qPR2CehZrIOkDT8PHJBGQJRo4xGUIB7Tpk38
+VCk+UZ0JCS2coY8VkY/9tqFJp/ZnnQQVmaNbdRqg7ECKL+bXnNo7yjzmazPZmPe3
+/ShbE0+CTt7LrjCaQAxWbeDzqfo1lQfgN1LulTm8MCXpQaJpv7v1VhIhQ7afjMYb
+4thW/ypHPiYS2YJCAkAVlua9Oxzzh1qJoh8Df19iHtpd79Q77X/qf+1JvITlMu0U
+gi7yEatmQcmYNws1mtTC1q2DXrO90c+NZ0LK/Alse6NRL/xiUdjug2iHeTf/idOR
+Gg/5dSZbnnlj1E5zjSMDkzg6EHAFmHV4jYGSAFLEQgp4V3ZhMVoWZrvvSHgKV/Qh
+FqrAK4INr1G2+/QTd09AIRzfy3/j6yD4A9iNaOsEf9Ua7Qh6RcALRCAZTWR5QtEf
+dX+iSNJ4E85qXs0PqwkMDkoaxIJ+tmIRJY7y8oeylV8cfGAi8Soubt/i3SlR8IHC
+uDMas/2OnwafK3N7ODeE1i7r7wkzQkSHaEz0TrF8XRnP25jAICCSLiMdAAjKfxVb
+EvzsFSuAy3Jt6bU3hSLY9o4YVYKE+68ITMv9yNjvTsEiW+T+IbN34w==
+-----END RSA PRIVATE KEY-----
diff --git a/tests/ssl/ca/ca.srl b/tests/ssl/ca/ca.srl
new file mode 100644
index 0000000..17128db
--- /dev/null
+++ b/tests/ssl/ca/ca.srl
@@ -0,0 +1 @@
+ADF62016AA40C9C3
diff --git a/tests/ssl/ca/server.cnf b/tests/ssl/ca/server.cnf
new file mode 100644
index 0000000..cd1fd1e
--- /dev/null
+++ b/tests/ssl/ca/server.cnf
@@ -0,0 +1,19 @@
+[ req ]
+default_bits           = 1024
+days                   = 3650
+distinguished_name     = req_distinguished_name
+attributes             = req_attributes
+prompt                 = no
+
+[ req_distinguished_name ]
+C                      = US
+ST                     = CA
+L                      = Oakland
+O                      = request
+OU                     = testing
+CN                     = testing.request.mikealrogers.com
+emailAddress           = mikeal at mikealrogers.com
+
+[ req_attributes ]
+challengePassword              = password challenge
+
diff --git a/tests/ssl/ca/server.crt b/tests/ssl/ca/server.crt
new file mode 100644
index 0000000..efe96ce
--- /dev/null
+++ b/tests/ssl/ca/server.crt
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIICejCCAeMCCQCt9iAWqkDJwzANBgkqhkiG9w0BAQUFADCBojELMAkGA1UEBhMC
+VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMRAwDgYDVQQKEwdyZXF1
+ZXN0MSYwJAYDVQQLEx1yZXF1ZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTESMBAG
+A1UEAxMJcmVxdWVzdENBMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlrZWFscm9n
+ZXJzLmNvbTAeFw0xMjAzMDEyMjUwNTZaFw0yMjAyMjcyMjUwNTZaMIGjMQswCQYD
+VQQGEwJVUzELMAkGA1UECBMCQ0ExEDAOBgNVBAcTB09ha2xhbmQxEDAOBgNVBAoT
+B3JlcXVlc3QxEDAOBgNVBAsTB3Rlc3RpbmcxKTAnBgNVBAMTIHRlc3RpbmcucmVx
+dWVzdC5taWtlYWxyb2dlcnMuY29tMSYwJAYJKoZIhvcNAQkBFhdtaWtlYWxAbWlr
+ZWFscm9nZXJzLmNvbTBcMA0GCSqGSIb3DQEBAQUAA0sAMEgCQQDgVl0jMumvOpmM
+20W5v9yhGgZj8hPhEQF/N7yCBVBn/rWGYm70IHC8T/pR5c0LkWc5gdnCJEvKWQjh
+DBKxZD8FAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEABShRkNgFbgs4vUWW9R9deNJj
+7HJoiTmvkmoOC7QzcYkjdgHbOxsSq3rBnwxsVjY9PAtPwBn0GRspOeG7KzKRgySB
+kb22LyrCFKbEOfKO/+CJc80ioK9zEPVjGsFMyAB+ftYRqM+s/4cQlTg/m89l01wC
+yapjN3RxZbInGhWR+jA=
+-----END CERTIFICATE-----
diff --git a/tests/ssl/ca/server.csr b/tests/ssl/ca/server.csr
new file mode 100644
index 0000000..a8e7595
--- /dev/null
+++ b/tests/ssl/ca/server.csr
@@ -0,0 +1,11 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIIBgjCCASwCAQAwgaMxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEQMA4GA1UE
+BxMHT2FrbGFuZDEQMA4GA1UEChMHcmVxdWVzdDEQMA4GA1UECxMHdGVzdGluZzEp
+MCcGA1UEAxMgdGVzdGluZy5yZXF1ZXN0Lm1pa2VhbHJvZ2Vycy5jb20xJjAkBgkq
+hkiG9w0BCQEWF21pa2VhbEBtaWtlYWxyb2dlcnMuY29tMFwwDQYJKoZIhvcNAQEB
+BQADSwAwSAJBAOBWXSMy6a86mYzbRbm/3KEaBmPyE+ERAX83vIIFUGf+tYZibvQg
+cLxP+lHlzQuRZzmB2cIkS8pZCOEMErFkPwUCAwEAAaAjMCEGCSqGSIb3DQEJBzEU
+ExJwYXNzd29yZCBjaGFsbGVuZ2UwDQYJKoZIhvcNAQEFBQADQQBD3E5WekQzCEJw
+7yOcqvtPYIxGaX8gRKkYfLPoj3pm3GF5SGqtJKhylKfi89szHXgktnQgzff9FN+A
+HidVJ/3u
+-----END CERTIFICATE REQUEST-----
diff --git a/tests/ssl/ca/server.js b/tests/ssl/ca/server.js
new file mode 100644
index 0000000..05e21c1
--- /dev/null
+++ b/tests/ssl/ca/server.js
@@ -0,0 +1,28 @@
+var fs = require("fs")
+var https = require("https")
+var options = { key: fs.readFileSync("./server.key")
+              , cert: fs.readFileSync("./server.crt") }
+
+var server = https.createServer(options, function (req, res) {
+  res.writeHead(200)
+  res.end()
+  server.close()
+})
+server.listen(1337)
+
+var ca = fs.readFileSync("./ca.crt")
+var agent = new https.Agent({ host: "localhost", port: 1337, ca: ca })
+
+https.request({ host: "localhost"
+              , method: "HEAD"
+              , port: 1337
+              , headers: { host: "testing.request.mikealrogers.com" }
+              , agent: agent
+              , ca: [ ca ]
+              , path: "/" }, function (res) {
+  if (res.client.authorized) {
+    console.log("node test: OK")
+  } else {
+    throw new Error(res.client.authorizationError)
+  }
+}).end()
diff --git a/tests/ssl/ca/server.key b/tests/ssl/ca/server.key
new file mode 100644
index 0000000..72d8698
--- /dev/null
+++ b/tests/ssl/ca/server.key
@@ -0,0 +1,9 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIBOwIBAAJBAOBWXSMy6a86mYzbRbm/3KEaBmPyE+ERAX83vIIFUGf+tYZibvQg
+cLxP+lHlzQuRZzmB2cIkS8pZCOEMErFkPwUCAwEAAQJAK+r8ZM2sze8s7FRo/ApB
+iRBtO9fCaIdJwbwJnXKo4RKwZDt1l2mm+fzZ+/QaQNjY1oTROkIIXmnwRvZWfYlW
+gQIhAPKYsG+YSBN9o8Sdp1DMyZ/rUifKX3OE6q9tINkgajDVAiEA7Ltqh01+cnt0
+JEnud/8HHcuehUBLMofeg0G+gCnSbXECIQCqDvkXsWNNLnS/3lgsnvH0Baz4sbeJ
+rjIpuVEeg8eM5QIgbu0+9JmOV6ybdmmiMV4yAncoF35R/iKGVHDZCAsQzDECIQDZ
+0jGz22tlo5YMcYSqrdD3U4sds1pwiAaWFRbCunoUJw==
+-----END RSA PRIVATE KEY-----
diff --git a/tests/ssl/npm-ca.crt b/tests/ssl/npm-ca.crt
new file mode 100644
index 0000000..fde2fe9
--- /dev/null
+++ b/tests/ssl/npm-ca.crt
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIIChzCCAfACCQDauvz/KHp8ejANBgkqhkiG9w0BAQUFADCBhzELMAkGA1UEBhMC
+VVMxCzAJBgNVBAgTAkNBMRAwDgYDVQQHEwdPYWtsYW5kMQwwCgYDVQQKEwNucG0x
+IjAgBgNVBAsTGW5wbSBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkxDjAMBgNVBAMTBW5w
+bUNBMRcwFQYJKoZIhvcNAQkBFghpQGl6cy5tZTAeFw0xMTA5MDUwMTQ3MTdaFw0y
+MTA5MDIwMTQ3MTdaMIGHMQswCQYDVQQGEwJVUzELMAkGA1UECBMCQ0ExEDAOBgNV
+BAcTB09ha2xhbmQxDDAKBgNVBAoTA25wbTEiMCAGA1UECxMZbnBtIENlcnRpZmlj
+YXRlIEF1dGhvcml0eTEOMAwGA1UEAxMFbnBtQ0ExFzAVBgkqhkiG9w0BCQEWCGlA
+aXpzLm1lMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLI4tIqPpRW+ACw9GE
+OgBlJZwK5f8nnKCLK629Pv5yJpQKs3DENExAyOgDcyaF0HD0zk8zTp+ZsLaNdKOz
+Gn2U181KGprGKAXP6DU6ByOJDWmTlY6+Ad1laYT0m64fERSpHw/hjD3D+iX4aMOl
+y0HdbT5m1ZGh6SJz3ZqxavhHLQIDAQABMA0GCSqGSIb3DQEBBQUAA4GBAC4ySDbC
+l7W1WpLmtLGEQ/yuMLUf6Jy/vr+CRp4h+UzL+IQpCv8FfxsYE7dhf/bmWTEupBkv
+yNL18lipt2jSvR3v6oAHAReotvdjqhxddpe5Holns6EQd1/xEZ7sB1YhQKJtvUrl
+ZNufy1Jf1r0ldEGeA+0ISck7s+xSh9rQD2Op
+-----END CERTIFICATE-----
diff --git a/tests/test-defaults.js b/tests/test-defaults.js
new file mode 100644
index 0000000..6c8b58f
--- /dev/null
+++ b/tests/test-defaults.js
@@ -0,0 +1,68 @@
+var server = require('./server')
+  , assert = require('assert')
+  , request = require('../main.js')
+  ;
+
+var s = server.createServer();
+
+s.listen(s.port, function () {
+  var counter = 0;
+  s.on('/get', function (req, resp) {
+    assert.equal(req.headers.foo, 'bar');
+    assert.equal(req.method, 'GET')
+    resp.writeHead(200, {'Content-Type': 'text/plain'});
+    resp.end('TESTING!');
+  });
+
+  // test get(string, function)
+  request.defaults({headers:{foo:"bar"}})(s.url + '/get', function (e, r, b){
+    if (e) throw e;
+    assert.deepEqual("TESTING!", b);
+    counter += 1;
+  });
+
+  s.on('/post', function (req, resp) {
+    assert.equal(req.headers.foo, 'bar');
+    assert.equal(req.headers['content-type'], 'application/json');
+    assert.equal(req.method, 'POST')
+    resp.writeHead(200, {'Content-Type': 'application/json'});
+    resp.end(JSON.stringify({foo:'bar'}));
+  });
+
+  // test post(string, object, function)
+  request.defaults({headers:{foo:"bar"}}).post(s.url + '/post', {json: true}, function (e, r, b){
+    if (e) throw e;
+    assert.deepEqual('bar', b.foo);
+    counter += 1;
+  });
+
+  s.on('/del', function (req, resp) {
+    assert.equal(req.headers.foo, 'bar');
+    assert.equal(req.method, 'DELETE')
+    resp.writeHead(200, {'Content-Type': 'application/json'});
+    resp.end(JSON.stringify({foo:'bar'}));
+  });
+
+  // test .del(string, function)
+  request.defaults({headers:{foo:"bar"}, json:true}).del(s.url + '/del', function (e, r, b){
+    if (e) throw e;
+    assert.deepEqual('bar', b.foo);
+    counter += 1;
+  });
+
+  s.on('/head', function (req, resp) {
+    assert.equal(req.headers.foo, 'bar');
+    assert.equal(req.method, 'HEAD')
+    resp.writeHead(200, {'Content-Type': 'text/plain'});
+    resp.end();
+  });
+
+  // test head.(object, function)
+  request.defaults({headers:{foo:"bar"}}).head({uri: s.url + '/head'}, function (e, r, b){
+    if (e) throw e;
+    counter += 1;
+    console.log(counter.toString() + " tests passed.")
+    s.close()
+  });
+
+})
diff --git a/tests/test-errors.js b/tests/test-errors.js
index a7db1f7..1986a59 100644
--- a/tests/test-errors.js
+++ b/tests/test-errors.js
@@ -27,4 +27,11 @@ try {
   assert.equal(e.message, 'Body attribute missing in multipart.')
 }
 
+try {
+  request(local, {multipart: [{}]})
+  assert.fail("Should have throw")
+} catch(e) {
+  assert.equal(e.message, 'Body attribute missing in multipart.')
+}
+
 console.log("All tests passed.")
diff --git a/tests/test-headers.js b/tests/test-headers.js
new file mode 100644
index 0000000..31fe3f4
--- /dev/null
+++ b/tests/test-headers.js
@@ -0,0 +1,52 @@
+var server = require('./server')
+  , assert = require('assert')
+  , request = require('../main.js')
+  , Cookie = require('../vendor/cookie')
+  , Jar = require('../vendor/cookie/jar')
+  , s = server.createServer()
+
+s.listen(s.port, function () {
+  var serverUri = 'http://localhost:' + s.port
+    , numTests = 0
+    , numOutstandingTests = 0
+
+  function createTest(requestObj, serverAssertFn) {
+    var testNumber = numTests;
+    numTests += 1;
+    numOutstandingTests += 1;
+    s.on('/' + testNumber, function (req, res) {
+      serverAssertFn(req, res);
+      res.writeHead(200);
+      res.end();
+    });
+    requestObj.url = serverUri + '/' + testNumber
+    request(requestObj, function (err, res, body) {
+      assert.ok(!err)
+      assert.equal(res.statusCode, 200)
+      numOutstandingTests -= 1
+      if (numOutstandingTests === 0) {
+        console.log(numTests + ' tests passed.')
+        s.close()
+      }
+    })
+  }
+
+  // Issue #125: headers.cookie shouldn't be replaced when a cookie jar isn't specified
+  createTest({headers: {cookie: 'foo=bar'}}, function (req, res) {
+    assert.ok(req.headers.cookie)
+    assert.equal(req.headers.cookie, 'foo=bar')
+  })
+
+  // Issue #125: headers.cookie + cookie jar
+  var jar = new Jar()
+  jar.add(new Cookie('quux=baz'));
+  createTest({jar: jar, headers: {cookie: 'foo=bar'}}, function (req, res) {
+    assert.ok(req.headers.cookie)
+    assert.equal(req.headers.cookie, 'foo=bar; quux=baz')
+  })
+
+  // There should be no cookie header when neither headers.cookie nor a cookie jar is specified
+  createTest({}, function (req, res) {
+    assert.ok(!req.headers.cookie)
+  })
+})
diff --git a/tests/test-httpModule.js b/tests/test-httpModule.js
index 6d8c83f..1866de2 100644
--- a/tests/test-httpModule.js
+++ b/tests/test-httpModule.js
@@ -60,16 +60,16 @@ plain_server.listen(plain_server.port, function() {
 })
 
 function run_tests(httpModules) {
-  var to_https = {'httpModules':httpModules, 'uri':'http://localhost:'+plain_server.port+'/to_https'}
-  var to_plain = {'httpModules':httpModules, 'uri':'https://localhost:'+https_server.port+'/to_plain'}
+  var to_https = 'http://localhost:'+plain_server.port+'/to_https'
+  var to_plain = 'https://localhost:'+https_server.port+'/to_plain'
 
-  request(to_https, function (er, res, body) {
+  request(to_https, {'httpModules':httpModules}, function (er, res, body) {
     assert.ok(!er, 'Bounce to SSL worked')
     assert.equal(body, 'https', 'Received HTTPS server body')
     done()
   })
 
-  request(to_plain, function (er, res, body) {
+  request(to_plain, {'httpModules':httpModules}, function (er, res, body) {
     assert.ok(!er, 'Bounce to plaintext server worked')
     assert.equal(body, 'plain', 'Received HTTPS server body')
     done()
diff --git a/tests/test-https.js b/tests/test-https-strict.js
similarity index 79%
copy from tests/test-https.js
copy to tests/test-https-strict.js
index df7330b..f53fc14 100644
--- a/tests/test-https.js
+++ b/tests/test-https-strict.js
@@ -1,8 +1,16 @@
+// a test where we validate the siguature of the keys
+// otherwise exactly the same as the ssl test
+
 var server = require('./server')
   , assert = require('assert')
   , request = require('../main.js')
-
-var s = server.createSSLServer();
+  , fs = require('fs')
+  , path = require('path')
+  , opts = { key: path.resolve(__dirname, 'ssl/ca/server.key')
+           , cert: path.resolve(__dirname, 'ssl/ca/server.crt') }
+  , s = server.createSSLServer(null, opts)
+  , caFile = path.resolve(__dirname, 'ssl/ca/ca.crt')
+  , ca = fs.readFileSync(caFile)
 
 var tests =
   { testGet :
@@ -69,6 +77,9 @@ s.listen(s.port, function () {
       var test = tests[i]
       s.on('/'+i, test.resp)
       test.uri = s.url + '/' + i
+      test.strictSSL = true
+      test.ca = ca
+      test.headers = { host: 'testing.request.mikealrogers.com' }
       request(test, function (err, resp, body) {
         if (err) throw err
         if (test.expectBody) {
diff --git a/tests/test-params.js b/tests/test-params.js
new file mode 100644
index 0000000..8354f6d
--- /dev/null
+++ b/tests/test-params.js
@@ -0,0 +1,92 @@
+var server = require('./server')
+  , assert = require('assert')
+  , request = require('../main.js')
+  ;
+
+var s = server.createServer();
+
+var tests =
+  { testGet :
+    { resp : server.createGetResponse("TESTING!")
+    , expectBody: "TESTING!"
+    }
+    , testGetChunkBreak :
+      { resp : server.createChunkResponse(
+        [ new Buffer([239])
+        , new Buffer([163])
+        , new Buffer([191])
+        , new Buffer([206])
+        , new Buffer([169])
+        , new Buffer([226])
+        , new Buffer([152])
+        , new Buffer([131])
+        ])
+      , expectBody: "Ω☃"
+      }
+    , testGetBuffer :
+      { resp : server.createGetResponse(new Buffer("TESTING!"))
+      , encoding: null
+      , expectBody: new Buffer("TESTING!")
+      }
+    , testGetJSON :
+       { resp : server.createGetResponse('{"test":true}', 'application/json')
+       , json : true
+       , expectBody: {"test":true}
+       }
+    , testPutString :
+      { resp : server.createPostValidator("PUTTINGDATA")
+      , method : "PUT"
+      , body : "PUTTINGDATA"
+      }
+    , testPutBuffer :
+      { resp : server.createPostValidator("PUTTINGDATA")
+      , method : "PUT"
+      , body : new Buffer("PUTTINGDATA")
+      }
+    , testPutJSON :
+      { resp : server.createPostValidator(JSON.stringify({foo: 'bar'}))
+      , method: "PUT"
+      , json: {foo: 'bar'}
+      }
+    , testPutMultipart :
+      { resp: server.createPostValidator(
+          '--frontier\r\n' +
+          'content-type: text/html\r\n' +
+          '\r\n' +
+          '<html><body>Oh hi.</body></html>' +
+          '\r\n--frontier\r\n\r\n' +
+          'Oh hi.' +
+          '\r\n--frontier--'
+          )
+      , method: "PUT"
+      , multipart:
+        [ {'content-type': 'text/html', 'body': '<html><body>Oh hi.</body></html>'}
+        , {'body': 'Oh hi.'}
+        ]
+      }
+  }
+
+s.listen(s.port, function () {
+
+  var counter = 0
+
+  for (i in tests) {
+    (function () {
+      var test = tests[i]
+      s.on('/'+i, test.resp)
+      //test.uri = s.url + '/' + i
+      request(s.url + '/' + i, test, function (err, resp, body) {
+        if (err) throw err
+        if (test.expectBody) {
+          assert.deepEqual(test.expectBody, body)
+        }
+        counter = counter - 1;
+        if (counter === 0) {
+          console.log(Object.keys(tests).length+" tests passed.")
+          s.close()
+        }
+      })
+      counter++
+    })()
+  }
+})
diff --git a/tests/test-pipes.js b/tests/test-pipes.js
index 5785475..1869874 100644
--- a/tests/test-pipes.js
+++ b/tests/test-pipes.js
@@ -179,4 +179,24 @@ s.listen(s.port, function () {
   validateForward.on('end', check)
   request.get('http://localhost:3453/forward1').pipe(validateForward)
 
+  // Test pipe options
+  s.once('/opts', server.createGetResponse('opts response'));
+
+  var optsStream = new stream.Stream();
+  optsStream.writable = true
+  
+  var optsData = '';
+  optsStream.write = function (buf) {
+    optsData += buf;
+    if (optsData === 'opts response') {
+      setTimeout(check, 10);
+    }
+  }
+
+  optsStream.end = function () {
+    assert.fail('end called')
+  };
+
+  counter++
+  request({url:'http://localhost:3453/opts'}).pipe(optsStream, { end : false })
 })
diff --git a/tests/test-qs.js b/tests/test-qs.js
new file mode 100644
index 0000000..1aac22b
--- /dev/null
+++ b/tests/test-qs.js
@@ -0,0 +1,28 @@
+var request = request = require('../main.js')
+  , assert = require('assert')
+  ;
+ 
+
+// Test adding a querystring
+var req1 = request.get({ uri: 'http://www.google.com', qs: { q : 'search' }})
+setTimeout(function() {
+	assert.equal('/?q=search', req1.path)
+}, 1)
+
+// Test replacing a querystring value
+var req2 = request.get({ uri: 'http://www.google.com?q=abc', qs: { q : 'search' }})
+setTimeout(function() {
+	assert.equal('/?q=search', req2.path)
+}, 1)
+
+// Test appending a querystring value to the ones present in the uri
+var req3 = request.get({ uri: 'http://www.google.com?x=y', qs: { q : 'search' }})
+setTimeout(function() {
+	assert.equal('/?x=y&q=search', req3.path)
+}, 1)
+
+// Test leaving a querystring alone
+var req4 = request.get({ uri: 'http://www.google.com?x=y'})
+setTimeout(function() {
+	assert.equal('/?x=y', req4.path)
+}, 1)
diff --git a/tests/test-redirect.js b/tests/test-redirect.js
index dfa086d..54fc19b 100644
--- a/tests/test-redirect.js
+++ b/tests/test-redirect.js
@@ -1,6 +1,8 @@
 var server = require('./server')
   , assert = require('assert')
   , request = require('../main.js')
+  , Cookie = require('../vendor/cookie')
+  , Jar = require('../vendor/cookie/jar')
 
 var s = server.createServer()
 
@@ -23,6 +25,14 @@ s.listen(s.port, function () {
     })
 
     s.on('/'+landing, function (req, res) {
+      if (req.method !== 'GET') { // We should only accept GET redirects
+        console.error("Got a non-GET request to the redirect destination URL");
+        resp.writeHead(400);
+        resp.end();
+        return;
+      }
+      // Make sure the cookie doesn't get included twice, see #139:
+      assert.equal(req.headers.cookie, 'foo=bar; quux=baz');
       hits[landing] = true;
       res.writeHead(200)
       res.end(landing)
@@ -30,7 +40,9 @@ s.listen(s.port, function () {
   }
 
   // Permanent bounce
-  request(server+'/perm', function (er, res, body) {
+  var jar = new Jar()
+  jar.add(new Cookie('quux=baz'))
+  request({uri: server+'/perm', jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
     try {
       assert.ok(hits.perm, 'Original request is to /perm')
       assert.ok(hits.perm_landing, 'Forward to permanent landing URL')
@@ -42,7 +54,7 @@ s.listen(s.port, function () {
   })
 
   // Temporary bounce
-  request(server+'/temp', function (er, res, body) {
+  request({uri: server+'/temp', jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
     try {
       assert.ok(hits.temp, 'Original request is to /temp')
       assert.ok(hits.temp_landing, 'Forward to temporary landing URL')
@@ -54,7 +66,7 @@ s.listen(s.port, function () {
   })
 
   // Prevent bouncing.
-  request({uri:server+'/nope', followRedirect:false}, function (er, res, body) {
+  request({uri:server+'/nope', jar: jar, headers: {cookie: 'foo=bar'}, followRedirect:false}, function (er, res, body) {
     try {
       assert.ok(hits.nope, 'Original request to /nope')
       assert.ok(!hits.nope_landing, 'No chasing the redirect')
@@ -65,10 +77,81 @@ s.listen(s.port, function () {
     }
   })
 
+  // Should not follow post redirects by default
+  request.post(server+'/temp', { jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
+    try {
+      assert.ok(hits.temp, 'Original request is to /temp')
+      assert.ok(!hits.temp_landing, 'No chasing the redirect when post')
+      assert.equal(res.statusCode, 301, 'Response is the bounce itself')
+      passed += 1
+    } finally {
+      done()
+    }
+  })
+
+  // Should follow post redirects when followAllRedirects true
+  request.post({uri:server+'/temp', followAllRedirects:true, jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
+    try {
+      assert.ok(hits.temp, 'Original request is to /temp')
+      assert.ok(hits.temp_landing, 'Forward to temporary landing URL')
+      assert.equal(body, 'temp_landing', 'Got temporary landing content')
+      passed += 1
+    } finally {
+      done()
+    }
+  })
+
+  request.post({uri:server+'/temp', followAllRedirects:false, jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
+    try {
+      assert.ok(hits.temp, 'Original request is to /temp')
+      assert.ok(!hits.temp_landing, 'No chasing the redirect')
+      assert.equal(res.statusCode, 301, 'Response is the bounce itself')
+      passed += 1
+    } finally {
+      done()
+    }
+  })
+
+  // Should not follow delete redirects by default
+  request.del(server+'/temp', { jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
+    try {
+      assert.ok(hits.temp, 'Original request is to /temp')
+      assert.ok(!hits.temp_landing, 'No chasing the redirect when delete')
+      assert.equal(res.statusCode, 301, 'Response is the bounce itself')
+      passed += 1
+    } finally {
+      done()
+    }
+  })
+
+  // Should not follow delete redirects even if followRedirect is set to true
+  request.del(server+'/temp', { followRedirect: true, jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
+    try {
+      assert.ok(hits.temp, 'Original request is to /temp')
+      assert.ok(!hits.temp_landing, 'No chasing the redirect when delete')
+      assert.equal(res.statusCode, 301, 'Response is the bounce itself')
+      passed += 1
+    } finally {
+      done()
+    }
+  })
+
+  // Should follow delete redirects when followAllRedirects true
+  request.del(server+'/temp', {followAllRedirects:true, jar: jar, headers: {cookie: 'foo=bar'}}, function (er, res, body) {
+    try {
+      assert.ok(hits.temp, 'Original request is to /temp')
+      assert.ok(hits.temp_landing, 'Forward to temporary landing URL')
+      assert.equal(body, 'temp_landing', 'Got temporary landing content')
+      passed += 1
+    } finally {
+      done()
+    }
+  })
+
   var reqs_done = 0;
   function done() {
     reqs_done += 1;
-    if(reqs_done == 3) {
+    if(reqs_done == 9) {
       console.log(passed + ' tests passed.')
       s.close()
     }
diff --git a/tests/test-tunnel.js b/tests/test-tunnel.js
new file mode 100644
index 0000000..58131b9
--- /dev/null
+++ b/tests/test-tunnel.js
@@ -0,0 +1,61 @@
+// test that we can tunnel a https request over an http proxy
+// keeping all the CA and whatnot intact.
+//
+// Note: this requires that squid is installed.
+// If the proxy fails to start, we'll just log a warning and assume success.
+
+var server = require('./server')
+  , assert = require('assert')
+  , request = require('../main.js')
+  , fs = require('fs')
+  , path = require('path')
+  , caFile = path.resolve(__dirname, 'ssl/npm-ca.crt')
+  , ca = fs.readFileSync(caFile)
+  , child_process = require('child_process')
+  , sqConf = path.resolve(__dirname, 'squid.conf')
+  , sqArgs = ['-f', sqConf, '-N', '-d', '5']
+  , proxy = 'http://localhost:3128'
+  , hadError = null
+
+var squid = child_process.spawn('squid', sqArgs);
+var ready = false
+
+squid.stderr.on('data', function (c) {
+  console.error('SQUIDERR ' + c.toString().trim().split('\n')
+               .join('\nSQUIDERR '))
+  ready = c.toString().match(/ready to serve requests/i)
+})
+
+squid.stdout.on('data', function (c) {
+  console.error('SQUIDOUT ' + c.toString().trim().split('\n')
+               .join('\nSQUIDOUT '))
+})
+
+squid.on('exit', function (c) {
+  console.error('exit '+c)
+  if (c && !ready) {
+    console.error('squid must be installed to run this test.')
+    c = null
+    hadError = null
+    process.exit(0)
+    return
+  }
+
+  if (c) {
+    hadError = hadError || new Error('Squid exited with '+c)
+  }
+  if (hadError) throw hadError
+})
+
+setTimeout(function F () {
+  if (!ready) return setTimeout(F, 100)
+  request({ uri: 'https://registry.npmjs.org/request/'
+          , proxy: 'http://localhost:3128'
+          , ca: ca
+          , json: true }, function (er, body) {
+    hadError = er
+    console.log(er || typeof body)
+    if (!er) console.log("ok")
+    squid.kill('SIGKILL')
+  })
+}, 100)
diff --git a/tunnel.js b/tunnel.js
new file mode 100644
index 0000000..453786c
--- /dev/null
+++ b/tunnel.js
@@ -0,0 +1,229 @@
+'use strict';
+
+var net = require('net');
+var tls = require('tls');
+var http = require('http');
+var https = require('https');
+var events = require('events');
+var assert = require('assert');
+var util = require('util');
+
+
+exports.httpOverHttp = httpOverHttp;
+exports.httpsOverHttp = httpsOverHttp;
+exports.httpOverHttps = httpOverHttps;
+exports.httpsOverHttps = httpsOverHttps;
+
+
+function httpOverHttp(options) {
+  var agent = new TunnelingAgent(options);
+  agent.request = http.request;
+  return agent;
+}
+
+function httpsOverHttp(options) {
+  var agent = new TunnelingAgent(options);
+  agent.request = http.request;
+  agent.createSocket = createSecureSocket;
+  return agent;
+}
+
+function httpOverHttps(options) {
+  var agent = new TunnelingAgent(options);
+  agent.request = https.request;
+  return agent;
+}
+
+function httpsOverHttps(options) {
+  var agent = new TunnelingAgent(options);
+  agent.request = https.request;
+  agent.createSocket = createSecureSocket;
+  return agent;
+}
+
+
+function TunnelingAgent(options) {
+  var self = this;
+  self.options = options || {};
+  self.proxyOptions = self.options.proxy || {};
+  self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets;
+  self.requests = [];
+  self.sockets = [];
+
+  self.on('free', function onFree(socket, host, port) {
+    for (var i = 0, len = self.requests.length; i < len; ++i) {
+      var pending = self.requests[i];
+      if (pending.host === host && pending.port === port) {
+        // Detect the request to connect same origin server,
+        // reuse the connection.
+        self.requests.splice(i, 1);
+        pending.request.onSocket(socket);
+        return;
+      }
+    }
+    socket.destroy();
+    self.removeSocket(socket);
+  });
+}
+util.inherits(TunnelingAgent, events.EventEmitter);
+
+TunnelingAgent.prototype.addRequest = function addRequest(req, host, port) {
+  var self = this;
+
+  if (self.sockets.length >= this.maxSockets) {
+    // We are over limit so we'll add it to the queue.
+    self.requests.push({host: host, port: port, request: req});
+    return;
+  }
+
+  // If we are under maxSockets create a new one.
+  self.createSocket({host: host, port: port, request: req}, function(socket) {
+    socket.on('free', onFree);
+    socket.on('close', onCloseOrRemove);
+    socket.on('agentRemove', onCloseOrRemove);
+    req.onSocket(socket);
+
+    function onFree() {
+      self.emit('free', socket, host, port);
+    }
+
+    function onCloseOrRemove(err) {
+      self.removeSocket();
+      socket.removeListener('free', onFree);
+      socket.removeListener('close', onCloseOrRemove);
+      socket.removeListener('agentRemove', onCloseOrRemove);
+    }
+  });
+};
+
+TunnelingAgent.prototype.createSocket = function createSocket(options, cb) {
+  var self = this;
+  var placeholder = {};
+  self.sockets.push(placeholder);
+
+  var connectOptions = mergeOptions({}, self.proxyOptions, {
+    method: 'CONNECT',
+    path: options.host + ':' + options.port,
+    agent: false
+  });
+  if (connectOptions.proxyAuth) {
+    connectOptions.headers = connectOptions.headers || {};
+    connectOptions.headers['Proxy-Authorization'] = 'Basic ' +
+        new Buffer(connectOptions.proxyAuth).toString('base64');
+  }
+
+  debug('making CONNECT request');
+  var connectReq = self.request(connectOptions);
+  connectReq.useChunkedEncodingByDefault = false; // for v0.6
+  connectReq.once('response', onResponse); // for v0.6
+  connectReq.once('upgrade', onUpgrade);   // for v0.6
+  connectReq.once('connect', onConnect);   // for v0.7 or later
+  connectReq.once('error', onError);
+  connectReq.end();
+
+  function onResponse(res) {
+    // Very hacky. This is necessary to avoid http-parser leaks.
+    res.upgrade = true;
+  }
+
+  function onUpgrade(res, socket, head) {
+    // Hacky.
+    process.nextTick(function() {
+      onConnect(res, socket, head);
+    });
+  }
+
+  function onConnect(res, socket, head) {
+    connectReq.removeAllListeners();
+    socket.removeAllListeners();
+
+    if (res.statusCode === 200) {
+      assert.equal(head.length, 0);
+      debug('tunneling connection has established');
+      self.sockets[self.sockets.indexOf(placeholder)] = socket;
+      cb(socket);
+    } else {
+      debug('tunneling socket could not be established, statusCode=%d',
+            res.statusCode);
+      var error = new Error('tunneling socket could not be established, ' +
+                            'sutatusCode=' + res.statusCode);
+      error.code = 'ECONNRESET';
+      options.request.emit('error', error);
+      self.removeSocket(placeholder);
+    }
+  }
+
+  function onError(cause) {
+    connectReq.removeAllListeners();
+
+    debug('tunneling socket could not be established, cause=%s\n',
+          cause.message, cause.stack);
+    var error = new Error('tunneling socket could not be established, ' +
+                          'cause=' + cause.message);
+    error.code = 'ECONNRESET';
+    options.request.emit('error', error);
+    self.removeSocket(placeholder);
+  }
+};
+
+TunnelingAgent.prototype.removeSocket = function removeSocket(socket) {
+  var pos = this.sockets.indexOf(socket)
+  if (pos === -1) {
+    return;
+  }
+  this.sockets.splice(pos, 1);
+
+  var pending = this.requests.shift();
+  if (pending) {
+    // If we have pending requests and a socket gets closed a new one
+    // needs to be created to take over in the pool for the one that closed.
+    this.createSocket(pending, function(socket) {
+      pending.request.onSocket(socket);
+    });
+  }
+};
+
+function createSecureSocket(options, cb) {
+  var self = this;
+  TunnelingAgent.prototype.createSocket.call(self, options, function(socket) {
+    // 0 is dummy port for v0.6
+    var secureSocket = tls.connect(0, mergeOptions({}, self.options, {
+      socket: socket
+    }));
+    cb(secureSocket);
+  });
+}
+
+
+function mergeOptions(target) {
+  for (var i = 1, len = arguments.length; i < len; ++i) {
+    var overrides = arguments[i];
+    if (typeof overrides === 'object') {
+      var keys = Object.keys(overrides);
+      for (var j = 0, keyLen = keys.length; j < keyLen; ++j) {
+        var k = keys[j];
+        if (overrides[k] !== undefined) {
+          target[k] = overrides[k];
+        }
+      }
+    }
+  }
+  return target;
+}
+
+
+var debug;
+if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
+  debug = function() {
+    var args = Array.prototype.slice.call(arguments);
+    if (typeof args[0] === 'string') {
+      args[0] = 'TUNNEL: ' + args[0];
+    } else {
+      args.unshift('TUNNEL:');
+    }
+    console.error.apply(console, args);
+  }
+} else {
+  debug = function() {};
+}
+exports.debug = debug; // for test

-- 
simplified HTTP request client for NodeJS



More information about the Pkg-javascript-commits mailing list