1 module.exports = regRequest
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")
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
17 var registry = this.conf.get('registry')
18 if (!registry) return cb(new Error(
19 "No registry url provided: " + method + " " + where))
21 // Since there are multiple places where an error could occur,
22 // don't let the cb be called more than once.
27 cb_.apply(null, arguments)
30 if (where.match(/^\/?favicon.ico/)) {
31 return cb(new Error("favicon.ico isn't a package, it's a picture."))
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
42 // resolve to a full url on the registry
43 if (!where.match(/^https?:\/\//)) {
44 this.log.verbose("url raw", where)
46 var q = where.split("?")
50 if (where.charAt(0) !== "/") where = "/" + where
51 where = "." + where.split("/").map(function (p) {
53 if (p.match(/^org.couchdb.user/)) {
54 return p.replace(/\//g, encodeURIComponent("/"))
56 return encodeURIComponent(p)
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)
64 var remote = url.parse(where)
65 , auth = this.conf.get('_auth')
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)
72 if (!validToken) token = null
73 else this.conf.set('_token', token)
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') }
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)
85 this.conf.set('_token', this.couchLogin.token)
86 return regRequest.call(this,
88 etag, nofollow, reauthed, cb_)
93 // now we either have a valid token, or an auth.
95 if (authRequired && !auth && !token) {
97 "Cannot insert data into the registry without auth"))
100 if (auth && !token && authRequired) {
101 remote.auth = new Buffer(auth, "base64").toString("utf8")
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
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/)) {
122 return cb(er, parsed, raw, response)
125 // Only retry on 408, 5xx or no `response`.
126 var statusCode = response && response.statusCode
128 var reauth = !reauthed &&
129 ( statusCode === 401 ||
130 statusCode === 400 ||
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_)
144 if (er && statusRetry && operation.retry(er)) {
145 self.log.info("retry", "will retry, error on last attempt: " + er)
148 cb.apply(null, arguments)
153 function makeRequest (method, remote, where, what, etag, nofollow, tok, cb_) {
158 cb_.apply(null, arguments)
161 var strict = this.conf.get('strict-ssl')
162 if (strict === undefined) strict = true
163 var opts = { url: remote
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 = {}
172 this.log.verbose("etag", etag)
173 headers[method === "GET" ? "if-none-match" : "if-match"] = etag
177 headers.cookie = 'AuthSession=' + tok.AuthSession
180 headers.accept = "application/json"
182 headers["user-agent"] = this.conf.get('user-agent') ||
183 'node/' + process.version
185 var p = this.conf.get('proxy')
186 var sp = this.conf.get('https-proxy') || p
187 opts.proxy = remote.protocol === "https:" ? sp : p
189 // figure out wth 'what' is
191 if (Buffer.isBuffer(what) || typeof what === "string") {
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
205 opts.followRedirect = false
208 this.log.http(method, remote.href || "/")
210 var done = requestDone.call(this, method, where, cb)
211 var req = request(opts, done)
214 req.on("socket", function (s) {
218 if (what && (what instanceof Stream)) {
223 // cb(er, parsed, raw, response)
224 function requestDone (method, where, cb) {
225 return function (er, response, data) {
226 if (er) return cb(er)
228 var urlObj = url.parse(where)
231 this.log.http(response.statusCode, url.format(urlObj))
235 if (Buffer.isBuffer(data)) {
236 data = data.toString()
239 if (data && typeof data === "string" && response.statusCode !== 304) {
241 parsed = JSON.parse(data)
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)
250 data = JSON.stringify(parsed)
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 )
261 if (parsed && response.headers.etag) {
262 parsed._etag = response.headers.etag
265 if (parsed && parsed.error && response.statusCode >= 400) {
266 var w = url.parse(where).pathname.substr(1)
268 if (!w.match(/^-/) && parsed.error === "not_found") {
270 name = w[w.indexOf("_rewrite") + 1]
271 er = new Error("404 Not Found: "+name)
276 parsed.error + " " + (parsed.reason || "") + ": " + w)
278 } else if (method !== "HEAD" && method !== "GET") {
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("/")
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")
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))
304 asyncMap(caches, rm, function () {})
306 return cb(er, parsed, data, response)