├── README.md
├── ppx.jquery.js
├── ppx.js
├── server.py
└── test
├── index.html
├── jquery.min.js
├── qunit.css
├── qunit.js
├── real-cors-server.html
├── sample.txt
├── server.html
├── test-jquery-plugin.js
├── test-real-cors.js
├── test-request.js
└── test-utils.js
/README.md:
--------------------------------------------------------------------------------
1 | PostMessage Proxied XMLHttpRequest (PPX) is a simple [polyfill][] that allows browsers without support for cross-origin XMLHttpRequests to do so via postMessage.
2 |
3 | The code has no dependencies and does not require JSON, which makes it about 2KB minified and gzipped.
4 |
5 | A simple jQuery plugin that allows jQuery-based ajax requests to transparently use the polyfill is also available.
6 |
7 | ## Usage
8 |
9 | Suppose you have a website at http://foo.com which exposes a cross-origin REST API that you'd like to access from http://bar.com.
10 |
11 | Create a file at http://foo.com/server.html and put the following code in it:
12 |
13 | ```html
14 |
15 |
16 |
PPX Server Frame
17 |
18 |
19 | ```
20 |
21 | This is the host iframe which will proxy requests for you.
22 |
23 | ### Basic Use
24 |
25 | From a page on bar.com, you can access foo.com like so:
26 |
27 | ```html
28 |
29 |
39 | ```
40 |
41 | As you can probably guess, `PPX.buildClientConstructor()` returns an object much like `window.XMLHttpRequest`. This can then be used as-is, or given to another third-party library to make cross-origin communication as familiar as a normal ajax request.
42 |
43 | ### Using PPX with jQuery
44 |
45 | The above example can be made simpler using the PPX jQuery plugin:
46 |
47 | ```html
48 |
49 |
50 |
51 |
57 | ```
58 |
59 | The call `jQuery.proxyAjaxThroughPostMessage()` sets up an [ajax prefilter][] which will automatically proxy requests to foo.com if the host browser doesn't already support CORS.
60 |
61 | ### Using PPX with jQuery and yepnope.js
62 |
63 | You can use PPX with [yepnope.js][] and jQuery, too:
64 |
65 | ```html
66 |
67 |
68 |
81 | ```
82 |
83 | This will only load PPX's JS code if CORS support isn't detected in the host browser.
84 |
85 | ## Development
86 |
87 | After cloning the git repository and entering its directory, you can start the development server by running:
88 |
89 | python server.py
90 |
91 | This will start two local web servers on ports 9000 and 9001. The functional tests make CORS requests from one to the other to ensure that everything works as expected.
92 |
93 | To start the tests, browse to http://localhost:9000/test/.
94 |
95 | ## Limitations
96 |
97 | Currently, the following features of the [XMLHttpRequest API][] are unsupported:
98 |
99 | * username and password arguments to `open()`
100 | * `getResponseHeader()` (though `getAllResponseHeaders()` is supported)
101 | * `responseXML`
102 |
103 | Several features of the massive [CORS Specification][] are unsupported:
104 |
105 | * Only [simple requests][] can be sent; anything requiring a preflighted request will be rejected for security purposes.
106 |
107 | * Response headers aren't automatically culled down to the [simple response header][] list as prescribed by the spec.
108 |
109 | * Because the `Origin` header can't be set by the same-origin proxied request, PPX sets an `X-Original-Origin` header with the origin of the window making the request. This may be used by servers in place of `Origin`, e.g. to set the appropriate value for `Access-Control-Allow-Origin` in the response.
110 |
111 | * Because the same-origin proxied request can't control whether or not a cookie is transmitted during its request, all cross-origin requests sent should be assumed to have them. Note that we don't currently check the value of `Access-Control-Allow-Credentials` before returning responses, either, so be *very careful* if your site uses cookies.
112 |
113 | ## Similar Projects
114 |
115 | [pmxdr][] provides similar functionality but doesn't provide an XMLHttpRequest API, so it can't necessarily be used as a drop-in replacement. It's also larger than PPX, but supports more features out-of-the-box.
116 |
117 | There's [xdomain](https://github.com/jpillora/xdomain). It doesn't use postMessage.
118 |
119 | ## License
120 |
121 | ```
122 | The MIT License (MIT)
123 |
124 | Copyright (c) 2011-2014 Atul Varma
125 |
126 | Permission is hereby granted, free of charge, to any person obtaining a copy
127 | of this software and associated documentation files (the "Software"), to deal
128 | in the Software without restriction, including without limitation the rights
129 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
130 | copies of the Software, and to permit persons to whom the Software is
131 | furnished to do so, subject to the following conditions:
132 |
133 | The above copyright notice and this permission notice shall be included in
134 | all copies or substantial portions of the Software.
135 |
136 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
137 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
138 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
139 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
140 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
141 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
142 | THE SOFTWARE.
143 | ```
144 |
145 | [Polyfill]: http://remysharp.com/2010/10/08/what-is-a-polyfill/
146 | [pmxdr]: https://github.com/eligrey/pmxdr
147 | [XMLHttpRequest API]: http://www.w3.org/TR/XMLHttpRequest/
148 | [CORS Specification]: http://www.w3.org/TR/cors/
149 | [simple requests]: https://developer.mozilla.org/En/HTTP_access_control#Simple_requests
150 | [simple response header]: http://www.w3.org/TR/cors/#simple-response-header
151 | [ajax prefilter]: http://api.jquery.com/extending-ajax/#Prefilters
152 | [yepnope.js]: http://yepnopejs.com/
153 |
--------------------------------------------------------------------------------
/ppx.jquery.js:
--------------------------------------------------------------------------------
1 | (function(jQuery) {
2 | jQuery.extend({
3 | proxyAjaxThroughPostMessage: function(url) {
4 | var Request = PPX.buildClientConstructor(url);
5 | var utils = PPX.utils;
6 | url = utils.absolutifyURL(url);
7 | jQuery.ajaxPrefilter(function(options, originalOptions, jqXHR) {
8 | if (((options.crossDomain && !jQuery.support.cors) ||
9 | options.usePostMessage) &&
10 | utils.isSameOrigin(url, utils.absolutifyURL(options.url))) {
11 | options.xhr = Request;
12 | options.crossDomain = false;
13 | jqXHR.isProxiedThroughPostMessage = true;
14 | }
15 | });
16 | }
17 | });
18 | })(jQuery);
19 |
--------------------------------------------------------------------------------
/ppx.js:
--------------------------------------------------------------------------------
1 | var PPX = (function() {
2 | var config = {
3 | requestHeaders: [
4 | "Accept",
5 | "Content-Type"
6 | ],
7 | requestMethods: [
8 | "GET",
9 | "POST",
10 | "HEAD"
11 | ],
12 | requestContentTypes: [
13 | "application/x-www-form-urlencoded",
14 | "multipart/form-data",
15 | "text/plain"
16 | ]
17 | };
18 |
19 | var utils = {
20 | warn: function warn(msg) {
21 | if (window.console && window.console.warn)
22 | window.console.warn(msg);
23 | },
24 | absolutifyURL: function absolutifyURL(url) {
25 | var a = document.createElement('a');
26 | a.setAttribute("href", url);
27 | return a.href;
28 | },
29 | on: function on(element, event, cb) {
30 | if (element.attachEvent)
31 | element.attachEvent("on" + event, cb);
32 | else
33 | element.addEventListener(event, cb, false);
34 | },
35 | off: function off(element, event, cb) {
36 | if (element.detachEvent)
37 | element.detachEvent("on" + event, cb);
38 | else
39 | element.removeEventListener(event, cb, false);
40 | },
41 | encode: function encode(data) {
42 | var parts = [];
43 | for (var name in data) {
44 | parts.push(name + "=" + encodeURIComponent(data[name]));
45 | }
46 | return parts.join("&");
47 | },
48 | decode: function decode(string) {
49 | return utils.parseUri("?" + string).queryKey;
50 | },
51 | isSameOrigin: function isSameOrigin(a, b) {
52 | a = utils.parseUri(a);
53 | b = utils.parseUri(b);
54 | return (a.protocol == b.protocol && a.authority == b.authority);
55 | },
56 | map: function map(array, cb) {
57 | var result = [];
58 | for (var i = 0; i < array.length; i++)
59 | result.push(cb(array[i]));
60 | return result;
61 | },
62 | trim: function trim(str) {
63 | return str.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
64 | },
65 | // parseUri 1.2.2
66 | // (c) Steven Levithan
67 | // MIT License
68 | parseUri: function parseUri(str) {
69 | var o = utils.parseUriOptions,
70 | m = o.parser[o.strictMode ? "strict" : "loose"].exec(str),
71 | uri = {},
72 | i = 14;
73 |
74 | while (i--) uri[o.key[i]] = m[i] || "";
75 |
76 | uri[o.q.name] = {};
77 | uri[o.key[12]].replace(o.q.parser, function ($0, $1, $2) {
78 | if ($1) uri[o.q.name][$1] = decodeURIComponent($2);
79 | });
80 |
81 | return uri;
82 | },
83 | parseUriOptions: {
84 | strictMode: false,
85 | key: ["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"],
86 | q: {
87 | name: "queryKey",
88 | parser: /(?:^|&)([^&=]*)=?([^&]*)/g
89 | },
90 | parser: {
91 | strict: /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,
92 | loose: /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/
93 | }
94 | },
95 | // Taken from jQuery.
96 | inArray: function inArray(elem, array, i) {
97 | var len;
98 | var indexOf = Array.prototype.indexOf;
99 |
100 | if (array) {
101 | if (indexOf) {
102 | return indexOf.call(array, elem, i);
103 | }
104 |
105 | len = array.length;
106 | i = i ? i < 0 ? Math.max(0, len + i) : i : 0;
107 |
108 | for (;i < len; i++) {
109 | // Skip accessing in sparse arrays
110 | if (i in array && array[i] === elem) {
111 | return i;
112 | }
113 | }
114 | }
115 |
116 | return -1;
117 | }
118 | };
119 |
120 | function validateRequest(data, access, channel) {
121 | if (!access.allowOrigin) {
122 | channel.error("CORS is unsupported at that path.");
123 | return false;
124 | }
125 |
126 | if (access.allowOrigin != "*" && data.origin != access.allowOrigin) {
127 | channel.error("message from invalid origin: " + data.origin);
128 | return;
129 | }
130 |
131 | return true;
132 | }
133 |
134 | function parseAccessControlHeaders(req) {
135 | return {
136 | allowOrigin: req.getResponseHeader('Access-Control-Allow-Origin'),
137 | };
138 | }
139 |
140 | function SimpleChannel(other, onMessage, onError) {
141 | function getOtherWindow() {
142 | return other.postMessage ? other : other.contentWindow;
143 | }
144 |
145 | var self = {
146 | onMessage: onMessage,
147 | onError: onError || function defaultOnError(message) {
148 | if (window.console && window.console.error)
149 | window.console.error(message);
150 | },
151 | destroy: function() {
152 | utils.off(window, "message", messageHandler);
153 | other = null;
154 | onMessage = null;
155 | },
156 | send: function(data) {
157 | getOtherWindow().postMessage(utils.encode(data), "*");
158 | },
159 | error: function(message) {
160 | getOtherWindow().postMessage(utils.encode({
161 | __simpleChannelError: message
162 | }), "*");
163 | }
164 | };
165 |
166 | function messageHandler(event) {
167 | if (event.source != getOtherWindow())
168 | return;
169 | var data = utils.decode(event.data);
170 | if ('__simpleChannelError' in data)
171 | self.onError(data.__simpleChannelError, event.origin);
172 | else
173 | self.onMessage(data, event.origin);
174 | }
175 |
176 | utils.on(window, "message", messageHandler);
177 | return self;
178 | }
179 |
180 | return {
181 | version: "0.1",
182 | utils: utils,
183 | config: config,
184 | startServer: function startServer(options) {
185 | options = options || {};
186 |
187 | var otherWindow = options.window || window.parent;
188 | var channel = SimpleChannel(otherWindow, function(data, origin) {
189 | if (data.cmd == "send") {
190 | var req = new XMLHttpRequest();
191 | data.origin = origin;
192 | data.headers = utils.decode(data.headers);
193 |
194 | if (!utils.isSameOrigin(window.location.href, data.url)) {
195 | channel.error("url does not have same origin: " + data.url);
196 | return;
197 | }
198 | if (utils.inArray(data.method, config.requestMethods) == -1) {
199 | channel.error("not a simple request method: " + data.method);
200 | return;
201 | }
202 |
203 | req.open(data.method, data.url);
204 | req.onreadystatechange = function() {
205 | if (req.readyState == 2) {
206 | var access = parseAccessControlHeaders(req);
207 | if (options.modifyAccessControl)
208 | options.modifyAccessControl(access, data);
209 | if (!validateRequest(data, access, channel)) {
210 | req.abort();
211 | return;
212 | }
213 | }
214 | channel.send({
215 | cmd: "readystatechange",
216 | readyState: req.readyState,
217 | status: req.status,
218 | statusText: req.statusText,
219 | responseText: req.responseText,
220 | responseHeaders: req.getAllResponseHeaders()
221 | });
222 | };
223 |
224 | var contentType = data.headers['Content-Type'];
225 | if (contentType &&
226 | utils.inArray(contentType, config.requestContentTypes) == -1) {
227 | channel.error("invalid content type for a simple request: " +
228 | contentType);
229 | return;
230 | }
231 |
232 | for (var name in data.headers)
233 | if (utils.inArray(name, config.requestHeaders) == -1) {
234 | if (name == 'X-Requested-With') {
235 | /* Just ignore jQuery's X-Requested-With header. */
236 | } else {
237 | channel.error("header '" + name + "' is not allowed.");
238 | return;
239 | }
240 | } else
241 | req.setRequestHeader(name, data.headers[name]);
242 |
243 | req.setRequestHeader("X-Original-Origin", data.origin);
244 | req.send(data.body || null);
245 | }
246 | });
247 | channel.send({cmd: "ready"});
248 | },
249 | buildClientConstructor: function buildClientConstructor(iframeURL) {
250 | return function PostMessageProxiedXMLHttpRequest() {
251 | var method;
252 | var url;
253 | var channel;
254 | var iframe;
255 | var headers = {};
256 | var responseHeaders = "";
257 |
258 | function cleanup() {
259 | if (channel) {
260 | channel.destroy();
261 | channel = null;
262 | }
263 | if (iframe) {
264 | document.body.removeChild(iframe);
265 | iframe = null;
266 | }
267 | }
268 |
269 | var self = {
270 | UNSENT: 0,
271 | OPENED: 1,
272 | HEADERS_RECEIVED: 2,
273 | LOADING: 3,
274 | DONE: 4,
275 | readyState: 0,
276 | status: 0,
277 | statusText: "",
278 | responseText: "",
279 | open: function(aMethod, aUrl) {
280 | method = aMethod;
281 | url = aUrl;
282 | self.readyState = self.OPENED;
283 | if (self.onreadystatechange)
284 | self.onreadystatechange();
285 | },
286 | setRequestHeader: function(name, value) {
287 | headers[name] = value;
288 | },
289 | getAllResponseHeaders: function() {
290 | return responseHeaders;
291 | },
292 | abort: function() {
293 | if (iframe) {
294 | cleanup();
295 | self.readyState = self.DONE;
296 | if (self.onreadystatechange)
297 | self.onreadystatechange();
298 | self.readyState = self.UNSENT;
299 | }
300 | },
301 | send: function(body) {
302 | if (self.readyState == self.UNSENT)
303 | throw new Error("request not initialized");
304 |
305 | iframe = document.createElement("iframe");
306 | channel = SimpleChannel(iframe, function(data) {
307 | switch (data.cmd) {
308 | case "ready":
309 | channel.send({
310 | cmd: "send",
311 | method: method,
312 | url: utils.absolutifyURL(url),
313 | headers: utils.encode(headers),
314 | body: body || ""
315 | });
316 | break;
317 |
318 | case "readystatechange":
319 | self.readyState = parseInt(data.readyState);
320 | self.status = parseInt(data.status);
321 | self.statusText = data.statusText;
322 | self.responseText = data.responseText;
323 | responseHeaders = data.responseHeaders;
324 | if (self.readyState == 4)
325 | cleanup();
326 | if (self.onreadystatechange)
327 | self.onreadystatechange();
328 | break;
329 | }
330 | }, function onError(message) {
331 | utils.warn(message);
332 | self.responseText = message;
333 | self.readyState = self.DONE;
334 | cleanup();
335 | if (self.onreadystatechange)
336 | self.onreadystatechange();
337 | });
338 |
339 | iframe.setAttribute("src", utils.absolutifyURL(iframeURL));
340 | iframe.style.display = "none";
341 | document.body.appendChild(iframe);
342 | }
343 | };
344 |
345 | return self;
346 | };
347 | }
348 | };
349 | })();
350 |
--------------------------------------------------------------------------------
/server.py:
--------------------------------------------------------------------------------
1 | import os
2 | import mimetypes
3 | import traceback
4 | from wsgiref.simple_server import make_server
5 | from wsgiref.util import FileWrapper
6 |
7 | ROOT = os.path.abspath(os.path.dirname(__file__))
8 |
9 | mimetypes.add_type('application/x-font-woff', '.woff')
10 |
11 | def simple_response(start, contents, code='200 OK', mimetype='text/plain'):
12 | start(code, [('Content-Type', mimetype),
13 | ('Content-Length', str(len(contents)))])
14 | return [contents]
15 |
16 | def handle_request(env, start, handlers):
17 | try:
18 | for handler in handlers:
19 | response = handler(env, start)
20 | if response is not None:
21 | return response
22 | return simple_response(start, "Not Found: %s" % env['PATH_INFO'],
23 | code='404 Not Found')
24 | except Exception:
25 | msg = "500 INTERNAL SERVER ERROR\n\n%s" % traceback.format_exc()
26 | return simple_response(start, msg, code='500 Internal Server Error')
27 |
28 | class BasicFileServer(object):
29 | def __init__(self, static_files_dir):
30 | self.ext_handlers = {}
31 | self.default_filenames = ['index.html']
32 | self.static_files_dir = static_files_dir
33 |
34 | def try_loading(self, filename, env, start):
35 | static_files_dir = self.static_files_dir
36 | fileparts = filename[1:].split('/')
37 | fullpath = os.path.join(static_files_dir, *fileparts)
38 | fullpath = os.path.normpath(fullpath)
39 | if (fullpath.startswith(static_files_dir) and
40 | not fullpath.startswith('.')):
41 | if os.path.isfile(fullpath):
42 | ext = os.path.splitext(fullpath)[1]
43 | handler = self.ext_handlers.get(ext)
44 | if handler:
45 | mimetype, contents = handler(env, static_files_dir, fullpath)
46 | return simple_response(start, contents, mimetype=mimetype)
47 | (mimetype, encoding) = mimetypes.guess_type(fullpath)
48 | if mimetype:
49 | filesize = os.stat(fullpath).st_size
50 | start('200 OK', [('Content-Type', mimetype),
51 | ('Content-Length', str(filesize))])
52 | return FileWrapper(open(fullpath, 'rb'))
53 | elif os.path.isdir(fullpath) and not filename.endswith('/'):
54 | start('302 Found', [('Location', env['SCRIPT_NAME'] +
55 | filename + '/')])
56 | return []
57 | return None
58 |
59 | def handle_request(self, env, start):
60 | filename = env['PATH_INFO']
61 |
62 | if filename.endswith('/'):
63 | for index in self.default_filenames:
64 | result = self.try_loading(filename + index, env, start)
65 | if result is not None:
66 | return result
67 | return self.try_loading(filename, env, start)
68 |
69 | def cors_handler(env, start):
70 | def response(contents, origin=None):
71 | final_headers = [
72 | ('Content-Type', 'text/plain'),
73 | ('Content-Length', str(len(contents)))
74 | ]
75 | if origin:
76 | final_headers.append(('Access-Control-Allow-Origin', origin))
77 | start('200 OK', final_headers)
78 | return [contents]
79 |
80 | if env['PATH_INFO'] == '/cors/origin-only-me':
81 | origin = (env.get('HTTP_ORIGIN') or
82 | env.get('HTTP_X_ORIGINAL_ORIGIN'))
83 | return response('hai2u', origin=origin)
84 |
85 | if env['PATH_INFO'] == '/cors/origin-all':
86 | return response('hai2u', origin='*')
87 |
88 | if env['PATH_INFO'] == '/cors/origin-all/post':
89 | length = env.get('CONTENT_LENGTH', '')
90 | data = 'nothing'
91 | if length:
92 | data = env['wsgi.input'].read(int(length))
93 | return response('received ' + data, origin='*')
94 |
95 | if env['PATH_INFO'] == '/cors/origin-foo.com':
96 | return response('hai2u', origin='http://foo.com')
97 |
98 | return None
99 |
100 | def run_cors_server(port, static_files_dir):
101 | file_server = BasicFileServer(static_files_dir)
102 | handlers = [cors_handler, file_server.handle_request]
103 |
104 | def application(env, start):
105 | return handle_request(env, start, handlers=handlers)
106 |
107 | httpd = make_server('', port, application)
108 | httpd.serve_forever()
109 |
110 | def run_server(port, static_files_dir):
111 | import threading
112 |
113 | s1 = threading.Thread(target=run_cors_server, args=(port+1, ROOT))
114 | s1.setDaemon(True)
115 | s1.start()
116 |
117 | file_server = BasicFileServer(static_files_dir)
118 | handlers = [file_server.handle_request]
119 |
120 | def application(env, start):
121 | return handle_request(env, start, handlers=handlers)
122 |
123 | httpd = make_server('', port, application)
124 |
125 | url = "http://127.0.0.1:%s/" % port
126 | print "development server started at %s" % url
127 | print "press CTRL-C to stop it"
128 |
129 | httpd.serve_forever()
130 |
131 | if __name__ == '__main__':
132 | run_server(9000, ROOT)
133 |
--------------------------------------------------------------------------------
/test/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 | PostMessageProxiedXHR Unit Tests
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |