Beware of ssl_requirement

Plugins, Ruby on Rails 6 Comments »

If you’re starting out with SSL and Rails, you’ll probably stumble upon ssl_requirement very quickly. It promises to make routing easy by automatically redirecting to SSL when required (hence the name…). However, in most cases, that’s not enough, and relying solely on ssl_requirement will leave you unprotected.

ssl_requirement really only protects you in one direction, when the client requests data that should be encrypted when sent from the server. However, it does not really do much for you in the oh-so-important case of transmitting sensitive data from the client to the server.

Now, if your entire site is SSL (ie. Apache redirects all incoming requests to HTTPS), then it’s not really a problem. Your form_tag or form_for calls will pick up on the fact that they are being served from an SSL protected page, and they themselves will submit to HTTPS. However, in the case of a non-encrypted page that has a form that should be encrypted (ie. login form on the homepage), the form will default to submitting to regular HTTP, since it defaults to use the protocol of the current page. This is where ssl_requirement does nothing to help us.

In this case, the client will POST the form unencrypted to your ssl_requirement protected action. ssl_requirement will determine that this particular action requires SSL, and sends a redirect to the HTTPS action, which the browser happily complies with. Unfortunately, at that point, it’s already too late, since the first transmission was unencrypted. Nothing breaks, and everything looks fine, but each and every form submission is being sent twice: once in the clear, and once with encryption. Not really what we wanted, right?

One solution is to always use named routes and set the protocol in the routing file. In this case, you must always use xxx_url (not xxx_path) in your form_for and form_tag calls. I have not personally verified that this works, but it seems like a decent solution.

Another way is to hack together alternate form_for and form_tag methods. These new helpers will test whether you’re currently in production or development mode and generate the HTTP or HTTPS form submission URLs accordingly. This is what we did for RioFlexPay, and it works fairly well.

In the end, we got rid of ssl_requirement altogether. It simply provided very little for us, and started to conflict with our Apache settings. In our case, we wanted the homepage to be unencrypted, but wanted all other pages to use SSL. This was fairly easy to set up with Apache rewrite rules. Unfortunately, this caused conflicts with ssl_requirement. The ssl_requirement plugin would see an action that wasn’t explicitly listed as allowing SSL and would redirect it to HTTP. Meanwhile, Apache would see an HTTP request for a non-homepage URI and redirect it to HTTPS. Thus, many of our actions resulted in infinite redirect loops, and of course we didn’t see this until we deployed to production, where SSL is enabled. Believe me, that was a late night of furious debugging. Simply removing ssl_requirement and allowing Apache to handle everything was our final solution.

So, just remember: ssl_requirement is not a magic bullet for SSL. You really have to step back and examine what you do and don’t want encrypted, and you need to think in terms of both client request and server response. Once you’ve decided on that, it’s time to make sure that your Apache rewrite rules, your ssl_requirement settings, and your link_to, form_for, and form_tag calls are all set up correctly. Only then can you rest easy.

Connecting to MySQL using SSL encryption in Ruby on Rails

Ruby on Rails 17 Comments »

Recently for Obsidian Portal, we decided that we wanted to move some particularly intensive graphic processing offline from the main server. It was simply consuming too much CPU time and making the entire site sluggish. Luckily for us, it was already set up as a background process that only needed to connect to the database and Amazon S3. So, to do it on a whole new server wouldn’t require changing any of the algorithms.

However, since the remote server would be connecting to the main database over the Internet, we decided that encrypting the communication was probably a good idea. None of the actual data is all that sensitive, but the database username/password definitely is. Plus, in general, our policy is that any communication to and from our server should be encrypted.

It turns out that enabling SSL in MySQL is not too hard, but there are a lot of steps to follow. Further, to Rails docs on using database.yml to set up the connection aren’t that great (big surprise there). So, to help out those who are in the same boat, here’s what I did.

Setting up the MySQL server

The first thing to do is read through the official MySQL docs on SSL connections. These provide a good overview on how to configure the server to allow for (or force) encrypted connections. However, they assume a little knowledge of SSL and CA’s and keys and whatnot. So, if you’re stumped, you can read the following steps on how to proceed.

Verify that SSL is supported

We’re running Ubuntu on our server, and the MySQL that comes with it has SSL support already compiled in, so that’s a big relief on our part. I hate compiling from source, especially something big and important like MySQL.

To verify that support is already compiled in, log in with the mysql client and try the following:

show variables like ‘have_ssl’;

If it says DISABLED, then you’re in the right place. If it says YES, then you’ve already set the server up and can skip to the client or Rails sections below. If it says anything else (like no variables are returned) then it’s time to recompile MySQL. That’s beyond the scope here, but I wish you the best of luck.

Create the required SSL keys and certificates and whatnot

I’m no security expert, so all the SSL / CA / certificate / key advice is at your own risk. I’m still learning a lot of this stuff.

