Ruby on Rails
ExampleCookieJarHelper (Version #18)

This is an example of a Helper? that was written to support HowToRenderProxiedPages. It is intended as a ProgrammingOutLoud example to demonstrate 1) the construction of a Helper? and the use of Ruby, as well as to provide a specific solution to a specific problem.

The problem

Rendering proxied pages may require that state for the remote session (in the form of HTTP Cookies) be kept by the rails application. Due to a number of complications, mostly arising from the ad-hoc (“it just growed like that”) design of the cookie system, and the relatively modular nature of the task I concluded that this was an ideal canidate for becoming a Helper.

The helper’s sole purpose is to define a class of objects that

  1. provides a simple interface to store and retrieve cookies as if it were a browser,
  2. can be serialized (and thus persisted as part of a rails session)

Basically, something similar to this java class, but a little more light weight.

The solution (incremental pseudo-code)

The first requirement can be psudo-coded as follows:


class A_cookie_jar
    def parse_cookie_from(uri,s)
        # parse the string s for a cookie, and store it 
        #     as coming from from uri
        end
    def cookies_for(uri)
        # search through the stored cookies, and return 
        #     any that apply to the specified URI.
        end
    end

This is easy to flesh out a bit.

  • We’ll need some sort of data structure to store the cookies in
  • We only want to give cookies to the sites they came from
  • We only want to return valid (e.g. unexpired) cookies

A quick peek at the specification led to the following additional points:

  • A cookie request consists of a name=value pair followed by some options
  • Their are just a handful of options we care about, and they all have pretty tightly defined formats

So now we have:


class A_cookie_jar < Array
    def initialize
        #define the @jar data structure
        end
    def parse_cookie_from(uri,s)
        # check s for reasonableness; ignore bad input
        # find a name/value pair and options in s
        # parse (& provide defaults) for options such as:
        #       expires=Friday, 01-Apr-2005 00:00:03 GMT
        #       path=/joke_ideas/office/index.html
        #       domain=www.jokers.edu
        #       secure
        # put the cookie in the jar
        end
    def cookies_for(uri)
        # construct an empty result set
        # check all the cookies
        #     add the name/value pair to the result, iff 
        #           * it applies to this uri
        #           * the cookie is valid 
        # return the result
        end
    end

Actually reading the documentaion lets us refine this further. For instance,

secure
If a cookie is marked secure, it will only be transmitted if the communications channel with the host is a secure one. Currently this means that secure cookies will only be sent to HTTPS (HTTP over SSL) servers.

If secure is not specified, a cookie is considered safe to be sent in the clear over unsecured channels.

class A_cookie_jar < Array
    def initialize
        #define the @jar data structure
        end
    def parse_cookie_from(uri,s)
        # check s for reasonableness; ignore bad input
        # split s into a ';'-delimited list consisting of 
        #    a name/value pair and 
        #    a (possibly empty) list of options
        # split the name and the value at the '='
        # determine default values for all the options
        # parse each option, overriding defaults 
        #     as appropriate.  Supported options will 
        #     match one of the following patterns
        #         /expires=.+?, (..-...-.... ..:..:.. GMT)/
        #         /path=(.*)/
        #         /domain=(.*)/
        #         /secure/
        # put the cookie in the jar
        end
    def cookies_for(uri)
        # construct an empty result set
        # check each domain suffix (longest first)
        #   check for path prefix (longest first)
        #     for each cookie in domain-suffix:path-prefix
        #          add the name/value pair to the result if
        #               * the cookie hasn't expired and 
        #               * the protocol is acceptable
        # return the result
        end
    end

This is fairly easy to translate into “hopeful Ruby”—by which I mean Ruby with the assumption that various objects support the methods I need and that they are called what I expect and work the way I think they should.

For the data structure, I chose a hash (indexed by domain suffix) of hashes (indexed by path prefix) of arrays of cookies. This looked like a good compromise between the demands of the two functions, and is about as space efficient as you’re likely to get.

To enforce the structure, I create the outermost Hash with a “default proc” to create a new nested Hash whenever a previously unseen key was accessed (e.g., when we get out first cookie for a domain). To keep this from creating an empty structure when we check for cookies from a domain that hasn’t given us any, I put a guard (if @jar.has_key? domain) on the access in cookies_for(uri).

