Rigel Group

They shoot Yaks, don't they?

DevOps for People Who Hate DevOps

DevOps started out as scrappy developers who were just trying to Get Shit Done. They needed a couple of machines set up, and thought, “Hey, I am programmer. Why don’t I program the machine to set itself up!”. Viola! Genius, I tell you! But then, sadly, DevOps grew to become a Movement, and was co-opted by System Administrators and large commercial enterprises looking for something complicated to sell. The tools (Puppet, Chef, Im looking at you) became Rube Goldberg contraptions orders of magnitude more complicated than the little old LAMP stack we were trying to setup in the first place. Now you need DevOps infrastructure to manage your infrastructure. DevOps took the place of SysAdmins, and the average developers were left behind.

Then, like a breath of fresh air, Ansible came on the scene. Billed as a dead-simple DevOps system that relies on nothing more than SSH, it was love at first sight. Unfortunately, Ansible, it seems, has succummed to the siren song of the Enterprise, and if you were to look at the AnsibleWorks web site today, you would weep at the amount of marketing techospeak that has been strewn about. But fear not! The core of Ansible has not changed, and if you can wade past the BS you will find a jewel that may become the sharpest tool in your belt.

So, if we break down Ansible to it’s core, it is essentially a way to script an SSH session to a server. There are no prerequisites for the server. As long as you can reach it via SSH, and it has Python installed, Ansible can manage it. The magic sauce that Ansible brings to the party, is that it’s playbooks are YAML files that declaritively specify how the machine should look, and Ansible will do whatever needs to be done to make the machine look like that. So running Ansible is idempotent, you can run the same playbook against a machine multiple times, and if everything has already been done, it won’t do it again.

Let’s get started, for OSX follow along:

Make sure you have homebrew installed, and then

1
$ brew install python

This gives you a nice local version of python we will install Ansible into.

1
2
3
4
5
$ pip install jinja2
$ pip install PyYAML
$ pip install paramiko
$ pip install boto
$ pip install ansible

will have an ansible command to use. We can test it out by doing this (on OSX, make sure you have Remote Login checked in System Preferences –> Sharing so that you can SSH into localhost):

1
2
3
4
5
6
7
$ echo "[local]\n127.0.0.1 ansible_python_interpreter=/usr/local/Cellar/python/2.7.3/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python" >> ~/ansible_hosts
$ export ANSIBLE_HOSTS=~/ansible_hosts
$ ansible all -m ping --ask-pass     #this will fail if you dont have an SSH server running on localhost.
127.0.0.1 | success >> {
    "changed": false,
    "ping": "pong"
}

launch_instance —ami=ami-bfd3a3d6 —type=m1.small —key=ansible-ec2-us-east —dns —groups=Web —region=us-east-1 ~/.boto

Now, we need a herd of boxen to manage, so for that grab your Amazon EC2 keys, make sure you have the EC2 command line tools installed and configured, and saddle up.

First, we are going to create a playbook, which is a YAML file that describes what we want done. This first playbook is a bit of a mindbender, because we are going to script Ansible to log into our local machine, and then run EC2 scripts to provision and boot EC2 instances. Once we have those, we can then use Ansible to manage our newborn servers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
---
# Ansible Playbook to create and manage EC2 servers

- name: Provision servers
  hosts: local
  connection: local
  user: john
  gather_facts: false

  tags:
      - provision

  vars:
      keypair: ~/ansible_ec2.pem
      instance_type: t1.micro
      security_group: Web
      # bitnami-cloud-us-west-2/lampstack/bitnami-lampstack-5.4.11-1-linux-ubuntu-12.04.1-x86_64-s3.manifest.xml
      image: ami-0021ab30
      instance_count: 2

  # Provision 2 servers...

  tasks:
    - name: Launch server
      local_action: ec2 keypair=${keypair} group=${security_group} instance_type=${instance_type} image=${image} wait=yes count=${instance_count}
      register: ec2

    # Use with_items to add each instances public IP to a new hostgroup for use in the next play.

    - name: Add new servers to host group
      local_action: add_host name=${item.public_dns_name} groups=deploy
      with_items: ${ec2.instances}

    - name: Wait for SSH to be available
      local_action: wait_for host=${item.public_dns_name} port=22
      with_items: ${ec2.instances}

    - name: Wait for full boot
      pause: seconds=15

# Now, configure our new servers

- name: Configure servers
  hosts: deploy
  user: ubuntu
  sudo: yes
  gather_facts: true

  tags:
    - config
    - configure

  # Install Java, install Elasticsearch, replace settings....

  tasks:

    - name: Install JRE
      apt: pkg=openjdk-6-jre-headless state=latest install_recommends=no update_cache=yes

    - name: Download ElasticSearch package
      get_url: url=http://download.elasticsearch.org/elasticsearch/elasticsearch/elasticsearch-0.90.0.deb dest=~/elasticsearch-0.90.0.deb

    - name: Install ES .deb file
      shell: dpkg -i ~/elasticsearch-0.90.0.deb
      notify: restart elasticsearch

    - name: Install cloud-aws plugin
      shell: /usr/share/elasticsearch/bin/plugin -install elasticsearch/elasticsearch-cloud-aws/1.11.0
      notify: restart elasticsearch

    - name: Make elasticsearch config dir
      file: path=/etc/elasticsearch/ state=directory

    - name: Copy over Elasticsearch settings
      copy: src=./elasticsearch/elasticsearch.yml dest=/etc/elasticsearch/elasticsearch.yml
      notify: restart elasticsearch

  handlers:
    - name: restart elasticsearch
      action: service name=elasticsearch state=restarted

Hacking the (Minecraft) Matrix With JRuby

If you have children of a certain age, or are a child at heart yourself, you have probably come across Minecraft, a wonderful game that proves that gameplay and creativity can still trump fancy graphics, explosions, and photoreal environments.

Minecraft is all about building things, but you build in the game, using only the tools that the game’s creator gives you. After having mastered the game from the inside, my sons wanted to see the matrix. They wanted to know how it worked, and change it, and come up with their own tools and their own rules. So, after poking around a bit, I discovered that Minecraft is written in Java, and there is a huge community of people who mod the game. But Java is not the best language to teach my 9 year old, so a bit more digging brought me to this awesome project by one the JRuby guys, Purugin. This lets you program Minecraft using Ruby, which sounds like a whole lot of fun, so let’s get started.

(These instructions assume you are on OSX. The same ideas should translate over to Windows as well.)

First, go to Minecraft and download (and purchase) the desktop client, and get that working on its own.

Next, we need to get the CraftBukkit server, which will hold our world and our custom code, and we will eventually connect our clients to this server.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mkdir ~/Code/CraftBukkit
cd ~/Code/CraftBukkit
curl -L http://dl.bukkit.org/downloads/craftbukkit/get/02084_1.5.1-R0.2/craftbukkit-beta.jar > craftbukkit.jar
echo "cd ~/Code/CraftBukkit" > start.sh
echo "java -Xms1024M -Xmx1024M -jar craftbukkit.jar -o true" >> start.sh
chmod +x start.sh

mkdir plugins
cd plugins
curl -LO http://dev.bukkit.org/media/files/675/889/purugin-1.4.7-R1.0.1-bukkit-1.4.7-R1.0-SNAPSHOT.jar
curl -LO https://github.com/enebo/Purugin/raw/master/examples/generators/cube.rb
curl -LO https://github.com/enebo/Purugin/raw/master/examples/generators/tunnel.rb
curl -LO https://github.com/enebo/Purugin/raw/master/examples/purogo.rb
mkdir purogo
cd purogo
curl -LO https://github.com/enebo/Purugin/raw/master/examples/purogo/tower.rb
curl -LO https://github.com/enebo/Purugin/raw/master/examples/purogo/pyramid.rb
curl -LO https://github.com/enebo/Purugin/raw/master/examples/purogo/star.rb
curl -LO https://github.com/enebo/Purugin/raw/master/examples/purogo/cube.rb

Now, let’s start the server:

1
~/Code/CraftBukkit/start.sh

You should see the server start up. Make sure you see lines like this in the output, which indicate that the JRuby plugins loaded OK:

1
2
3
4
5
6
7
08:31:54 [INFO] [PuruginPlugin] Loading PuruginPlugin v1.4.7-R1.0.1
08:32:05 [INFO] [PuruginPlugin] Enabling PuruginPlugin v1.4.7-R1.0.1
08:32:05 [INFO] [Cube Generator] version 0.2 ENABLED
08:32:05 [INFO] [purogo] version 0.2 ENABLED
08:32:05 [INFO] [Tunnel Generator] version 0.1 ENABLED
08:32:05 [INFO] Done (8.607s)! For help, type "help" or "?"
>

At this point, you can run the Minecraft client App, and say Multiplayer –> Direct Connect to localhost, and you should connect to our server.

As you are playing Minecraft, you can issue commands by typing /, so lets try our first command by typing this in the Minecraft client:

1
/cube 5 5 5

This will create a 5-block cube in front of you. The code that made that happen is here

cube.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class CubeGenerationPlugin
  include Purugin::Plugin
  description 'Cube Generator', 0.2

  def on_enable
    public_command('cube', 'make n^3 cube of type', '/cube {dim}') do |me, *args|
      dim = error? args[0].to_i, "Must specify an integer size"
      error? dim > 0, "Size must be an integer >0"
      type = args.length > 1 ? args[1].to_sym : :glass
      z_block = error? me.target_block.block_at(:up), "No block targeted"

      me.msg "Creating cube of #{type} and size #{dim}"
      dim.times do
        y_block = z_block
        dim.times do
          x_block = y_block
          dim.times do
            x_block.change_type type
            x_block = x_block.block_at(:north)
          end
          y_block = y_block.block_at(:east)
        end
        z_block = z_block.block_at(:up)
      end
      me.msg "Done creating cube of #{type} and size #{dim}"
    end
  end
end

Any .rb file you put in the ~/Code/CraftBukkit/plugins directory, will be automatically picked up and available for you to call.

So, that’s great, and it’s Ruby, which is a bit nicer (IMHO) than Java, however it’s still a bit beyond my 9-year-old son. Luckily, there is also a simple Logo implementation available to us, so that in Minecraft we can type

1
/draw tower

and you should see a chicken drawing a tower in front of you. The code that does this, is

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
turtle("tower") do
  # Draw base of cube
  square do
    4.times do |i|
      mark i
      forward 5
      turnleft 90
    end
  end

  pillars do
    4.times do |i|
      goto i
      turnup 90
      forward 5
    end # Still at top of last pillar
    turndown 90
  end

  3.times do
    square
    pillars
  end
  square
end

Ahh, a much nicer syntax for a child to grasp, and they can see their creation come to life in front of them, which is highly motivating.

Any .rb file (written in the Logo-ish syntax) you put in ~/Code/CraftBukkit/plugins/purogo will be available to call with the /draw command.

So, we have barely scatched the surface of what can be done, but it is wonderful to see a child’s eyes light up the first time they “hack the matrix” and write code that creates something they can actually see in the Minecraft world. Many thanks to Tom Enebo for creating Purugin, and hopefully sparking an interest in programming in our children.

Validating SAML Tickets in JRuby

UPDATE This has problems with newer Java versions. A solution is here.

Implementing SAML is an effort in frustration and should be avoided at all costs. However, if you have the unenviable task of doing so, you may enjoy this little tale of misery, which fortunately does have a happy ending.

Let’s say you want to implement a Single-Sign-On (SSO) solution for your Rails app, and SAML is the protocol you are going to use. If you are running MRI Ruby, then everything is peachy, and you actually have some really good choices. My favorite is Samlr by Morten Primdahl from Zendesk. It is a well-architected gem that implements the parts of SAML you would care about for the standard use-cases of integrating with Microsoft’s ADFS or OneLogin or Okta.

If you are using Omniauth then there is a nice little gem to hook everything together omniauth-samlr.

Under the covers, Samlr, and a lot of other SAML implementations for Ruby, use Nokogiri to work with the XML. This will be important later in our story.

Now, if you are deploying your Rails app on Torquebox (and really, why wouldn’t you?), that means you are running in JRuby land. Usually this is not a problem. But it this case, it is. But first, a little background.

The SAML protocol relies on your browser passing around XML documents to different parties to verify your identity. Portions of the XML are cryptographically signed to prevent tampering. This is done according to the XML-Signature spec. However, due to the way XML is processed, each party needs to make sure they sign the exact same XML document or snippet with the same ordering of attributes, same whitespace handling, same namespace declarations, etc etc. This process is called Canonicalization.

Whew. So now we know enough to understand why this little Nokogiri bug (or rather, incomplete feature) is going to cause us problems with SAML. Nokogiri Issue #808, which says that Canonicalization (sometimes referred to as C14N) is broken in the JRuby version.

OK, so we are stymied. But wait! We are running on JRuby, right? That means we have access to a whole universe of Enterprise-y java goodness. In fact, the “gold standard” for implementing XML Signature is the JSR-105 API. So lets use that, shall we?

