diff --git a/src/.eslintrc.json b/src/.eslintrc.json
index caa332ff8bf7..bca6fbc072a1 100644
--- a/src/.eslintrc.json
+++ b/src/.eslintrc.json
@@ -154,6 +154,7 @@
/* urlUtils.js */
"urlResolve": false,
"urlIsSameOrigin": false,
+ "urlIsAllowedOriginChecker": false,
/* ng/controller.js */
"identifierForController": false,
diff --git a/src/ng/http.js b/src/ng/http.js
index 995a70ec8abb..6024c9c0cd1e 100644
--- a/src/ng/http.js
+++ b/src/ng/http.js
@@ -39,7 +39,7 @@ function $HttpParamSerializerProvider() {
* * `{'foo': {'bar':'baz'}}` results in `foo=%7B%22bar%22%3A%22baz%22%7D` (stringified and encoded representation of an object)
*
* Note that serializer will sort the request parameters alphabetically.
- * */
+ */
this.$get = function() {
return function ngParamSerializer(params) {
@@ -106,7 +106,7 @@ function $HttpParamSerializerJQLikeProvider() {
* });
* ```
*
- * */
+ */
this.$get = function() {
return function jQueryLikeParamSerializer(params) {
if (!params) return '';
@@ -253,7 +253,7 @@ function isSuccess(status) {
*
* @description
* Use `$httpProvider` to change the default behavior of the {@link ng.$http $http} service.
- * */
+ */
function $HttpProvider() {
/**
* @ngdoc property
@@ -286,7 +286,7 @@ function $HttpProvider() {
* If specified as string, it is interpreted as a function registered with the {@link auto.$injector $injector}.
* Defaults to {@link ng.$httpParamSerializer $httpParamSerializer}.
*
- **/
+ */
var defaults = this.defaults = {
// transform incoming response data
transformResponse: [defaultHttpResponseTransform],
@@ -331,7 +331,7 @@ function $HttpProvider() {
*
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
* otherwise, returns the current configured value.
- **/
+ */
this.useApplyAsync = function(value) {
if (isDefined(value)) {
useApplyAsync = !!value;
@@ -355,7 +355,7 @@ function $HttpProvider() {
*
* @returns {boolean|Object} If a value is specified, returns the $httpProvider for chaining.
* otherwise, returns the current configured value.
- **/
+ */
this.useLegacyPromiseExtensions = function(value) {
if (isDefined(value)) {
useLegacyPromise = !!value;
@@ -376,9 +376,49 @@ function $HttpProvider() {
* array, on request, but reverse order, on response.
*
* {@link ng.$http#interceptors Interceptors detailed info}
- **/
+ */
var interceptorFactories = this.interceptors = [];
+ /**
+ * @ngdoc property
+ * @name $httpProvider#xsrfWhitelistedOrigins
+ * @description
+ *
+ * Array containing URLs whose origins are considered trusted enough to receive the XSRF token.
+ * See the {@link ng.$http#security-considerations Security Considerations} sections for more
+ * details on XSRF.
+ *
+ * **Note:** An "origin" consists of the [URI scheme](https://en.wikipedia.org/wiki/URI_scheme),
+ * the [hostname](https://en.wikipedia.org/wiki/Hostname) and the
+ * [port number](https://en.wikipedia.org/wiki/Port_(computer_networking).
+ *
+ *
+ * It is not possible to whitelist specific URLs/paths. The `path`, `query` and `fragment` parts
+ * of a URL will be ignored. For example, `https://foo.com/path/bar?query=baz#fragment` will be
+ * treated as `https://foo.com/`, meaning that **all** requests to URLs starting with
+ * `https://foo.com/` will include the XSRF token.
+ *
+ *
+ * ## Example
+ *
+ * ```
+ * // App served from `https://example.com`
+ * angular.
+ * module('xsrfWhitelistedOriginsExample', []).
+ * config(['$httpProvider', function($httpProvider) {
+ * $httpProvider.xsrfWhitelistedOrigins.push('https://api.example.com/');
+ * }]).
+ * run(['$http', function($http) {
+ * // The XSRF token will be sent
+ * $http.get('https://api.example.com/preferences').then(...);
+ *
+ * // The XSRF token will NOT be sent
+ * $http.get('https://stats.example.com/activity').then(...);
+ * }]);
+ * ```
+ */
+ var xsrfWhitelistedOrigins = this.xsrfWhitelistedOrigins = [];
+
this.$get = ['$browser', '$httpBackend', '$$cookieReader', '$cacheFactory', '$rootScope', '$q', '$injector',
function($browser, $httpBackend, $$cookieReader, $cacheFactory, $rootScope, $q, $injector) {
@@ -402,6 +442,11 @@ function $HttpProvider() {
? $injector.get(interceptorFactory) : $injector.invoke(interceptorFactory));
});
+ /**
+ * A function to check request URLs against a list of allowed origins.
+ */
+ var urlIsAllowedOrigin = urlIsAllowedOriginChecker(xsrfWhitelistedOrigins);
+
/**
* @ngdoc service
* @kind function
@@ -778,25 +823,42 @@ function $HttpProvider() {
* which the attacker can trick an authenticated user into unknowingly executing actions on your
* website. Angular provides a mechanism to counter XSRF. When performing XHR requests, the
* $http service reads a token from a cookie (by default, `XSRF-TOKEN`) and sets it as an HTTP
- * header (`X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read the
- * cookie, your server can be assured that the XHR came from JavaScript running on your domain.
- * The header will not be set for cross-domain requests.
+ * header (by default `X-XSRF-TOKEN`). Since only JavaScript that runs on your domain could read
+ * the cookie, your server can be assured that the XHR came from JavaScript running on your
+ * domain.
*
* To take advantage of this, your server needs to set a token in a JavaScript readable session
* cookie called `XSRF-TOKEN` on the first HTTP GET request. On subsequent XHR requests the
- * server can verify that the cookie matches `X-XSRF-TOKEN` HTTP header, and therefore be sure
- * that only JavaScript running on your domain could have sent the request. The token must be
- * unique for each user and must be verifiable by the server (to prevent the JavaScript from
+ * server can verify that the cookie matches the `X-XSRF-TOKEN` HTTP header, and therefore be
+ * sure that only JavaScript running on your domain could have sent the request. The token must
+ * be unique for each user and must be verifiable by the server (to prevent the JavaScript from
* making up its own tokens). We recommend that the token is a digest of your site's
* authentication cookie with a [salt](https://en.wikipedia.org/wiki/Salt_(cryptography))
* for added security.
*
- * The name of the headers can be specified using the xsrfHeaderName and xsrfCookieName
- * properties of either $httpProvider.defaults at config-time, $http.defaults at run-time,
- * or the per-request config object.
+ * The header will — by default — **not** be set for cross-domain requests. This
+ * prevents unauthorized servers (e.g. malicious or compromized 3rd-party APIs) from gaining
+ * access to your users' XSRF tokens and exposing them to Cross Site Request Forgery. If you
+ * want to, you can whitelist additional origins to also receive the XSRF token, by adding them
+ * to {@link ng.$httpProvider#xsrfWhitelistedOrigins xsrfWhitelistedOrigins}. This might be
+ * useful, for example, if your application, served from `example.com`, needs to access your API
+ * at `api.example.com`.
+ * See {@link ng.$httpProvider#xsrfWhitelistedOrigins $httpProvider.xsrfWhitelistedOrigins} for
+ * more details.
+ *
+ *
+ * **Warning**
+ * Only whitelist origins that you have control over and make sure you understand the
+ * implications of doing so.
+ *
+ *
+ * The name of the cookie and the header can be specified using the `xsrfCookieName` and
+ * `xsrHeaderName` properties of either `$httpProvider.defaults` at config-time,
+ * `$http.defaults` at run-time, or the per-request config object.
*
* In order to prevent collisions in environments where multiple Angular apps share the
- * same domain or subdomain, we recommend that each application uses unique cookie name.
+ * same domain or subdomain, we recommend that each application uses a unique cookie name.
+ *
*
* @param {object} config Object describing the request to be made and how it should be
* processed. The object has following properties:
@@ -1286,7 +1348,7 @@ function $HttpProvider() {
// if we won't have the response in cache, set the xsrf headers and
// send the request to the backend
if (isUndefined(cachedResp)) {
- var xsrfValue = urlIsSameOrigin(config.url)
+ var xsrfValue = urlIsAllowedOrigin(config.url)
? $$cookieReader()[config.xsrfCookieName || defaults.xsrfCookieName]
: undefined;
if (xsrfValue) {
diff --git a/src/ng/urlUtils.js b/src/ng/urlUtils.js
index ee58b435f3e9..2d005b42d486 100644
--- a/src/ng/urlUtils.js
+++ b/src/ng/urlUtils.js
@@ -10,6 +10,18 @@ var urlParsingNode = window.document.createElement("a");
var originUrl = urlResolve(window.location.href);
+/**
+ * Compare the origins of two parsed URL objects.
+ *
+ * @param {Object} url1 - The first parsed URL object to compare.
+ * @param {Object} url2 - The second parsed URL object to compare.
+ *
+ * @returns {boolean} - Whether the origins of the two URLs are the same.
+ */
+function sameOrigin(url1, url2) {
+ return (url1.protocol === url2.protocol) && (url1.host === url2.host);
+}
+
/**
*
* Implementation Notes for non-IE browsers
@@ -83,14 +95,43 @@ function urlResolve(url) {
}
/**
- * Parse a request URL and determine whether this is a same-origin request as the application document.
+ * Parse a request URL and determine whether this is a same-origin request as the application
+ * document.
*
* @param {string|object} requestUrl The url of the request as a string that will be resolved
* or a parsed URL object.
* @returns {boolean} Whether the request is for the same origin as the application document.
*/
function urlIsSameOrigin(requestUrl) {
- var parsed = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
- return (parsed.protocol === originUrl.protocol &&
- parsed.host === originUrl.host);
+ var parsedUrl = (isString(requestUrl)) ? urlResolve(requestUrl) : requestUrl;
+ return sameOrigin(parsedUrl, originUrl);
+}
+
+/**
+ * Create a function that can check a URL's origin against a list of allowed/whitelisted origins.
+ * The current location's origin is implicitly trusted.
+ *
+ * @param {string[]} whitelistedOriginUrls - A list of URLs (strings), whose origins are trusted.
+ *
+ * @returns {Function} - A function that receives a URL (string or parsed URL object) and returns
+ * whether it is of an allowed origin.
+ */
+function urlIsAllowedOriginChecker(whitelistedOriginUrls) {
+ var parsedAllowedOriginUrls = [originUrl].concat(whitelistedOriginUrls.map(urlResolve));
+
+ /**
+ * Check whether the specified URL (string or parsed URL object) has an origin that is allowed
+ * based on a list of whitelisted-origin URLs. The current location's origin is implicitly
+ * trusted.
+ *
+ * @param {string|Object} requestUrl - The URL to be checked (provided as a string that will be
+ * resolved or a parsed URL object).
+ *
+ * @returns {boolean} - Whether the specified URL is of an allowed origin.
+ */
+ return function urlIsAllowedOrigin(requestUrl) {
+ var parsedUrl = isString(requestUrl) ? urlResolve(requestUrl) : requestUrl;
+
+ return parsedAllowedOriginUrls.some(sameOrigin.bind(null, parsedUrl));
+ };
}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 2bf89b2bb07e..390b5eb59110 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -144,6 +144,7 @@
/* urlUtils.js */
"urlResolve": false,
"urlIsSameOrigin": false,
+ "urlIsAllowedOriginChecker": false,
/* karma */
"dump": false,
diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js
index 709766122c14..269a718efaa3 100644
--- a/test/ng/httpSpec.js
+++ b/test/ng/httpSpec.js
@@ -9,22 +9,17 @@ describe('$http', function() {
return Object.keys(params).join('_');
};
- beforeEach(function() {
+ beforeEach(module(function($exceptionHandlerProvider) {
+ $exceptionHandlerProvider.mode('log');
+
callback = jasmine.createSpy('done');
mockedCookies = {};
- module({
- $$cookieReader: function() {
- return mockedCookies;
- }
- });
- });
+ }));
beforeEach(module({
+ $$cookieReader: function() { return mockedCookies; },
customParamSerializer: customParamSerializer
}));
- beforeEach(module(function($exceptionHandlerProvider) {
- $exceptionHandlerProvider.mode('log');
- }));
afterEach(inject(function($exceptionHandler, $httpBackend, $rootScope) {
forEach($exceptionHandler.errors, function(e) {
@@ -35,7 +30,6 @@ describe('$http', function() {
throw 'Unhandled exceptions trapped in $exceptionHandler!';
}
- $rootScope.$digest();
$httpBackend.verifyNoOutstandingExpectation();
}));
@@ -784,18 +778,6 @@ describe('$http', function() {
$httpBackend.flush();
});
- it('should not set XSRF cookie for cross-domain requests', inject(function($browser) {
- mockedCookies['XSRF-TOKEN'] = 'secret';
- $browser.url('http://host.com/base');
- $httpBackend.expect('GET', 'http://www.test.com/url', undefined, function(headers) {
- return isUndefined(headers['X-XSRF-TOKEN']);
- }).respond('');
-
- $http({url: 'http://www.test.com/url', method: 'GET', headers: {}});
- $httpBackend.flush();
- }));
-
-
it('should not send Content-Type header if request data/body is undefined', function() {
$httpBackend.expect('POST', '/url', undefined, function(headers) {
return !headers.hasOwnProperty('Content-Type');
@@ -827,32 +809,6 @@ describe('$http', function() {
$httpBackend.flush();
});
- it('should set the XSRF cookie into a XSRF header', inject(function() {
- function checkXSRF(secret, header) {
- return function(headers) {
- return headers[header || 'X-XSRF-TOKEN'] === secret;
- };
- }
-
- mockedCookies['XSRF-TOKEN'] = 'secret';
- mockedCookies['aCookie'] = 'secret2';
- $httpBackend.expect('GET', '/url', undefined, checkXSRF('secret')).respond('');
- $httpBackend.expect('POST', '/url', undefined, checkXSRF('secret')).respond('');
- $httpBackend.expect('PUT', '/url', undefined, checkXSRF('secret')).respond('');
- $httpBackend.expect('DELETE', '/url', undefined, checkXSRF('secret')).respond('');
- $httpBackend.expect('GET', '/url', undefined, checkXSRF('secret', 'aHeader')).respond('');
- $httpBackend.expect('GET', '/url', undefined, checkXSRF('secret2')).respond('');
-
- $http({url: '/url', method: 'GET'});
- $http({url: '/url', method: 'POST', headers: {'S-ome': 'Header'}});
- $http({url: '/url', method: 'PUT', headers: {'Another': 'Header'}});
- $http({url: '/url', method: 'DELETE', headers: {}});
- $http({url: '/url', method: 'GET', xsrfHeaderName: 'aHeader'});
- $http({url: '/url', method: 'GET', xsrfCookieName: 'aCookie'});
-
- $httpBackend.flush();
- }));
-
it('should send execute result if header value is function', function() {
var headerConfig = {'Accept': function() { return 'Rewritten'; }};
@@ -902,20 +858,6 @@ describe('$http', function() {
expect(config.foo).toBeUndefined();
});
-
- it('should check the cache before checking the XSRF cookie', inject(function($cacheFactory) {
- var testCache = $cacheFactory('testCache');
-
- spyOn(testCache, 'get').and.callFake(function() {
- mockedCookies['XSRF-TOKEN'] = 'foo';
- });
-
- $httpBackend.expect('GET', '/url', undefined, function(headers) {
- return headers['X-XSRF-TOKEN'] === 'foo';
- }).respond('');
- $http({url: '/url', method: 'GET', cache: testCache});
- $httpBackend.flush();
- }));
});
@@ -2128,6 +2070,139 @@ describe('$http', function() {
});
+ describe('XSRF', function() {
+ var $http;
+ var $httpBackend;
+
+ beforeEach(module(function($httpProvider) {
+ $httpProvider.xsrfWhitelistedOrigins.push('https://whitelisted.example.com/');
+ }));
+
+ beforeEach(inject(function(_$http_, _$httpBackend_) {
+ $http = _$http_;
+ $httpBackend = _$httpBackend_;
+ }));
+
+
+ it('should set the XSRF cookie into an XSRF header', function() {
+ function checkXsrf(secret, header) {
+ return function checkHeaders(headers) {
+ return headers[header || 'X-XSRF-TOKEN'] === secret;
+ };
+ }
+
+ mockedCookies['XSRF-TOKEN'] = 'secret';
+ mockedCookies['aCookie'] = 'secret2';
+ $httpBackend.expect('GET', '/url', null, checkXsrf('secret')).respond(null);
+ $httpBackend.expect('POST', '/url', null, checkXsrf('secret')).respond(null);
+ $httpBackend.expect('PUT', '/url', null, checkXsrf('secret')).respond(null);
+ $httpBackend.expect('DELETE', '/url', null, checkXsrf('secret')).respond(null);
+ $httpBackend.expect('GET', '/url', null, checkXsrf('secret', 'aHeader')).respond(null);
+ $httpBackend.expect('GET', '/url', null, checkXsrf('secret2')).respond(null);
+
+ $http({method: 'GET', url: '/url'});
+ $http({method: 'POST', url: '/url', headers: {'S-ome': 'Header'}});
+ $http({method: 'PUT', url: '/url', headers: {'Another': 'Header'}});
+ $http({method: 'DELETE', url: '/url', headers: {}});
+ $http({method: 'GET', url: '/url', xsrfHeaderName: 'aHeader'});
+ $http({method: 'GET', url: '/url', xsrfCookieName: 'aCookie'});
+
+ $httpBackend.flush();
+ });
+
+
+ it('should support setting a default XSRF cookie/header name', function() {
+ $http.defaults.xsrfCookieName = 'aCookie';
+ $http.defaults.xsrfHeaderName = 'aHeader';
+
+ function checkHeaders(headers) {
+ return headers.aHeader === 'secret';
+ }
+
+ mockedCookies.aCookie = 'secret';
+ $httpBackend.expect('GET', '/url', null, checkHeaders).respond(null);
+
+ $http.get('/url');
+
+ $httpBackend.flush();
+ });
+
+
+ it('should support overriding the default XSRF cookie/header name per request', function() {
+ $http.defaults.xsrfCookieName = 'aCookie';
+ $http.defaults.xsrfHeaderName = 'aHeader';
+
+ function checkHeaders(headers) {
+ return headers.anotherHeader === 'anotherSecret';
+ }
+
+ mockedCookies.anotherCookie = 'anotherSecret';
+ $httpBackend.expect('GET', '/url', null, checkHeaders).respond(null);
+
+ $http.get('/url', {
+ xsrfCookieName: 'anotherCookie',
+ xsrfHeaderName: 'anotherHeader'
+ });
+
+ $httpBackend.flush();
+ });
+
+
+ it('should check the cache before checking the XSRF cookie', inject(function($cacheFactory) {
+ function checkHeaders(headers) {
+ return headers['X-XSRF-TOKEN'] === 'foo';
+ }
+ function setCookie() {
+ mockedCookies['XSRF-TOKEN'] = 'foo';
+ }
+
+ var testCache = $cacheFactory('testCache');
+ spyOn(testCache, 'get').and.callFake(setCookie);
+
+ $httpBackend.expect('GET', '/url', null, checkHeaders).respond(null);
+ $http.get('/url', {cache: testCache});
+
+ $httpBackend.flush();
+ }));
+
+
+ it('should not set an XSRF header for cross-domain requests', function() {
+ function checkHeaders(headers) {
+ return isUndefined(headers['X-XSRF-TOKEN']);
+ }
+ var currentUrl = 'https://example.com/path';
+ var requestUrl = 'https://api.example.com/path';
+
+ mockedCookies['XSRF-TOKEN'] = 'secret';
+ $httpBackend.expect('GET', requestUrl, null, checkHeaders).respond(null);
+
+ $http.get(requestUrl);
+
+ $httpBackend.flush();
+ });
+
+
+ it('should set an XSRF header for cross-domain requests to whitelisted origins',
+ inject(function($browser) {
+ function checkHeaders(headers) {
+ return headers['X-XSRF-TOKEN'] === 'secret';
+ }
+ var currentUrl = 'https://example.com/path';
+ var requestUrl = 'https://whitelisted.example.com/path';
+
+ $browser.url(currentUrl);
+
+ mockedCookies['XSRF-TOKEN'] = 'secret';
+ $httpBackend.expect('GET', requestUrl, null, checkHeaders).respond(null);
+
+ $http.get(requestUrl);
+
+ $httpBackend.flush();
+ })
+ );
+ });
+
+
it('should pass timeout, withCredentials and responseType', function() {
var $httpBackend = jasmine.createSpy('$httpBackend');
@@ -2348,5 +2423,4 @@ describe('$http param serializers', function() {
//a[]=b&a[]=c&d[0][e]=f&d[0][g]=h&d[]=i&d[2][j]=k
});
});
-
});
diff --git a/test/ng/urlUtilsSpec.js b/test/ng/urlUtilsSpec.js
index cd2519d98452..07ecd7c0f7d6 100644
--- a/test/ng/urlUtilsSpec.js
+++ b/test/ng/urlUtilsSpec.js
@@ -3,11 +3,12 @@
describe('urlUtils', function() {
describe('urlResolve', function() {
it('should normalize a relative url', function() {
- expect(urlResolve("foo").href).toMatch(/^https?:\/\/[^/]+\/foo$/);
+ expect(urlResolve('foo').href).toMatch(/^https?:\/\/[^/]+\/foo$/);
});
+
it('should parse relative URL into component pieces', function() {
- var parsed = urlResolve("foo");
+ var parsed = urlResolve('foo');
expect(parsed.href).toMatch(/https?:\/\//);
expect(parsed.protocol).toMatch(/^https?/);
expect(parsed.host).not.toBe("");
@@ -23,22 +24,110 @@ describe('urlUtils', function() {
});
});
- describe('isSameOrigin', function() {
- it('should support various combinations of urls - both string and parsed', inject(function($document) {
- function expectIsSameOrigin(url, expectedValue) {
- expect(urlIsSameOrigin(url)).toBe(expectedValue);
- expect(urlIsSameOrigin(urlResolve(url))).toBe(expectedValue);
- }
- expectIsSameOrigin('path', true);
- var origin = urlResolve($document[0].location.href);
- expectIsSameOrigin('//' + origin.host + '/path', true);
- // Different domain.
- expectIsSameOrigin('http://example.com/path', false);
- // Auto fill protocol.
- expectIsSameOrigin('//example.com/path', false);
- // Should not match when the ports are different.
- // This assumes that the test is *not* running on port 22 (very unlikely).
- expectIsSameOrigin('//' + origin.hostname + ':22/path', false);
- }));
+
+ describe('urlIsSameOrigin', function() {
+ it('should support various combinations of urls - both string and parsed',
+ inject(function($document) {
+ function expectIsSameOrigin(url, expectedValue) {
+ expect(urlIsSameOrigin(url)).toBe(expectedValue);
+ expect(urlIsSameOrigin(urlResolve(url))).toBe(expectedValue);
+ }
+
+ expectIsSameOrigin('path', true);
+
+ var origin = urlResolve($document[0].location.href);
+ expectIsSameOrigin('//' + origin.host + '/path', true);
+
+ // Different domain.
+ expectIsSameOrigin('http://example.com/path', false);
+
+ // Auto fill protocol.
+ expectIsSameOrigin('//example.com/path', false);
+
+ // Should not match when the ports are different.
+ // This assumes that the test is *not* running on port 22 (very unlikely).
+ expectIsSameOrigin('//' + origin.hostname + ':22/path', false);
+ })
+ );
+ });
+
+
+ describe('urlIsAllowedOriginChecker', function() {
+ var origin = urlResolve(window.location.href);
+ var urlIsAllowedOrigin;
+
+ beforeEach(function() {
+ urlIsAllowedOrigin = urlIsAllowedOriginChecker([
+ 'https://foo.com/',
+ origin.protocol + '://bar.com:1337/'
+ ]);
+ });
+
+
+ it('should implicitly allow the current origin', function() {
+ expect(urlIsAllowedOrigin('path')).toBe(true);
+ });
+
+
+ it('should check against the list of whitelisted origins', function() {
+ expect(urlIsAllowedOrigin('https://foo.com/path')).toBe(true);
+ expect(urlIsAllowedOrigin(origin.protocol + '://bar.com:1337/path')).toBe(true);
+ expect(urlIsAllowedOrigin('https://baz.com:1337/path')).toBe(false);
+ expect(urlIsAllowedOrigin('https://qux.com/path')).toBe(false);
+ });
+
+
+ it('should support both strings and parsed URL objects', function() {
+ expect(urlIsAllowedOrigin('path')).toBe(true);
+ expect(urlIsAllowedOrigin(urlResolve('path'))).toBe(true);
+ expect(urlIsAllowedOrigin('https://foo.com/path')).toBe(true);
+ expect(urlIsAllowedOrigin(urlResolve('https://foo.com/path'))).toBe(true);
+ });
+
+
+ it('should return true only if the origins (protocol, hostname, post) match', function() {
+ var differentProtocol = (origin.protocol !== 'http') ? 'http' : 'https';
+ var differentPort = (parseInt(origin.port, 10) || 0) + 1;
+ var url;
+
+
+ // Relative path
+ url = 'path';
+ expect(urlIsAllowedOrigin(url)).toBe(true);
+
+
+ // Same origin
+ url = origin.protocol + '://' + origin.host + '/path';
+ expect(urlIsAllowedOrigin(url)).toBe(true);
+
+ // Same origin - implicit protocol
+ url = '//' + origin.host + '/path';
+ expect(urlIsAllowedOrigin(url)).toBe(true);
+
+ // Same origin - different protocol
+ url = differentProtocol + '://' + origin.host + '/path';
+ expect(urlIsAllowedOrigin(url)).toBe(false);
+
+ // Same origin - different port
+ url = origin.protocol + '://' + origin.hostname + ':' + differentPort + '/path';
+ expect(urlIsAllowedOrigin(url)).toBe(false);
+
+
+ // Allowed origin
+ url = origin.protocol + '://bar.com:1337/path';
+ expect(urlIsAllowedOrigin(url)).toBe(true);
+
+ // Allowed origin - implicit protocol
+ url = '//bar.com:1337/path';
+ expect(urlIsAllowedOrigin(url)).toBe(true);
+
+ // Allowed origin - different protocol
+ url = differentProtocol + '://bar.com:1337/path';
+ expect(urlIsAllowedOrigin(url)).toBe(false);
+
+ // Allowed origin - different port
+ url = origin.protocol + '://bar.com:1338/path';
+ expect(urlIsAllowedOrigin(url)).toBe(false);
+ });
});
});