Twitch Midway Gateway

Auto-renew token from Midway Gateway on XHR requests and perform background fetches every 5 minutes to avoid token timeout issues

// ==UserScript==
// @name         Twitch Midway Gateway
// @namespace    http://devinfra.internal.justin.tv/
// @version      0.1
// @description  Auto-renew token from Midway Gateway on XHR requests and perform background fetches every 5 minutes to avoid token timeout issues
// @author       hvr
// @match        https://git.xarth.tv/*
// @grant        none
// ==/UserScript==

(function() {
    var method;
    var noop = function () {};
    var methods = [
        'assert', 'clear', 'count', 'debug', 'dir', 'dirxml', 'error',
        'exception', 'group', 'groupCollapsed', 'groupEnd', 'info', 'log',
        'markTimeline', 'profile', 'profileEnd', 'table', 'time', 'timeEnd',
        'timeline', 'timelineEnd', 'timeStamp', 'trace', 'warn'
    ];
    var length = methods.length;
    var console = (window.console = window.console || {});

    while (length--) {
        method = methods[length];

        // Only stub undefined methods.
        if (!console[method]) {
            console[method] = noop;
        }
    }
}());

(function(window, undefined) {
    var $ = window.jQuery;
    var namespace = function() {
        window.Amazon = window.Amazon || {};
        window.Amazon.IDP = window.Amazon.IDP || {};
        window.Amazon.IDP.config = window.Amazon.IDP.config || {};
        window.Amazon.IDP.internal = window.Amazon.IDP.internal || {};
        return window.Amazon.IDP;
    }();

    // A factory for the browser's native XHR. Initialize to a plain-old function returning a new XHR.
    // We'll overwrite appropriately later on in this function if we override the XHR.prototype (we usually do).
    var nativeXhrFactory = function() {
        return new XMLHttpRequest();
    };

    // A place to hold a user-defined blacklist that specifies domains that should be omitted from authentication checks.
    // If an Ajax call is being made to a domain in this blacklist, then performAuthenticationSteps should not be called.
    // This is useful in situations where you want to use OpenID for the most part, but there's one domain or two that you call
    // that doesn't use OpenID authentication, and for your purposes you want to avoid 404s to /sso/login of that domain.
    // Properties:
    // -domains: an array of domains the client has elected to omit from authentication. Of the form "foo.amazon.com[:port]".
    // -matchesUrl: function(url) -> true/false. Check if the domain for the given URL is in the blacklist.
    var domainBlacklist = function() {
        var self = {domains: []};
        var cfg = Amazon.IDP.config.excludeDomains;
        if (cfg && (cfg instanceof Array)) {
            self.domains = cfg;
        }
        self.matchesUrl = function(url) {
            if (self.domains.length == 0) { return false; }
            var domain = getUrlProperties(url).host; //.host returns the hostname + port
            for (var i = 0; i < self.domains.length; i++) { // Looping through instead of using indexOf since IE8 does not support it.
                                                            // List should be small enough that doing this does cause a performance problem.
                if (domain == self.domains[i]) {
                    return true;
                }
            }
            return false;
        };
        return self;
    }();

    // A placeholder for a user-defined function that decides whether a call should be authenticated or not.
    // This is meant to be a catch-all for any esoteric cases where a client needs to exclude certain paths/urls/calls/etc
    // from authentication, but defaultOff and domainBlacklist won't satisfy their use case. So they can hook in here and
    // decide if they want to cancel authentication given the arguments provided to XMLHttpRequest.open().
    // Amazon.IDP.config.shouldAuthenticate = function(xhrArgs) : return true/false, where xhrArgs is an array of arguments
    // passed into xhr.open();
    var shouldAuthenticateHook = function() {
        var cfg = Amazon.IDP.config.shouldAuthenticate;
        if (cfg && typeof(cfg) == "function") {
            return cfg;
        }
        return function() { return true; };
    }();

    // A cache to store the authentication result and TTL, keyed by domain.
    // The idea is that the /sso/login endpoint can include the validity period
    // of the token/rfp cookies within an is_authenticated = true response.
    // By storing that result we won't need to call /sso/login for subsequent
    // calls during the validity period. This should save us a round-trip for
    // most of the time, meaning that the vast majority of Ajax calls should
    // consist of a single round-trip. Cache is cleared on every page load.
    var authCache = createCache(5*60*1000); // Use a 5 minute padding (expressed in millis)

    // Create a new cache.
    // If paddingMillis is provided, then the TTL stored in the cache
    // will be paddingMillis milliseconds less than the actual expiryTime.
    // This is intended to avoid race-conditions between the time the cache is checked and the time
    // the request is received on the server.
    function createCache(paddingMillis) {
        var self = {};
        var cache = {}; // Map associating endpoints and their cookie expiry times (unix epoch)

        if (!paddingMillis) {
            paddingMillis = 0;
        }

        // Return true if the stored TTL for the given endpoint is later than the current time.
        // Return false otherwise, or if there is no such TTL.
        self.isAuthenticated = function(endpoint) {
            var expiryTime = cache[endpoint];
            if (!expiryTime) {
                return false;
            }
            var now = new Date().getTime();
            return now < expiryTime;
        };

        // Put a TTL for a given endpoint.
        self.put = function(endpoint, expiryTime) {
            if (typeof(expiryTime) != "number") {
                expiryTime = parseInt(expiryTime);
                if (isNaN(expiryTime)) {
                    throw "expiryTime must be a valid unix epoch";
                }
            }

            cache[endpoint] = expiryTime - paddingMillis;
        };

        return self;
    }

    // Utility method to make an AJAX call via the browser's native XHR.
    // Accepts the following options:
    // url: string (required),
    // success(token, textStatus, xhr): callback - Called when the XHR response is 200. Default handler does nothing.
    // error(xhr, textStatus, errorThrown): callback - Called when the XHR response is not 200. Default handler throws an exception.
    function nativeXhrCall(options) {
        var url = options.url;
        // Need to use XHR directly, since jQuery.ajax adds headers that cause preflighting,
        // which fails in some browsers, i.e., IE (see what I did there?)
        var xhr = nativeXhrFactory();
        var DONE = xhr.DONE || 4;

        // Set up the callback
        var cbSuccess = options.success || function() {}; // No-op if no success callback
        var cbError = options.error || defaultXhrErrback; // Default error function bubbles up the exception
        var complete = false;
        xhr.onreadystatechange = function() {
            if (complete) {
                return;
            }
            // Wait until done.
            if (xhr.readyState != DONE) {
                return;
            }
            complete = true;
            if (xhr.status == 200) {
                // Success, and the response body is the token, so pass it to the callback.
                cbSuccess(xhr.responseText, xhr.statusText, xhr);
            } else {
                // Something went wrong, so call the error handler
                cbError(xhr, xhr.statusText, null);
            }
        };

        // Make the request
        try {
            xhr.open("GET", url);
            xhr.withCredentials = true;
            xhr.send();
        } catch (e) {
            cbError(xhr, xhr.statusText, e);
        }

    }

    // A handy utility method that can act as the default error handler
    // for an XHR if none was provided by the user
    function defaultXhrErrback(xhr, textStatus, errorThrown) {
        if (errorThrown) {
            throw errorThrown;
        }
        throw {message: "XHR call returned with non-200 status code", xhr: xhr};
    }

    // Calls the RFP API in the Relying Party to populate amzn_sso_rfp token and validate id_token.
    // Options:
    // -url: string (required)
    // -success(payload): callback (required) - payload is the response from the RFP call
    // -error(xhr, textStatus, errorThrown) - thrown on non-200 response. Expect this to be triggered since we
    //                                      - may be calling servers that are not using OpenID and will
    //                                      - respond with 404. We want to handle this gracefully.
    function callRfpEndpoint(options) {
        var url = options.endpoint + "/sso/login";
        if (options.token) {
            url = url + "?id_token=" + options.token;
        }

        debouncedXhrCall({
            url: url,
            success: function(data, status, jqXHR) {
                    // if response is a JSON string, parse into an object
                    // Otherwise assume it's an object
                    if (typeof(data) == "string") {
                        options.success(JSON.parse(data));
                    } else {
                        options.success(data);
                    }
                },
            error: options.error
        });
    }

    // Delegates to nativeXhrCall, but will piggyback on the result of
    // an already outstanding duplicate request if one exists. Note that
    // this supports a very limited number of options compared to the
    // native XHR)
    // url: string (required),
    // success(token, textStatus, xhr): callback - Called when the XHR response is 200. Default handler does nothing.
    // error(xhr, textStatus, errorThrown): callback - Called when the XHR response is not 200. Default handler throws an exception.
    // debounceKey: string (optional) the key used to club requests together. Defaults to url if not provided.
    function debouncedXhrCall(options) {
        var debounceKey = options.url;
        if (options.debounceKey) {
            debounceKey = options.debounceKey;
        }

        var inflightRequest = debounceableRequests[debounceKey];

        var cbSuccess = options.success || function() {};
        var cbError = options.error || defaultXhrErrback;

        // If there is an existing in-flight request, debounce to it
        if (inflightRequest) {
            inflightRequest.successCallbacks.push(cbSuccess);
            inflightRequest.errorCallbacks.push(cbError);
            return;
        }

        // There is no pre-existing request, so bootstrap the in-flight bookkeeping
        inflightRequest = {
            successCallbacks: [cbSuccess],
            errorCallbacks: [cbError]
        };
        debounceableRequests[debounceKey] = inflightRequest;

        // Helper to create a master callback that safely fans out to multiple child
        // callbacks. It also de-registers the request's in-flight bookkeeping. If a
        // child callback throws an error, it will defer throwing the error until
        // all other child callbacks have been called. If multiple child callbacks
        // throw an error, the resulting thrown error object is the array of deferred
        // errors, rather than just one error.
        var createFanoutCallback = function(callbacks) {
            return function() {
                // Success or fail, the request is done
                delete debounceableRequests[debounceKey];

                var callbackErrors = [];
                for (var i = 0; i < callbacks.length; i++) {
                    var callback = callbacks[i];
                    try {
                        callback.apply(this, arguments);
                    }
                    catch (e) {
                        callbackErrors.push(e);
                    }
                }
                if (callbackErrors.length === 1) {
                    throw callbackErrors[0];
                } else if (callbackErrors.length > 1) {
                    throw callbackErrors;
                }
            };
        };

        nativeXhrCall({
            url: options.url,
            success: createFanoutCallback(inflightRequest.successCallbacks),
            error: createFanoutCallback(inflightRequest.errorCallbacks)
        });
    }
    var debounceableRequests = {};

    // Utility function for calling the IDP and retrieving the token
    // Accepts the following options:
    // idpUrl: string (required),
    // redirectUri: string (required) used for validation, doesn't actually redirect,
    // endpoint: string (required)
    // success(token, textStatus, xhr): callback,
    // error(xhr, textStatus, errorThrown): callback
    function callIdp(options) {
        // Make sure the idpUrl doesn't already have a redirect_uri parameter.
        // If it does, get rid of it, and replace it with the actual redirectUri
        var idpUrl = removeQueryParam(options.idpUrl, "redirect_uri");
        var encodedRedirectUri = encodeURIComponent(options.redirectUri);

        idpUrl = appendQueryParam(idpUrl, "redirect_uri", encodedRedirectUri);

        // Debounce based on the customer endpoint, since tokens are issued per
        // endpoint, not per something as specific as redirect_uri
        debouncedXhrCall({
            url: idpUrl,
            debounceKey: options.endpoint,
            success: options.success,
            error: options.error
        });
    }

    // Utility function for deconstructing a URL.
    // Need the client ID, which is the hostname[:port] of the url, and need absolute url.
    function getUrlProperties(url) {
        // Use the DOM to avoid having to use regex.
        var a = document.createElement('a');
        a.href = url;
        a.href = a.href; // I'm not kidding.
                         // a.href automatically expands out to the full URL, but in IE the other fields are not automatically updated
                         // so you get hostname="", protocol = ":", etc for a relative URL. But setting href to the full URL updates all
                         // the fields. Hence, this *ridiculous* statement bandages the IE issue.
        var host = (a.hostname + (a.port ? ":" + a.port : "")); // Can't just use a.host because IE sneaks in a :443 if there is no port number.
        var endpoint = a.protocol + "//" + host;
        var pathname = a.pathname || "";
        if (pathname && pathname[0] != "/") {
            // IE9.0 and below do not include the leading slash
            pathname = "/" + pathname;
        }
        return {
            absoluteUrl: a.href, // Turns a relative URL into an absolute one.
            host: host,
            endpoint: endpoint,
            base: endpoint + pathname,
            query: a.search,
            fragment: a.hash
        };
    }

    // Given a url and query parameter, return the url with all occurrences of the query parameter removed, if it is present.
    // Otherwise return the url unaltered.
    function removeQueryParam(url, paramName) {
        var urlProps = getUrlProperties(url);
        if (!urlProps.query) {
            return url;
        }
        var parts = urlProps.query.split('&');
        if (parts[0].charAt(0) == "?") {
            parts[0] = parts[0].substring(1);
        }
        var remainingParts = new Array();
        for (var i = 0; i < parts.length; i++) {
            var p = parts[i];
            if (!p) {
                // Can happen with extraneous leading/trailing '&'s, or double ampersands
                continue;
            }
            var keyVal = p.split('=');
            if (keyVal[0] == paramName) {
                // Found the param we want to remove
                continue;
            }
            remainingParts.push(p);
        }

        var newQuery = "?" + remainingParts.join('&');
        return overwriteQueryStr(urlProps, newQuery);
    }

    // Return a url with the new query string. urlProps is unchanged.
    function overwriteQueryStr(urlProps, newQueryStr) {
        return urlProps.base + newQueryStr + urlProps.fragment;
    }

    // Append a query string parameter to a given query string. Return the resulting query string.
    function appendQueryParam(queryStr, queryParameter, queryValue) {
        var queryArg = queryParameter + "=" + queryValue;
        if (queryStr.indexOf("?") == -1) {
            return "?" + queryArg;
        } else {
            return queryStr + "&" + queryArg;
        }
    }

    // Append the query parameter to the URL defined by the output of getUrlProperties
    function appendQueryString(urlProps, queryParameter, queryValue) {
        var queryStr = appendQueryParam(urlProps.query, queryParameter, queryValue);
        return overwriteQueryStr(urlProps, queryStr);
    }

    // Take into account the case where T/F was return as a string in a JSON response, for example
    function isTrue(arg) {
        return arg == true || arg == "true";
    }

    // Return true if an XHR call, represented by "xhrArgs", should be authenticated via performAuthenticationSteps.
    // Return false otherwise.
    // params: xhrArgs - an array representing the input parameters to an XHR.open() call.
    // Currently a call should not be authenticated iff it is synchronous, since authentication may require a cross-domain
    // call to the IDP, which is forbidden for some browsers. Customers who use synchronous calls should turn on Amazon.IDP.config.periodicRefresh.
    function shouldAuthenticateCall(xhrArgs) {
        var isAsync = xhrArgs[2] == false ? false : true;
        var isBlacklisted = domainBlacklist.matchesUrl(xhrArgs[1]);
        var hookResult = shouldAuthenticateHook(xhrArgs) == false ? false : true;

        return isAsync && !isBlacklisted && hookResult;
    }

    // Function to encapsulate the steps in ensuring that the user will be successfully
    // authenticated to the RP, in other words
    // -Check the TTL cache to see if we already have a valid authentication against the given endpoint. If so, complete the original request.
    // -Otherwise, check the validity of the token cookie against /sso/login in the RP (+ get rfp cookie)
    // -If token cookie is not valid,
    // --Fetch a new token from the IDP
    // --Call /sso/login?id_token=token to have it added as a cookie. Update the cache with the returned TTL, if it is provided.
    // -Complete the original request
    // Params:
    // -url: string - the url of the RP where the request is being made
    // -success: function(String requestUrl) - called after all authN steps are completed successfully, where requestUrl
    //     is the new url where the request should be made.
    // -error: function(XMLHttpRequest xhr, String textStatus, String errorThrown) - called if there
    //     is an error while trying to call the IdP, where xhr is the XMLHttpRequest used,
    //     textStatus is the textStatus in the xhr, and errorThrown is the corresponding HTTP
    //     error text (or null)
    // Any unrecoverable errors will be thrown from this function. Otherwise, authentication failures
    // will just simply carry through to the request where a standard 401 would be returned from the RP.
    function performAuthenticationSteps(options, retrynum) {
        var MAX_RETRIES = 3;
        if (!retrynum) { retrynum = 0; }

        var url = options.url;
        var success = options.success;
        var error = options.error;

        var urlProps = getUrlProperties(url);

        // Check the cache to see if the authentication is still valid for the domain in question.
        if (authCache.isAuthenticated(urlProps.endpoint)) {
            // Short-circuit and make the call to the RP
            makeRequestToRP();
            return;
        }

        callRfpEndpoint({
          endpoint: urlProps.endpoint,
          success: function(payload) {
            if (!payload || !payload.hasOwnProperty("is_authenticated")) {
                // Got a 200 but response was absent or malformed. Log to console and make request to RP
                // This shouldn't happen, but handle just in case?
                console.warn("Received 200 response but no payload from /sso/login");
                makeRequestToRP();
            } else if (isTrue(payload.is_authenticated)) {
                // If isAuthenticated == true, then bypass calling the IDP and directly make the request
                authenticationSuccess(payload);
            } else {
                // Otherwise, isAuthenticated == false, so call the IDP as usual then make the request
                var idpUrl = payload.authn_endpoint;
                if (!idpUrl) {
                    // Error condition. authn_endpoint must be provided. Throw an exception to the browser.
                    throw {message: "OpenID: Received instructions to fetch token, but no authn_endpoint provided", payload: payload};
                }
                var cookiesDisabled = isTrue(payload.no_cookie_token);
                fetchTokenAndContinue(idpUrl, cookiesDisabled);
            }
          },
          error: function(jqXHR) {
            // Treating this as "this is not an openID endpoint" so just make the original request and forget
            // all the OpenID semantics.
            makeRequestToRP();
          }
        });

        // Call the IDP to get the token
        var fetchTokenAndContinue = function(idpUrl, cookiesDisabled) {
            callIdp({
                idpUrl: idpUrl,
                redirectUri: urlProps.absoluteUrl,
                endpoint: urlProps.endpoint,
                success: function(token, textStatus, jqXHR) {
                    if (cookiesDisabled) {
                        // Cookies disabled in the handler. No need for
                        // second call to RFP endpoint. Just call the RP.
                        makeRequestToRP(token);

                    } else {
                        // Make the second call to the RFP endpoint, this time with the id_token
                        // as the query param
                        callRfpEndpoint({
                            endpoint: urlProps.endpoint,
                            token: token,
                            success: function(payload) {
                                if (!payload.is_authenticated) {
                                    console.warn({message: "OpenID: did not receive 'true' for is_authenticated from second call to /sso/login", payload: payload});
                                    doRetry(makeRequestToRP);
                                } else {
                                    authenticationSuccess(payload);
                                }
                            },
                            error: function(jqXHR) {
                                console.warn({message: "OpenID: received non-200 response from second call to /sso/login", xhr: jqXHR});
                                doRetry(makeRequestToRP);
                            }
                        });
                    }
                },
                error: error
            });
        }

        // Retry performAuthenticationSteps if we haven't yet reached the maximum number of retries.
        // Param: onRetryLimitReached - the function to run when the max number of retries has been reached.
        var doRetry = function(onRetryLimitReached) {
            if (retrynum < MAX_RETRIES) {
                console.log("OpenID: Retrying performAuthenticationSteps");
                performAuthenticationSteps(options, retrynum + 1);
            } else {
                onRetryLimitReached();
            }
        };

        // Put the expiration time in the cache, if present, then call the RP
        var authenticationSuccess = function(payload) {
            // Feature detection here. Cache is ignored if the client handlers don't vend expiry times in the response.
            if (payload.expires_at) {
                authCache.put(urlProps.endpoint, payload.expires_at);
            }
            makeRequestToRP();
        };

        // Supply the callback with the new url, which, if "token" is not provided, will be the original target url
        // Otherwise it will be the target url with the token added as a query param.
        function makeRequestToRP(token) {
            var requestUrl = urlProps.absoluteUrl;
            if (token) {
                requestUrl = appendQueryString(urlProps, "id_token", token);
            }
            success(requestUrl);
        };
    }

    // The OpenID implementation of xhr factory method.
    // Idea: Interfere as little as possible with the default implementation. Override any methods
    // we need in order to perform the auth work, and delegate to the original xhr methods
    // for doing the actual calls. Avoid rewriting the fundamental XHR logic.
    function overrideXhr(xhr, callback) {
        // Save the original send function
        var origSendFunc = xhr.send;
        // Save the original open function
        var origOpenFunc = xhr.open;
        // Save the original requestHeader function
        var origSetRequestHeaderFunc = xhr.setRequestHeader;
        // And the original abort function
        var origAbortFunc = xhr.abort;

        // override the open(), setRequestHeaders(), and send() methods in the prototype.
        xhr.open = function(method, url, async, user, pass) {
            this._Sentry_openArgs = arguments;

            // Call the original open here to put the xhr in the opened state
            // Need this to mimic a real xhr since some methods/properties can only be set
            // if it is in opened state.
            origOpenFunc.apply(this, arguments);
        };

        xhr.setRequestHeader = function(header, value) {
            if (!this._Sentry_headers) {
                this._Sentry_headers = {};
            }
            this._Sentry_headers[header] = value;
        };

        // Provide the new send function.
        // -Start by performing any necessary authentication steps (see performAuthenticationSteps())
        // -Open request to the destination url.
        // -Make the original request as intended.
        xhr.send = function(data) {
            var xhrInstance = this;
            var args = this._Sentry_openArgs;
            var headers = this._Sentry_headers || {};

            var url = args[1];

            this._Sentry_abortCalled = false; // If it's true at this point, then that means abort was called before send(),
                                        // so we're going to ignore it.

            var makeCall = function(requestUrl) {
                args[1] = requestUrl;

                // Call the original open, with the originally provided args (+ modified url)
                origOpenFunc.apply(xhrInstance, args);
                // Set any request headers we received
                for (var header in headers) {
                    if (headers.hasOwnProperty(header)) {
                        origSetRequestHeaderFunc.call(xhrInstance, header, headers[header]);
                    }
                }
                // Call original send to make the request.
                origSendFunc.call(xhrInstance, data);
                if (xhrInstance._Sentry_abortCalled) {
                    origAbortFunc.call(xhrInstance);
                }
            };

            if (shouldAuthenticateCall(args)) {
                performAuthenticationSteps({
                    url: url,
                    success: makeCall
                });
            } else {
                makeCall(url);
            }
        };

        // The abort function is slightly complicated by the additional Ajax calls in performAuthenticationSteps().
        // Behavior of a normal xhr.abort():
        // 1) before calling open(): no effect. When open() and subsequently send() are called, the request is made as usual.
        // 2) after calling open() but before calling send(): InvalidStateError is thrown when send() is called, because the state
        //  has been reset to UNOPENED
        // 3) after calling open() and send(): the request in-flight is cancelled and the error/success callbacks are not engaged.
        //  --However, jQuery "complete" callbacks are engaged. (i.e. onreadystatechange still fires)
        // With our implementation:
        // -item 2) behaves the same as item 1) since we call xhr.open() again when the client calls send().
        //  -- This isn't a big deal since at worst we are more forgiving. If we really want to we could fake the state ourselves
        //     but that doesn't seem warranted right now.
        // -item 3) cancels an in-flight xhr.send() request (the one visible to the user), and the success/error callbacks are not engaged.
        //  -- If abort() is called after we have already called xhr.send(), then the experience is exactly the same as with a normal XHR.
        //  -- Otherwise, if abort() is called after our send() is called but before we call xhr.send(), we simulate the abort by just calling
        //     it immediately after calling xhr.send() so that onreadystatechange still fires.
        //  -- For simplicity, we don't cancel any in-flight authentication requests. This is OK since they are hidden from the client anyway.
        xhr.abort = function() {
            this._Sentry_abortCalled = true;
            origAbortFunc.call(this);
        };

        // Call the callback if provided, and pass it the original methods.
        if (callback) {
            callback({
                origOpenFunc: origOpenFunc,
                origSendFunc: origSendFunc,
                origSetRequestHeaderFunc: origSetRequestHeaderFunc,
                origAbortFunc: origAbortFunc
            });
        }

    }

    if (namespace.config.periodicRefresh) {
        // This is to support the use case where client code wants to make sychronous Ajax calls.
        // Under normal operation we may call the IDP to fetch a new token. Since this call is
        // cross-domain and authenticated, some browsers will require that it be asynchronous.
        // So the only way to support sync calls in the general case is to make sure that the end-user's cookies
        // are always valid. To do that we will periodically refresh the tokens by calling
        // performAuthenticationSteps.
        // Note that this is only relevant for calls to the current server, not calls to CORS endpoints,
        // as those will run into the same browser issue if executed synchronously.
        var noop = function() {};
        var INTERVAL_MILLIS = 30*1000; // Refresh every 60 seconds.
        var endpoint = getUrlProperties(window.location.href).endpoint;

        setInterval(function() {
            performAuthenticationSteps({url: endpoint, success: noop});
        }, INTERVAL_MILLIS);

        // Perform the first one immediately.
        performAuthenticationSteps({url: endpoint, success: noop});
    }

    if (namespace.config.defaultOff) {
        // Client has indicated that they don't want the XHR object to be monkey-patched
        // nor do they want form POSTs to be overridden.

        nativeXhrFactory = function() {
            // Since we're not doing any funny business with the XHR, just return a plain old XHR
            return new XMLHttpRequest();
        };

        // Assign the native xhr factory to the namespace in case client code specifically wants to use it.
        namespace.nativeXhr = nativeXhrFactory;
        namespace.xhr = function() {
            // Create a new XHR, override the necessary methods, and return.
            var xhr = nativeXhrFactory();
            overrideXhr(xhr);
            return xhr;
        };

        // Exit early since the rest of the function does automagic default-on stuff.
        return;
    }

    // Monkey-patch the XHR prototype so that any invocation of new XMLHttpRequest() in client code
    // will automatically use our implementation, making for a seamless transition.
    overrideXhr(XMLHttpRequest.prototype, function(params) {
        // In the callback, set the native xhr factory by creating a new
        // xhr with the original methods.
        nativeXhrFactory = function() {
            var xhr = new XMLHttpRequest();
            // Reset overridden methods to the originals.
            xhr.open = function() {
                params.origOpenFunc.apply(xhr, arguments);
            };
            xhr.send = function() {
                params.origSendFunc.apply(xhr, arguments);
            };
            xhr.setRequestHeader = function() {
                params.origSetRequestHeaderFunc.apply(xhr, arguments);
            };
            xhr.abort = function() {
                params.origAbortFunc.apply(xhr);
            };
            return xhr;
        };
    });
    namespace.nativeXhr = nativeXhrFactory;
    namespace.xhr = function() {
        // Since we've overridden the prototype, just use the normal constructor.
        return new XMLHttpRequest();
    };
    namespace.internal.performAuthenticationSteps = performAuthenticationSteps;

    // jQuery's default XHR factory should just call the constructor, but explicitly override it in case
    // it does something wonky.
    if ($) {
        $.ajaxSetup({
            xhr: namespace.xhr
        });
    }

    // ---Form handling section---

    function isFormElement(element) {
        return (element && element.nodeName == "FORM");
    }

    // Elementary Map implementation with key = form and value = button clicked
    // Assume here that delete is not necessary, and that overwrite will do.
    // The click handler will store relevant click data (form, button) here, and the submit handler will
    // use it to add a hidden field to the form before submitting.
    var formSubmitClicks = function() {
        var self = {};
        var clicks = [];

        function indexOf(form) {
            for (var i = 0; i < clicks.length; i++) {
                if (clicks[i].form == form) { return i; }
            }
            return -1;
        }

        self.contains = function(form) {
            return indexOf(form) != -1;
        };

        self.get = function(form) {
            var index = indexOf(form);
            if (index == -1) { return null; }
            return clicks[index].button;
        };

        self.put = function(form, button) {
            var obj = {form: form, button: button};
            var index = indexOf(form);
            if (index == -1) {
                clicks.push(obj);
            } else {
                clicks[index] = obj;
            }
        };

        return self;
    }();

    // Intercept form submissions to fetch the token from the IdP.
    // This method should be compatible with both jQuery events and normal events.
    // The two APIs are nearly the same, but take care to make sure this is the case
    // when using new event methods.
    // "target" is added if we need to call this function directly... some browsers don't
    // allow you to directly set the event.target, so we emulate it by passing it as a parameter
    var formSubmissionCallback = function(event, target) {
        var form = event.target || target;
        if (!isFormElement(form)) {
            return;
        }

        var url = form.getAttribute("action");
        if (!url) {
            // By http://www.whatwg.org/specs/web-apps/current-work/multipage/forms.html#form-submission-algorithm
            // use the document URL as the action if it is not provided by the form.
            url = document.URL;
        }
        // Use this to determine whether we should submit the form later. jQuery vends isDefaultPrevented(), so check for that too.
        var defaultPrevented = event.isDefaultPrevented ? event.isDefaultPrevented() : event.defaultPrevented;
        if (defaultPrevented === undefined) {
            // Can happen with older versions of IE (<= 8)
            defaultPrevented = (event.returnValue === undefined) ? false : !event.returnValue;
        }

        if (defaultPrevented) {
            // Form submit has been aborted by the application, so just exit and do nothing
            return;
        }

        // Prevent the form from submitting on its own (the default action for form submissions).
        // Form will be manually submitted after the ID token is retrieved from the IdP.
        if (event.preventDefault) {
            event.preventDefault();
        } else {
            event.returnValue = false;
        }

        performAuthenticationSteps({
            url: url,
            success: function(requestUrl) {
                form.setAttribute("action", requestUrl);

                var inputElement = null;
                if (formSubmitClicks.contains(form)) {
                    // We arrived here by way of a click from an important button.
                    // Create a hidden element and copy button name and value over
                    var submitButton = formSubmitClicks.get(form);
                    inputElement = document.createElement("input");
                    inputElement.type = "hidden";
                    inputElement.name = submitButton.getAttribute("name");
                    inputElement.value = submitButton.getAttribute("value");

                    // Add to the parent form
                    form.appendChild(inputElement);
                }

                try {
                    if (HTMLFormElement) {
                        // IE8 does not honor this check and fails on "HTMLFormElement.prototype.submit"
                        // The only way to detect it is by catching the exception. Its ugly but so is IE8!!
                        var no_error = false;
                        try {
                            var x = HTMLFormElement.prototype.submit;
                            no_error = true;
                        }catch (e) {
                            form.submit();
                        }
                        // Make a best-effort attempt to submit the form without using form.submit(), since
                        // it will be overridden if the form has an element named "submit"
                        if(no_error == true) {
                             HTMLFormElement.prototype.submit.apply(form);
                        }
                    } else {
                        // If HTMLFormElement is not exposed by the browser (Internet Explorer + webpage is not in standards mode)
                        // Then use form.submit().
                        form.submit();
                    }
                } finally {
                    // Revert the value of form.action.
                    form.setAttribute("action", url);
                    if (inputElement) {
                        // Shouldn't be necessary, but do it just in case
                        form.removeChild(inputElement);
                    }
                }
            }
        });

        // Do not call event.stopPropagation() since we do want the event to bubble up afterwards.
        return false;
    };

    // If the element is a submit button with a name we need to add a hidden element to the form before its submitted
    // so that the value is not lost
    var clickCallback = function(event) {
        var element = event.target;

        if (!(element && element.getAttribute("type") == "submit" && element.getAttribute("name") && element.getAttribute("name") != "")) {
            // Not a form input that contributes a value, so don't care.
            return;
        }

        var submitButton = element;
        var parentForm = submitButton.form;
        if (!parentForm) {
            // This button was not placed within a form. Ignore.
            return;
        }

        // Register the form and the button that was clicked.
        // It will be picked up and used by the submit event handler.
        // Reasoning: We don't immediately add a hidden field to the DOM until we know that it actually
        // results in a submission -- there are various false positives, like right-click,
        // and there's the possibility that another handler after this one kills the event, cancelling the submssion.
        // So we want to avoid potentially polluting the form and causing other problems.
        //
        // But if the click turns out to not be a submission, aren't we erroneously loading it into the map?
        // No, since the real submission will overwrite the value.
        // There is the possibility that we register a click for a relevant form button (type=submit and name=something)
        // AND it doesn't submit AND the real submission is by the application's form.submit() or something
        // AND the extra value that we end up consequently submitting with the form causes a problem on the server.
        // But for now let's just assume that this is remote enough that we don't need to worry about it.
        formSubmitClicks.put(parentForm, submitButton);
    };

    if ($) {
        // If we have access to jQuery, then use it -- it provides what we need for Chrome, FF, and IE >= 8,
        // and we can hook into direct jQuery(form).submit() calls, which we cannot do with normal document.forms["form_id"].submit() calls.
        if ($(document).on) {
            $(document).on("submit", formSubmissionCallback);
            $(document).on("click", clickCallback);
        } else { // Pre-1.7
            $(document).bind("submit", formSubmissionCallback);
            $(document).bind("click", clickCallback);
        }
    } else if (document.addEventListener) {
        // Perform this in the bubble phase and give other handlers a chance to execute first.
        document.addEventListener("submit", formSubmissionCallback, false);
        document.addEventListener("click", clickCallback, false);
    } else if (document.attachEvent) {
        // Required for IE8 and below.
        document.attachEvent("onreadystatechange", function() {
            if ( document.readyState === "complete") {
                document.detachEvent("onreadystatechange", arguments.callee);

                // The submit event will not bubble up to the document, so we must attach the callback to each form.
                // TODO: This will not account for forms added afterwards.
                var forms = document.getElementsByTagName("form");
                for (var i = 0; i < forms.length; i++) {
                    (function(){
                        var form = forms[i];
                        form.attachEvent("onsubmit", function(event) {
                            event.target = form;
                            formSubmissionCallback(event);
                        });
                        var inputs = form.getElementsByTagName("input");
                        for (var j = 0; j < inputs.length; j++) {
                            var input = inputs[j];
                            if (input.getAttribute("type") == "submit") {
                                input.attachEvent("onclick", function(e) {
                                    e.target = input;
                                    clickCallback(e);
                                });
                            }
                        }
                    })();
                }
            }
        });
    }

    // Use this instead of form.submit(), where "form" is a plain DOM form object.
    // We do this because an HTML form.submit() does not fire events, so we're taken out of the loop.
    // Code that does jQuery(form).submit() does not need to be changed -- it is automatically handled.
    // Note: We directly invoke the event handler; we don't call dispatchEvent or fireEvent because
    // we don't want to fundamentally change the behavior of form.submit(). All we want to do is
    // inject our handling code.
    namespace.submitForm = function(form) {
        var e = document.createEvent("Event");
        e.initEvent("submit", true, true);
        formSubmissionCallback(e, form);
    };

})(window);

(function() {
    window.setInterval(function() {
        console.log("Background fetch")
        var req = new window.XMLHttpRequest()
        req.open("GET","https://git.xarth.tv")
        req.send()
    }, 180 * 1000);
})()