1
feb 10

Ruby & mDNS: Finding Information About Your Network

I've been working on a little project that was scanning information on my network and I was trying to figure out how I can get human friendly names to go with IP addresses. Not surprisingly there is a gem that does most of the work, DNSSD.

Installation

gem install dnssd

Discover Services

The process is two fold. First you need to get all the neighbours who are broadcasting on a certain service. I've used _afpovertcp as most OS X boxes broadcast AFP, but there are loads you can listen for. Check out the list at: DNS SRV (RFC 2782).

hosts = {}

begin
  # Only listen for one second
  timeout 1 do
    browser.browse('_afpovertcp._tcp', 'local') do |reply|
      hosts[reply.fullname] = reply
      next if reply.flags.more_coming?

      hosts.sort_by do |_, host|
        [(host.flags.add? ? 0 : 1), host.fullname]
      end.each do |_, host|
        next unless host.flags.add?

        # Do something with each of the services we've found.
      end
    end
  end
rescue Timeout::Error ; end

Find IPs for Hosts

Next we want to look up the IPs for a specific mDNS client. As each remote host can have several names for an address we will need to add all the addresses for the remote host. We can use the resolve to find the addresses:

ip_addresses = []
# Assume reply is one of the hosts we've enumerated
DNSSD.resolve!(reply) do |r|
  service = DNSSD::Service.new
  begin
    # Use the server object to get address info about the target
    service.getaddrinfo(r.target, 0) do |addrinfo|
      ip_addresses << addrinfo.address
      break unless addrinfo.flags.more_coming?
    end
  ensure
    service.stop
  end
end

All Together Now

+
require 'rubygems'
require 'dnssd'

hosts = {}
hostnames = {}

begin
  browser = DNSSD::Service.new
  
  timeout 1 do
    browser.browse('_afpovertcp._tcp', 'local') do |reply|
      hosts[reply.fullname] = reply
      next if reply.flags.more_coming?

      hosts.sort_by do |_, host|
        [(host.flags.add? ? 0 : 1), host.fullname]
      end.each do |_, host|
        next unless host.flags.add?

        ip_addresses = []
        DNSSD.resolve!(host) do |r|
          service = DNSSD::Service.new
          begin
            service.getaddrinfo(r.target, 0) do |addrinfo|
              ip_addresses << addrinfo.address
              break unless addrinfo.flags.more_coming?
            end
          ensure
            service.stop
          end

          break
        end
        ip_addresses.each { |ip| hostnames[ip] = host.name }
      end
    end
  end
rescue Timeout::Error ; end

# hostnames will now contain a hash of ip_address => hostname pairs
24
jan 10

Using Paperclip with Datamapper & Sinatra

I love using Sinatra, for small projects it's so fast to get things up and running. I had a small project for a file hosting service that I wanted to get running on Sinatra with Paperclip. Although Paperclip was originally built for rails Ken Robertson of In Valid Logic ported it to Datamapper.

Setup The Model

This starts to make things really easy. Simply declare you model with some extensions and you're almost set:

class Resource
  include DataMapper::Resource
  include Paperclip::Resource

  property :id,     Serial

  has_attached_file :file,
                    :url => "/system/:attachment/:id/:style/:basename.:extension",
                    :path => "#{APP_ROOT}/public/system/:attachment/:id/:style/:basename.:extension"
end

You'll need to specify your :url and :path options as the ones built into dm-paperclip are merb centric which won't quite work. Also set APP_ROOT to where ever you're application root directory with you static Sintra folder is.

Setup Your Route

In your routes you'll have something like:

post '/upload' do
  resource = Resource.new(:file => make_paperclip_mash(params[:file]))
  halt "Something went wrong..." unless resource.save
end

