Jul 7

On my latest project, I was faced with a challenge: build Flash widgets that displayed dynamic data and could be embedded on any web page. Phase two of the widgets called for user interaction with the widget, as opposed to simply displaying content. It seemed that Flex would be the most logical technology to use for this project.

I used the Services and AMFPHP modules for Drupal to expose content via web services for my Flex widget to consume. However, I ran into a problem with the critical piece that Flex needs in order to get remote data. As a workaround to the browser security settings that prevent cross-site scripting, Adobe chose to implement an opt-in solution in the form of a file called crossdomain.xml. With crossdomain.xml, a site owner may allow a list of domains to read its data and the client, Flash in this case, is responsible for enforcement. As the business case for the project called for the widget to be embedded on any domain, I needed to use a promiscuous crossdomain policy, allowing access from all domains:

XML:
  1. <cross-domain-policy>
  2.   <allow-access-from domain="*"/>
  3. </cross-domain-policy>

Because of this policy, the widget was allowed to read all data on the site that the user has access to, including any (authenticated) content and (session) cookies. This could easily lead to privacy violations, account takeovers, theft of sensitive data, and bypassing of CSRF protections.

Other domains in a similar predicament have simply hosted their APIs on a different domain, thus preventing access to user data on the root domain. Unfortunately, the nature of Drupal doesn't allow for splitting the Services functionality out from the rest of the platform.

My solution was to write a forwarder, which is just a really simple PHP script, that is hosted on a subdomain. This subdomain hosts the promiscuous crossdomain policy, and the policy file on the root domain is configured to only accept requests from the subdomain. The forwarder does two things: it first makes sure that only requests to /services/amfphp are allowed; if allowed, the $HTTP_RAW_POST_DATA is sent to the root domain via cURL.

PHP:
  1. <?php
  2. // Configuration variables
  3. $server = 'http://mydomain.com/services/amfphp';
  4.  
  5. $request_uri = trim($_SERVER['REQUEST_URI']);
  6. if ($request_uri[0] === '/'){
  7.   $request_uri = substr($request_uri, 1);
  8. }
  9.  
  10. // Split the uri into components
  11. list($handler, $protocol) = split('/', $request_uri);
  12.  
  13. // Filter out unwanted requests
  14. if(($handler != 'services') && ($protocol != 'amfphp')){
  15.   exit();
  16. }
  17.  
  18. // Handle the post from the flash/flex client
  19. $xml = $HTTP_RAW_POST_DATA;
  20. if(strlen($xml) == 0){
  21.   exit();
  22. }
  23.  
  24. // Set the headers
  25. $header[] = "Content-type: text/xml";
  26. $header[] = "Content-length: ".strlen($xml);
  27.  
  28. $ch = curl_init($server);
  29. curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
  30. curl_setopt($ch, CURLOPT_TIMEOUT, 30);
  31. curl_setopt($ch, CURLOPT_HTTPHEADER, $header);
  32. curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
  33.  
  34. $response = curl_exec($ch);
  35.  
  36. if (curl_errno($ch)) {
  37.   exit();
  38. }
  39. else {
  40.   curl_close($ch);
  41.   if(strlen($response) == 0){
  42.     exit();
  43.   }
  44.   print $response;
  45. }
  46.  
  47. exit();
  48.  
  49. ?>

Because the subdomain doesn't host any data at all, the security risk has been removed. If you are working with Drupal and Flash remoting, you'll need to consider the risks associated with promiscuous crossdomain policy files. While my solution certainly isn't the only solution, it is pretty effective and simple.

tags: , , , , , , , , , , ,

Apr 3

drupal1.pngDrupal is incredibly flexible, but in current versions, lacks the ability to export content easily in the form of widgets. However, the Services module gives you that flexibility in a very easy to use manner.

Services allows you to expose pieces of your Drupal site, such as user, node, and views methods. Combine this with the integration of AMFPHP, you can build some extremely fast Flash and Flex widgets that display dynamic data and can be embedded on any website.

However, there is a catch.

I was working with another developer on a Facebook application that displayed my Flex widget, and it seems that in Facebook, you have to provide a static image for the user to click on in order to active the flash. This is clunky, and not the ideal solution for me.

Another option was to attempt to create cross-domain javascript widgets. I looked at JSON Server, which is a Services wrapper that returns data in JSON. This module worked great on the same domain, but failed on cross-domain calls. The reason why is that the module only accepts POST requests.

I ended up patching the module to accept GET requests structured in the following manner:

