Limitations of Apple iOS captive portal web browser

At my employer Softhouse  (the best IT consultancy in Karlskrona, and we’re hiring!) I recently got the requirement to make an AngularJS web application work properly in the captive portal window on Apple iOS.

These requests are made with HTTP header:

User-Agent: CaptiveNetworkSupport-325.10.1 wispr

A captive portal is the login screen you get when you connect to many wifi access points at hotels, airports, and similar. The application works fine in a normal Safari browser on iOS, but not in the captive portal web browser.

The captive portal web browser is almost a complete web browser, but while normal Safari session on iOS can be debugged over USB, it is not possible to debug the captive portal web browser! (If you figure it out, please comment below!)

When I could not access the JavaScript console, I had to resort to debug by displaying application state on the web page.

Finally I discovered that the JavaScript application stopped working after calling Windows.sessionStorage.setItem().

Yes, for reasons hidden in the annals of history the AngularJS application depends on Windows.sessionStorage. The original developers probably had a good reason. Or at least some reason.

The session storage service in the application looked something like this:

return {
   get: function(key) {
      return sessionStorage.getItem(key);
   },
   set: function(key, val) {
      sessionStorage.setItem(key, val);
   },
   unset: function(key) {
      sessionStorage.removeItem(key);
   }
};

So maybe sessionStorage is not defined? I tried a simple:

if (sessionStorage) {
    return {
        get: function (key) {
            return sessionStorage.getItem(key);
        },
        set: function (key, val) {
            sessionStorage.setItem(key, val);
        },
        unset: function (key) {
            sessionStorage.removeItem(key);
        }
    };
} else {
    var session = {};
    return {
        get: function (key) {
            return session[key];
        },
        set: function (key, val) {
            session[key] = String(val);
        },
        unset: function (key) {
            delete session[key];
        }
    };
}

But, same error!

According to Apple’s own documentation on Key-Value Storage, Safari can throw an exception from sessionStorage.setItem() and a Stack Overflow comment states:

I found the same behaviour on Safari to be caused by private mode browsing. It seems that while on private browsing Safari does not allow local storage to be used at all.

Maybe the captive portal works like the private mode  browsing…?

So, let’s try this code:

var hasSessionStorage = !!sessionStorage;

if (hasSessionStorage) {
    try {
        sessionStorage.setItem("hasSessionStorage", true);
    }
    catch (e) {
        hasSessionStorage = false;
    }
}

if (hasSessionStorage) {
    return {
        get: function (key) {
            return sessionStorage.getItem(key);
        },
        set: function (key, val) {
            sessionStorage.setItem(key, val);
        },
        unset: function (key) {
            sessionStorage.removeItem(key);
        }
    };
} else {
    var session = {};
    return {
        get: function (key) {
            return session[key];
        },
        set: function (key, val) {
            session[key] = String(val);
        },
        unset: function (key) {
            delete session[key];
        }
    };
}

Finally, it works!

ng-transclude that also transcludes scope

With the built-in ng-transclude the the scopes become siblings. What if we want the transcluded HTML to inherit the scope of our directive?

Thanks to Controlling Scope on the Transclude Function on ng-nuggets I was able to figure this out:

    angular
        .module('myApp.myModule')
        .directive('myDirective', myDirective);

    function myDirective() {
        return {
            controller: Controller,
            transclude: true,
            restrict: 'E',
            controllerAs: 'vm',
            link: linkThatTranscludeWithOurScope,
            scope: {}
        };
    }

    function linkThatTranscludeWithOurScope(scope, element, attrs, ctrl, transclude) {
        transclude(scope, function (clone) {
            element.append(clone);
        });
    }

PS. Do not include any template with this directive!

David’s technology radar

Inspired by ThoughtWorks Technology Radar, here is an attempt to write down my current world view:

Adopt

Trial

  • Mozilla Persona – use the same e-mail address and password to login to many web sites (not so many yet though)
  • OpenShift – cloud application platform
  • “Private cloud” – run your own “cloud” server
  • Raspberry Pi – small and cheap computer
  • Server Sent Events
  • WebSockets