The tricky part here is the make_paperclip_mash method. Paperclip expects the file object loaded from the form to be in a different form than what is created by default. To fix this We create a Mash (which is just a Hash, unless you're actually using merb) with some specific attributes:

def make_paperclip_mash(file_hash)
  mash = Mash.new
  mash['tempfile'] = file_hash[:tempfile]
  mash['filename'] = file_hash[:filename]
  mash['content_type'] = file_hash[:type]
  mash['size'] = file_hash[:tempfile].size
  mash
end

That should pretty much get everything up and running.

Putting it all Together

To get the demo working you'll need a bunch of gems:

sudo gem install datamapper sinatra dm-paperclip haml

And the code:

+
require 'rubygems'
require 'sinatra'
require 'datamapper'
require 'dm-paperclip'
require 'haml'
require 'fileutils'

APP_ROOT = File.expand_path(File.dirname(__FILE__))

DataMapper::setup(:default, "sqlite3://#{APP_ROOT}/db.sqlite3")

class Resource
  include DataMapper::Resource
  include Paperclip::Resource

  property :id,     Serial

  has_attached_file :file,
                    :url => "/:attachment/:id/:style/:basename.:extension",
                    :path => "#{APP_ROOT}/public/:attachment/:id/:style/:basename.:extension"
end

Resource.auto_migrate!

def make_paperclip_mash(file_hash)
  mash = Mash.new
  mash['tempfile'] = file_hash[:tempfile]
  mash['filename'] = file_hash[:filename]
  mash['content_type'] = file_hash[:type]
  mash['size'] = file_hash[:tempfile].size
  mash
end

post '/upload' do
  halt 409, "File doesn't appeart to have any content." unless params[:file][:tempfile].size > 0
  @resource = Resource.new(:file => make_paperclip_mash(params[:file]))
  halt 409, "Something went wrong...\n#{resource.errors.inspect}" unless @resource.save

  haml :upload
end

get '/' do
  haml :index
end

__END__
@@ index
%form{:action => '/upload', :enctype => 'multipart/form-data', :method => 'POST'}
  %input{:type => 'file', :name => 'file'}
  %input{:type => 'submit'}

@@ upload
%p= "Shiny, your file #{@resource.file.name} uploaded!"
%img{:src => @resource.file.url }
21
jan 10

Let Flash Die (YouTube Supports HTML5!)

Flash has been a giant on the Internet for some time. It's done some a lot of good and a lot of bad. Either way it's a gross mess, most things can't index it, it's piggishly CPU heavy much of the time, and many sites that use it are usability nightmares. HTML5 and stylesheets are very powerful, throw a bit of Javascript on them and you can have beautiful sites, you don't need Flash people.

It's time for it to start dying, and Google, doing no evil, is on the side of good. They've added beta HTML5 support. It only works in Chrome and Safari right now. Firefox doesn't support the video format they stream (h264, Firefox support Ogg Theora), so I'd imagine they'd have to re-encode which would be huge amount of data to process.

There's also a handy Safari addon called ClickToFlash which disables flash in websites until you click the frame where it'd be. This is great for getting rid of countless advertisements and websites that like to talk to you. You can also convert your videos to HTML5 videos on YouTube with ClickToFlash, which really ins't necessary with Google's beta.

For Firefox you've got Flashblock which has similar features to ClickToFlash without the YouTube support (which isn't really necessary now Google has added the beta opt in).

YouTube HTML5 support

ClickToFlash

Flashblock

20
dec 09

Subclassing NSManagedObject

So I looked high an low and it took me a while to find out some of the best practices when creating a subclass of NSManagedObject. For the most part you don't even need to all the properties will be there without any coding needed, but frequently you need to subclass it, to create logic and the like.

In my case I needed to be able to enumerate certain properties so I wanted to know the best way to subclass it and declare the properties on the model. I guess this is here for my own interest.

So you have a model called country with id, name, and population:

Country.h:

@interface Country : NSManagedObject {
}

@property (nonatomic, retain) NSNumber *id;
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSNumber *population;
@end

Country.m:

@implementation Country
@dynamic id;
@dynamic name;
@dynamic population;
@end

Now methods like class_copyPropertyList will return the attributes specified. Sweet!

17
dec 09

Having Authlogic Check Login & EMail on Authentication

This is kinda documented in the Authlogic docs, but it's not super clear, so I figured I'd try to clear things up.

Problem

I have an email field that isn't always populated and I have a login field that isn't always populated (they are unique). How do I get the authentication to check both columns?

Using find_by_login_method

Authlogic lets you override just about anything, you just need to know the where and the how.

First off you'll need to create you custom model finder in your User model:

class User < ActiveRecord::Base
  def self.find_by_username_or_email(login)
    find_by_username(login) || find_by_email(login)
  end
end