First, let’s take a look at a SAML response ticket, which usually looks something like this:

saml_response.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<?xml version="1.0"?>
<samlp:Response Destination="https://example.org/saml/endpoint" ID="samlr123" InResponseTo="samlr789" IssueInstant="2012-08-07T22:42:45Z" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
    <saml:Issuer>ResponseBuilder IdP</saml:Issuer>
    <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
        <SignedInfo>
            <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
            <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
            <Reference URI="#samlr123">
                <Transforms>
                    <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                    <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                        <InclusiveNamespaces PrefixList="#default samlp saml ds xs xsi" xmlns="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                    </Transform>
                </Transforms>
                <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                <DigestValue>qrDVhkkXlV9eA32p/l6NcQbkCJc=</DigestValue>
            </Reference>
        </SignedInfo>
        <SignatureValue>HVd+DQCgPO4YVS0q8iL1HR7Hh8v0J4Z7qg4vANzFoYhgEXnoOym2Ynntvb7ugTu4B41G0B5Rx7DGP2fTrZ3qyA==</SignatureValue>
        <KeyInfo>
            <X509Data>
                <X509Certificate>MIIBjTCCATegAwIBAgIBATANBgkqhkiG9w0BAQUFADBPMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZXhhbXBsZS5vcmcxHTAbBgNVBAsMFFphbWwgUmVzcG9uc2VCdWlsZGVyMQswCQYDVQQDDAJDQTAeFw0xMjA4MDgwMjAxMDlaFw0zMjA4MDMwMjAxMTRaME8xCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtleGFtcGxlLm9yZzEdMBsGA1UECwwUWmFtbCBSZXNwb25zZUJ1aWxkZXIxCzAJBgNVBAMMAkNBMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALb9pPmyHrbZJMDLLkVsHzzXvP7DFcPiYdaNU50l5znRr8ZGhwRZFAwKroOxXwhK5e9lz06C+kGqnL1v10h1BEUCAwEAATANBgkqhkiG9w0BAQUFAANBAKU10RznL2p7xRhO9vOh0CY+gWYmT2kbkLTVRYLApghQFAW8EzIHC/NggfEHM554ykzbbPwjSvM7cRBBDHYuWoY=</X509Certificate>
            </X509Data>
        </KeyInfo>
    </Signature>
    <samlp:Status>
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </samlp:Status>
    <saml:Assertion ID="samlr456" IssueInstant="2012-08-07T22:42:45Z" Version="2.0" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
        <saml:Issuer>ResponseBuilder IdP</saml:Issuer>
        <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
            <SignedInfo>
                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
                <Reference URI="#samlr456">
                    <Transforms>
                        <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                            <InclusiveNamespaces PrefixList="#default samlp saml ds xs xsi" xmlns="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                        </Transform>
                    </Transforms>
                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                    <DigestValue>2W5OKvcKDQHoXRpku9S57Q0uUME=</DigestValue>
                </Reference>
            </SignedInfo>
            <SignatureValue>Fi2qxMs0Nf05Iz5NY/eW1Q7/pIn4BY7bHAbBeJGr+dShNPG35vkp16HpeLmrK2fOjgE6sdYxVsbOpsJ6j9pYbQ==</SignatureValue>
            <KeyInfo>
                <X509Data>
                    <X509Certificate>MIIBjTCCATegAwIBAgIBATANBgkqhkiG9w0BAQUFADBPMQswCQYDVQQGEwJVUzEUMBIGA1UECgwLZXhhbXBsZS5vcmcxHTAbBgNVBAsMFFphbWwgUmVzcG9uc2VCdWlsZGVyMQswCQYDVQQDDAJDQTAeFw0xMjA4MDgwMjAxMDlaFw0zMjA4MDMwMjAxMTRaME8xCzAJBgNVBAYTAlVTMRQwEgYDVQQKDAtleGFtcGxlLm9yZzEdMBsGA1UECwwUWmFtbCBSZXNwb25zZUJ1aWxkZXIxCzAJBgNVBAMMAkNBMFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBALb9pPmyHrbZJMDLLkVsHzzXvP7DFcPiYdaNU50l5znRr8ZGhwRZFAwKroOxXwhK5e9lz06C+kGqnL1v10h1BEUCAwEAATANBgkqhkiG9w0BAQUFAANBAKU10RznL2p7xRhO9vOh0CY+gWYmT2kbkLTVRYLApghQFAW8EzIHC/NggfEHM554ykzbbPwjSvM7cRBBDHYuWoY=</X509Certificate>
                </X509Data>
            </KeyInfo>
        </Signature>
        <saml:Subject>
            <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress">[email protected]</saml:NameID>
            <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                <saml:SubjectConfirmationData InResponseTo="samlr789" NotOnOrAfter="2012-08-07T22:43:45Z" Recipient="https://example.org/saml/endpoint"/>
            </saml:SubjectConfirmation>
        </saml:Subject>
        <saml:Conditions NotBefore="2012-08-07T22:41:45Z" NotOnOrAfter="2012-08-07T22:43:45Z">
            <saml:AudienceRestriction>
                <saml:Audience>example.org</saml:Audience>
            </saml:AudienceRestriction>
        </saml:Conditions>
        <saml:AuthnStatement AuthnInstant="2012-08-07T22:42:45Z" SessionIndex="samlr456">
            <saml:AuthnContext>
                <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
            </saml:AuthnContext>
        </saml:AuthnStatement>
        <saml:AttributeStatement>
            <saml:Attribute Name="tags">
                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">mean horse</saml:AttributeValue>
            </saml:Attribute>
            <saml:Attribute Name="things">
                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">one</saml:AttributeValue>
                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">two</saml:AttributeValue>
                <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:string">three</saml:AttributeValue>
            </saml:Attribute>
        </saml:AttributeStatement>
    </saml:Assertion>
</samlp:Response>

So, we whip up (and by “whip up”, I mean hours of trawling the web, skimming hundreds of pages of specs and docs, and cargo-culting just enough code to work) a Java class that is a command line tool as well as exposing a method for our Ruby code to call. It will be a simple function that will validate the signatures on our SAML response.

Validator.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
import javax.xml.crypto.*;
import javax.xml.crypto.dsig.*;
import javax.xml.crypto.dom.*;
import javax.xml.crypto.dsig.dom.DOMValidateContext;
import javax.xml.crypto.dsig.keyinfo.*;
import java.io.FileInputStream;
import java.security.*;
import java.security.cert.*;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import java.io.StringReader;
import java.io.File;
import java.util.Scanner;
/**
 * Validating an XML Signature using the JSR 105 API. It assumes the key needed to
 * validate the signature is contained in a KeyInfo node.
 */