In order to get your server set up, you will need 3 files: A certification authority (CA), a certificate, and a key. Like I said, I really don’t know what all these things are. I just have a vague understanding.

To create my necessary files, I used TinyCA2, which I heavily recommend. It provides a GUI for using OpenSSL. Otherwise, get ready for lots of arcane command lines. If you’re on debian/ubuntu, all you need to do is run the following:

sudo apt-get install tinyca
tinyca2

Using TinyCA2, the process is a snap. It will walk you through creating a CA, then generating a certificate and key from that CA. I don’t know what options are required, but I got away with specifying only a common name for the CA and the certificate. Plus, I also used 1024 bit encryption since I’m not sure what level MySQL supports. I also heard somewhere that the common name for your CA and the certificate should be different, so watch out for that.

Once you’ve created your CA, certificate, and key, then you need to export them as pem files. In order to get MySQL to read the key, I had to export the key without a password. This is generally very bad advice, since if anyone gets the key they can pose as you. However, if the key is password locked, then MySQL would have to get the password from you somehow (Apache does this on startup), and maybe that’s just not supported. Please correct me if I’m wrong.

Configure MySQL to use the generated files

Copy the 3 files into /etc/mysql and then edit /etc/mysql/my.cnf Add the following lines:

ssl-ca=/etc/mysql/cacert.pem
ssl-cert=/etc/mysql/my-new-server-cert.pem
ssl-key=/etc/mysql/my-new-server-key.pem

Note! Make sure you check the file permissions to ensure that the mysql user can read the files. If you followed my previous bad advice and exported your key without a password, then it’s extremely important to strictly control the read permissions on these files. Chown them to the mysql user or use groups. Do not just chmod 777 and blissfully continue.

Restart your MySQL server and make sure there are no startup errors.

Test that everything works

The first thing to do is log in on the mysql client and check the have_ssl variable. Just run the following from the mysql client.

show variables like ‘have_ssl’;

If it says YES, then you’re good to go. If not, something went wrong and you need to retrace your steps to find out what’s up.

Client setup and testing

Now that the server is setup, let’s verify that we can connect with a client. The first thing to do is create a new user that only has SSL connection availability. Connect as root (or someone with grant privileges) and run the following:

GRANT ALL on somedatabase.* TO ‘ssluser’@'localhost’ IDENTIFIED BY ‘some_password’ REQUIRE SSL;

This will create a user that can only connect from localhost and must use SSL. Now, to make sure that everything is working, try this:

mysql -ussluser -p –ssl-ca=/etc/mysql/cacert.pem