Next you need to tell Authlogic to use that instead of the build in one. This was the unclear part. You put that in your UserSession object:

class UserSession < Authlogic::Session::Base  
  find_by_login_method :find_by_login_or_email
end

Anyway I hope that helps somebody.

15
dec 09

Battle with Conflicting Ruby & MacRuby Gems

I've been playing with macruby in my down time a bit lately, and it's wicked cool, but there are a couple things to be careful of. One is how gems work.

First off all the macruby commands are prefixed with mac so they don't collide with the regular commands. This is important seeing as how the runtime is a fair bit different on this inside.

This works very well except when the default path for installing binaries for gems is the same on all ruby version. Namely /usr/bin/ so when you install a gem as root it will blow out your built in on and then start making calls to the macruby binaries.

Then you start getting stuff like this:

Rails requires RubyGems >= 1.3.2 (you have 1.3.1). Please `gem update --system` and try again.

When gem -v reports 1.3.5 and confusion sets in (and hours of frustration).

So the trick is, get macgems to install just to the local .gem folder. Or go and make sure that gems are killing each other's binaries. Then you just need to make sure your paths are all happy.

4
dec 09

HighLine

Asking for input in a Rake Task

It's pretty easy asking for input in a rake task, but no what you'd expect.

This doesn't work:

puts "Please enter a username:"
username = gets.chomp

You need to actually specify STDIN:

puts "Please enter a username:"
username = STDIN.gets.chomp

This is all fine an dandy for non-secure data, but what about passwords, it'd be nice to have them not show up. Well there's a gem for that.

A better way: HighLine

HighLine makes command-line interaction very simple:

Password Input:

password = HighLine.new.ask("Please enter the 'admin' password: ") do |q|
  q.echo = false 
end

Validation:

username = HighLine.new.ask("Please enter the 'username': ") do |q|
  q.validate = /[a-zA-z][a-zA-Z0-9]{2,8}$/
end

It does a whole lot of other stuff too, so check out the docs.

Basic Rails Usage:

environment.rb:

gem.config 'highline', :lib => false, :version => '>=1.5.1'

rake.task:

require 'highline'
  
namespace :generate do
  desc "Do something with CLI"
  task "Enter username & password" do
    username = HighLine.new.ask("Please enter the 'username': ") do |q|
      q.validate = /[a-zA-z][a-zA-Z0-9]{2,8}$/
    end
    password = HighLine.new.ask("Please enter the 'admin' password: ") do |q|
      q.echo = false
    end
  end
end
28
nov 09

Javascript Getters & Setters

K well this doesn't work in IE--shocking I know--however, it's pretty cool (the ActionScript folk will like it). You can have getter and setter properties in Javascript now. No need to have a winter.setSnow("Go Away") and winter.getSnow() style functions.

It's easy as pie:

Winter = function() {
  var privateStyleWinterStatus = "All the time";

  return {
    get snow: function() { return privateStyleWinterStatus; },
    set snow: function(val) { privateStyleWinterStatus = val; }
  };
};

// And now we can go like:

var winter = new Winter;
winter.snow = 'Go Away!';
console.log(winter.snow);

No I can hear the questions: "That's all fine and dandy, but what if already have an object?". Well that's a bit more cryptic, but good all the same (and you don't get stuck with the rigidity of ActionScript on this one):


var weather = function() {
  return ["blizzard", "clear", "light skiff", "freezing rain"][Math.round(Math.random() * 4)]
};

winter.__defineGetter__("weather", weather);

console.log(winter.weather);
console.log(winter.weather);
console.log(winter.weather);
console.log(winter.weather);
  
24
nov 09

Formtastic for an Authlogic Session

With the latest version of Formtastic (0.9.4) I started getting errors going to my login page:

ActionView::TemplateError (undefined method `content_columns' for UserSession:Class)

It seems that the latest version uses the content_columns method to get information about the model. This confuses our Authlogic Authlogic::Session::Base model as it's not really an ActiveRecord and it doesn't have this method.

Solution: Don't use the form.inputs method

Change this:

- semantic_form_for @user_session do |form|
  = form.inputs
  = form.buttons

To This:

- semantic_form_for @user_session do |form|
  - form.inputs do
    = form.input :login
    = form.input :password
  = form.buttons