For finding cookies I decided to pretend there was a way to generate all the non-empty prefixes or sufixes of a string (using a specified delimiter) longest first. In other words, I decided to assume that "The price of freedom".prefixes(' ') would give me ["The price of freedom","The price of","The price","The"]. With that it’s easy to implement the search for applicable cookies.

The result is pretty much a line-by-line translation of the PseudoCode? into HopefulRuby:

alpha version


class A_cookie_jar
    def initialize
        @jar = Hash.new { Hash.new { [] } }
        end
    def parse_cookie_from(uri,s)
        return unless s.is_a? String and s.length > 2
        name_value,*options = cookie.split(';')
        name,value = name_value.split('=')
        acceptable_protocols,domain,path,expires = ['http','https'],uri.host,uri.path,nil
        options.each { |option| case option
            when /expires=.+?, (..-...-.... ..:..:.. GMT)/
                expires = DateTime.parse($1)
            when /path=(.*)/                             
                path = $1
            when /domain=(.*)/                           
                domain = $1
            when /secure/                               
                acceptable_protocols = ['https']
            end}
        @jar[domain][path] << [name,value,expires, acceptable_protocols]
        end
    def cookies_for(uri)
        result = Hash.new { [] }
        uri.host.suffixes('.').each { |domain|
            uri.path.prefixes.each { |path|
                @jar[domain][path].each { |name,value,expires,acceptable_protocols|
                    result[name] << value if 
                        DateTime.now < expires and                           
                        acceptable_protocols.include? uri.scheme
                    } if @jar[domain].has_key? path
                } if @jar.has_key? domain
            }
        def result.to_s
            keys.collect { |name|
                self[name].collect { |value| "#{name}=#{value}" }
                }.flatten.join(';')
            end
        result
        end
    end

Testing

More writeup to follow…

Problems found in testing:

  • Strings don’t actually support prefixes and suffixes
  • Persisting a hash with a default proc
  • Same name overrides
  • Some servers return two-digit dates
  • Rails combines set-cookie headers with a ", "
  • Users generallly (but not always) want cookies in “cookie header” format

The final results


class String
    def prefixes(delimiter='/')
        segments = split(delimiter)
        (1..segments.length).collect { |i| segments[0..-i].join(delimiter) }
        end
    def suffixes(delimiter='/')
        segments = split(delimiter)
        (1..segments.length).collect { |i| segments[(i-1)..-1].join(delimiter) }
        end
    end

class A_cookie_jar
    def initialize
        @jar = {}
        end
    def parse_cookie_from(uri,s)
        return unless s.is_a? String and s.length > 2
        cookies = s.gsub(/, ([^\d])/,';;\1').split(';;')
        cookies.each { |cookie|
            name_value,*options = cookie.split(';')
            name,value = name_value.split('=')
            acceptable_protocols,domain,path,expires = ['http','https'],uri.host,uri.path,nil
            options.each { |option| case option
                when /expires=.+?, (..-...-.. ..:..:..(..)? GMT)/
                    expires = DateTime.parse($1,:guess_year)
                when /path=(.*)/                     
                    path = $1
                when /domain=(.*)/                      
                    domain = $1
                when /secure/                                
                    acceptable_protocols = ['https']
                end}
            ((@jar[domain] ||= {})[path] ||= []) << [name,value,expires, acceptable_protocols]
            }
        end
    def cookies_for(uri)
        result = {}
        uri.host.suffixes('.').each { |domain|
            uri.path.prefixes.each { |path|
                @jar[domain][path].each { |name,value,expires,acceptable_protocols|
                    (result[name] ||= []) << value if DateTime.now < expires and acceptable_protocols.include? uri.scheme
                    } if @jar[domain].has_key? path
                } if @jar.has_key? domain
            }
        def result.to_s
            keys.collect { |name|
                self[name].collect { |value| "#{name}=#{value}" }
                }.flatten.join(';')
            end
        result
        end
    end

Bugs

Regular expression for parsing date have error: it should be /expires=.+?, (..-...-..(..)? ..:..:.. GMT)/

In cookies_for method, path prefixes does not include trailing slash so cookie is never received for ‘/’. Quick solution is to add path += '/' for every path. bmihelac

