"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const core_1 = require("@opencensus/core");
const httpModule = require("http");
const semver = require("semver");
const shimmer = require("shimmer");
const url = require("url");
const uuid = require("uuid");
const express_1 = require("./express");
class HttpPlugin extends core_1.BasePlugin {
    constructor(moduleName) {
        super(moduleName);
    }
    applyPatch() {
        this.logger.debug('applying patch to %s@%s', this.moduleName, this.version);
        shimmer.wrap(this.moduleExports, 'request', this.getPatchOutgoingRequestFunction());
        if (semver.satisfies(this.version, '>=8.0.0')) {
            shimmer.wrap(this.moduleExports, 'get', () => {
                return function getTrace(options, callback) {
                    const req = httpModule.request(options, callback);
                    req.end();
                    return req;
                };
            });
        }
        if (this.moduleExports && this.moduleExports.Server &&
            this.moduleExports.Server.prototype) {
            shimmer.wrap(this.moduleExports.Server.prototype, 'emit', this.getPatchIncomingRequestFunction());
        }
        else {
            this.logger.error('Could not apply patch to %s.emit. Interface is not as expected.', this.moduleName);
        }
        return this.moduleExports;
    }
    applyUnpatch() {
        shimmer.unwrap(this.moduleExports, 'request');
        if (semver.satisfies(this.version, '>=8.0.0')) {
            shimmer.unwrap(this.moduleExports, 'get');
        }
        if (this.moduleExports && this.moduleExports.Server &&
            this.moduleExports.Server.prototype) {
            shimmer.unwrap(this.moduleExports.Server.prototype, 'emit');
        }
    }
    isIgnored(url, request, list) {
        if (!list) {
            return false;
        }
        for (const pattern of list) {
            if (this.isSatisfyPattern(url, request, pattern)) {
                return true;
            }
        }
        return false;
    }
    isSatisfyPattern(url, request, pattern) {
        if (typeof pattern === 'string') {
            return pattern === url;
        }
        else if (pattern instanceof RegExp) {
            return pattern.test(url);
        }
        else if (typeof pattern === 'function') {
            return pattern(url, request);
        }
        else {
            throw new TypeError('Pattern is in unsupported datatype');
        }
    }
    getPatchIncomingRequestFunction() {
        return (original) => {
            const plugin = this;
            return function incomingRequest(event, ...args) {
                if (event !== 'request') {
                    return original.apply(this, arguments);
                }
                const request = args[0];
                const response = args[1];
                const path = url.parse(request.url).pathname;
                plugin.logger.debug('%s plugin incomingRequest', plugin.moduleName);
                if (plugin.isIgnored(path, request, plugin.options.ignoreIncomingPaths)) {
                    return original.apply(this, arguments);
                }
                const propagation = plugin.tracer.propagation;
                const headers = request.headers;
                const getter = {
                    getHeader(name) {
                        return headers[name];
                    }
                };
                const context = propagation ? propagation.extract(getter) : null;
                const traceOptions = {
                    name: path,
                    kind: core_1.SpanKind.SERVER,
                    spanContext: context !== null ? context : undefined
                };
                return plugin.createSpan(traceOptions, rootSpan => {
                    if (!rootSpan)
                        return original.apply(this, arguments);
                    plugin.tracer.wrapEmitter(request);
                    plugin.tracer.wrapEmitter(response);
                    const originalEnd = response.end;
                    response.end = function () {
                        response.end = originalEnd;
                        const returned = response.end.apply(this, arguments);
                        const requestUrl = url.parse(request.url || 'localhost');
                        const host = headers.host || 'localhost';
                        const userAgent = (headers['user-agent'] || headers['User-Agent']);
                        rootSpan.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_HOST, host.replace(/^(.*)(\:[0-9]{1,5})/, '$1'));
                        rootSpan.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_METHOD, request.method || 'GET');
                        rootSpan.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_PATH, `${requestUrl.pathname}`);
                        let route = `${requestUrl.path}`;
                        const middlewareStack = request[express_1.kMiddlewareStack];
                        if (middlewareStack) {
                            route = middlewareStack
                                .filter(path => path !== '/')
                                .map(path => {
                                return path[0] === '/' ? path : '/' + path;
                            }).join('');
                        }
                        rootSpan.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_ROUTE, route);
                        rootSpan.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_USER_AGENT, userAgent);
                        rootSpan.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, response.statusCode.toString());
                        rootSpan.setStatus(HttpPlugin.convertTraceStatus(response.statusCode));
                        rootSpan.addMessageEvent(core_1.MessageEventType.RECEIVED, uuid.v4().split('-').join(''));
                        rootSpan.end();
                        return returned;
                    };
                    return original.apply(this, arguments);
                });
            };
        };
    }
    getPatchOutgoingRequestFunction() {
        return (original) => {
            const plugin = this;
            const kind = plugin.moduleName === 'https' ? 'HTTPS' : 'HTTP';
            return function outgoingRequest(options, callback) {
                if (!options) {
                    return original.apply(this, arguments);
                }
                let pathname = '';
                let method = 'GET';
                let origin = '';
                if (typeof (options) === 'string') {
                    const parsedUrl = url.parse(options);
                    options = parsedUrl;
                    pathname = parsedUrl.pathname || '/';
                    origin = `${parsedUrl.protocol || 'http:'}//${parsedUrl.host}`;
                }
                else {
                    if (options.headers &&
                        options.headers['x-opencensus-outgoing-request']) {
                        plugin.logger.debug('header with "x-opencensus-outgoing-request" - do not trace');
                        return original.apply(this, arguments);
                    }
                    try {
                        pathname = options.pathname || '';
                        if (pathname.length === 0 && typeof options.path === 'string') {
                            pathname = url.parse(options.path).pathname || '';
                        }
                        method = options.method || 'GET';
                        origin = `${options.protocol || 'http:'}//${options.host}`;
                    }
                    catch (e) {
                        return original.apply(this, arguments);
                    }
                }
                const request = original.apply(this, arguments);
                if (plugin.isIgnored(origin + pathname, request, plugin.options.ignoreOutgoingUrls)) {
                    return request;
                }
                plugin.tracer.wrapEmitter(request);
                plugin.logger.debug('%s plugin outgoingRequest', plugin.moduleName);
                const traceOptions = {
                    name: `${kind.toLowerCase()}-${(method || 'GET').toLowerCase()}`,
                    kind: core_1.SpanKind.CLIENT
                };
                if (!plugin.tracer.currentRootSpan) {
                    plugin.logger.debug('outgoingRequest starting a root span');
                    return plugin.tracer.startRootSpan(traceOptions, plugin.getMakeRequestTraceFunction(request, options, plugin));
                }
                else {
                    plugin.logger.debug('outgoingRequest starting a child span');
                    const span = plugin.tracer.startChildSpan(traceOptions.name, traceOptions.kind);
                    return (plugin.getMakeRequestTraceFunction(request, options, plugin))(span);
                }
            };
        };
    }
    getMakeRequestTraceFunction(request, options, plugin) {
        return (span) => {
            plugin.logger.debug('makeRequestTrace');
            if (!span) {
                plugin.logger.debug('makeRequestTrace span is null');
                return request;
            }
            const setter = {
                setHeader(name, value) {
                    if (plugin.hasExpectHeader(options) && options.headers) {
                        if (options.__cloned !== true) {
                            options = Object.assign({}, options);
                            options.headers = Object.assign({}, options.headers);
                            options.__cloned = true;
                        }
                        options.headers[name] = value;
                    }
                    else {
                        request.setHeader(name, value);
                    }
                }
            };
            const propagation = plugin.tracer.propagation;
            if (propagation) {
                propagation.inject(setter, span.spanContext);
            }
            request.on('response', (response) => {
                plugin.tracer.wrapEmitter(response);
                plugin.logger.debug('outgoingRequest on response()');
                response.on('end', () => {
                    plugin.logger.debug('outgoingRequest on end()');
                    const method = response.method ? response.method : 'GET';
                    const headers = options.headers;
                    const userAgent = headers ? (headers['user-agent'] || headers['User-Agent']) : null;
                    if (options.host || options.hostname) {
                        const value = options.host || options.hostname;
                        span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_HOST, `${value}`);
                    }
                    span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_METHOD, method);
                    span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_PATH, `${options.path}`);
                    span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_ROUTE, `${options.path}`);
                    if (userAgent) {
                        span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_USER_AGENT, userAgent.toString());
                    }
                    span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE, `${response.statusCode}`);
                    span.setStatus(HttpPlugin.convertTraceStatus(response.statusCode || 0));
                    span.addMessageEvent(core_1.MessageEventType.SENT, uuid.v4().split('-').join(''));
                    span.end();
                });
                response.on('error', error => {
                    span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_ERROR_NAME, error.name);
                    span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_ERROR_MESSAGE, error.message);
                    span.setStatus(core_1.CanonicalCode.UNKNOWN);
                    span.end();
                });
            });
            request.on('error', error => {
                span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_ERROR_NAME, error.name);
                span.addAttribute(HttpPlugin.ATTRIBUTE_HTTP_ERROR_MESSAGE, error.message);
                span.setStatus(core_1.CanonicalCode.UNKNOWN);
                span.end();
            });
            plugin.logger.debug('makeRequestTrace return request');
            return request;
        };
    }
    createSpan(options, fn) {
        const forceChildspan = this.options.createSpanWithNet === true;
        if (forceChildspan) {
            const span = this.tracer.startChildSpan(options.name, options.kind);
            return fn(span);
        }
        else {
            return this.tracer.startRootSpan(options, fn);
        }
    }
    static convertTraceStatus(statusCode) {
        if (statusCode < 200 || statusCode > 504) {
            return TraceStatusCodes.UNKNOWN;
        }
        else if (statusCode >= 200 && statusCode < 400) {
            return TraceStatusCodes.OK;
        }
        else {
            switch (statusCode) {
                case (400):
                    return TraceStatusCodes.INVALID_ARGUMENT;
                case (504):
                    return TraceStatusCodes.DEADLINE_EXCEEDED;
                case (404):
                    return TraceStatusCodes.NOT_FOUND;
                case (403):
                    return TraceStatusCodes.PERMISSION_DENIED;
                case (401):
                    return TraceStatusCodes.UNAUTHENTICATED;
                case (429):
                    return TraceStatusCodes.RESOURCE_EXHAUSTED;
                case (501):
                    return TraceStatusCodes.UNIMPLEMENTED;
                case (503):
                    return TraceStatusCodes.UNAVAILABLE;
                default:
                    return TraceStatusCodes.UNKNOWN;
            }
        }
    }
    hasExpectHeader(options) {
        return !!(options.headers &&
            options.headers.Expect);
    }
}
HttpPlugin.ATTRIBUTE_HTTP_HOST = 'http.host';
HttpPlugin.ATTRIBUTE_HTTP_METHOD = 'http.method';
HttpPlugin.ATTRIBUTE_HTTP_PATH = 'http.path';
HttpPlugin.ATTRIBUTE_HTTP_ROUTE = 'http.route';
HttpPlugin.ATTRIBUTE_HTTP_USER_AGENT = 'http.user_agent';
HttpPlugin.ATTRIBUTE_HTTP_STATUS_CODE = 'http.status_code';
HttpPlugin.ATTRIBUTE_HTTP_ERROR_NAME = 'http.error_name';
HttpPlugin.ATTRIBUTE_HTTP_ERROR_MESSAGE = 'http.error_message';
exports.HttpPlugin = HttpPlugin;
var TraceStatusCodes;
(function (TraceStatusCodes) {
    TraceStatusCodes[TraceStatusCodes["UNKNOWN"] = 2] = "UNKNOWN";
    TraceStatusCodes[TraceStatusCodes["OK"] = 0] = "OK";
    TraceStatusCodes[TraceStatusCodes["INVALID_ARGUMENT"] = 3] = "INVALID_ARGUMENT";
    TraceStatusCodes[TraceStatusCodes["DEADLINE_EXCEEDED"] = 4] = "DEADLINE_EXCEEDED";
    TraceStatusCodes[TraceStatusCodes["NOT_FOUND"] = 5] = "NOT_FOUND";
    TraceStatusCodes[TraceStatusCodes["PERMISSION_DENIED"] = 7] = "PERMISSION_DENIED";
    TraceStatusCodes[TraceStatusCodes["UNAUTHENTICATED"] = 16] = "UNAUTHENTICATED";
    TraceStatusCodes[TraceStatusCodes["RESOURCE_EXHAUSTED"] = 8] = "RESOURCE_EXHAUSTED";
    TraceStatusCodes[TraceStatusCodes["UNIMPLEMENTED"] = 12] = "UNIMPLEMENTED";
    TraceStatusCodes[TraceStatusCodes["UNAVAILABLE"] = 14] = "UNAVAILABLE";
})(TraceStatusCodes = exports.TraceStatusCodes || (exports.TraceStatusCodes = {}));
exports.plugin = new HttpPlugin('http');
//# sourceMappingURL=data:application/json;base64,[encoded data, 16328 characters]