If you are able to log in and don’t get the dreaded SSL ERROR message, then everything is great! If you do get an error, the first thing to check is the read permissions on the cacert.pem file. It must be readable by the current user. If your read permissions are set correctly and you’re still getting errors…sorry I can’t help. :(

Before we move on, it’s important to note that we’re using the same CA pem file as we did to create the server’s certificate and key. I really don’t understand why MySQL clients are required to specify a CA certificate, and I don’t know which are allowed. Presumably, you can set the server to allow clients to specify one of the major CAs (like Verisign or GoDaddy). Still, that’s beyond my knowledge. I tried creating a second CA and specifying that on the client side, but the server refused the connection. For now, it seems that you will simply have to copy the CA certificate to every remote client that wishes to connect to this server. That seems strange to me, and I’m probably wrong here. Please correct me in the comments.

Connecting from Rails

A small bump in the road…

The official Rails MysqlAdapter documentation lists the SSL parameters to use in database.yml. Unfortunately, the docs seem to be out of date and missing a very important parameter, sslca, the certificate authority file we need for every SSL client connection.

Further, the actual adapter code will not set any of the ssl parameters unless the sslkey option is set. This parameter is unnecessary in many cases, such as where you only want to require SSL communication and not X509 authentication of the client.

I have created a ticket and patch at lighthouse to cover this issue, but for now, you’re going to have to take a few extra steps to get things working.

Create a client key and certificate

While not strictly necessary according to MySQL, due to the coding of the Rails MySQL Adapter, you will need a client key and certificate. Like I said, I’ve submitted a patch, but no sense waiting on that.

Fire up TinyCA2 again and create a new certificate (select new client certificate and key) using the same CA that you used to create the server’s certificate and key. Again, export both the certificate (call it something like mysql-client-cert.pem) and the key (mysql-client-key.pem). Also, remember to export the key without a password!

Put these somewhere accessible to your Rails app. I will assume that you put them in the db directory. Make sure they are readable by your Rails app’s web server user.

Finally, place a copy of your cacert.pem in the db directory as well. Using TinyCA2, just go to the CA tab and click the export button. Drop in the db directory and check the file permissions.

Update your database user permissions

Update your Rails app’s user in mysql to give them remote access permissions. Assuming they are currently set to access only from localhost, the following line will extend access to your remote client.

GRANT ALL on my_rails_app_db.* TO ‘my_rails_app_db_user’@'my.remote.client.com’ IDENTIFIED BY ‘somepassword’ REQUIRE SSL;

Check your MySQL options and firewall

Make sure your firewall is set to allow incoming connections on the MySQL port (defaul 3306) and that your MySQL server is set to allow connections from more than just localhost.

Test using the bare mysql client

At this point, I would test that MySQL is accepting outside SSL connections by trying to connect using the mysql client from the remote machine. Something like:

mysql -umy_rails_app_db_user -psomepassword -hmy.mysql.server –ssl-ca=/path/to/rails/app/db/cacert.pem

If you cannot successfully connect using this, then you’ll need to troubleshoot your remote connection before trying to do anything with the Rails connection.

Update your database.yml

Add the following lines to your database.yml

sslca: /path/to/rails/app/db/cacert.pem
sslkey: /path/to/rails/app/db/mysql-client-key.pem
sslcert: /path/to/rails/app/db/mysql-client-cert.pem

Fire it up!

Fire up a Rails console on your remote client. If all goes well, you will be presented with the standard console prompt. You can verify that everything is working by executing the following

ActiveRecord::Base.connection.execute(“show status like ‘Ssl_cipher’;”).fetch_row

If you see something like DHE-RSA-AES256-SHA, then you’re set!

Congratulations! Your communications are now encrypted!

File summary

Since we’re dealing with so darn many pem files, I thought it might be nice to have an index of exactly which files you need and where you need them.

On the MySQL server

/etc/mysql/cacert.pem
/etc/mysql/mysql-server-cert.pem
/etc/mysql/mysql-server-key.pem
All files must be readable by the MySQL user

On the remote client

/path/to/rails/app/db/cacert.pem
/path/to/rails/app/db/mysql-client-key.pem
/path/to/rails/app/db/mysql-client-cert.pem
All files must be readable by the Rails webserver user

The cacert.pem files must be the same, and all the certificates and keys must be generated using the cacert (CA). In addition, the keys must be exported without passwords. Divert from this at your own risk!

Correct me please!

As I have said, I am not a security expert. If there is anything in here that is bad practice or just blatantly wrong, please correct me! The official documentation isn’t great, so it’s up to us (the community) to help each other out.

Help us climb

If you like this article, please link to it with the text AisleTen’s guide to connecting to MySQL using SSL encryption in Ruby on Rails. A few incoming links like that will help this article be near the top when people google for MySQL and SSL and Rails.

Installing GrowlNotify and Autotest for BDD use with Rspec on Leopard

Ruby on Rails 24 Comments »

I’m a big fan of Behavior Driven Development (BDD). It really illustrates how one little change in your code can have significant impact on the rest of you application, which you would never be able to catch without testing. Not to to mention the benefit of being able to write code and make sure it works even if you don’t have production data available.

So, upon completing a clean upgrade to Leopard I noticed that after installing Growl and growlnotify, that growlnotify would not work even through Growl itself was working.

1) First let’s set up the .autotest file. For this you’ll need the gems rspec (1.1.3), ZenTest (3.9.1), and redgreen since ZenTest change how it handled exceptions in 3.9.

sudo gem install rspec
sudo gem install ZenTest
sudo gem install redgreen

Now open up your ~/.autotest file in Textmate

mate ~/.autotest

Paste in the following code. Notice the exceptions at the bottom of the file that really helps speed up autotests as well as keeps your cpu usage low. My Macbook would get really hot really fast prior to using this execptions.

require ‘autotest/redgreen’

module Autotest::Growl
def self.growl title, msg, img, pri=0, stick=”"
system “growlnotify -n autotest –image #{img} -p #{pri} -m #{ msg.inspect} #{title} #{stick}”
end

Autotest.add_hook :ran_command do |autotest|
filtered = autotest.results.grep(/\d+\s.*examples?/)
output = filtered.empty? ? ” : filtered.last.slice(/(\d+)\s.*examples?,\s(\d+)\s.*failures?(?:,\s(\d+)\s.*pending)?/)
if output =~ /[1-9]\sfailures?/
growl “Test Results”, “#{output}”, “~/Library/autotest/rails_fail.png”
elsif output =~ /pending/
growl “Test Results”, “#{output}”, “~/Library/autotest/rails_pending.png”
else
growl “Test Results”, “#{output}”, “~/Library/autotest/rails_ok.png”
end
end

end

Autotest.add_hook :initialize do |at|
%w{.svn .hg .git vendor}.each {|exception| at.add_exception(exception)}
end

Next, you’ll want to download the Pass, Fail, and Pending images below and place them in ~/Library/autotest:

rails_ok.png rails_pending.png rails_fail.png