public class Validator {

    //
    // Synopsis: java Validator [document]
    //
    //    where "document" is the name of a file containing the XML document
    //    to be validated.
    //
    public static void main(String[] args) throws Exception {
        String samlResponse = new String(readFile(args[0]));
        System.out.println("Valid?: " + validate(samlResponse));
    }

    // Validator.validate(saml_response) returns boolean indicating if the doc has been validated
    public static boolean validate(String samlResponse) {
        boolean coreValidity = false;
        try {
            DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
            dbf.setNamespaceAware(true);
            Document doc = dbf.newDocumentBuilder().parse(new InputSource(new StringReader(samlResponse)));

            // Find Signature element
            NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature");
            if (nl.getLength() == 0) {
                throw new Exception("Cannot find Signature element");
            }

            // Create a DOM XMLSignatureFactory that will be used to unmarshal the
            // document containing the XMLSignature
            XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM");

            // Create a DOMValidateContext and specify a KeyValue KeySelector
            // and document context
            DOMValidateContext valContext = new DOMValidateContext(new RawX509KeySelector(), nl.item(0));

            XMLSignature signature = fac.unmarshalXMLSignature(valContext);

            // Validate the XMLSignature (generated above)
            coreValidity = signature.validate(valContext);
        } catch (Exception ex) {
            System.out.println("[SAML Validator] Exception:" + ex.getMessage());
            coreValidity = false;
        }
        // // Check core validation status
        // if (coreValidity == false) {
        //     System.err.println("Signature failed core validation");
        //     boolean sv = signature.getSignatureValue().validate(valContext);
        //     System.out.println("signature validation status: " + sv);
        //     // check the validation status of each Reference
        //     Iterator i = signature.getSignedInfo().getReferences().iterator();
        //     for (int j=0; i.hasNext(); j++) {
        //         boolean refValid =
        //             ((Reference) i.next()).validate(valContext);
        //         System.out.println("ref["+j+"] validity status: " + refValid);
        //     }
        // } else {
        //     System.out.println("Signature passed core validation");
        // }
        return coreValidity;
    }

    /**
     * KeySelector which would retrieve the X509Certificate out of the
     * KeyInfo element and return the public key.
     * NOTE: If there is an X509CRL in the KeyInfo element, then revoked
     * certificate will be ignored.
     */
    public static class RawX509KeySelector extends KeySelector {

        public KeySelectorResult select(KeyInfo keyInfo,
                                        KeySelector.Purpose purpose,
                                        AlgorithmMethod method,
                                        XMLCryptoContext context)
            throws KeySelectorException {
            if (keyInfo == null) {
                throw new KeySelectorException("Null KeyInfo object!");
            }
            // search for X509Data in keyinfo
            Iterator<?> iter = keyInfo.getContent().iterator();
            while (iter.hasNext()) {
                XMLStructure kiType = (XMLStructure) iter.next();
                if (kiType instanceof X509Data) {
                    X509Data xd = (X509Data) kiType;
                    Object[] entries = xd.getContent().toArray();
                    X509CRL crl = null;
                    // Looking for CRL before finding certificates
                    for (int i = 0; (i < entries.length && crl == null); i++) {
                        if (entries[i] instanceof X509CRL) {
                            crl = (X509CRL) entries[i];
                        }
                    }
                    Iterator<?> xi = xd.getContent().iterator();
                    while (xi.hasNext()) {
                        Object o = xi.next();
                        // skip non-X509Certificate entries
                        if (o instanceof X509Certificate) {
                            if ((purpose != KeySelector.Purpose.VERIFY) &&
                                (crl != null) &&
                                crl.isRevoked((X509Certificate)o)) {
                                continue;
                            } else {
                                return new SimpleKeySelectorResult
                                    (((X509Certificate)o).getPublicKey());
                            }
                        }
                    }
                }
            }
            throw new KeySelectorException("No X509Certificate found!");
        }
    }

    private static class SimpleKeySelectorResult implements KeySelectorResult {
        private PublicKey pk;
        SimpleKeySelectorResult(PublicKey pk) {
            this.pk = pk;
        }

        public Key getKey() { return pk; }
    }

    private static String readFile(String pathname) throws Exception {
        File file = new File(pathname);
        StringBuilder fileContents = new StringBuilder((int)file.length());
        Scanner scanner = new Scanner(file);
        String lineSeparator = System.getProperty("line.separator");

        try {
            while(scanner.hasNextLine()) {
                fileContents.append(scanner.nextLine() + lineSeparator);
            }
            return fileContents.toString();
        } finally {
            scanner.close();
        }
    }
}

You will also need this jar file xmlsec-1.5.3 which contains the JSR-105 API.

You can compile the code with javac Validator, and run it with java Validator saml_response.xml

The really cool part, since we are running on the JVM anyway, is that you can call this from your JRuby code, like this:

1
2
3
4
5
6
7
8
#!/bin/env jruby
# Tell JRuby where your xmlsec jar file is by appending a path to CLASSPATH...
$CLASSPATH << File.join(File.dirname(__FILE__))
import "Validator"


saml_response = File.read("saml_response.xml")
Validator.validate(saml_response)

So now that we have a way of validating the signatures, we can patch Samlr to use this function when running under JRuby. It’s a hack, but it works. The patched gem is here, and the relevant part is here:

signature.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
if RUBY_ENGINE == 'jruby'
  $CLASSPATH << File.join(File.dirname(__FILE__))
  import "Validator"
end

module Samlr
  class Signature
  ...
    def verify!
      raise SignatureError.new("No signature at #{prefix}/ds:Signature") unless present?

      verify_fingerprint! unless options[:skip_fingerprint]

      # HACK since Nokogiri doesnt support C14N under JRuby.
      # So we use the Validate.java class to do the validation using JSR-105 API in xmlsec-1.5.3.jar
      if RUBY_ENGINE == 'jruby'
        unless Validator.validate(@original.to_s)
          raise SignatureError.new("Signature validation error (java).")
        end
      else
        verify_digests!
        verify_signature!
      end

      true
    end
  ...
  end
end

So thats it! I hope someone out there will find this useful. And yes, I am well aware of the irony of using the Java superpowers of JRuby to work around a problem that only occurs if you are running on JRuby.

TorqueBox Background Processes and ActiveRecord

TorqueBox makes it really easy to send jobs to a background process, similiar to Delayed Job or Resque, only you dont need to run and monitor yet another system, it’s all contained in the TorqueBox app server (which is actually JBOSS).