Assess

  • Chef or Puppet
  • Docker
  • Gearman
  • Vagrant

Script to update an OpenShift repository with changes made on server

If you haven’t tried OpenShift yet, you should!

Anyway I have a WordPress installation there. When upgrading WordPress to a new version, files are changed on the server but they would be overwritten if I push the repository. So I created a script to download a snapshot of the OpenShift app, extract the changes in the php directory where WordPress resides, and add them to repository.

It is fully automated except for the crucial git push.

The script is called update-repo-from-snapshot.sh and is meant to be commited to the root of each repository.

PHP Allowed memory size of 134217728 bytes exhausted (tried to allocate 71 bytes)

I’m a very happy user of New Relic for my websites, even if I’m only leeching on the free tier.

One of my WordPress sites, taurin.se has been suffering from this out-of-memory error for quite some time now and I finally tracked it down. New Relic shows the stack trace, with the failing line on top:

in wpdb::get_results called at /usr/share/wordpress/wp-includes/wp-db.php (1413)
in wpdb::get_results called at /usr/share/wordpress/wp-includes/meta.php (295)
in update_meta_cache called at /usr/share/wordpress/wp-includes/post.php (4133)
in update_postmeta_cache called at /usr/share/wordpress/wp-includes/post.php (4113)
in update_post_caches called at /usr/share/wordpress/wp-includes/query.php (2534)
in WP_Query::get_posts called at /usr/share/wordpress/wp-includes/query.php (2695)
in WP_Query::query called at /usr/share/wordpress/wp-includes/query.php (2791)
.
.
.

So, it fails when updating the postmeta cache. What’s that? Well, had a look in the database.

In the database I found the wp_postmeta table. A quick check revealed that it contained thousands of syndication_item_hash entries. These are added by the FeedWordPress plugin.

Unfortunately taurin.se is quite low priority for me, and the blogs I have been syndicating seem quite dead. But the upside is that I could just disable FeedWordPress and delete all syndication_item_hash entries from the wp_postmeta table.

Now my web server should work much better!

Using mod_spamhaus to block TOR in Apache

Some web spammers use the Tor Project to hide their wrongdoings. Because of this, I want to block Tor exit nodes from submitting forms on my web sites. However, there are many legitmate uses of the Tor Project, so I don’t want to block GET requests but primarily POST requests.
  1. Run sudo apt-get install libapache2-mod-spamhaus
  2. Open /etc/apache2/mods-enabled/mod-spamhaus.conf for editing (as root)
  3. Edit the MS_METHODS configuration setting. Make sure that POST is included and GET is not. Example:
    MS_METHODS  POST,PUT,OPTIONS,CONNECT
  4. Edit the MS_Dns configuration setting. If the IP address of your server is 198.51.100.222, and the port of your web server is 80 as usual, you start with the port number, reverse the numbers in the IP address and the setting becomes:
    MS_Dns 80.222.100.51.198.ip-port.exitlist.torproject.org
    So, for IP address A.B.C.D and port E, the value becomes E.D.C.B.A.ip-port.exitlist.torproject.org
  5. You may also want to edit the MS_CustomError setting. In my case it looks like this:
    MS_CustomError “Limited access for certain clients. Please contact abuse at 2good.net to get full access to our services.”
  6. Enable the apache module with sudo a2enmod mod-spamhaus
  7. Restart apache with sudo service apache2 restart
Your will get log entries like this:

[Fri Nov 16 04:28:05 2012] [crit] [client 37.59.162.218] mod_spamhaus: address 218.162.59.37.80.222.10.74.109.ip-port.exitlist.torproject.org is blacklisted. Deny connection to forum.taurin.se/index.php, referer: http://forum.taurin.se/index.php?topic=1731.0

Injecting context into controller?

I’m toying with my own PHP micro-framework. I’m trying to make it both simple and easy; explicit but not verbose. (Good luck to me!)

For testability I have abstracted the Superglobals so that I have full control of them in my tests. Currently they are passed as a parameter to the controller action. Would it make sense to have them injected into the controller instead? It might be simpler, but it is less explicit.