First, the prerequisite to growlnotify is Growl. You can download it from http://growl.info/. It’s a graphical installer, so you shouldn’t have an issues.

Once you’ve installed Growl, pop open the terminal and enter the following commands.

cd /Volumes/Growl 1.1.2/Extras/growlnotify
sudo sh install.sh

Give it a test run:

growlnotify -m “Testing growlnotify” Test

So if that doesn’t work, it’s probably because of an issue with the default permissions and Mac OS X 10.5 Leopard. Here’s what my permssions were. (I have no idea why the @ symbol is there)

cd /usr/local/bin
ls -l
-rwxr-xr-x@ 1 ryan 501 130288 Jan 22 17:38 growlnotify

To fix the permissions, I ran the following:

sudo chown root:admin growlnotify

Resulting in the following permssions:

cd /usr/local/bin
ls -l
-rwxr-xr-x@ 1 root admin 130288 Jan 22 17:38 growlnotify

Give it a test run again:

growlnotify -m “Testing growlnotify” Test

Now you can change directories to your rails app and run autotest and growlnotify should be working now..

cd ~/myrailsapp
autotest

Growlnotify Green

Growlnofity Red

References:
http://blog.codefront.net/2007/04/01/get-your-testing-results-via-growl-notifications/
http://www.danielfischer.com/2007/05/14/ruby-on-rails-bdd-with-autotest-growl-rspec/
http://railsontherun.com/2008/1/30/misc-tips-and-tricks/

The search for credit card processing part 1 – TrustCommerce

Business, Plugins, Ruby on Rails 4 Comments »

We have finally gotten to the point where we are ready to start offering subscriptions to Obsidian Portal. We don’t expect there will be a lot of interest, but it’s always a sort of chicken v. egg problem. If you don’t have paying subscribers, then it’s not worth the effort to make the features. Conversely, without the features, no one is going to pay. On second thought, I guess it’s not chicken and egg, it’s pretty clear: you need features or no one will pay. ;)

Asking for payment means you will need to be able to accept it. Currency on the web is passed almost exclusively via credit cards (except for PayPal…), so that’s the direction we need to go in. That requires us to select a credit card processor. For today, we will be looking at TrustCommerce.

I won’t go into the details of how credit card processing works, mainly because I don’t really understand it myself. Suffice it to say, there are a lot of middle-men, and they are all trying to take a cut. Each cut is either a percentage of the total charge or a flat fee or both. So, a typical fee structure might be $0.30 flat fee plus 2.5% of the total transaction.

Note: If you don’t care about the analysis and just want to see a rundown of their prices, then jump to the pricing.

Go easy on me; it’s my first time

When selecting a processing agent, our first priority right now is ease of use. We don’t expect there will be a lot of people signing up for our premium service, so we don’t want to expend a lot of effort on a payment system only to never see it used. Also, we’re willing to pay a higher rate to the processor since 3% of $30/month is a lot different than 3% of $30,000/month. I’ll pay 3% vs 2.5% if the 3% service takes 2 hours to implement and the 2.5% service takes 10. So, for us, ease of use trumps competitive pricing.

Since we’re talking about subscriptions as opposed to purchases, there is a recurring element to the payments. Since we want easy-to-implement solutions, we are scoping our search to only include the payment processors that offer a recurring service. This is a very important thing to note, especially if you’re in the same boat. A 1-time payment processor model (like Google Checkout) just will not work if you want to do subscriptions. The main reason is that you will have to store the users’ credit card info on your server in order to pass it to the payment processor each billing cycle. Do not do this! There are actual laws and regulations detailing what sort of security procedures you have to maintain in order to hold that sort of sensitive data. It’s much easier to simply pay someone else to deal with that crap. If you do choose to store their info in your database, you should be looking for a lawyer right now, not a payment processor.

Just plug in your credit card info

In Rails, ease of use means finding a plugin. I write a lot about plugins on this blog, so why should credit card processing be any different? Doing a quick Google search led me to the TrustCommerce subscription payment plugin.

Finding this bit of code brought a smile to my face, as I thought I had just finished 90% of the work. Sign up for an account, drop in the plugin, and wait for the money to roll in. Too bad there were a few red flags that derailed the money train.

Sitting by the phone

TrustCommerce does not list any pricing on their website. Instead, they say you have to sign up for a test account, and then you’ll be contacted. Not a big deal, I guess. So, I signed up for a test account.

The first red flag went up when I did not get an immediate callback. Sure, I signed up at 11:00pm Eastern Time, but that’s normal business hours in Internet time. In other words, if you’re an Internet company that requires phone contact, you had better have someone manning the phone at all hours. A lot of Web jockeys like me have a regular 9-5 job that precludes us from doing our business dealings during normal business hours. I want to deal with companies that understand this and have staff available during my normal business hours.

Red flags: 1