This is an example of a Helper? that was written to support HowToRenderProxiedPages. It is intended as a ProgrammingOutLoud example to demonstrate 1) the construction of a Helper? and the use of Ruby, as well as to provide a specific solution to a specific problem.

The problem

Rendering proxied pages may require that state for the remote session (in the form of HTTP Cookies) be kept by the rails application. Due to a number of complications, mostly arising from the ad-hoc (“it just growed like that”) design of the cookie system, and the relatively modular nature of the task I concluded that this was an ideal canidate for becoming a Helper.

The helper’s sole purpose is to define a class of objects that

  1. provides a simple interface to store and retrieve cookies as if it were a browser,
  2. can be serialized (and thus persisted as part of a rails session)

Basically, something similar to this java class, but a little more light weight.

The solution (incremental pseudo-code)

The first requirement can be psudo-coded as follows:


class A_cookie_jar
    def parse_cookie_from(uri,s)
        # parse the string s for a cookie, and store it 
        #     as coming from from uri
        end
    def cookies_for(uri)
        # search through the stored cookies, and return 
        #     any that apply to the specified URI.
        end
    end

This is easy to flesh out a bit.

  • We’ll need some sort of data structure to store the cookies in
  • We only want to give cookies to the sites they came from
  • We only want to return valid (e.g. unexpired) cookies

A quick peek at the specification led to the following additional points:

  • A cookie request consists of a name=value pair followed by some options
  • Their are just a handful of options we care about, and they all have pretty tightly defined formats

So now we have:


class A_cookie_jar < Array
    def initialize
        #define the @jar data structure
        end
    def parse_cookie_from(uri,s)
        # check s for reasonableness; ignore bad input
        # find a name/value pair and options in s
        # parse (& provide defaults) for options such as:
        #       expires=Friday, 01-Apr-2005 00:00:03 GMT
        #       path=/joke_ideas/office/index.html
        #       domain=www.jokers.edu
        #       secure
        # put the cookie in the jar
        end
    def cookies_for(uri)
        # construct an empty result set
        # check all the cookies
        #     add the name/value pair to the result, iff 
        #           * it applies to this uri
        #           * the cookie is valid 
        # return the result
        end
    end

Actually reading the documentaion lets us refine this further. For instance,

secure
If a cookie is marked secure, it will only be transmitted if the communications channel with the host is a secure one. Currently this means that secure cookies will only be sent to HTTPS (HTTP over SSL) servers.

If secure is not specified, a cookie is considered safe to be sent in the clear over unsecured channels.

class A_cookie_jar < Array
    def initialize
        #define the @jar data structure
        end
    def parse_cookie_from(uri,s)
        # check s for reasonableness; ignore bad input
        # split s into a ';'-delimited list consisting of 
        #    a name/value pair and 
        #    a (possibly empty) list of options
        # split the name and the value at the '='
        # determine default values for all the options
        # parse each option, overriding defaults 
        #     as appropriate.  Supported options will 
        #     match one of the following patterns
        #         /expires=.+?, (..-...-.... ..:..:.. GMT)/
        #         /path=(.*)/
        #         /domain=(.*)/
        #         /secure/
        # put the cookie in the jar
        end
    def cookies_for(uri)
        # construct an empty result set
        # check each domain suffix (longest first)
        #   check for path prefix (longest first)
        #     for each cookie in domain-suffix:path-prefix
        #          add the name/value pair to the result if
        #               * the cookie hasn't expired and 
        #               * the protocol is acceptable
        # return the result
        end
    end

This is fairly easy to translate into “hopeful Ruby”—by which I mean Ruby with the assumption that various objects support the methods I need and that they are called what I expect and work the way I think they should.

For the data structure, I chose a hash (indexed by domain suffix) of hashes (indexed by path prefix) of arrays of cookies. This looked like a good compromise between the demands of the two functions, and is about as space efficient as you’re likely to get.

To enforce the structure, I create the outermost Hash with a “default proc” to create a new nested Hash whenever a previously unseen key was accessed (e.g., when we get out first cookie for a domain). To keep this from creating an empty structure when we check for cookies from a domain that hasn’t given us any, I put a guard (if @jar.has_key? domain) on the access in cookies_for(uri).