However, if you are going to be doing something ActiveRecord-y in the background, there are a few things you need to realize. When you use ActiveRecord in Rails, a middleware is installed that closes database connections and returns them to the connection pool after each web request. But when you are using ActiveRecord outside of a web request cycle, you need to worry about the connections yourself.

If you dont do anything, and you use ActiveRecord in your TorqueBox background processes or message queues, eventually you will run out of database connections, as they are not being closed. You can track this issue here.

The convention in Rails for handling connections yourself goes like this:

1
2
3
ActiveRecord::Base.connection_pool.with_connection do
  Post.find(1) # etc
end

This ensures that after you are done with the connection it gets closed. But using this technique is a bit awkward when using background processes. Plus you have to remember to do it every time. Another approach is to monkey patch the MessageProcessor class like so:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  module TorqueBox
    module Messaging
      class MessageProcessor
        def process!(message)
          @message = message
          begin
            on_message( message.decode )
          rescue Exception => e
            on_error( e )
          ensure
            ActiveRecord::Base.connection_handler.clear_active_connections!
          end
        end
      end
    end
  end

Here we just add an ensure block to make sure we always clear out the connections when we are done. This will apply to all message processors, which includes the Backgroundable processor.

It looks like there are some chainable middleware constructs that could be used to do this in a cleaner way. That’s a project for another day.

Torquebox Shims for MRI Ruby

Torquebox is hands down the best way to deploy a high-performance, highly available Rails application. All the power of Java/JVM/JBOSS, with none of the XML. :) But, doing your development on Torquebox is still a bit of a pain when compared to rails s or using POW. With a little effort, you can build a Rails app that will run fine on MRI as well as on JRuby and under Torquebox.

For certain things, Rails makes it easy to choose different subsystems depending on the environment you are running in. So, if you want to take advantage of Infinispan caching when running under Torquebox in production, but use the standard in-memory cache in development/MRI, then in your production.rb you say config.cache_store = :torque_box_store, and in development.rb you say config.cache_store = :memory_store.

For other things, we can set up shims for Torquebox-specific features when running under MRI. The first thing we need to do is determine when we are running in a Torquebox environment, and when we are not. The simplest thing is usually the best, so lets set a system level environment variable called INSIDE_TORQUEBOX to true, and use that to activate the shims. In production we can do this in our startup scripts just before we launch Torquebox.

Now, back in our Rails app, we have an initializer called 00_torquebox.rb that looks like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
if ENV["INSIDE_TORQUEBOX"] == "true"
  Rails.logger.info "[TorqueBoxInitializer] Inside TorqueBox"
else
  Rails.logger.info "[TorqueBoxInitializer] NOT inside Torquebox, shimming TB functions..."
  module TorqueBox
    module Messaging
      class MessageProcessor
        attr_accessor :message

        def initialize
          @message = nil
        end

        def on_error(error)
          raise error
        end

        def on_message(body)
          throw "Your subclass must implement on_message(body)"
        end

        def process!(message)
          @message = message
          begin
            on_message( message.decode )
          rescue Exception => e
            on_error( e )
          end
        end
      end #MessageProcessor

      module Backgroundable
        extend ActiveSupport::Concern
        module ClassMethods
          def always_background(*methods)
          end
        end
      end #Backgroundable

    end #Messaging

    module Injectors
      # Look in the torquebox.yml file to map a queue name to a class
      def self.config
        @@config ||= YAML.load_file("#{Rails.root}/config/torquebox.yml")
      end

      class FakeQueue
        def initialize(klass)
          @klass = klass
        end
        def publish(msg)
          Rails.logger.info "[Queue] USING FAKE QUEUE #{@klass.name} -- PROCESSING EVENT DIRECTLY!"
          k = @klass.new
          k.on_message(msg)
        end
      end

      def fetch(queue_name)
        klass = Injectors.config['messaging'][queue_name].constantize
        FakeQueue.new(klass)
      end
      alias_method :inject, :fetch
      alias_method :__inject__, :fetch
    end #Injectors

  end #TorqueBox

  # Mixin to all AR models
  ActiveRecord::Base.send(:include, TorqueBox::Messaging::Backgroundable)

end

(gist here)

The main things addressed in the above shim, is stubbing out always_background so that is just ignored when running on MRI, and also short-circuiting out the message processors, such that if you were to say

1
2
queue = fetch('/queues/myqueue')
queue.publish(data)

then when running on MRI it will not try to publish the data to some non-existant queue, but instead figure out which message processor would have been called by Torquebox and just call it directly.

There is now a no-op Gem in the Torquebox repo started by Joe Kutner that came to be after we had set up this initializer, so you might want to check that out as well.

Greasemonkey for IE

Recently, I needed to inject a bit of Javascript into every web page. If I were using a modern browser, this would have been a non-issue. Chrome/Safari/Firefox all have plug-in architectures that allow for easy custom mods. Unfortunately, I needed to do this for IE. After much searching around, it seems that there are really no good solutions to this problem for Internet Explorer. There use to be a thing called Trixie that puportedly ran Greasemonkey scripts, but its website is defunct. There is also IE7Pro which is a massively invasive extension and not suitable for our simple requirement.

So, looks like we have to whip one up ourselves. How hard could that be, right? Turns out it’s annoying, excruciatingly difficult. The fine folks at Add-In-Express make things a lot easier. So, you will need this product to follow along.

The goal is simple, inject an arbitrary snippet of Javascript into every web page visited by the browser. To do this, fire up Visual Studio and create a new project from the Add In Express templates.

I would suggest targeting .NET Framework 3.5 instead of 4, since it is much more compatible. For example, try installing .NET 4.x on Windows Server 2008 R2 SP1. Go on, I dare ya!

Anyway, once that is done, we just need to add a few bits to the template that the AddInExpress folks so kindly made for us. Find the IEModule.cs file, and make it look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
using System;
using System.Runtime.InteropServices;
using System.ComponentModel;
using System.Windows.Forms;
using IE = Interop.SHDocVw;
using AddinExpress.IE;
using System.IO;
using mshtml;

namespace KaleoInjectorV2
{
    /// <summary>
    /// Add-in Express for Internet Explorer Module
    /// </summary>
    [ComVisible(true), Guid("2C8C66D2-0D13-4B46-871F-910B5ABC25AD")]
    public class IEModule : AddinExpress.IE.ADXIEModule
    {
        private String globalScript = "/* c:\\MyApp\\injector.js empty or not found. */";
        private Boolean globalScriptLoaded = false;