The ball sits in my court

The second red flag went up at their lackluster eventual response. My cell is in a dead zone at work, so whenever I leave for lunch, I get all my messages. On the day after requesting contact, I had a voicemail message from TrustCommerce. Still no pricing info, just a short message to call them back. Seeing as how I was busy, I couldn’t do it right away. Then I forgot. Dead silence on their end. No e-mails, no more calls, nothing.

Now a lot of people may disagree with me on this, but I think they should have been hitting my inbox and voicemail pretty hard. “Mr. Wedemeyer, we’re still interested in talking to you about blah blah.” or “Send us an e-mail with the best time to call you.” That’s how the mortgage people behaved when I used LendingTree. Sure, it was annoying, but you knew they wanted your business. To me, an anemic response indicates that someone isn’t really serious about recruiting me as a customer.

Red flags: 2

Little fish: prepare to get fried

When I finally did get in touch with someone from TrustCommerce, he was quite happy to answer my pricing questions. I don’t know if I’m allowed to post that info, but since they didn’t expressly forbid it, here you go:

Basic pricing

  • $95 1-time fee
  • $20 / month
  • $0.20 / transaction

Citadel (recurring payments)

  • $145 1-time fee
  • $10 / month
  • $0.10 / month / billing id (ie. subscription)

Holy crap! $240 just to get started, plus an additional $30 per month, just to be allowed to use their service? Seeing as how I expect Obsidian Portal to be making around $10 / month, at least until we can recruit more people, this is insane! I politely said thank you to the salesman, hung up the phone, and started writing this post.

I guess I see these huge front-loaded fees like this: If you’re making enough money that the fees don’t matter, then you already have a lot of subscribers, which means you’re already handling credit cards. Maybe their service is so great compared to the competition that it’s worth it for the big boys. But, if you’re a small time operator like me, forget about it.

Red flags: 240 + 30 / month

The search continues

Although I said pricing was not our top priority, the front loaded fees with TrustCommerce completely invalidate them as a viable option. It would be a very long time before we paid off the initial investment, and with our none-to-clear business prospects with Obsidian Portal, that’s a gamble I’m not willing to take.

In the next exciting chapter we will be looking at Amazon Flexible Payment System (FPS). This new web service from Amazon is meant to rival Google Checkout and PayPal. I’ve been extremely pleased with S3, and maybe they can do one better with FPS. Stay tuned to find out.

Meet 1/2 of AisleTen at Barcamp Atlanta

BarCamp, BarCamp Atlanta, Promotion No Comments »

BarCamp Atlanta

Following Ryan’s experiences at Barcamp San Diego, I’ve decided to attend and (hopefully) present at Barcamp Atlanta. If I get the chance, I will be doing a presentation on custom Google Maps using S3. I will center on Ruby on Rails, of course, but most of the idea is language agnostic.

I’m hoping to use BarCamp as an opportunity to network with other Atlanta area entrepreneurs and hackers. I’ve got a lot of ideas and the skills to execute them, but I’m looking for people that are better than me in the marketing department. If you’re the kind of person who can sell water to a fish, then look me up at BarCamp!

Here’s a picture of me, looking as I probably will at the conference (lost and confused). Please don’t be shy, just come up and introduce yourself.
Micah

attachment_fu + S3 + ruby tile cutter + Google Maps = Easy custom maps in Ruby on Rails

Plugins, Ruby on Rails 12 Comments »

For Obsidian Portal, we wanted the ability for users to upload their own maps. The simplest way to do this would be to allow them to upload an image, then display it statically. While not a terrible feature, it has some definite limitations.

What we really wanted was a way to allow users to upload maps and navigate around them as everyone is accustomed to with Google Maps. Luckily, the guys at Google make just such a thing possible. As they say, a picture is worth a thousand words, so here are some examples of what we’ve accomplished. I will add more as users begin uploading.

  • Kensing This is the main island for my D&D 3.5 campaign.
  • Caedwyr Isle. This is another D&D 3.5 campaign, run by one of our users.

Note: Sorry if any of those links are 404s. The users can delete the maps as they please.

Over the course of a 3-day weekend, I was able to go from nothing to everything. I had a lot of stumbling blocks, though, and with this tutorial it should go much faster.

So, without further ado, let’s get some background on the technology that drives Google Maps: tiles!

What’s a tile?

The data set for Google Maps is just a giant collections of 256×256 images called tiles. When you view a map in your browser, the smarts in the system determine what part of the Earth is visible on your screen and requests only the tiles for that portion. The tiles are then laid out in a grid pattern to make a seamless image. Moving the map around spawns off asynchronous requests back to the server to get more tiles. In this way, the maps load quickly, and allow unlimited scrolling.

