Cross Domain data channel with HTML5 Canvas
May 6, 2012 Leave a comment
Standard Ajax is restricted to single origin policy so JSONP is the de-facto way for exchanging data with cross-domain sources and it works pretty well. Alternative, though bit hacky way, is to use HTML5 Canvas as cross domain work-around using pseudo Images as “covert channel”.
Basic idea is simple, javascript in client requests image file from 3rd party site where server encodes a data to the Image, client can use cookies and url parameters to identify itself as desired. Then client renders image, and decodes the data from the image pixels.
Backend
Backend needs to be able to construct images with custom pixel level data, in this example we use Node.js and canvas module that is server side HTML5 Canvas implementation based on Cairo graphics library.
This function accepts any object and returns canvas object that contains the objects JSON presentation encoded in image pixels.
function encodeDataToImage( data ) { // Convert data to binary buffer while being utf-8 var s = encodeURIComponent( JSON.stringify(data) ); var buffer = new Buffer(s, 'utf8'); var pixelc = (buffer.length / 3) + (buffer.length % 3 ? 1 : 0) // Encode data as PNG image var Canvas = require('canvas'); var canvas = new Canvas(pixelc, 1) var ctx = canvas.getContext('2d'); var imgdata = ctx.getImageData(0, 0, pixelc, 1); for (var i=0, k=0; i < pixelc * 4; i += 4 ) { imgdata.data[i + 3] = 0xFF; // set alpha to full opaque for (var j=0; j < 3 && k < buffer.length; k++, j++ ) { imgdata.data[i + j] = buffer[k]; } } // set "image" data ctx.putImageData(imgdata, 0, 0); return canvas; }
Define xd request handler that builds and sends the data coded image to the client. (Example in Express.js).
someapp.get('/xd', function(req, res ) { // do here something with query or cookies, like resolve uid and set // data. // Example data var data = { a: 1, en: 'owl', fi: 'pöllö', es: 'búho', uid: req.query.uid } var canvas = encodeDataToImage( data ); var img = canvas.toBuffer(); res.contentType('png'); res.header('Content-Length', img.length); res.send( img ); });
Browser
At browser side load the image and decode it back to object
function queryXD( query, callback ) { var img = new Image(); img.src = 'http://some.site.example.com/xd?' + query; img.addEventListener('load', function() { // Image loaded, create temporary canvas var canvas = document.createElement('canvas'); var ctx = canvas.getContext('2d'); // draw image on canvas canvas.width = img.width; canvas.height = img.height; ctx.drawImage( img, 0, 0 ); // collect bytes from image pixels var bytes = []; var imgdata = ctx.getImageData(0, 0, img.width, img.height); for (var i=0; i < img.width * 4; i++ ) { if ( i && (i + 1) % 4 == 0) { i++; } var b = imgdata.data[i]; if (!b) { break; } bytes.push( b ); } // convert bytes to string and parse JSON var s = decodeURIComponent( String.fromCharCode.apply(null, bytes) ); var data = JSON.parse(s); callback(false, data); }, false); // image failed to load img.addEventListener('error', function(err) { callback(err); }, false); }
And now its simple to do cross domain data exchange like
queryXD('uid=2134', function(err, data) { alert(data.en + ' is ' + data.fi + ' in Finnish and ' + data.es + ' in Spanish'); });
Caveats
Proxies and browsers like to cache the images, so use every time unique dummy parameter to force fetch.