Oasis.js helps you structure communication between multiple untrusted sandboxes. It builds on the APIs in HTML5, backporting them to older browsers when necessary.
Oasis.js uses the concepts in capability-based security to help you safely expose capabilities and data to untrusted code, secure in the knowledge that the sandboxes can only communicate with each other and the outside world through those capabilities.
Download
You can download the latest version of Oasis.js here.
Getting Started
First, create a service that you would like to expose:
1 2 3 |
var PingService = Oasis.Service.extend({
});
|
Next, create a sandbox with access to that service. The sandbox's URL should be a JavaScript file. Oasis.js will take care of creating an iframe (or WebWorker) and executing the JavaScript inside of it.
1 2 3 4 5 6 7 |
oasis.createSandbox({ url: 'pingpong.js', capabilities: ['ping'], services: { ping: PingService } }); |
Note that you don't need to create your own Oasis
instance. Oasis.js
creates one for you, oasis
, and this implicit instance is enough for most
cases.
This will give the sandbox access to the PingService
.
In pingpong.js
, we'll set up a consumer for the service:
1 2 3 |
var PingConsumer = Oasis.Consumer.extend({
});
|
And connect it to the service in the parent:
1 2 3 4 5 |
oasis.connect({ consumers: { ping: PingConsumer } }) |
That gives you an idea of the basic communication structure. Now,
let's update the PingService
to ping the sandbox.
1 2 3 4 5 |
var PingService = Oasis.Service.extend({ initialize: function() { this.send('ping'); } }); |
The initialize
method on your service will be called once the
handshake with the sandbox is complete. In this case, the service
sends a ping
message to the sandbox.
To respond back to the host environment, let's update our PingConsumer
.
1 2 3 4 5 6 7 |
var PingConsumer = Oasis.Consumer.extend({ events: { ping: function() { this.send('pong'); } } }); |
Great! Now we have the host environment sending a message to the
sandbox, and the sandbox sending a message back. Let's close the
loop by listening for pong
on the PingService
.
1 2 3 4 5 6 7 8 9 10 11 |
var PingService = Oasis.Service.extend({ initialize: function() { this.send('ping'); }, events: { pong: function() { alert("Got a pong!"); } } }); |
This is the basic flow of events in Oasis. First, Oasis.js does the handshake for you. Once the handshake is complete, you can send events back and forth.
Requests
In the above example, the ping
event was really sent from the host
to the sandbox as a request for a pong
.
Because this is so common, Oasis.js provides a request
API. Let's
take a look at what it looks like to make a request from the host to
the sandbox.
1 2 3 4 5 6 7 |
var PingService = Oasis.Service.extend({ initialize: function() { this.request('ping').then(function(data) { console.log(data); }); } }); |
And here's how the sandbox would respond to the request:
1 2 3 4 5 6 7 |
var PingConsumer = Oasis.Consumer.extend({ requests: { ping: function() { return 'pong'; } } }); |
The request
API also supports Promises, a standard way of waiting for a
result for some asynchronous request.
In this case, the Promise crosses the boundary into the sandbox, and can be fulfilled in the Consumer.
1 2 3 4 5 6 7 8 9 10 |
var PingConsumer = Oasis.Consumer.extend({ requests: { ping: function() { return new Oasis.RSVP.Promise(function (resolve){ // do something asynchronous and then: resolve('pong'); }); } } }); |
Instead of listening for a ping
event, we're listening for a ping
request. Request handlers get a resolver for the promise on the other side
of the boundary that they can resolve with some data.
The interaction between the host and sandbox looks very similar to the interaction with events.
Either side of the boundary (the host and the sandbox) can initiate a request
that is handled by the requests
hash on the other side.
Sending Data With an Event
When sending an event, you can send additional data. Simply pass additional
parameters to the send
method.
1 2 3 4 5 |
var PingService = Oasis.Service.extend({ initialize: function() { this.send('ping', { additional: 'data' }); } }); |
The additional parameters will be available in the event handler on the other side of the pipe.
1 2 3 4 5 6 7 |
var PingConsumer = Oasis.Consumer.extend({ events: { ping: function(options) { console.log(options.additional); // 'data' } } }); |
The data gets passed along across the boundary.
Sending Data With a Request
Similarly, you can send additional parameters across the boundary with a request.
1 2 3 4 5 6 7 |
var PingService = Oasis.Service.extend({ initialize: function() { this.request('ping', { name: 'tom' }).then(function(data) { console.log(data); }); } }); |
The additional parameters are available on the request handler.
1 2 3 4 5 6 7 |
var PingConsumer = Oasis.Consumer.extend({ requests: { ping: function(options) { return 'hello ' + options.name; } } }); |
This will send a request to the sandbox with { name: 'tom' }
as options, which
the handler in the sandbox will use in its response.
Cross Boundary Communication
When sending data with an event or in response to a request, you are sending data through a MessageChannel.
Modern browser support sending any of the following types across the boundary:
- Boolean
- Number
- String
- Date
- Regexp
- File*
- Blob*
- FileList*
- ImageData*
- ImageBitmap*
- Object containing any of these
- Array containing any of these
The original version of Web Messaging only supported strings for communication. In older browsers, Oasis.js will attempt to polyfill the cloning algorithm used by newer browsers. However, cloning types marked with a * in the above list is not possible. The good news is that most older browsers that don't support these new types also don't support "structured cloning".
For the broadest compatibility, you should limit the objects you send
across the boundary (using send
, request
or resolve
) to the list
of types in the list above without an *.