By default, the map pulls the tiles from the main server at Google. This makes sense as most people using the API want to display actual images of the Earth, or the nicely made street maps that Google does. The clever guys at Google, however, made it possible to switch this up and request the images from anywhere, thereby allowing developers to serve up their own tiles, yet rely on Google Maps to lay them out and stitch them together appropriately.

Tile cutter? Already written…

Ok, so now we know that we can tell Google to get the images from somewhere else, but now we need to make the images. It’s a lot to ask from your users to take their images and cut them into 256×256 chunks. So, we will need a tile cutter, or program that slices an image into the tiles.

Luckily, a ruby tile cutter does exist, and it works perfectly with Google Maps straight out of the box. The tile cutter does not have a lot of options, but for basic tile cutting it’s perfect.

Tile server? We choose S3.

We’ve got tiles, but now we need to serve them out. Mongrel is great for Rails apps, but not so great for serving images. A lot of sites (including Obsidian Portal) use some funky rewrite rules to try to get Apache to serve the static images and such. Still, serving tiles is a fairly intense task. Every time the user moves the map or zooms in and out, it will make several image requests to the tile server. For a mongrel server, this means it’s serving images rather than handling Rails requests, which is a big no-no.

Riding to our rescue is Amazon’s S3 (Simple Storage Service). Every object stored there can be made publicly accessible, and S3 will serve up the content with the correct content-type header. That’s perfect! If we can build our URLs correctly, then S3 becomes our tile server, thereby offloading all the heavy lifting to them instead of our Mongrel server.

From what I’ve seen so far, S3 is an excellent tile server, at least in terms of speed. Tiles are served faster from S3 than Google’s servers, in my subjective experience. Further, it was even faster than Mongrel serving the tiles from localhost to localhost. Your maps will load quickly, and be very responsive.

Finally, utilizing S3 allows us to store an unlimited number of tiles, which is very important if you have several zoom levels. The ceiling is determined only by the limit on your credit card ;)

attachment_fu, bringing order to chaos

Every S3 bucket is a wild, untamed jungle, and things can easily get lost. Without directory structures or meaningful modes of organization, it can be very easy for objects to go in and never come out. Therefore, it’s very, very important to keep track of everything that is being placed in S3. For us, that means having a database record for every file in S3.

Luckily, that’s exactly how attachment_fu sees things as well. Every file managed by attachment_fu has a corresponding record somewhere, and when that record is deleted (using the destroy method), attachment_fu handles deleting the associated file. This is extremely handy for a map that may have 64, 256, or even 1024 associated tiles images.

Further, S3 communication is built right in to attachment_fu, so you, the developer, barely have to learn anything at all about interacting with S3. Just let the plugin do it.

Enough chit-chat! Show us some code!

Before you jump in and start coding, there are some steps to take first. I won’t go into detail, since it’s outside the scope of this article, but here they are:

  1. Sign up for a Google Maps API key
  2. Sign up for an Amazon S3 account
  3. Install attachment_fu
  4. Install RMagick (it’s needed by the tile cutter)

Get the modified tile cutter

The YM4R tile cutter is a command-line tool by default. Since it’s written in ruby, there’s really no reason not to make the calls directly, rather than resorting to using the shell. I slightly modified the tile cutter for this purpose, as well as to enclose it inside a module. I tried to retain its ability to be used from the shell, but I didn’t test extensively. It works for our purposes, though.

Get the modified tile cutter here: tile_image.rb Drop it into your /lib directory and everything will work like magic.

The MapImage and MapTile models

# Copyright (c) 2007 AisleTen, LLC – micah @aisleten.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the “Software”), to deal in the Software without
# restriction, including without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require ‘fileutils’
require ’tile_image’
require ‘aws/s3′
class MapImage < Image
include Tiler
include AWS::S3

has_attachment :content_type => ["image/jpeg"],
:storage => :s3,
:path_prefix => “map_images”,
:size => 1.kilobytes..500.kilobytes,
:resize_to => “1024×1024!”,
:thumbnails => {:thumb_512 => “512×512!”, :thumb_256 => “256×256!”}

after_attachment_saved {|x| x.send(“create_tiles”) unless x.parent_id }

validates_as_attachment

has_many :map_tiles, :dependent => :destroy

protected