        public IEModule()
        {
            InitializeComponent();
            //Please write any initialization code in the OnConnect event handler
        }

        public IEModule(IContainer container)
        {
            container.Add(this);

            InitializeComponent();
            //Please write any initialization code in the OnConnect event handler
        }

        #region Component Designer generated code
        /// <summary>
        /// Required by designer
        /// </summary>
        private System.ComponentModel.IContainer components;

        /// <summary>
        /// Required by designer support - do not modify
        /// the following method
        /// </summary>
        private void InitializeComponent()
        {
            //
            // IEModule
            //
            this.HandleShortcuts = true;
            this.LoadInMainProcess = false;
            this.ModuleName = "MyApp";
            this.HandleDocumentUICommands = true;
            // Make our event handler run whenever the HTML document has completed loading
            this.DocumentComplete2 += new AddinExpress.IE.ADXIEDocumentComplete2_EventHandler(this.IEModule_DocumentComplete2);
        }
        #endregion

        #region ADX automatic code

        // Required by Add-in Express - do not modify
        // the methods within this region

        public override System.ComponentModel.IContainer GetContainer()
        {
            if (components == null)
                components = new System.ComponentModel.Container();
            return components;
        }

        [ComRegisterFunctionAttribute]
        public static void RegisterIEModule(Type t)
        {
            AddinExpress.IE.ADXIEModule.RegisterIEModuleInternal(t);
        }

        [ComUnregisterFunctionAttribute]
        public static void UnregisterIEModule(Type t)
        {
            AddinExpress.IE.ADXIEModule.UnregisterIEModuleInternal(t);
        }

        [ComVisible(true)]
        public class IECustomContextMenuCommands :
            AddinExpress.IE.ADXIEModule.ADXIEContextMenuCommandDispatcher
        {
        }

        [ComVisible(true)]
        public class IECustomCommands :
            AddinExpress.IE.ADXIEModule.ADXIECommandDispatcher
        {
        }

        #endregion

        public IE.WebBrowser IEApp
        {
            get
            {
                return (this.IEObj as IE.WebBrowser);
            }
        }

        public mshtml.HTMLDocument HTMLDocument
        {
            get
            {
                return (this.HTMLDocumentObj as mshtml.HTMLDocument);
            }
        }

        private void IEModule_DocumentComplete2(object pDisp, string url, bool rootDocLoaded)
        {
            if (rootDocLoaded && !String.IsNullOrEmpty(url) && !url.StartsWith("about:") && this.notInjectedYet())
            {
                this.loadGlobalScript();
                HTMLDocument doc = this.HTMLDocument;
                var head = doc.getElementsByTagName("head").item(null, 0) as mshtml.IHTMLElement;
                IHTMLScriptElement scriptObject = (IHTMLScriptElement)doc.createElement("script");
                ((IHTMLElement)scriptObject).id = @"myapp-injector";
                scriptObject.type = @"text/javascript";
                scriptObject.text = @"/* Injecting c:\MyApp\injector.js */  " + this.globalScript;
                ((HTMLHeadElement)head).appendChild((IHTMLDOMNode)scriptObject);

                Marshal.ReleaseComObject(scriptObject);
                Marshal.ReleaseComObject(head);
            }
        }

        private Boolean notInjectedYet()
        {
            HTMLDocument doc = this.HTMLDocument;
            IHTMLElement script = doc.getElementById("myapp-injector");
            return (script == null);
        }


        // Load the javascript from the file system. In this simple example we are just loading it from a hard-coded path
        private void loadGlobalScript()
        {
            if (!this.globalScriptLoaded)
            {
                try
                {
                    using (StreamReader sr = new StreamReader("C:\\MyApp\\injector.js"))
                    {
                        this.globalScript = sr.ReadToEnd();
                        this.globalScriptLoaded = true;
                    }
                }
                catch (Exception e)
                {
                    MessageBox.Show(@"MyApp Browser Extension Error reading C:\\MyApp\\injector.js -- " + e.Message);
                }
            }
        }

    }
}

And that’s it. Add-In-Express will also generate a nice setup.exe package for you, which can be installed on any windows box and away you go. Once your custom javascript is injected into the page, you can do whatever you like using standard web technologies. Here is an example of what that might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// Injector.js

// Set regex used later to determine which pages we should run on.
INJECTOR_URL_REGEX = /google\.com/;