JavaScript:
  1. function get(){
  2.   headElement = document.getElementsByTagName("head").item(0);
  3.   var script = document.createElement("script");
  4.   script.setAttribute("type", "text/javascript");
  5.   // The callback parameter is needed so that the JSON gets returned correctly in order to be handled by the output function
  6.   script.setAttribute("src","http://server.com/services/json?method=views.getView&view_name=some_view&callback=display");
  7.   headElement.appendChild(script);
  8. }
  9.  
  10. function display(obj){
  11.   var theDiv = document.createElement("div");
  12.   theDiv.innerHTML = obj.data;
  13.   document.body.appendChild(theDiv);
  14. }

You'll note that I am doing script tag inserts into the head of the calling website. The script source is set to the path of my JSON server, with the services method as a parameter. The services method takes arguments, which differ depending on the service you are calling. In the case of the Views.getView service, you have to supply the name of the view to call.

The last parameter specifies the name of your output function. My patch takes this callback and wraps the JSON-formatted data returned by the service with the name of the callback. The callback is extremely important, as the JSON sent back by the server acts as a call to your defined callback function. From there, your callback function can display the data however it chooses.

Implementation of this method on a remote site is very easy:

JavaScript:
  1. <script type="text/javascript" src="http://server.com/path/to/javascript.js"></script>
  2. <script type="text/javascript">get();</script>

For my widget, I created a custom service that returned the output of a theme() function to render the widget template. This html output was then displayed by my JSON callback function.

Using the Services and (patched) JSON Server modules for Drupal, you can quite easily set up cross-domain javascript/html widgets that can be embedded anywhere.

tags: , , , , , , , , ,

Apr 18

I recently built an RSS feed for work, and noticed that in IE 7, the RSS toolbar button was not lit up. A little research on the Microsoft Team RSS blog showed me the solution. Add the following to the section of your website:

HTML:
  1. <link rel="alternate" type="application/rss+xml" title="your feed title here" href="http://www.company.com/feedurl.rss">

tags: , , , , , ,

Jan 25

Frequently asked questions

If you can't find the information you're looking in this FAQ, please, contact me. I will be glad to help.

HELP! It doesn't work! What should I do?

Well, basically neither the server script, nor the client script does anything special that is not supported by your server or your browser. So don't worry, it should work in 99,9% cases. Here are the possible workarounds:

Make sure you installed AJAXExtended correctly

If you haven't deleted the test files from the server yet, this check is a piece of cake. Fire up you browser and type in the following address: http://www.yourwebserver.com/path-to-the-php-files/tests/ (notice the tests folder). If it shows a green box saying everything is ok, it means just that: AJAXExtended is installed properly. If it shows a red box or something else, please, check the installation instructions.

Make sure you got an error handling function

During a server-to-server request the proxy might encounter different kind of problems and it can successfully cope with some of them. So the server won't just die unexpectedly and forget about the client request. AJAXExtended allows you to handle these errors. Just like that:

var request = new XMLHTTP()
request.onerror = function(description) {
alert('Error: ' + description)
}

For instance, if the requested server is down or it's name cannot be resolved, AJAXExtended will trigger the error handler and pass on the following description: “Error occurred while connecting to the specified host”.

How does the script access other domains?

The trick is that the script doesn't use the native XMLHTTP object. It works just the same, but it employs a different technology: dynamic SCRIPT tags. It is not a security vulnerability in any way. It's an absolutely standard feature, supported by all browsers.

How does the script handle long requests?

Since the script relies on dynamic inclusion of the SCRIPT tags, all the data we upload to the server is generally a part of the SCRIPT url. Although, the specs impose no limit on the length of the GET request, all browsers do. The tests say we can be quite safe if the length of the SCRIPT url doesn't exceed 2000 symbols. If the request is longer than 2000 symbols (the limit is configurable), the script splits the request into parts.

For instance, instead of doing this:

http://ajaxextended.com/api/? ... a very-very long string that might break the browser...

we're doing this:

http://ajaxextended.com/api/? ... 1st part of this request ...
http://ajaxextended.com/api/? ... 2nd part of this request ...

In the meantime, the server caches all the parts of the request. And when all the parts are received and assembled, the server backend finally performs the server-to-server request.

How does the server cache multipart requests?

If the request is too long, the client script splits the data into multiple parts that do not exceed the specified maximum length. When a server receives a part of the multipart request it writes the data into a cache file. The name of the cache file is the same as the unique ID of the request (yes, all requests have a unique ID). When all parts are received, the script removes the cache file and performs the server-to-server request.

What is the syntax of the response?

If you've ever heard of JSON, it'll be really easy to understand how the response is constructed. Here is a real example of the reponse (getting the homempage of google.ru):