# Creates the resized images needed for tile creation.
# Then calls the tile cutter.
def create_tiles
tmpfiles = []
[:thumb_256, :thumb_512, nil].each do |t|
# Pulls down the image from S3
key = self.full_filename(t)
logger.debug(“Retrieving #{key} prior to tiling.”)
data = S3Object.value(key, @@bucket_name)

# Store to a tempfile
tmp = Tempfile.new(“tile”)
tmp.write(data)
tmp.close
tmpfiles < < tmp
end

tmp_paths = tmpfiles.collect {|f| f.path}

tp = Tiler::TileParam.new(Tiler::Point.new(0,0), 0, Tiler::Point.new(0,0), 1)
zoom = 0..2

# Tile them in a temp dir in /tmp
tile_dir = File.join("/tmp", self.id.to_s)
FileUtils.mkdir_p(tile_dir)
logger.debug("Creating tiles in #{tile_dir}")
get_tiles(tile_dir, tmp_paths, tp, zoom)
create_tile_model_objects(tile_dir)
FileUtils.remove_entry_secure(tile_dir, true)
logger.debug("Deleting temporary tile directory: #{tile_dir}")
end

# Creates a MapTile object for each tile. This allows us to track the tiles individually
# in the database to prevent them from being orphaned in S3.
def create_tile_model_objects(tile_dir)
Dir.foreach(tile_dir) do |tile_name|
if tile_name.include?('.jpg')
t = MapTile.new
t.map_image = self
t.filename = tile_name
t.content_type = "image/jpeg"
t.temp_path = File.join(tile_dir, tile_name)

logger.debug("Saving MapTile for #{tile_name}")
t.save
end
end
end
end

This is a model for a map with 3 levels of zoom. As required by the tile cutter, we create 3 different size images: 256x256 for zoom 0, 512x512 for zoom 1, and 1024x1024 for zoom 2. Since I'm lazy, I just let attachment_fu create the images by resizing the original to 1024x1024 and then using the built in thumbnail functionality to get the other sizes.

After the images are created and sized we use tile_image (via the get_tiles function) to create all the tiles in a subdirectory of /tmp. These are then used to create MapTile objects (shown below), which get uploaded back to S3.

Astute readers will note that any non-square image will get distorted by this. One solution is to pre-pad the image with extra space to make it square. That is left as an exercise to the reader ;) (Note: If someone comes up with a good way to do this with RMagick, send me the code and I'll post it and credit you.)

Astute readers will also notice that I'm wasting time and bandwidth pulling the images back from S3 after uploading them, rather than breaking in to the attachment_fu upload cycle and tiling the images before they're uploaded. I'm lazy and it works. Send the code if you know how to do it better.

Note: MapImage subclasses from Image, which is an abstract base class I use for all my images managed by attachment_fu. Single-table inheritance allows me to store multiple kinds of images in the same "images" table this way. This is not necessary to get the custom maps to work, it's just how I like to do things.

# Copyright (c) 2007 AisleTen, LLC - micah@aisleten.com
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use, copy, modify, merge, publish,
# distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
# BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

class MapTile < ActiveRecord::Base
has_attachment :content_type => ["image/jpeg"],
:storage => :s3

belongs_to :map_image

# We overwrite the base_path method for the tiles in order to place them
# in the tiles “directory” under their parent map ID on S3
def base_path
File.join(“map_images”, self.map_image_id.to_s, “tiles”)
end
end

The MapTile class is very simple, thanks to attachment_fu. The only real trick is to overwrite the base_path method. This allows us to modify the S3 key (ie “file path”) given to the MapTile image when it is stored. Instead of being stored using its own ID, we would like to store it using the ID of its parent MapImage. This is the structure that will allow us to serve up the tiles.

The View…now with added JavaScript!

google.load(“maps”, “2″);

var map_z_offset = 2;
var map_zoom_levels = 3;

function load() {
var customType = customMap();
var map = new google.maps.Map2(document.getElementById(“map”), {mapTypes: [customType]});
var newCenter = new google.maps.LatLng(79.1, -135);
map.setCenter(newCenter, (map_z_offset));
map.addControl(new google.maps.SmallMapControl());
}

function customMap() {
var copyCollection = new google.maps.CopyrightCollection(”);
var copyright = new google.maps.Copyright(1, new google.maps.LatLngBounds(new google.maps.LatLng(-90, -180), new google.maps.LatLng(90, 180)), 0, “”);
copyCollection.addCopyright(copyright);

var high_level = map_z_offset;
var low_level = map_z_offset + map_zoom_levels – 1;
var tilelayers = [new google.maps.TileLayer(copyCollection, high_level, low_level)];
tilelayers[0].getTileUrl = CustomGetTileUrl;

var custom = new google.maps.MapType(tilelayers, new google.maps.MercatorProjection(low_level + 1), “Chart”, {errorMessage:”"});
return custom;
}

function CustomGetTileUrl(point, zoom) {
var img_id = “< %= @map_image.id %>“;
var bucket = “< %= @map_image.bucket_name %>“;
return “http://s3.amazonaws.com/” + bucket + “/map_images/” + img_id + “/tiles/tile_” + (zoom – map_z_offset) + “_” + point.x + “_” + point.y + “.jpg”;
}

google.setOnLoadCallback(load);

Warning: I will be the first to admit that I don’t know squat about JavaScript. I’m learning as fast as I can, but it probably looks terrible to someone who really knows what they’re doing.

Most of this code was lifted, almost untouched, from the Mapki page on creating custom maps (see references below). The Google Maps code specific portions of creating custom maps has been understood for a long time now. Still, I’ll explain some of the things we’re doing that are special.

I am offsetting the map by 2 zoom levels. So, when we have a tile for zoom 0, we will display it at zoom 2. This will effectively give some buffer space around the image. If you display exactly according to the zoom level, it will look a little strange. At zoom 0 (all the way out), it assumes you’re looking at the entire world (which is roughly spherical), and therefore will place the same tile multiple times in a line. This works for a world map, but looks strange for anything smaller. So, we offset a bit by fooling the system into thinking that we’re zoomed in. That way, it will pad around our image with blank space.

Near the top, you will see that I am setting the Lat/Lng to a seemingly arbitrary number. This is related to the zoom offset. Tile location 0,0 is somewhere in the arctic ocean. When the map first displays, I want zoom level 0 (a single tile) to be visible in the center. I didn’t find a quick and easy way to calculate from tile X,Y to Lat/Lng, so I just started experimenting with values. (79.1, -135) works well for zoom offset 2, while (66.66666, -90) works well for zoom offset 1. If you want to offer arbitrary zoom offsets, you’ll need to come up with a better way of handling this. Send me the code and I’ll post it.

Finally, the real meat! GetCustomTileUrl is where all the magic happens. This is how we tell Google Maps to use our custom tile server instead of the default one. The parameters are an X,Y point and a zoom. Our tile cutter names the tiles perfectly for this, and we placed them in S3 according to the ID of their parent MapImage, so all we have to do is construct the URL based on this ID.

That’s It!

Now we’ve put all the pieces together. attachment_fu handles uploading and resizing, tile_image handles cutting the tiles, and S3 handles serving them out.

Issues

Image processing on the mongrel thread

If you were paying attention, you’ve realized that when uploading a map everything happens during a single HTTP request. By my estimation, uploading to S3, downloading the resized images, running the tile cutter, and then re-uploading the images takes between 5-10 seconds for a map with 3 zoom levels. In my case, the end total is 26 images that have to be created and moved. For any reasonable size site, tying up the web server for 10 seconds is out of the question. You can counter this with more mongrels, but that’s an approach that won’t scale. Plus, the time increases exponentially with more zoom levels. Tiling 6 or 7 zoom levels could take several minutes, even on a beefy machine.

Fortunately, there’s no reason the image processing and uploading has to be done on the main thread. Simply allow the users to upload the image and push the single image to S3. In the database, flag it as “not tiled” Then, asynchronously, have a script that periodically wakes and scans the database for maps that need to be tiled. It does the tiling work and uploading and then flips the flag in the database to “tiling finished.” For the user, they upload their map and then are taken to a screen that says, “We are currently preparing your map. Please wait a few minutes for the process to complete.” In reality, if the number of maps is low, and the zoom level is small, it could only take 10-20 seconds, assuming your script wakes up frequently enough to check.

Maps must be square

Because of the way the tile cutter works (or at least how I understand it), the starting image must be perfectly square. This can be dealt with by padding the uploaded image with a neutral (or transparent, for PNG) background until it has square dimensions, then send it to the tiler. Like I said before, if someone has a good way of doing this with RMagick, send the code and I’ll post it and give credit.

No map markers

This isn’t technically an issue, it’s just another feature I want :) Next up on the feature list is the ability to add markers to your map, and drag them around. By using Google Maps as the backend, this should be quite easy to do, at least I hope so. I’ll cover that in part 2, if I ever get around to it.

Resources

These are required resources for this to work at all. If for some reason you are unable to use any one of the following, the entire approach falls apart.

  • AWS::S3 Library – The main page for the Amazon S3 Ruby library. It has great examples and documentation.
  • Amazon S3 Homepage – Go here to sign up for Amazon Web Services (AWS). Make sure to also sign up for S3, since AWS is comprised of many services, and you have to sign up individually for each one.
  • Mike Clark’s attachment_fu tutorial – Pretty much the tutorial/howto for attachment_fu
  • YM4R – Includes the tile cutter. While not strictly necessary if you use my modified tile cutter, they still deserve the credit and so are listed in the required resources.
  • Google Maps API Key signup – You will need a Google Maps API key, otherwise you can’t use the map code. It’s free, so don’t worry about it.
  • RMagick – This is a ruby interface to the ImageMagick (or GraphicsMagick) libraries. It is needed for all the image resizing and cutting.

These are some resources that I found incredibly useful when trying to figure all this stuff out. They give some good background.

Credits

Thanks to Jordan Bethea (a player in my D&D campaign) for suggesting the feature, and thanks to Scott Turnbull for guiding me to MapWoW.com and giving me tips on how to implement it.

Thanks for reading, and if you liked the article, please consider Digging it or voting for it on your favorite social bookmarking site.

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS Log in