// Include the handly yepnope library to conditionally load jQuery only if the current page doesnt already have it.
/*yepnope1.5.x|WTFPL*/
(function(a,b,c){function d(a){return"[object Function]"==o.call(a)}function e(a){return"string"==typeof a}function f(){}function g(a){return!a||"loaded"==a||"complete"==a||"uninitialized"==a}function h(){var a=p.shift();q=1,a?a.t?m(function(){("c"==a.t?B.injectCss:B.injectJs)(a.s,0,a.a,a.x,a.e,1)},0):(a(),h()):q=0}function i(a,c,d,e,f,i,j){function k(b){if(!o&&g(l.readyState)&&(u.r=o=1,!q&&h(),l.onload=l.onreadystatechange=null,b)){"img"!=a&&m(function(){t.removeChild(l)},50);for(var d in y[c])y[c].hasOwnProperty(d)&&y[c][d].onload()}}var j=j||B.errorTimeout,l=b.createElement(a),o=0,r=0,u={t:d,s:c,e:f,a:i,x:j};1===y[c]&&(r=1,y[c]=[]),"object"==a?l.data=c:(l.src=c,l.type=a),l.width=l.height="0",l.onerror=l.onload=l.onreadystatechange=function(){k.call(this,r)},p.splice(e,0,u),"img"!=a&&(r||2===y[c]?(t.insertBefore(l,s?null:n),m(k,j)):y[c].push(l))}function j(a,b,c,d,f){return q=0,b=b||"j",e(a)?i("c"==b?v:u,a,b,this.i++,c,d,f):(p.splice(this.i++,0,a),1==p.length&&h()),this}function k(){var a=B;return a.loader={load:j,i:0},a}var l=b.documentElement,m=a.setTimeout,n=b.getElementsByTagName("script")[0],o={}.toString,p=[],q=0,r="MozAppearance"in l.style,s=r&&!!b.createRange().compareNode,t=s?l:n.parentNode,l=a.opera&&"[object Opera]"==o.call(a.opera),l=!!b.attachEvent&&!l,u=r?"object":l?"script":"img",v=l?"script":u,w=Array.isArray||function(a){return"[object Array]"==o.call(a)},x=[],y={},z={timeout:function(a,b){return b.length&&(a.timeout=b[0]),a}},A,B;B=function(a){function b(a){var a=a.split("!"),b=x.length,c=a.pop(),d=a.length,c={url:c,origUrl:c,prefixes:a},e,f,g;for(f=0;f<d;f++)g=a[f].split("="),(e=z[g.shift()])&&(c=e(c,g));for(f=0;f<b;f++)c=x[f](c);return c}function g(a,e,f,g,h){var i=b(a),j=i.autoCallback;i.url.split(".").pop().split("?").shift(),i.bypass||(e&&(e=d(e)?e:e[a]||e[g]||e[a.split("/").pop().split("?")[0]]),i.instead?i.instead(a,e,f,g,h):(y[i.url]?i.noexec=!0:y[i.url]=1,f.load(i.url,i.forceCSS||!i.forceJS&&"css"==i.url.split(".").pop().split("?").shift()?"c":c,i.noexec,i.attrs,i.timeout),(d(e)||d(j))&&f.load(function(){k(),e&&e(i.origUrl,h,g),j&&j(i.origUrl,h,g),y[i.url]=2})))}function h(a,b){function c(a,c){if(a){if(e(a))c||(j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}),g(a,j,b,0,h);else if(Object(a)===a)for(n in m=function(){var b=0,c;for(c in a)a.hasOwnProperty(c)&&b++;return b}(),a)a.hasOwnProperty(n)&&(!c&&!--m&&(d(j)?j=function(){var a=[].slice.call(arguments);k.apply(this,a),l()}:j[n]=function(a){return function(){var b=[].slice.call(arguments);a&&a.apply(this,b),l()}}(k[n])),g(a[n],j,b,n,h))}else!c&&l()}var h=!!a.test,i=a.load||a.both,j=a.callback||f,k=j,l=a.complete||f,m,n;c(h?a.yep:a.nope,!!i),i&&c(i)}var i,j,l=this.yepnope.loader;if(e(a))g(a,0,l,0);else if(w(a))for(i=0;i<a.length;i++)j=a[i],e(j)?g(j,0,l,0):w(j)?B(j):Object(j)===j&&h(j,l);else Object(a)===a&&h(a,l)},B.addPrefix=function(a,b){z[a]=b},B.addFilter=function(a){x.push(a)},B.errorTimeout=1e4,null==b.readyState&&b.addEventListener&&(b.readyState="loading",b.addEventListener("DOMContentLoaded",A=function(){b.removeEventListener("DOMContentLoaded",A,0),b.readyState="complete"},0)),a.yepnope=k(),a.yepnope.executeStack=h,a.yepnope.injectJs=function(a,c,d,e,i,j){var k=b.createElement("script"),l,o,e=e||B.errorTimeout;k.src=a;for(o in d)k.setAttribute(o,d[o]);c=j?h:c||f,k.onreadystatechange=k.onload=function(){!l&&g(k.readyState)&&(l=1,c(),k.onload=k.onreadystatechange=null)},m(function(){l||(l=1,c(1))},e),i?k.onload():n.parentNode.insertBefore(k,n)},a.yepnope.injectCss=function(a,c,d,e,g,i){var e=b.createElement("link"),j,c=i?h:c||f;e.href=a,e.rel="stylesheet",e.type="text/css";for(j in d)e.setAttribute(j,d[j]);g||(n.parentNode.insertBefore(e,n),m(c,0))}})(this,document);

( function() {
  // Safe log function for IE
  function log(msg){if (window['console'] !== undefined){console.log(msg)} }

  if (!INJECTOR_URL_REGEX.test(document.location.href)) {
    log("Injector Not Activated. " + document.location.href + " does not match " + INJECTOR_URL_REGEX);
  } else {
    log("Injector Activated. " + document.location.href + " matched " + INJECTOR_URL_REGEX);
    yepnope({
      test: typeof(jQuery) == 'undefined',
      yep: [
        "https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"
      ],
      both: [
        "http://mysite.com/my_custom_javascript.js"
      ],
      complete: function() {
        log("Injector Completed");
      }
    });
  }

})();

Annotating the Rails View Cache

The Rails View Cache is a powerful tool to use against those pesky performance problems. Caching is, as they say, a bit like violence — if it’s not solving your problem, you aren’t using enough of it. However, they also say that caching is one of the truly hard problems in computer science. Which leads me to this blog post…

Let’s suppose that after liberally sprinkling cache statements everywhere in your view code, your site is super fast, and everything is working fine. Except when it isn’t. So you’re spelunking through the HTML in the browser trying to debug things, but how can you tell which HTML content came from the cache? And when was it cached?

Just pop this short incantation in config/initializers/annotate_view_cache.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
module ActionView
  module Helpers
    module CacheHelper
      private
      def fragment_for(name = {}, options = nil, &block) #:nodoc:
        if fragment = controller.read_fragment(name, options)
          fragment
        else
          # VIEW TODO: Make #capture usable outside of ERB
          # This dance is needed because Builder can't use capture
          pos = output_buffer.length
          yield
          output_safe = output_buffer.html_safe?
          fragment = output_buffer.slice!(pos..-1)
          if output_safe
            self.output_buffer = output_buffer.class.new(output_buffer)
          end

          # BEGIN MODIFIED CODE
          annotated_fragment = "<!-- Begin Cache: #{ActiveSupport::Cache.expand_cache_key(name)} at #{Time.zone.now} -->\n" << fragment << "\n<!-- End Cache: #{ActiveSupport::Cache.expand_cache_key(name)} -->\n"

          controller.write_fragment(name, annotated_fragment, options)
        end
      end
    end
  end
end

and then you will see HTML comments like this around cached content:

<!-- Begin Cache: v3/right-sidebar at 2012-11-10 18:49:33 -0800 -->

<p>Some cached content here</p>

<!-- End Cache: v3/right-sidebar -->

This makes it easy to see whats happening and track down any issues. It is worth noting that this is a very brittle mod, if new versions of Rails change how things work internally this will probably blow up in your face. You have been warned.

Page-Specific Javascript Using Coffeescript

Rails apps that take advantage of the asset pipeline typically have all their javascript files packaged up (and perhaps minified) into one file, mainly for performance reasons. So that means every page in your app loads all the javascript, and you are left with figuring out some way to have some specific javascript code run only on a specific page. There are loads of techniques out on the net that accomplish this, but here is another one we came up with, which leverages the beauty of Coffeescript. If you are not a fan, stop reading now.

OK, you’re still here, which means you are not averse to some Coffeescript, so lets get started. The first thing we do is create a Base class that will house all code that should be run on every page.

