ff45e0e8eb0863f92e50d5a7c7e4e37b57a9c078
[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) {
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              , strictSSL: strict }
167     , headers = opts.headers = {}
168   if (etag) {
169     this.log.verbose("etag", etag)
170     headers[method === "GET" ? "if-none-match" : "if-match"] = etag
171   }
172
173   if (tok) {
174     headers.cookie = 'AuthSession=' + tok.AuthSession
175   }
176
177   headers.accept = "application/json"
178
179   headers["user-agent"] = this.conf.get('user-agent') ||
180                           'node/' + process.version
181
182   var p = this.conf.get('proxy')
183   var sp = this.conf.get('https-proxy') || p
184   opts.proxy = remote.protocol === "https:" ? sp : p
185
186   // figure out wth 'what' is
187   if (what) {
188     if (Buffer.isBuffer(what) || typeof what === "string") {
189       opts.body = what
190       headers["content-type"] = "application/json"
191       headers["content-length"] = Buffer.byteLength(what)
192     } else if (what instanceof Stream) {
193       headers["content-type"] = "application/octet-stream"
194       if (what.size) headers["content-length"] = what.size
195     } else {
196       delete what._etag
197       opts.json = what
198     }
199   }
200
201   if (nofollow) {
202     opts.followRedirect = false
203   }
204
205   this.log.http(method, remote.href || "/")
206
207   var done = requestDone.call(this, method, where, cb)
208   var req = request(opts, done)
209
210   req.on("error", cb)
211   req.on("socket", function (s) {
212     s.on("error", cb)
213   })
214
215   if (what && (what instanceof Stream)) {
216     what.pipe(req)
217   }
218 }
219
220 // cb(er, parsed, raw, response)
221 function requestDone (method, where, cb) {
222   return function (er, response, data) {
223     if (er) return cb(er)
224
225     var urlObj = url.parse(where)
226     if (urlObj.auth)
227       urlObj.auth = '***'
228     this.log.http(response.statusCode, url.format(urlObj))
229
230     var parsed
231
232     if (Buffer.isBuffer(data)) {
233       data = data.toString()
234     }
235
236     if (data && typeof data === "string" && response.statusCode !== 304) {
237       try {
238         parsed = JSON.parse(data)
239       } catch (ex) {
240         ex.message += "\n" + data
241         this.log.verbose("bad json", data)
242         this.log.error("registry", "error parsing json")
243         return cb(ex, null, data, response)
244       }
245     } else if (data) {
246       parsed = data
247       data = JSON.stringify(parsed)
248     }
249
250     // expect data with any error codes
251     if (!data && response.statusCode >= 400) {
252       return cb( response.statusCode + " "
253                + require("http").STATUS_CODES[response.statusCode]
254                , null, data, response )
255     }
256
257     var er = null
258     if (parsed && response.headers.etag) {
259       parsed._etag = response.headers.etag
260     }
261
262     if (parsed && parsed.error && response.statusCode >= 400) {
263       var w = url.parse(where).pathname.substr(1)
264       var name
265       if (!w.match(/^-/) && parsed.error === "not_found") {
266         w = w.split("/")
267         name = w[w.indexOf("_rewrite") + 1]
268         er = new Error("404 Not Found: "+name)
269         er.code = "E404"
270         er.pkgid = name
271       } else {
272         er = new Error(
273           parsed.error + " " + (parsed.reason || "") + ": " + w)
274       }
275     } else if (method !== "HEAD" && method !== "GET") {
276       // invalidate cache
277       // This is irrelevant for commands that do etag caching, but
278       // ls and view also have a timed cache, so this keeps the user
279       // from thinking that it didn't work when it did.
280       // Note that failure is an acceptable option here, since the
281       // only result will be a stale cache for some helper commands.
282       var path = require("path")
283         , p = url.parse(where).pathname.split("/")
284         , _ = "/"
285         , caches = p.map(function (part) {
286             part = part.replace(/:/g, "_")
287             return _ = path.join(_, part)
288           }).map(function (cache) {
289             return path.join(this.conf.get('cache'), cache, ".cache.json")
290           }, this)
291
292       // if the method is DELETE, then also remove the thing itself.
293       // Note that the search index is probably invalid.  Whatever.
294       // That's what you get for deleting stuff.  Don't do that.
295       if (method === "DELETE") {
296         p = p.slice(0, p.indexOf("-rev"))
297         p = p.join("/").replace(/:/g, "_")
298         caches.push(path.join(this.conf.get('cache'), p))
299       }
300
301       asyncMap(caches, rm, function () {})
302     }
303     return cb(er, parsed, data, response)
304   }.bind(this)
305 }