"use strict";

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.AutoDiscovery = undefined;

var _bluebird = require("bluebird");

var _bluebird2 = _interopRequireDefault(_bluebird);

var _regenerator = require("babel-runtime/regenerator");

var _regenerator2 = _interopRequireDefault(_regenerator);

var _getIterator2 = require("babel-runtime/core-js/get-iterator");

var _getIterator3 = _interopRequireDefault(_getIterator2);

var _keys = require("babel-runtime/core-js/object/keys");

var _keys2 = _interopRequireDefault(_keys);

var _createClass2 = require("babel-runtime/helpers/createClass");

var _createClass3 = _interopRequireDefault(_createClass2);

var _classCallCheck2 = require("babel-runtime/helpers/classCallCheck");

var _classCallCheck3 = _interopRequireDefault(_classCallCheck2);

var _url = require("url");

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

var logger = require("./logger"); /*
                                  Copyright 2018 New Vector Ltd
                                  
                                  Licensed under the Apache License, Version 2.0 (the "License");
                                  you may not use this file except in compliance with the License.
                                  You may obtain a copy of the License at
                                  
                                      http://www.apache.org/licenses/LICENSE-2.0
                                  
                                  Unless required by applicable law or agreed to in writing, software
                                  distributed under the License is distributed on an "AS IS" BASIS,
                                  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
                                  See the License for the specific language governing permissions and
                                  limitations under the License.
                                  */

/** @module auto-discovery */

// Dev note: Auto discovery is part of the spec.
// See: https://matrix.org/docs/spec/client_server/r0.4.0.html#server-discovery

/**
 * Description for what an automatically discovered client configuration
 * would look like. Although this is a class, it is recommended that it
 * be treated as an interface definition rather than as a class.
 *
 * Additional properties than those defined here may be present, and
 * should follow the Java package naming convention.
 */
var DiscoveredClientConfig = // eslint-disable-line no-unused-vars
// Dev note: this is basically a copy/paste of the .well-known response
// object as defined in the spec. It does have additional information,
// however. Overall, this exists to serve as a place for documentation
// and not functionality.
// See https://matrix.org/docs/spec/client_server/r0.4.0.html#get-well-known-matrix-client

function DiscoveredClientConfig() {
    (0, _classCallCheck3.default)(this, DiscoveredClientConfig);

    /**
     * The homeserver configuration the client should use. This will
     * always be present on the object.
     * @type {{state: string, base_url: string}} The configuration.
     */
    this["m.homeserver"] = {
        /**
         * The lookup result state. If this is anything other than
         * AutoDiscovery.SUCCESS then base_url may be falsey. Additionally,
         * if this is not AutoDiscovery.SUCCESS then the client should
         * assume the other properties in the client config (such as
         * the identity server configuration) are not valid.
         */
        state: AutoDiscovery.PROMPT,

        /**
         * If the state is AutoDiscovery.FAIL_ERROR or .FAIL_PROMPT
         * then this will contain a human-readable (English) message
         * for what went wrong. If the state is none of those previously
         * mentioned, this will be falsey.
         */
        error: "Something went wrong",

        /**
         * The base URL clients should use to talk to the homeserver,
         * particularly for the login process. May be falsey if the
         * state is not AutoDiscovery.SUCCESS.
         */
        base_url: "https://matrix.org"
    };

    /**
     * The identity server configuration the client should use. This
     * will always be present on teh object.
     * @type {{state: string, base_url: string}} The configuration.
     */
    this["m.identity_server"] = {
        /**
         * The lookup result state. If this is anything other than
         * AutoDiscovery.SUCCESS then base_url may be falsey.
         */
        state: AutoDiscovery.PROMPT,

        /**
         * The base URL clients should use for interacting with the
         * identity server. May be falsey if the state is not
         * AutoDiscovery.SUCCESS.
         */
        base_url: "https://vector.im"
    };
};

/**
 * Utilities for automatically discovery resources, such as homeservers
 * for users to log in to.
 */