v35422275185316._parse({
'success': '1',
'responseHeaders': {
'Cache-Control': 'private',
'Content-Type': 'text/html',
'Content-Encoding': 'gzip',
'Server': 'GWS/2.1',
'Content-Length': '1852',
'Date': 'Mon, 06 Feb 2006 18:55:53 GMT',
'Connection': 'Keep-Alive'
},
'status': '200',
'statusText': 'OK',
'responseText': ' ...the code of the page...'
})

As you can see, all the data is encapsulated in curly brackets {}, that represent objects in JavaScript. Note, that both keys and values are quoted. v35422275185316 is the unique ID of the request. Here's a brief description of the properties:

success
Possible values are 0, 1. Normally, it is 1. If an error occurs during the request, it is 0.
responseHeaders
HTTP headers received from the third-party server
status
HTTP status code (200, 404, etc.)
statusText
The text that describes the HTTP status code ('OK', 'Not found', etc.)
responseText
The body of the reponse (quotes escaped)

If a fatal error occurs (the server script cannot connect to the specified website or it cannot create a cache file, or anything of the kind), the response will have only two properties:

v35422275185316._parse({
'success': '0',
'description': 'Cannot connect to www.unreachableserver.com'
})
Jan 25

Installation instructions

The following document outlines the process of installation of AJAXExtended to you web server. Please note, however, that in order to use these instructions, you should have some basic server administration skills. Be sure, you're comfortable with terms like file permissions, scripts, cgi-bin, sockets etc.

1. Downloading the latest copy

AJAXExtended is stored in a Subversion repository at http://www.professionalconsulting.ru/svn/ajaxextended/trunk. If you have subversion installed on your local machine, you can check out the current version by typing svn co http://www.professionalconsulting.ru/svn/ajaxextended/trunk. If you have no subversion, you can either install it or click on the link above and download all the files one-by-one with your browser. In this case, be sure to keep the same directory structure.

Package description

  • MIT-LICENSE
  • INSTALLATION.html
  • README.html
  • xmlhttp.js
  • php.server/
    • data/
      • empty
    • tests/
      • index.php
      • base.php
      • session.php
      • simpletest/
        • compatibility.php
        • dumper.php
        • errors.php
        • expectation.php
        • invoker.php
        • reflection_php4.php
        • reflection_php5.php
        • reporter.php
        • scorer.php
        • simpletest.php
        • test_case.php
        • unit_tester.php
    • xmlhttp/
      • base.php
      • session.php
      • pear/
        • json.php
        • pear.php
        • request.php
        • socket.php
        • url.php
    • index.php
  • python.server/
    • httplib.py

/xmlhttp.js — that's the javascript part of AJAXExtended. It contains all the JS classes required for the XMLHTTP magic to work.

/python.server/ — that's just a placeholder for the coming-soon Python version.

/php.server/ — this directory contains the server part of AJAXExtended (PHP version). Make sure you have all these files and directories on your computer (including the empty data directory). Some of these files (e.g. unit-testing files) won't be required for production use, but for installation purposes they're required.

2. Uploading the files to your web server

  • First of all, upload all the files contained in the /php.server/ to any directory on your webserver that is available for public access.
  • Open xmlhttp.js in any text editor and change the line that says apiURL: 'http://api.ajaxextended.com/' to apiURL: 'http://www.yourwebserver.com/path-to-the-php-files/index.php'. The 'index.php' part is optional, you can skip it if you're sure you web server will treat this file as the default index file. In any case 'index.php' should correspond with the '/php.server/index.php' package file. Save xmlhttp.js and upload it to your web server.

3. Checking the server scripts

Once you're done uploading the files, fire up your browser window and type in the following address: http://www.yourwebserver.com/path-to-the-php-files/tests/ Depending on what you see on the screen, there're several options:

  1. You see a green box that says everything's just fine. You're almost done. Skip to to next step.
  2. You see a red box that says something failed (and mentions 'testcreatingsessionfile'). Change the permissions of the /data/ directory so that the PHP script has enough priviliges to write into this directory. A common (but not the best) solution is to do chmod 777, but it might be different with your server configuration.
  3. You see the source code of the script. Your web server does not support PHP or this functionality is disabled. In this case you should refer to your web server manual.

It doesn't work and you don't have a clue?

Send me an e-mail describing the problem and don't forget to include the description of you web server, the results of script testing (probably, even a screenshot) and other relevant information. Without this information, I won't be able to help you. Usually, you can expect a reply within a day.

4. Embedding AJAXExtended in your web pages

Insert a script tag:


in every html page of your site that will use AJAXExtended.

5. Test it

