npm: Upgrade to 1.3.19
[platform/upstream/nodejs.git] / deps / npm / node_modules / npm-registry-client / lib / request.js
1 module.exports = regRequest
2
3 var url = require("url")
4   , fs = require("graceful-fs")
5   , rm = require("rimraf")
6   , asyncMap = require("slide").asyncMap
7   , Stream = require("stream").Stream
8   , request = require("request")
9   , retry = require("retry")
10
11 function regRequest (method, where, what, etag, nofollow, reauthed, cb_) {
12   if (typeof cb_ !== "function") cb_ = reauthed, reauthed = false
13   if (typeof cb_ !== "function") cb_ = nofollow, nofollow = false
14   if (typeof cb_ !== "function") cb_ = etag, etag = null
15   if (typeof cb_ !== "function") cb_ = what, what = null
16
17   var registry = this.conf.get('registry')
18   if (!registry) return cb(new Error(
19     "No registry url provided: " + method + " " + where))
20
21   // Since there are multiple places where an error could occur,
22   // don't let the cb be called more than once.
23   var errState = null
24   function cb (er) {
25     if (errState) return
26     if (er) errState = er
27     cb_.apply(null, arguments)
28   }
29
30   if (where.match(/^\/?favicon.ico/)) {
31     return cb(new Error("favicon.ico isn't a package, it's a picture."))
32   }
33
34   var adduserChange = /^\/?-\/user\/org\.couchdb\.user:([^\/]+)\/-rev/
35   , adduserNew = /^\/?-\/user\/org\.couchdb\.user:([^\/]+)/
36   , nu = where.match(adduserNew)
37   , uc = where.match(adduserChange)
38   , isUpload = what || this.conf.get('always-auth')
39   , isDel = method === "DELETE"
40   , authRequired = isUpload && !nu || uc || isDel
41
42   // resolve to a full url on the registry
43   if (!where.match(/^https?:\/\//)) {
44     this.log.verbose("url raw", where)
45
46     var q = where.split("?")
47     where = q.shift()
48     q = q.join("?")
49
50     if (where.charAt(0) !== "/") where = "/" + where
51     where = "." + where.split("/").map(function (p) {
52       p = p.trim()
53       if (p.match(/^org.couchdb.user/)) {
54         return p.replace(/\//g, encodeURIComponent("/"))
55       }
56       return encodeURIComponent(p)
57     }).join("/")
58     if (q) where += "?" + q
59     this.log.verbose("url resolving", [registry, where])
60     where = url.resolve(registry, where)
61     this.log.verbose("url resolved", where)
62   }
63
64   var remote = url.parse(where)
65   , auth = this.conf.get('_auth')
66
67   if (authRequired && !this.conf.get('always-auth')) {
68     var couch = this.couchLogin
69     , token = couch && (this.conf.get('_token') || couch.token)
70     , validToken = token && couch.valid(token)
71
72     if (!validToken) token = null
73     else this.conf.set('_token', token)
74
75     if (couch && !token) {
76       // login to get a valid token
77       var a = { name: this.conf.get('username'),
78                 password: this.conf.get('_password') }
79       var args = arguments
80       return this.couchLogin.login(a, function (er, cr, data) {
81         if (er || !couch.valid(couch.token)) {
82           er = er || new Error('login error')
83           return cb(er, cr, data)
84         }
85         this.conf.set('_token', this.couchLogin.token)
86         return regRequest.call(this,
87                                method, where, what,
88                                etag, nofollow, reauthed, cb_)
89       }.bind(this))
90     }
91   }
92
93   // now we either have a valid token, or an auth.
94
95   if (authRequired && !auth && !token) {
96     return cb(new Error(
97       "Cannot insert data into the registry without auth"))
98   }
99
100   if (auth && !token && authRequired) {
101     remote.auth = new Buffer(auth, "base64").toString("utf8")
102   }
103
104   // Tuned to spread 3 attempts over about a minute.
105   // See formula at <https://github.com/tim-kos/node-retry>.
106   var operation = retry.operation({
107     retries: this.conf.get('fetch-retries') || 2,
108     factor: this.conf.get('fetch-retry-factor'),
109     minTimeout: this.conf.get('fetch-retry-mintimeout') || 10000,
110     maxTimeout: this.conf.get('fetch-retry-maxtimeout') || 60000
111   })
112
113   var self = this
114   operation.attempt(function (currentAttempt) {
115     self.log.info("trying", "registry request attempt " + currentAttempt
116         + " at " + (new Date()).toLocaleTimeString())
117     makeRequest.call(self, method, remote, where, what, etag, nofollow, token
118                      , function (er, parsed, raw, response) {
119       if (!er || er.message.match(/^SSL Error/)) {
120         if (er)
121           er.code = 'ESSL'
122         return cb(er, parsed, raw, response)
123       }
124
125       // Only retry on 408, 5xx or no `response`.
126       var statusCode = response && response.statusCode
127
128       var reauth = !reauthed &&
129                    ( statusCode === 401 ||
130                      statusCode === 400 ||
131                      statusCode === 403 )
132       if (reauth)
133         reauthed = true
134
135       var timeout = statusCode === 408
136       var serverError = statusCode >= 500
137       var statusRetry = !statusCode || timeout || serverError
138       if (reauth && this.conf.get('_auth') && this.conf.get('_token')) {
139         this.conf.del('_token')
140         this.couchLogin.token = null
141         return regRequest.call(this, method, where, what,
142                                etag, nofollow, reauthed, cb_)
143       }
144       if (er && statusRetry && operation.retry(er)) {
145         self.log.info("retry", "will retry, error on last attempt: " + er)
146         return
147       }
148       cb.apply(null, arguments)
149     }.bind(this))
150   }.bind(this))
151 }
152
153 function makeRequest (method, remote, where, what, etag, nofollow, tok, cb_) {
154   var cbCalled = false
155   function cb () {
156     if (cbCalled) return
157     cbCalled = true
158     cb_.apply(null, arguments)
159   }
160
161   var strict = this.conf.get('strict-ssl')
162   if (strict === undefined) strict = true
163   var opts = { url: remote
164              , method: method
165              , ca: this.conf.get('ca')
166              , localAddress: this.conf.get('local-address')
167              , cert: this.conf.get('cert')
168              , key: this.conf.get('key')
169              , strictSSL: strict }
170     , headers = opts.headers = {}
171   if (etag) {
172     this.log.verbose("etag", etag)
173     headers[method === "GET" ? "if-none-match" : "if-match"] = etag
174   }
175
176   if (tok) {
177     headers.cookie = 'AuthSession=' + tok.AuthSession
178   }
179
180   headers.accept = "application/json"
181
182   headers["user-agent"] = this.conf.get('user-agent') ||
183                           'node/' + process.version
184
185   var p = this.conf.get('proxy')
186   var sp = this.conf.get('https-proxy') || p
187   opts.proxy = remote.protocol === "https:" ? sp : p
188
189   // figure out wth 'what' is
190   if (what) {
191     if (Buffer.isBuffer(what) || typeof what === "string") {
192       opts.body = what
193       headers["content-type"] = "application/json"
194       headers["content-length"] = Buffer.byteLength(what)
195     } else if (what instanceof Stream) {
196       headers["content-type"] = "application/octet-stream"
197       if (what.size) headers["content-length"] = what.size
198     } else {
199       delete what._etag
200       opts.json = what
201     }
202   }
203
204   if (nofollow) {
205     opts.followRedirect = false
206   }
207
208   this.log.http(method, remote.href || "/")
209
210   var done = requestDone.call(this, method, where, cb)
211   var req = request(opts, done)
212
213   req.on("error", cb)
214   req.on("socket", function (s) {
215     s.on("error", cb)
216   })
217
218   if (what && (what instanceof Stream)) {
219     what.pipe(req)
220   }
221 }
222
223 // cb(er, parsed, raw, response)
224 function requestDone (method, where, cb) {
225   return function (er, response, data) {
226     if (er) return cb(er)
227
228     var urlObj = url.parse(where)
229     if (urlObj.auth)
230       urlObj.auth = '***'
231     this.log.http(response.statusCode, url.format(urlObj))
232
233     var parsed
234
235     if (Buffer.isBuffer(data)) {
236       data = data.toString()
237     }
238
239     if (data && typeof data === "string" && response.statusCode !== 304) {
240       try {
241         parsed = JSON.parse(data)
242       } catch (ex) {
243         ex.message += "\n" + data
244         this.log.verbose("bad json", data)
245         this.log.error("registry", "error parsing json")
246         return cb(ex, null, data, response)
247       }
248     } else if (data) {
249       parsed = data
250       data = JSON.stringify(parsed)
251     }
252
253     // expect data with any error codes
254     if (!data && response.statusCode >= 400) {
255       return cb( response.statusCode + " "
256                + require("http").STATUS_CODES[response.statusCode]
257                , null, data, response )
258     }
259
260     var er = null
261     if (parsed && response.headers.etag) {
262       parsed._etag = response.headers.etag
263     }
264
265     if (parsed && parsed.error && response.statusCode >= 400) {
266       var w = url.parse(where).pathname.substr(1)
267       var name
268       if (!w.match(/^-/) && parsed.error === "not_found") {
269         w = w.split("/")
270         name = w[w.indexOf("_rewrite") + 1]
271         er = new Error("404 Not Found: "+name)
272         er.code = "E404"
273         er.pkgid = name
274       } else {
275         er = new Error(
276           parsed.error + " " + (parsed.reason || "") + ": " + w)
277       }
278     } else if (method !== "HEAD" && method !== "GET") {
279       // invalidate cache
280       // This is irrelevant for commands that do etag caching, but
281       // ls and view also have a timed cache, so this keeps the user
282       // from thinking that it didn't work when it did.
283       // Note that failure is an acceptable option here, since the
284       // only result will be a stale cache for some helper commands.
285       var path = require("path")
286         , p = url.parse(where).pathname.split("/")
287         , _ = "/"
288         , caches = p.map(function (part) {
289             part = part.replace(/:/g, "_")
290             return _ = path.join(_, part)
291           }).map(function (cache) {
292             return path.join(this.conf.get('cache'), cache, ".cache.json")
293           }, this)
294
295       // if the method is DELETE, then also remove the thing itself.
296       // Note that the search index is probably invalid.  Whatever.
297       // That's what you get for deleting stuff.  Don't do that.
298       if (method === "DELETE") {
299         p = p.slice(0, p.indexOf("-rev"))
300         p = p.join("/").replace(/:/g, "_")
301         caches.push(path.join(this.conf.get('cache'), p))
302       }
303
304       asyncMap(caches, rm, function () {})
305     }
306     return cb(er, parsed, data, response)
307   }.bind(this)
308 }