var AutoDiscovery = exports.AutoDiscovery = function () {
    function AutoDiscovery() {
        (0, _classCallCheck3.default)(this, AutoDiscovery);
    }

    (0, _createClass3.default)(AutoDiscovery, null, [{
        key: "fromDiscoveryConfig",


        /**
         * Validates and verifies client configuration information for purposes
         * of logging in. Such information includes the homeserver URL
         * and identity server URL the client would want. Additional details
         * may also be included, and will be transparently brought into the
         * response object unaltered.
         * @param {string} wellknown The configuration object itself, as returned
         * by the .well-known auto-discovery endpoint.
         * @return {Promise<DiscoveredClientConfig>} Resolves to the verified
         * configuration, which may include error states. Rejects on unexpected
         * failure, not when verification fails.
         */
        value: function () {
            var _ref = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee(wellknown) {
                var clientConfig, hsUrl, hsVersions, isUrl, failingClientConfig, isResponse;
                return _regenerator2.default.wrap(function _callee$(_context) {
                    while (1) {
                        switch (_context.prev = _context.next) {
                            case 0:
                                // Step 1 is to get the config, which is provided to us here.

                                // We default to an error state to make the first few checks easier to
                                // write. We'll update the properties of this object over the duration
                                // of this function.
                                clientConfig = {
                                    "m.homeserver": {
                                        state: AutoDiscovery.FAIL_ERROR,
                                        error: AutoDiscovery.ERROR_INVALID,
                                        base_url: null
                                    },
                                    "m.identity_server": {
                                        // Technically, we don't have a problem with the identity server
                                        // config at this point.
                                        state: AutoDiscovery.PROMPT,
                                        error: null,
                                        base_url: null
                                    }
                                };

                                if (!(!wellknown || !wellknown["m.homeserver"])) {
                                    _context.next = 6;
                                    break;
                                }

                                logger.error("No m.homeserver key in config");

                                clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
                                clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;

                                return _context.abrupt("return", _bluebird2.default.resolve(clientConfig));

                            case 6:
                                if (wellknown["m.homeserver"]["base_url"]) {
                                    _context.next = 11;
                                    break;
                                }

                                logger.error("No m.homeserver base_url in config");

                                clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
                                clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;

                                return _context.abrupt("return", _bluebird2.default.resolve(clientConfig));

                            case 11:

                                // Step 2: Make sure the homeserver URL is valid *looking*. We'll make
                                // sure it points to a homeserver in Step 3.
                                hsUrl = this._sanitizeWellKnownUrl(wellknown["m.homeserver"]["base_url"]);

                                if (hsUrl) {
                                    _context.next = 16;
                                    break;
                                }

                                logger.error("Invalid base_url for m.homeserver");
                                clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HS_BASE_URL;
                                return _context.abrupt("return", _bluebird2.default.resolve(clientConfig));

                            case 16:
                                _context.next = 18;
                                return (0, _bluebird.resolve)(this._fetchWellKnownObject(hsUrl + "/_matrix/client/versions"));

                            case 18:
                                hsVersions = _context.sent;

                                if (!(!hsVersions || !hsVersions.raw["versions"])) {
                                    _context.next = 23;
                                    break;
                                }

                                logger.error("Invalid /versions response");
                                clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID_HOMESERVER;
                                return _context.abrupt("return", _bluebird2.default.resolve(clientConfig));

                            case 23:

                                // Step 4: Now that the homeserver looks valid, update our client config.
                                clientConfig["m.homeserver"] = {
                                    state: AutoDiscovery.SUCCESS,
                                    error: null,
                                    base_url: hsUrl
                                };

                                // Step 5: Try to pull out the identity server configuration
                                isUrl = "";

                                if (!wellknown["m.identity_server"]) {
                                    _context.next = 39;
                                    break;
                                }

                                // We prepare a failing identity server response to save lines later
                                // in this branch. Note that we also fail the homeserver check in the
                                // object because according to the spec we're supposed to FAIL_ERROR
                                // if *anything* goes wrong with the IS validation, including invalid
                                // format. This means we're supposed to stop discovery completely.
                                failingClientConfig = {
                                    "m.homeserver": {
                                        state: AutoDiscovery.FAIL_ERROR,
                                        error: AutoDiscovery.ERROR_INVALID_IS,

                                        // We'll provide the base_url that was previously valid for
                                        // debugging purposes.
                                        base_url: clientConfig["m.homeserver"].base_url
                                    },
                                    "m.identity_server": {
                                        state: AutoDiscovery.FAIL_ERROR,
                                        error: AutoDiscovery.ERROR_INVALID_IS,
                                        base_url: null
                                    }
                                };

                                // Step 5a: Make sure the URL is valid *looking*. We'll make sure it
                                // points to an identity server in Step 5b.

                                isUrl = this._sanitizeWellKnownUrl(wellknown["m.identity_server"]["base_url"]);

                                if (isUrl) {
                                    _context.next = 32;
                                    break;
                                }

                                logger.error("Invalid base_url for m.identity_server");
                                failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IS_BASE_URL;
                                return _context.abrupt("return", _bluebird2.default.resolve(failingClientConfig));

                            case 32:
                                _context.next = 34;
                                return (0, _bluebird.resolve)(this._fetchWellKnownObject(isUrl + "/_matrix/identity/api/v1"));

                            case 34:
                                isResponse = _context.sent;

                                if (!(!isResponse || !isResponse.raw || isResponse.action !== "SUCCESS")) {
                                    _context.next = 39;
                                    break;
                                }

                                logger.error("Invalid /api/v1 response");
                                failingClientConfig["m.identity_server"].error = AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER;
                                return _context.abrupt("return", _bluebird2.default.resolve(failingClientConfig));

                            case 39:

                                // Step 6: Now that the identity server is valid, or never existed,
                                // populate the IS section.
                                if (isUrl && isUrl.length > 0) {
                                    clientConfig["m.identity_server"] = {
                                        state: AutoDiscovery.SUCCESS,
                                        error: null,
                                        base_url: isUrl
                                    };
                                }

                                // Step 7: Copy any other keys directly into the clientConfig. This is for
                                // things like custom configuration of services.
                                (0, _keys2.default)(wellknown).map(function (k) {
                                    if (k === "m.homeserver" || k === "m.identity_server") {
                                        // Only copy selected parts of the config to avoid overwriting
                                        // properties computed by the validation logic above.
                                        var notProps = ["error", "state", "base_url"];
                                        var _iteratorNormalCompletion = true;
                                        var _didIteratorError = false;
                                        var _iteratorError = undefined;

                                        try {
                                            for (var _iterator = (0, _getIterator3.default)((0, _keys2.default)(wellknown[k])), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
                                                var prop = _step.value;

                                                if (notProps.includes(prop)) continue;
                                                clientConfig[k][prop] = wellknown[k][prop];
                                            }
                                        } catch (err) {
                                            _didIteratorError = true;
                                            _iteratorError = err;
                                        } finally {
                                            try {
                                                if (!_iteratorNormalCompletion && _iterator.return) {
                                                    _iterator.return();
                                                }
                                            } finally {
                                                if (_didIteratorError) {
                                                    throw _iteratorError;
                                                }
                                            }
                                        }
                                    } else {
                                        // Just copy the whole thing over otherwise
                                        clientConfig[k] = wellknown[k];
                                    }
                                });

                                // Step 8: Give the config to the caller (finally)
                                return _context.abrupt("return", _bluebird2.default.resolve(clientConfig));

                            case 42:
                            case "end":
                                return _context.stop();
                        }
                    }
                }, _callee, this);
            }));

            function fromDiscoveryConfig(_x) {
                return _ref.apply(this, arguments);
            }

            return fromDiscoveryConfig;
        }()

        /**
         * Attempts to automatically discover client configuration information
         * prior to logging in. Such information includes the homeserver URL
         * and identity server URL the client would want. Additional details
         * may also be discovered, and will be transparently included in the
         * response object unaltered.
         * @param {string} domain The homeserver domain to perform discovery
         * on. For example, "matrix.org".
         * @return {Promise<DiscoveredClientConfig>} Resolves to the discovered
         * configuration, which may include error states. Rejects on unexpected
         * failure, not when discovery fails.
         */

    }, {
        key: "findClientConfig",
        value: function () {
            var _ref2 = (0, _bluebird.coroutine)( /*#__PURE__*/_regenerator2.default.mark(function _callee2(domain) {
                var clientConfig, wellknown;
                return _regenerator2.default.wrap(function _callee2$(_context2) {
                    while (1) {
                        switch (_context2.prev = _context2.next) {
                            case 0:
                                if (!(!domain || typeof domain !== "string" || domain.length === 0)) {
                                    _context2.next = 2;
                                    break;
                                }

                                throw new Error("'domain' must be a string of non-zero length");

                            case 2:

                                // We use a .well-known lookup for all cases. According to the spec, we
                                // can do other discovery mechanisms if we want such as custom lookups
                                // however we won't bother with that here (mostly because the spec only
                                // supports .well-known right now).
                                //
                                // By using .well-known, we need to ensure we at least pull out a URL
                                // for the homeserver. We don't really need an identity server configuration
                                // but will return one anyways (with state PROMPT) to make development
                                // easier for clients. If we can't get a homeserver URL, all bets are
                                // off on the rest of the config and we'll assume it is invalid too.

                                // We default to an error state to make the first few checks easier to
                                // write. We'll update the properties of this object over the duration
                                // of this function.
                                clientConfig = {
                                    "m.homeserver": {
                                        state: AutoDiscovery.FAIL_ERROR,
                                        error: AutoDiscovery.ERROR_INVALID,
                                        base_url: null
                                    },
                                    "m.identity_server": {
                                        // Technically, we don't have a problem with the identity server
                                        // config at this point.
                                        state: AutoDiscovery.PROMPT,
                                        error: null,
                                        base_url: null
                                    }
                                };

                                // Step 1: Actually request the .well-known JSON file and make sure it
                                // at least has a homeserver definition.

                                _context2.next = 5;
                                return (0, _bluebird.resolve)(this._fetchWellKnownObject("https://" + domain + "/.well-known/matrix/client"));

                            case 5:
                                wellknown = _context2.sent;

                                if (!(!wellknown || wellknown.action !== "SUCCESS")) {
                                    _context2.next = 11;
                                    break;
                                }

                                logger.error("No response or error when parsing .well-known");
                                if (wellknown.reason) logger.error(wellknown.reason);
                                if (wellknown.action === "IGNORE") {
                                    clientConfig["m.homeserver"] = {
                                        state: AutoDiscovery.PROMPT,
                                        error: null,
                                        base_url: null
                                    };
                                } else {
                                    // this can only ever be FAIL_PROMPT at this point.
                                    clientConfig["m.homeserver"].state = AutoDiscovery.FAIL_PROMPT;
                                    clientConfig["m.homeserver"].error = AutoDiscovery.ERROR_INVALID;
                                }
                                return _context2.abrupt("return", _bluebird2.default.resolve(clientConfig));

                            case 11:
                                return _context2.abrupt("return", AutoDiscovery.fromDiscoveryConfig(wellknown.raw));

                            case 12:
                            case "end":
                                return _context2.stop();
                        }
                    }
                }, _callee2, this);
            }));

            function findClientConfig(_x2) {
                return _ref2.apply(this, arguments);
            }

            return findClientConfig;
        }()

        /**
         * Sanitizes a given URL to ensure it is either an HTTP or HTTP URL and
         * is suitable for the requirements laid out by .well-known auto discovery.
         * If valid, the URL will also be stripped of any trailing slashes.
         * @param {string} url The potentially invalid URL to sanitize.
         * @return {string|boolean} The sanitized URL or a falsey value if the URL is invalid.
         * @private
         */

    }, {
        key: "_sanitizeWellKnownUrl",
        value: function _sanitizeWellKnownUrl(url) {
            if (!url) return false;

            try {
                // We have to try and parse the URL using the NodeJS URL
                // library if we're on NodeJS and use the browser's URL
                // library when we're in a browser. To accomplish this, we
                // try the NodeJS version first and fall back to the browser.
                var parsed = null;
                try {
                    if (_url.URL) parsed = new _url.URL(url);else parsed = new URL(url);
                } catch (e) {
                    parsed = new URL(url);
                }

                if (!parsed || !parsed.hostname) return false;
                if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return false;

                var port = parsed.port ? ":" + parsed.port : "";
                var path = parsed.pathname ? parsed.pathname : "";
                var saferUrl = parsed.protocol + "//" + parsed.hostname + port + path;
                if (saferUrl.endsWith("/")) {
                    saferUrl = saferUrl.substring(0, saferUrl.length - 1);
                }
                return saferUrl;
            } catch (e) {
                logger.error(e);
                return false;
            }
        }

        /**
         * Fetches a JSON object from a given URL, as expected by all .well-known
         * related lookups. If the server gives a 404 then the `action` will be
         * IGNORE. If the server returns something that isn't JSON, the `action`
         * will be FAIL_PROMPT. For any other failure the `action` will be FAIL_PROMPT.
         *
         * The returned object will be a result of the call in object form with
         * the following properties:
         *   raw: The JSON object returned by the server.
         *   action: One of SUCCESS, IGNORE, or FAIL_PROMPT.
         *   reason: Relatively human readable description of what went wrong.
         *   error: The actual Error, if one exists.
         * @param {string} url The URL to fetch a JSON object from.
         * @return {Promise<object>} Resolves to the returned state.
         * @private
         */

    }, {
        key: "_fetchWellKnownObject",
        value: function () {
            var _ref3 = (0, _bluebird.method)(function (url) {
                return new _bluebird2.default(function (resolve, reject) {
                    var request = require("./matrix").getRequest();
                    if (!request) throw new Error("No request library available");
                    request({ method: "GET", uri: url, timeout: 5000 }, function (err, response, body) {
                        if (err || response.statusCode < 200 || response.statusCode >= 300) {
                            var action = "FAIL_PROMPT";
                            var reason = (err ? err.message : null) || "General failure";
                            if (response.statusCode === 404) {
                                action = "IGNORE";
                                reason = AutoDiscovery.ERROR_MISSING_WELLKNOWN;
                            }
                            resolve({ raw: {}, action: action, reason: reason, error: err });
                            return;
                        }

                        try {
                            resolve({ raw: JSON.parse(body), action: "SUCCESS" });
                        } catch (e) {
                            var _reason = AutoDiscovery.ERROR_INVALID;
                            if (e.name === "SyntaxError") {
                                _reason = AutoDiscovery.ERROR_INVALID_JSON;
                            }
                            resolve({
                                raw: {},
                                action: "FAIL_PROMPT",
                                reason: _reason,
                                error: e
                            });
                        }
                    });
                });
            });

            function _fetchWellKnownObject(_x3) {
                return _ref3.apply(this, arguments);
            }

            return _fetchWellKnownObject;
        }()
    }, {
        key: "ERROR_INVALID",

        // Dev note: the constants defined here are related to but not
        // exactly the same as those in the spec. This is to hopefully
        // translate the meaning of the states in the spec, but also
        // support our own if needed.

        get: function get() {
            return "Invalid homeserver discovery response";
        }
    }, {
        key: "ERROR_GENERIC_FAILURE",
        get: function get() {
            return "Failed to get autodiscovery configuration from server";
        }
    }, {
        key: "ERROR_INVALID_HS_BASE_URL",
        get: function get() {
            return "Invalid base_url for m.homeserver";
        }
    }, {
        key: "ERROR_INVALID_HOMESERVER",
        get: function get() {
            return "Homeserver URL does not appear to be a valid Matrix homeserver";
        }
    }, {
        key: "ERROR_INVALID_IS_BASE_URL",
        get: function get() {
            return "Invalid base_url for m.identity_server";
        }
    }, {
        key: "ERROR_INVALID_IDENTITY_SERVER",
        get: function get() {
            return "Identity server URL does not appear to be a valid identity server";
        }
    }, {
        key: "ERROR_INVALID_IS",
        get: function get() {
            return "Invalid identity server discovery response";
        }
    }, {
        key: "ERROR_MISSING_WELLKNOWN",
        get: function get() {
            return "No .well-known JSON file found";
        }
    }, {
        key: "ERROR_INVALID_JSON",
        get: function get() {
            return "Invalid JSON";
        }
    }, {
        key: "ALL_ERRORS",
        get: function get() {
            return [AutoDiscovery.ERROR_INVALID, AutoDiscovery.ERROR_GENERIC_FAILURE, AutoDiscovery.ERROR_INVALID_HS_BASE_URL, AutoDiscovery.ERROR_INVALID_HOMESERVER, AutoDiscovery.ERROR_INVALID_IS_BASE_URL, AutoDiscovery.ERROR_INVALID_IDENTITY_SERVER, AutoDiscovery.ERROR_INVALID_IS, AutoDiscovery.ERROR_MISSING_WELLKNOWN, AutoDiscovery.ERROR_INVALID_JSON];
        }

        /**
         * The auto discovery failed. The client is expected to communicate
         * the error to the user and refuse logging in.
         * @return {string}
         * @constructor
         */

    }, {
        key: "FAIL_ERROR",
        get: function get() {
            return "FAIL_ERROR";
        }

        /**
         * The auto discovery failed, however the client may still recover
         * from the problem. The client is recommended to that the same
         * action it would for PROMPT while also warning the user about
         * what went wrong. The client may also treat this the same as
         * a FAIL_ERROR state.
         * @return {string}
         * @constructor
         */

    }, {
        key: "FAIL_PROMPT",
        get: function get() {
            return "FAIL_PROMPT";
        }

        /**
         * The auto discovery didn't fail but did not find anything of
         * interest. The client is expected to prompt the user for more
         * information, or fail if it prefers.
         * @return {string}
         * @constructor
         */

    }, {
        key: "PROMPT",
        get: function get() {
            return "PROMPT";
        }

        /**
         * The auto discovery was successful.
         * @return {string}
         * @constructor
         */

    }, {
        key: "SUCCESS",
        get: function get() {
            return "SUCCESS";
        }
    }]);
    return AutoDiscovery;
}();
//# sourceMappingURL=autodiscovery.js.map