Start your javascript application and make sure AJAXExtended works.

6. Delete unnecessary files

Remove the /tests/ directory from your web server.

You're done. Congratulations!

Just don't forget to get back to AJAXExtended website every once in a while to upgrade you local copy to the latest stable release.

Jan 25

What is AJAXExtended?

AJAXExtended is a JS/server library that make cross-domain AJAX requests possible. It does not use any proprietary methods or security holes — it relies on a server-side proxy to fetch data from any 3rd party server.

Although a server proxy might seem a bit obvious and a way-too-simple solution, it's a bit more complicated than it seems. To emulate a cross-domain XMLHTTPRequest, a server proxy should support HTTP headers (including cookies), HTTP authentication, data re-encoding and more. AJAXExtended does just it all.

The JavaScript part of AJAXExtended has the same properties, methods and events as the native browser implementation. You don't have to rewrite your code — just plug in the AJAXExtended and you're in business.

Key features

  • Cross-domain AJAX requests
  • HTTP headers support (yes, you can manipulate cookies)
  • Works in 99%+ browsers (no matter what security restrictions apply)
  • Native syntax

How does this work?

AJAX Extended loads the data via dynamic SCRIPT tags. It doesn't use the standard XMLHTTP objects (i.e. ActiveX and XMLHttpRequest) or IFRAMEs, because these have lots of drawbacks. Just to name a few:

  • Microsoft.XMLHTTP relies on ActiveX that makes it virtually impossible to use XMLHTTP in a strict security environment.
  • Both XMLHttpRequest (Mozilla, Firefox, Safari) and Microsoft.XMLHTTP impose strict cross-domain restrictions that cannot be overridden in a cross-browser way.
  • IFRAME used to be the only solution for Opera users (and still is for older versions). It consumes lots of memory (since technically every IFRAME is a new browser window) therefore it is quite slow and it breaks the browser history.

AJAXExtended is free of all these drawbacks since it relies on a very simple technology: creating dynamic SCRIPT tags. No cross-domain or other security restrictions and no usability problems (breaking browser history). It works in all modern browsers: IE5+, Mozilla, Firefox, Opera, Safari (except a buggy 2.0 version, 2.0.2 is ok).

Security

Too many people speculate on the issue of AJAX cross-domain requests. However, there're no security implications whatsoever: no possibility of attacks, no personal data theft. No nothing. Most of those people who consider cross-domain requests (through JS) a vulnerability just don't get the idea.

Below you will find some short key points. For an in-depth overview of the underlying security issues, please, take a look at this article: AJAX greatest security myth busted.

Third-party website ruining your website
The data you get from a third-party website is not executed. It means that any malicious script that might be received from a third-party website won't ever run. Unless you explicitly order so.
Stealing personal information
You can't steal user's personal information that belongs to a third-party website (cookies, that is). The user's browser connects to the server proxy (not the third-party website), so it never sends out any cookies belonging to the third-party website. Cookies are safe. Absolutely.
Stealing secret information (from the intranet)
One thing to remember: the data is received via the server-backend. The server has the same visibility scope as any other computer on the net. So it can't get any data from the intranet or any other closed part of the net that the user has access to.
Stealing not-so-secret information
Some people argue, that using cross-domain requests you may steal content from a third-party website. But AJAX has absolutely nothing to do with it. One can just as easily copy&paste the content from any source to one's own website. It's a matter of ethics, not technology.

Interface

The JavaScript class contains all the same properties, events, and methods as the original XMLHttpRequest objects (except browser-specific methods and properties). You don't have to rewrite your code. All you have to do is to include the supplied javascript file.

Example

Instead of doing this (in Internet Explorer):

xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");

or this (in Firefox):

xmlhttp = new XMLHttpRequest();

with AJAXExtended you can do it like this (in any browser):

xmlhttp = new XMLHTTP();

Just don't forget to include the supplied javascript file:

Methods, properties and events

  • method abort ()
  • method addEventListener (type, listener, useCapture) not implemented
  • method dispatchEvent (event) not implemented
  • method getAllResponseHeaders ()
  • method getResponseHeader (name)
  • method open (method, url, [async, [username, [password]]])
  • method openRequest (method, url, [async, [username, [password]]])
  • method overrideMimeType (mimetype)
  • method removeEventListener (type, listener, useCapture) not implemented
  • method send ([data])
  • method setRequestHeader (name, value)
  • property status
  • property statusText
  • property responseText
  • property responseBody not implemented
  • property responseStream not implemented
  • property responseXML supported in previous version
  • property readyState
  • event onreadystatechange
  • event onerror
  • event onload
  • event onprogress not implemented