blob: c496441972772a0952efd847f56b71c8dbd66a08 [file] [log] [blame]
var querystring = require('querystring'),
http = require('./http'),
fs = require('fs'),
path = require('path'),
url = require('url'),
stream = require('readable-stream'),
HttpDuplex = require('./http_duplex'),
debug = require('debug')('modem'),
util = require('util'),
splitca = require('split-ca'),
JSONStream = require('JSONStream'),
isWin = require('os').type() === 'Windows_NT';
var defaultOpts = function() {
var split;
var opts = {};
if (!process.env.DOCKER_HOST) {
// Windows socket path: //./pipe/docker_engine ( Windows 10 )
// Linux & Darwin socket path: /var/run/docker.sock
opts.socketPath = isWin ? '//./pipe/docker_engine' : '/var/run/docker.sock';
} else if (process.env.DOCKER_HOST.indexOf('unix://') === 0) {
// Strip off unix://, fall back to default of /var/run/docker.sock if
// unix:// was passed without a path
opts.socketPath = process.env.DOCKER_HOST.substring(7) || '/var/run/docker.sock';
} else {
split = /(?:tcp:\/\/)?(.*?):([0-9]+)/g.exec(process.env.DOCKER_HOST);
if (!split || split.length !== 3) {
throw new Error('DOCKER_HOST env variable should be something like tcp://localhost:1234');
}
opts.port = split[2];
if (process.env.DOCKER_TLS_VERIFY === '1' || opts.port === '2376') {
opts.protocol = 'https';
} else {
opts.protocol = 'http';
}
opts.host = split[1];
if (process.env.DOCKER_CERT_PATH) {
opts.ca = splitca(path.join(process.env.DOCKER_CERT_PATH, 'ca.pem'));
opts.cert = fs.readFileSync(path.join(process.env.DOCKER_CERT_PATH, 'cert.pem'));
opts.key = fs.readFileSync(path.join(process.env.DOCKER_CERT_PATH, 'key.pem'));
}
}
return opts;
};
var Modem = function(opts) {
if (!opts) {
opts = defaultOpts();
}
this.socketPath = opts.socketPath;
this.host = opts.host;
this.port = opts.port;
this.version = opts.version;
this.key = opts.key;
this.cert = opts.cert;
this.ca = opts.ca;
this.timeout = opts.timeout;
this.checkServerIdentity = opts.checkServerIdentity;
if (this.key && this.cert && this.ca) {
this.protocol = 'https';
}
this.protocol = opts.protocol || this.protocol || 'http';
};
Modem.prototype.dial = function(options, callback) {
var opts, address, data;
var self = this;
if (options.options) {
opts = options.options;
}
// Prevent credentials from showing up in URL
if (opts && opts.authconfig) {
delete opts.authconfig;
}
if (this.version) {
options.path = '/' + this.version + options.path;
}
if (this.host) {
var parsed = url.parse(self.host);
address = url.format({
'protocol': parsed.protocol || self.protocol,
'hostname': parsed.hostname || self.host,
'port': self.port
});
address = url.resolve(address, options.path);
} else {
address = options.path;
}
if (options.path.indexOf('?') !== -1) {
if (opts && Object.keys(opts).length > 0) {
address += this.buildQuerystring(opts._query || opts);
} else {
address = address.substring(0, address.length - 1);
}
}
var optionsf = {
path: address,
method: options.method,
headers: options.headers || {},
key: self.key,
cert: self.cert,
ca: self.ca
};
if (this.checkServerIdentity) {
optionsf.checkServerIdentity = this.checkServerIdentity;
}
if (options.authconfig) {
optionsf.headers['X-Registry-Auth'] = options.authconfig.key || options.authconfig.base64 ||
new Buffer(JSON.stringify(options.authconfig)).toString('base64');
}
if (options.registryconfig) {
optionsf.headers['X-Registry-Config'] = options.registryconfig.base64 ||
new Buffer(JSON.stringify(options.registryconfig)).toString('base64');
}
if (options.file) {
if (typeof options.file === 'string') {
data = fs.readFileSync(path.resolve(options.file));
} else {
data = options.file;
}
optionsf.headers['Content-Type'] = 'application/tar';
} else if (opts && options.method === 'POST') {
data = JSON.stringify(opts._body || opts);
optionsf.headers['Content-Type'] = 'application/json';
}
if (typeof data === "string") {
optionsf.headers['Content-Length'] = Buffer.byteLength(data);
} else if (Buffer.isBuffer(data) === true) {
optionsf.headers['Content-Length'] = data.length;
} else {
optionsf.headers['Transfer-Encoding'] = 'chunked';
}
if (options.hijack) {
optionsf.headers.Connection = 'Upgrade';
optionsf.headers.Upgrade = 'tcp';
}
if (this.socketPath) {
optionsf.socketPath = this.socketPath;
} else {
var urlp = url.parse(address);
optionsf.hostname = urlp.hostname;
optionsf.port = urlp.port;
optionsf.path = urlp.path;
}
this.buildRequest(optionsf, options, data, callback);
};
Modem.prototype.buildRequest = function(options, context, data, callback) {
var self = this;
var req = http[self.protocol].request(options, function() {});
debug('Sending: %s', util.inspect(options, {
showHidden: true,
depth: null
}));
if (self.timeout) {
req.on('socket', function(socket) {
socket.setTimeout(self.timeout);
socket.on('timeout', function() {
debug('Timeout of %s ms exceeded', self.timeout);
req.abort();
});
});
}
if (context.hijack === true) {
req.on('upgrade', function(res, sock, head) {
return callback(null, sock);
});
}
req.on('response', function(res) {
if (context.isStream === true) {
self.buildPayload(null, context.isStream, context.statusCodes, context.openStdin, req, res, null, callback);
} else {
var chunks = '';
res.on('data', function(chunk) {
chunks += chunk;
});
res.on('end', function() {
debug('Received: %s', chunks);
var json;
try {
json = JSON.parse(chunks);
} catch (e) {
json = chunks;
}
self.buildPayload(null, context.isStream, context.statusCodes, false, req, res, json, callback);
});
}
});
req.on('error', function(error) {
self.buildPayload(error, context.isStream, context.statusCodes, false, {}, {}, null, callback);
});
if (typeof data === "string" || Buffer.isBuffer(data)) {
req.write(data);
} else if (data) {
data.pipe(req);
}
if (!context.hijack && !context.openStdin && (typeof data === "string" || data === undefined || Buffer.isBuffer(data))) {
req.end();
}
};
Modem.prototype.buildPayload = function(err, isStream, statusCodes, openStdin, req, res, json, cb) {
if (err) return cb(err, null);
if (statusCodes[res.statusCode] !== true) {
getCause(isStream, res, json, function(err, cause) {
var msg = new Error(
'(HTTP code ' + res.statusCode + ') ' +
(statusCodes[res.statusCode] || 'unexpected') + ' - ' +
(cause.message || cause) + ' '
);
msg.reason = statusCodes[res.statusCode];
msg.statusCode = res.statusCode;
msg.json = json;
cb(msg, null);
});
} else {
if (openStdin) {
cb(null, new HttpDuplex(req, res));
} else if (isStream) {
cb(null, res);
} else {
cb(null, json);
}
}
function getCause(isStream, res, json, callback) {
var chunks = '';
if (isStream) {
res.on('data', function(chunk) {
chunks += chunk;
});
res.on('end', function() {
callback(null, chunks);
});
} else {
callback(null, json);
}
}
};
Modem.prototype.demuxStream = function(stream, stdout, stderr) {
var header = null;
stream.on('readable', function() {
header = header || stream.read(8);
while (header !== null) {
var type = header.readUInt8(0);
var payload = stream.read(header.readUInt32BE(4));
if (payload === null) break;
if (type == 2) {
stderr.write(payload);
} else {
stdout.write(payload);
}
header = stream.read(8);
}
});
};
Modem.prototype.followProgress = function(stream, onFinished, onProgress) {
var parser = JSONStream.parse(),
output = [];
parser.on('root', onStreamEvent);
parser.on('error', onStreamError);
parser.on('end', onStreamEnd);
stream.pipe(parser);
function onStreamEvent(evt) {
if (!(evt instanceof Object)) {
evt = {};
}
output.push(evt);
if (evt.error) {
return onStreamError(evt.error);
}
if (onProgress) {
onProgress(evt);
}
}
function onStreamError(err) {
parser.removeListener('root', onStreamEvent);
parser.removeListener('error', onStreamError);
parser.removeListener('end', onStreamEnd);
onFinished(err, output);
}
function onStreamEnd() {
onFinished(null, output);
}
};
Modem.prototype.buildQuerystring = function(opts) {
var clone = {};
// serialize map values as JSON strings, else querystring truncates.
Object.keys(opts).map(function(key, i) {
clone[key] = (opts[key] && typeof opts[key] === 'object') ?
JSON.stringify(opts[key]) : opts[key];
});
return querystring.stringify(clone);
};
module.exports = Modem;