1
2
3
4
5
6
7
8
9
10
11
window.MyApp ||= {}
class MyApp.Base

  # Rails server code can pass in some bootstrap data if necessary, which we will save off for use anywhere on the client
  constructor: (bootstrap_data={}) ->
    @bd = bootstrap_data
    # Put your general code here. Don't worry about document ready since this
    # code will only be called once the document is ready

    # MAKE SURE YOU RETURN this
    this

Next, create a class for each Rails Controller, so for example if we have a Posts Controller in our app, we would create this Coffeescript class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
window.MyApp ||= {}
class MyApp.Posts extends Base

  constructor: () ->
      # By calling super, we make sure that all the code in the Base class constructor gets run
    super

    # Your code goes here that should run on every page of the Posts controller, regardless of the action

    this


  index: () ->
    # Code that should run ONLY for the index action

  show: () ->
    # Code that should run ONLY for the show action

So, once you have all your Coffeescript files all bundled up and served to the browser in one large bunch, how does the browser know which code to run? We just have one more thing to do, and that is put this snippet of javascript in a script tag in your Rails layout file:

1
2
3
4
5
6
jQuery(function() {
  window.$M = new (MyApp[<%= params[:controller] %>] || Kaleo.Base)(@bootstrap_data);
  if (typeof $M[<%= params[:action] %>] === 'function') {
    return $M[<%= params[:action] %>].call();
  }
});

Like anything in software, there are many ways to do this, the above uses ERB snippets to inject the controller and action names into the javascript code. If your action sets the @bootstrap_data variable to some json, that will get passed into the Coffeescript classes we made above. The shortcut variable $M is created that gives you access to the class that was created, so you can get the boostrap data with $M.bd.

Opening Links in PhoneGap Apps in Mobile Safari

The Cordova (nee PhoneGap) project lets you wrap your HTML5 app up in a nice native wrapper for deployment to iOS or Android mobile devices. I am using this on a current project built with the Sencha Touch framework, and for the most part it has been a great experience.

However, there is this issue that was causing us some problems on iOS. The way PhoneGap works, it hosts your HTML5 app in a WebView, and you tell it which links should open in the WebView, and which should “shell out” of the app and open in Mobile Safari. You do this by “whitelisting” certain hosts via the ExternalHosts setting in Cordova.plist.

Our issue was that we had user-generated content as part of our app, and the users could enter links to arbitrary hosts on the web, both in anchor tags and image tags. If you tell PhoneGap to whitelist all hosts, “*”, then all links will open in the WebView. This is bad, since if a user clicks on a link to a website, that website will load in the PhoneGap WebView, with no browser chrome, and no way to get “back” to your application. The HTML way to tell the browser you want content to open in a new window, is to use the target=“_blank” attribute. Unfortunately, this does not work in PhoneGap.

A longer-term fix involves getting intimately familiar with the bowels of iOS events with names like UIWebViewNavigationTypeOther and UIWebViewNavigationTypeLinkClicked. A short-term hack is what we needed, and it looks like this:

First, in your PhoneGap/Cordova project, paste in the following code in your MainViewController.m file. This snippet will examine any request to load a specific URL, and look for a fragment of “phonegap=external”. (i.e. http://mysite.com?page=1#phonegap=external) If it finds this snippet, it loads the URL in Mobile Safari, which will cause your app to stop, and Mobile Safari to launch.

1
2
3
4
5
6
7
8
9
10
11
12
13
- (BOOL) webView:(UIWebView*)theWebView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType
{
    NSURL *url = [request URL];
    if ( ([url fragment] != NULL) && ([[url fragment] rangeOfString:@"phonegap=external"].location != NSNotFound))
    {
        if ([[UIApplication sharedApplication] canOpenURL:url]) {
            [[UIApplication sharedApplication] openURL:url];
            return NO;
        }
    }

  return [super webView:theWebView shouldStartLoadWithRequest:request navigationType:navigationType];
}

The next piece of the puzzle was to munge the URLs in our app to add the fragment, but only if we wanted them to open in an external browser — in keeping with HTML conventions we will signify this with a target=“_blank”. We placed this in the <head> of our index.html, and it adds a listener that gets fired before the link is acted on (thats the ‘true’ as the last parameter to addEventListener) and gives us a chance to munge it on its way to PhoneGap.

1
2
3
4
5
document.addEventListener('click', function(e) {
  if (e.srcElement.target === "_blank" && e.srcElement.href.indexOf("#phonegap=external") === -1) {
    e.srcElement.href = e.srcElement.href + "#phonegap=external";
  }
}, true);

That should do the trick!

A Batmanjs Mixin for Cycling CSS Class Names

Recently we needed to cycle through some CSS class names when rendering a Batmanjs template. Something like alternating ‘odd’ and ‘even’ classes on table rows, for example. This turned out to be harder than I had first thought it would be. I tried several approaches, but this was the one that I finally got to work.

First, create a Cycler class. (Using a Batman object for this is probably overkill, but it does give me one more opportunity to type ‘Batman’.)

1
2
3
4
5
6
7
8
9
10
11
12
13
MyApp.Cycler extends Batman.Object
  isObservable: false

  constructor: (@values) ->
    @values = @values.split(',') if typeof @values is 'string'
    @idx = -1

  reset: ->
    @idx = -1

  next: ->
    @idx = (@idx + 1) % @values.length
    @values[@idx]

Now we create the mixin…

1
2
3
4
5
6
7
# Use a global property to hold the cycler, which holds the state
Batman.mixins.cycler =
  initialize: ->
    $node = $(@)
    cyclerName = "cycler-#{$node.data('cycler-name')}"
    MyApp.getOrSet(cyclerName, => new MyApp.Cycler($node.data('cycler-values')))
    $node.addClass(MyApp.get(cyclerName).next())

Now, to use it in your template, you can do something like this:

1
2
3
4
5
<ul>
  <li data-foreach-post="posts" data-mixin="cycler" data-cycler-values="left,right" data-cycler-name="all-posts">
    <a data-route="post" data-bind="post.title"></a>
  </li>
</ul>

Which would render to something like this (leaving out the batman tags for clarity)

1
2
3
4
5
6
7
8
9
10
11
<ul>
  <li class="left">
    <a href="/posts/1">Post 1</a>
  </li>
  <li class="right">
    <a href="/posts/2">Post 2</a>
  </li>
  <li class="left">
    <a href="/posts/3">Post 3</a>
  </li>
</ul>

So, that’s it. As you can see we have to create some global properties, and name them, so that we can have multiple cyclers in the app without conflict. This does bother me a bit, but there are plenty of other dragons to slay, so it will have to do for the time being.