pmxdr is a cross-domain XHR JavaScript library standard that doesn't rely on any
proprietary technologies like Flash, but instead uses the HTML5 postMessage API to make
requests. pmxdr stands for postMessage cross-domain requester. It respects most applicable
HTTP access control headers, even on browsers that don't support them but do support
postMessage, like Firefox 3. There is one drawback though--it requires that a pmxdr host
endpoint be on the target domain at /pmxdr/api
(can be configured to use other locations).
If you would like to implement your own pmxdr client library or pmxdr host library, please refer to the pmxdr standard at the bottom of this README.
The following browsers support pmxdr. If you know of a browser that works but is not listed, comment on this page and tell me the browser.
- Gecko 1.9.x
- Firefox 3
- Firefox 3.1/3.5
- Seamonkey 2
- Songbird 0.3+
- Flock 2
- Epiphany 2
- Google Chrome 2
- Opera
- Opera 10
- Opera 9.5+ with
MessageEvent.origin
getter fix (http://-only, no https://)
- Safari 4
- IE 8
You can try out a demo of it in action here. In two of the tests in the demo,
code.eligrey.com requests some JSON data (which turns out not to just be some JSON, but
some JavaScript code too) from eligrey.com without using the current insecure method of
inserting a script tag and handing all control over to the other website. If the demo
was done with conventional <script>
tags, the page's title would have been sent to
a (nonexistent) website that harvests page titles.
The reference client library uses the following object-oriented API. Pass a pmxdr
instance's request method an object containing any of these properties or an array of
such objects to do mutiple requests to utilize pmxdr. To create a pmxdr instance, create
a variable as new pmxdr(host)
where host is any URI from the website you want to
request (pmxdr figures out where the API is located automatically). If you don't need a
reusable pmxdr instance (ie. only plan on making one request), just use pmxdr.request
.
If you use an instance, the uri
parameter can be a relative URI. The following doesn't
apply to pmxdr.request
, only reusable instances: Only call the request
method on an
instance once the interface iframe has loaded, which you can find out when instance calls
it's onload
method once it's loaded if you set it. To start loading the instance, you
call it's init
method. To remove the interface frame, you call it's unload
method.
The following are parameters supported in a request object. Passing a string instead of
an object with parameters is the same as passing an object with only the uri
parameter
defined as the string.
method
: (Optional) Case-insensitive string representing the HTTP request method. Defaults to an instance'sdefaultRequestMethod
(which defaults topmxdr.prototype.defaultRequestMethod
and is usually"GET"
but can be changed).uri
: (Required) The full absolute URI to be requested.headers
: (Optional) An object with properties and values for headers to be sent. Example:{"X-User": "eli", "X-Real-Referrer": location.href}
.data
: (Optional) Data to send in a request. This is where you put the body of a POST request.callback
: (Optional) Function to be called when request is complete.contentType
: (Optional) Equivalent to setting the "Content-Type" header in the headers field. Defaults to the instance'sdefaultContentType
property. If you want to simulate a form POST request, don't forget to set this toapplication/x-www-form-urlencoded
.timeout
: (Optional) Time in milliseconds that the request has to finish. If the request does not finish before the timeout, the callback (if any) is sent a TIMEOUT error code. Defaults to an instance's defaultTimeout (which defaults topmxdr.prototype.defaultTimeout
).
pmxdr.prototype.defaultTimeout
is not set by default in this client library but you
can set it to have a global default timeout.
To completely remove pmxdr, call its destruct
method, which removes all event
listeners and deletes the pmxdr variable. This does not delete any still-existing
interface frames so don't forget to unload
them when you are done to save memory.
The reference host library fully supports the pmxdr standard. It needs no configuration
and defaults to the most restrictive settings of always requiring
Access-Control-Allow-Origin
and not trusting any origin by default, including the same
website itself. When you set up the host library, it must be accessible via
http://your-domain/pmxdr/api
for interoperability with pmxdr clients using the default
endpoint configuration.
The response object will be in this format:
/* req == XMLHttpRequest response */
{
pmxdr : true,
data : req.responseText,
headers : {}, // all HTTP headers (with header names lower-case)
status : req.status, // 200, 404, etc.
statusText : req.statusText, // "OK", "Not Found", etc.
id : some_id // this will be the same as the id sent in the request object
}
// requesting domain is eligrey.com
// example.com/search is a fictional search service that supports pmxdr requests
pmxdr.request({
uri : "http://example.com/search.json?q=foo",
callback: handleResponse
});
function handleResponse(response) {
if (!response.error) { // request successfull
print(response.headers["content-type"]) // prints "application/json"
print(response.data)
// prints [{"name":"foo Inc","url":"http://foo.example/","description":"the world leader in foo"},{"name":"foo experts","url":"http://foobar.example/","description":"we are experts on foo!"}]
} else print("Error: " + response.error);
}
This creates a temporary iframe which is deleted after the response is recieved with an src of "http://example.com/pmxdr/api" and sends the following JSON-encoded object to it using postMessage (the id is randomly generated with collision-detection):
{pmxdr: true, method: "GET", uri: "http://example.com/search.json?q=foo", id:"5832485502879199"} The pmxdr host (example.com/pmxdr/api) requests http://example.com/search.json?q=foo using XHR and the headers that it receives include the following:
Content-Type: application/json
Access-Control-Allow-Origin: http://example.com, http://eligrey.com, http://an-example-com-affiliate.com
Access-Control-Allow-Methods: GET, POST
Note: Access-Control-Allow-Origin: *
would also work but would allow all requests.
The pmxdr host then checks to see if the original requesting window is permitted to get the data from the HTTP request. If the original requesting window is permitted to view this data, it sends back the following response:
{pmxdr: true, headers:{"content-type":"application/json" /*, ...the rest of the headers...*/}, status: 200, data: '[{"name":"foo Inc","url":"http://foo.example/","description":"the world leader in foo"},{"name":"foo experts","url":"http://foobar.example/","description":"we are experts on foo!"}]', id:"5832485502879199"} Now that you got the basics, let's do a little more advanced example which uses example.com's more advanced fictional "search2" interface using a pmxdr instance and then doing two requests.:
var pmxdrInstance = new pmxdr("http://example.com");
pmxdrInstance.onload = function() {
this.request([{
method : "post",
uri : "/search2.json",
data : "q=foo",
callback: handleResponse,
headers : {"X-Service-Key": "hunter2", "X-User": "eli", "X-Real-Referrer": location.href},
timeout : 30000 /* == 30 seconds*/
}]);
}
function handleResponse(response) {
// do stuff with response
pmxdrInstance.unload();
}
pmxdrInstance.init();
This creates a temporary iframe which is deleted after the response is recieved OR after the timeout (30000ms, 30 seconds) with an src of "http://example.com/pmxdr/api" and sends the following JSON-encoded object to it using postMessage:
{pmxdr: true, method: "post", uri: "http://example.com/search2.json", data: "q=foo", headers: {"X-Service-Key": "hunter2", "X-User": "eli"}, id:"9130873488166656"}
The pmxdr host (example.com/pmxdr/api) requests http://example.com/search/ using an XHR
POST
request with the data, "q=foo". It also sends the headers, X-Service-Key and
X-User. The Content-Type header. The headers that it receives include the following:
Content-Type: application/json
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: POST
The pmxdr host then checks to see if the original requesting window is permitted to get the data and returns this if it is allowed:
{pmxdr: true, headers:{"content-type":"application/json" /*, ...the rest of the headers...*/}, data:'[{"name":"foo Inc","url":"http://foo.example/","description":"the world leader in foo"},{"name":"foo experts","url":"http://foobar.example/","description":"we are experts on foo!"}]', status:200, statusText:"OK" id:"9130873488166656"}
If the request is denied due to having a disallowed origin, it would return this:
{pmxdr: true, error: "DISALLOWED_ORIGIN", id:"9130873488166656", status:200, statusText:"OK"}
This example shows how to use the response headers:
pmxdr.request({ // Print the server of pmxdr host
uri : "http://pmxdr-host-enabled-website.example/",
callback : function(response){ print(response.headers.server) },
})
DISALLOWED_ORIGIN
: The host was not allowed to request the resource.DISALLOWED_REQUEST_METHOD
: Request method (ie. GET, POST, etc.) was not allowed.LOAD_ERROR
: The requested resource did not return a 2xx HTTP response.UNKNOWN
: Unknown error.TIMEOUT
: Request timed out. Note: This error is not normally sent from the pmxdr host but is usually sent to the callback directly on the client-side. A host may return this error if it wishes to have a timeout.
At any point before the callback for a request is called, you may abort the request by
calling it's abort
method. If you gave an array of requests, call the abort
method
on the appropriate index:
var request = pmxdr.request("http://example.com/foo.html");
request.abort();
// or
var requests = pmxdr.request([
"http://example.com/foo.html",
"http://example.com/bar.html"
]);
for (var i=0; i<requests.length; i++) {
requests[i].abort();
}
This interface also includes a callback method, which you can call to call the callback and remove the request before receiving the response.
The client library has the following public configuration variables:
pmxdr.prototype.defaultTimeout
: Not defined initially. Define this to apply a timeout to all pmxdr calls that don't specify an explicit timeout.pmxdr.prototype.defaultRequestMethod
: Initially set to "GET". This is the default request method if a request method if not specified in a pmxdr call.pmxdr.prototype.defaultContentType
: Not defined initially. Define this to specify a default Content-Type header value to send all data with. (ie.application/x-www-form-urlencoded
for POST form requests)pmxdr.interfaceFrameParent
: Parent node where interface frames are temporarily appended to. Initially set todocument.documentElement || document.getElementsByTagName("head")[0] || document.body || document.getElementsByTagName("body")[0]
.pmxdr.endpoints
: Array of endpoints so that you can change the defaultpmxdr/api
per origin at your desire.- You can override defaults for a whole pmxdr instance by specifying the property under
the instance. For example:
var foo=new pmxdr("http://example.com"); foo.defaultTimeout = 60000
.
Example usage of pmxdr.endpoints
:
pmxdr.endpoints.push({
origin: 'http://example.com',
endpoint: '/utils/pmxdr/api
});
pmxdr.endpoints.push({
origin: /^https?:\/\/some\.domain\.com\/some\/project,
endpoint: '/some/project/pmxdr/api.php'
});
The host library also has a private array named alwaysTrustedOrigins, which can be filled
with protocol+host+non-standard port strings and regular expressions. Any website with an
origin matching one of the strings or regular expressions in the array is allowed to skip
the Access-Control-Allow-Origin header verification. This does not mean that the trusted
host is also allowed to skip checks with the Access-Control-Allow-Methods
and
Access-Control-Allow-Headers
headers.
The following is an example configuration of alwaysTrustedOrigins
:
var alwaysTrustedOrigins = [ // change to always trusted origins
/^[\w-]+:(?:\/\/)?([\w\.-]+\.)?eligrey\.com(?::\d+)?$/, // any origin on any protocol that has a domain that ends in eligrey.com
"http://www.google.com", // only http://www.google.com is allowed, not http://foo.google.com:30/
/\.example\.net$/, // *.example.net
/^https?:\/\/([\w\.-]+\.)?example\.com$/ // these will all be allowed: https://example.com, http://example.com, https://*.example.com, http://*.example.com
];
The pmxdr (postMessage cross-domain requester) standard defines the standard
communication format used by pmxdr clients and hosts via the postMessage interface.
You may use this standard to implement your own pmxdr-compliant libraries. The key words
"must", "must not", "required", "shall", "shall not", "should", "should not",
"recommended", "may", and "optional" in this document are to be interpreted as described
in RFC 2119. pmxdr hosts must always be accessible through the following relative
location from a website and should not redirect to another location: /pmxdr/api
.
Note: All requests and responses to hosts and clients must be valid JSON format. The order of properties in request and response JSON objects must not affect any aspect of communication between host and client.
Requests to pmxdr hosts must be in a valid JSON object. The requests must have all of
the following required properties in the root of the JSON object (ie. if "a" is required,
it must be at {a}
, not {foo:{a}}
). All properties must be treated as case-sensitive.
pmxdr
- Required property. Must be
true
.
- Required property. Must be
id
- Optional property. Should be a string. It is recommended to make this a unique
identification that is not the same as the identification of a request still being
processed by the pmxdr host. Must not be null or anything that can
== false
in JavaScript. If this property is specified in a request, the host must define an identical id property in it's response JSON object.
- Optional property. Should be a string. It is recommended to make this a unique
identification that is not the same as the identification of a request still being
processed by the pmxdr host. Must not be null or anything that can
uri
- Required property. Should be an absolute or relative location accessible by the
pmxdr host. The host must attempt to request this location using an
XMLHttpRequest and should parse the following response headers and the origin
of the requesting client and determine if the client is allowed to access the
resource. Other methods may be used like the
alwaysTrustedOrigins
array used in the pmxdr host library reference implementation.Access-Control-Allow-Origin
Access-Control-Allow-Methods
- Required property. Should be an absolute or relative location accessible by the
pmxdr host. The host must attempt to request this location using an
XMLHttpRequest and should parse the following response headers and the origin
of the requesting client and determine if the client is allowed to access the
resource. Other methods may be used like the
method
- Required property. It must be a string and must be treated as case-insensitive and
converted to upper-case by the host. This must be used as the method for the
XMLHttpRequest used to request
uri
.
- Required property. It must be a string and must be treated as case-insensitive and
converted to upper-case by the host. This must be used as the method for the
XMLHttpRequest used to request
data
- Optional property. It must be a string and it should be sent as the body of an HTTP request if specified in a request.
headers
- Optional property. It must be an object. All of it's properties should be strings.
A JSON object in this format must be sent if no errors occurred and the requesting origin is allowed to view the response.
pmxdr
- Required property. Must be
true
.
- Required property. Must be
id
- If the request object specifies an id property, this property must be identical to that property.
status
- This property must be identical to the
XMLHttpRequest
request'sstatus
property.
- This property must be identical to the
statusText
- This property must be identical to the
XMLHttpRequest
request'sstatusText
property.
- This property must be identical to the
data
- Required property. Must be the value of the responseText property of the XMLHttpRequest.
headers
- Required property. For every response header, the header name must be converted to lower-case and put as a property in this object with the header's value as the property's value.
A JSON object in this format must be sent if an error occurred or the requesting origin is allowed to view the response.
pmxdr
- Required property. Must be
true
.
- Required property. Must be
id
- If the request object specifies an id property, this property must be identical to that property.
status
- This property must be identical to the
XMLHttpRequest
request'sstatus
property.
- This property must be identical to the
statusText
- This property must be identical to the
XMLHttpRequest
request'sstatusText
property.
- This property must be identical to the
error
- Required property. Should be a valid error code from the list of standard error codes below but may be any message. This must be a string and must be upper-case.
- Standard error codes
DISALLOWED_ORIGIN
: The request origin was not allowed to request the resource.DISALLOWED_REQUEST_METHOD
: Request method (ie. GET, POST, ect.) was not allowed.LOAD_ERROR
: The requested resource did not return a 2xx HTTP response.UNKNOWN
: Unknown error. If you are lazy or just don't want to let a website know which thing is not allowed, make your host library send this instead of any error.TIMEOUT
: Request timed out.
pmxdr request and response format extensions (extra properties on request and response
JSON objects) must be put in an "x"
object. For example, the foobar extension would use
x.foobar
as it's property. The x
object must be treated as case sensitive. The
following is an example "remaining-requests"
response format extension that signifies
how many more requests are allowed to be made:
{"pmxdr":true, /*...*/ "x":{"remaining-requests":5}}