For finding cookies I decided to pretend there was a way to generate all the non-empty prefixes or sufixes of a string (using a specified delimiter) longest first. In other words, I decided to assume that "The price of freedom".prefixes(' ') would give me ["The price of freedom","The price of","The price","The"]. With that it’s easy to implement the search for applicable cookies.

The result is pretty much a line-by-line translation of the PseudoCode? into HopefulRuby:

alpha version


class A_cookie_jar
    def initialize
        @jar = Hash.new { Hash.new { [] } }
        end
    def parse_cookie_from(uri,s)
        return unless s.is_a? String and s.length > 2
        name_value,*options = cookie.split(';')
        name,value = name_value.split('=')
        acceptable_protocols,domain,path,expires = ['http','https'],uri.host,uri.path,nil
        options.each { |option| case option
            when /expires=.+?, (..-...-.... ..:..:.. GMT)/
                expires = DateTime.parse($1)
            when /path=(.*)/                             
                path = $1
            when /domain=(.*)/                           
                domain = $1
            when /secure/                               
                acceptable_protocols = ['https']
            end}
        @jar[domain][path] << [name,value,expires, acceptable_protocols]
        end
    def cookies_for(uri)
        result = Hash.new { [] }
        uri.host.suffixes('.').each { |domain|
            uri.path.prefixes.each { |path|
                @jar[domain][path].each { |name,value,expires,acceptable_protocols|
                    result[name] << value if 
                        DateTime.now < expires and                           
                        acceptable_protocols.include? uri.scheme
                    } if @jar[domain].has_key? path
                } if @jar.has_key? domain
            }
        def result.to_s
            keys.collect { |name|
                self[name].collect { |value| "#{name}=#{value}" }
                }.flatten.join(';')
            end
        result
        end
    end

Testing

More writeup to follow…

Problems found in testing:

  • Strings don’t actually support prefixes and suffixes
  • Persisting a hash with a default proc
  • Same name overrides
  • Some servers return two-digit dates
  • Rails combines set-cookie headers with a ", "
  • Users generallly (but not always) want cookies in “cookie header” format

The final results


class String
    def prefixes(delimiter='/')
        segments = split(delimiter)
        (1..segments.length).collect { |i| segments[0..-i].join(delimiter) }
        end
    def suffixes(delimiter='/')
        segments = split(delimiter)
        (1..segments.length).collect { |i| segments[(i-1)..-1].join(delimiter) }
        end
    end

class A_cookie_jar
    def initialize
        @jar = {}
        end
    def parse_cookie_from(uri,s)
        return unless s.is_a? String and s.length > 2
        cookies = s.gsub(/, ([^\d])/,';;\1').split(';;')
        cookies.each { |cookie|
            name_value,*options = cookie.split(';')
            name,value = name_value.split('=')
            acceptable_protocols,domain,path,expires = ['http','https'],uri.host,uri.path,nil
            options.each { |option| case option
                when /expires=.+?, (..-...-.. ..:..:..(..)? GMT)/
                    expires = DateTime.parse($1,:guess_year)
                when /path=(.*)/                     
                    path = $1
                when /domain=(.*)/                      
                    domain = $1
                when /secure/                                
                    acceptable_protocols = ['https']
                end}
            ((@jar[domain] ||= {})[path] ||= []) << [name,value,expires, acceptable_protocols]
            }
        end
    def cookies_for(uri)
        result = {}
        uri.host.suffixes('.').each { |domain|
            uri.path.prefixes.each { |path|
                @jar[domain][path].each { |name,value,expires,acceptable_protocols|
                    (result[name] ||= []) << value if DateTime.now < expires and acceptable_protocols.include? uri.scheme
                    } if @jar[domain].has_key? path
                } if @jar.has_key? domain
            }
        def result.to_s
            keys.collect { |name|
                self[name].collect { |value| "#{name}=#{value}" }
                }.flatten.join(';')
            end
        result
        end
    end

Bugs

Regular expression for parsing date have error: it should be /expires=.+?, (..-...-..(..)? ..:..:.. GMT)/

In cookies_for method, path prefixes does not include trailing slash so cookie is never received for ‘/’. Quick solution is to add path += '/' for every path. bmihelac