treading the rails with ruby shoes

59
Treading the Rails with Ruby Shoes push, pull and instant messaging http://slides.games-with-brains.net /

Upload: eleanor-mchugh

Post on 24-Apr-2015

6.845 views

Category:

Technology


3 download

DESCRIPTION

RailsConf Europe 2008 presentation exploring Shoes, Asynchronous IP networking, hybrid cryptography and instant messaging in the context of the InterWeb.

TRANSCRIPT

Page 1: Treading the Rails with Ruby Shoes

Treading the Rails with Ruby Shoes

push, pull and instant messaging

http://slides.games-with-brains.net/

Page 2: Treading the Rails with Ruby Shoes
Page 3: Treading the Rails with Ruby Shoes

it’s a lovely day for walking...

Eleanor McHugh

Rom

ek S

zcz

esn

iak

what lovely Shoes they’re wearing

and really good for gripping Rails

bet they could do real-time in no time

swap a few messages...

play some fun games...

you’re aware this is a web conference?

hmm... we’re not so sure about that!

Page 4: Treading the Rails with Ruby Shoes
Page 5: Treading the Rails with Ruby Shoes

but don’t forget your umbrella!

danger! experimental code and concepts ahead

for entertainment purposes only

examples will not be discussed in detail

bugs ahoy!!! use with extreme caution!!!

obvious security holes will leak your data!!!

any resemblance to actual code & conceptstm, living or dead, is pure coincidence

Page 6: Treading the Rails with Ruby Shoes

today’s itinerary

a brief history of internet applications

a crash course in shoemaking

chat: from sockets to XMPP

privacy & crypto in a Rails setting

Rails as a push-driven chat server

Page 7: Treading the Rails with Ruby Shoes

no one can afford a sense of perspective

Page 8: Treading the Rails with Ruby Shoes

in the beforetime

dedicated mainframe terminals

ARPAnet

TCP/IP: the birth of the Internet

X.25 and JANET

UUCP: Unix to Unix CoPy

FidoNET and the world of BBSes

Page 9: Treading the Rails with Ruby Shoes

gossip and confusion

Email

Usenet

Telnet

FTP

MUDs

IRC

Page 10: Treading the Rails with Ruby Shoes

the medium is the message

Memex & Hyperlinks

Project Xanadu

Hypercard

Gopher

HTML

Google

Page 11: Treading the Rails with Ruby Shoes

the diamond age

how we display the content matters

ActiveX

Java Applets & Flash

DHTML & ECMAScript

CSS

AJAX and Web 2.0

Page 12: Treading the Rails with Ruby Shoes

calling all stations: this is Rails

we now live in the application age

database + http = content retrieval

html + scripting = interface experience

retrieval + experience = application

Page 13: Treading the Rails with Ruby Shoes

so it’s not about the browser?

Service Oriented Web Architectures

RPC: using the machine

REST: viewing the data

XML puts the X in AJAX

JSON makes AJAX redundant

and sockets mean all bets are off!

Page 14: Treading the Rails with Ruby Shoes

introducing shoes

Page 15: Treading the Rails with Ruby Shoes

what’s in the box?

a tiny toolkit inspired by the web

part of Why’s Hackety Hack project

2D graphics, text and audio/video embedding

runs on Linux, Windows and MacOS X

still a little rough around the edges

but lots of fun to play with!!!

Page 16: Treading the Rails with Ruby Shoes

working with shoes

built-in manual

console

exceptions

warn, info, debug

Page 17: Treading the Rails with Ruby Shoes

my first calculator

calculator model

numeric display

command buttons

keyboard handling

Page 18: Treading the Rails with Ruby Shoes

the calculation engineclass MathEngine attr_reader :memory, :operator attr_accessor :operand, :total

def initialize clear @memory = 0.0 end

def clear @operand = 0.0 @total = nil @operator = nil end

def memory command case command when :recall then @operand = @memory when :clear then @memory = 0.0 when :add then @memory += @operand when :subtract then @memory -= @operand else raise "not a memory action" end end

def operator= operator unless @total @total = @operand @operand = 0.0 end @operator = operator.to_sym end

def evaluate if @operator then begin case @operator when :+, :-, :*, :/ @total = @total.send(@operator, @operand) else raise "invalid operator" end rescue TypeError raise "operand required" end end endend

Page 19: Treading the Rails with Ruby Shoes

getting digitalclass DigitField < Widget attr_reader :digits

def DigitField.validate_number_format number raise unless number.match(/-?[1-9]?\d*\.?\d*/) || number.match(/-?0\.\d*/) end

def DigitField.format_leading_zeroes number # only allow a single leading zero number.sub!(/^(-)?(0*)/, '\10') # and discard it unless followed by a decimal point number.sub!(/^(-)?0([^.])/, '\1\2') end

def DigitField.format_minus_zero number number.sub!(/^-(0*)\.?(0*)$/, '0.0') end

def initialize background lightgrey, :curve => 5, :margin => 2 @number_field = para :size => 20, :stroke => dimgray, :margin => 8 clear end

def refresh @number_field.replace(strong(@digits)) end

def clear

@digits = "" refresh end

def digits= new_digits DigitField.validate_number_format new_digits @digits = new_digits DigitField.format_leading_zeroes @digits DigitField.format_minus_zero @digits refresh end

def value @digits.to_f end

def value= number self.digits = number.to_s end

def << new_digits self.digits = (@digits + new_digits)[0, 13] end

def chop! self.digits = @digits.chop! endend

Page 20: Treading the Rails with Ruby Shoes

it’s all about controlShoes.app :height => 300, :width => 220, :resizable => false do @calculator = MathEngine.new

def reset_state @state = nil @calculator.clear @display.value = @calculator.operand end

def perform_calculation @calculator.evaluate @display.value = @calculator.total end

def calculate_total @state = :calculate perform_calculation @display.value = @calculator.total end

def accept_digit digit case @state when :query @display << digit else @state = :query @display.digits = digit end @calculator.operand = @display.value end

def reject_digit @display.chop! case @state when :query @calculator.operand = @display.value else @calculator.total = @display.value end end

def accept_operator operator perform_calculation if @state == :query @state = :operator @calculator.operator = operator end

def number_button digit button digit, :width => 50 do accept_digit digit end end

def operator_button decal, operator = nil button decal, :width => 50 do accept_operator operator || decal.to_sym end end

Page 21: Treading the Rails with Ruby Shoes

but layout’s nice too def memory_button decal, operator, options = {} button decal, :width => 50 do @state = :memory @calculator.memory operator @display.value = @calculator.operand if options[:update_display] end end

background darkorange, :curve => 10 stack :margin => 5 do @display = digitfield reset_state

flow :margin => 4 do memory_button "M+", :add memory_button "M-", :subtract memory_button "MC", :clear memory_button "MR", :recall, :update_display => true end

[%w(7 8 9 /), %w(4 5 6 *), %w(1 2 3 -)].each do |buttons| flow :margin => 4 do 1.upto(3) { number_button buttons.shift } operator_button buttons.shift end end

flow :margin => 4 do [".", "0"].each { |decal| number_button decal } button "C", :width => 50 do reset_state end operator_button "+" end

flow :margin => 4 do button "=", :width => 200 do calculate_total end end end

keypress do |key| case key when '0'..'9', '.' then accept_digit key when 'c', 'C' then reset_state when '=', "\n" then calculate_total when '+', '-', '*', '/' then accept_operator key when :delete, :backspace then reject_digit end endend

Page 22: Treading the Rails with Ruby Shoes

precious gems

installs exclusively for Shoes

no native extensions on Leopard

still a work in progress

Shoes.setup do gem 'json_pure'end

require 'json'

Shoes.app do # Application bodyend

Page 23: Treading the Rails with Ruby Shoes

being a web clientShoes.app do stack do title "Exercising Shoes", :size => 20 @status = para "" title "Headers", :size => 16 @headers = para "" title "Content", :size => 16 @content = para "" button "load data" do @status.text = "Loading data..." download "http://www.stevex.net/dump.php", :method => "POST", :body => "v=1.0&q=shoes", :save => "data.txt" do |dump| # This block is called when the download completes @status.text = "You request resulted in the following response" @headers.text = dump.response.headers.inspect require 'hpricot' @content.text = Hpricot(body = IO.read("data.txt")).inner_text end end endend

supports http requests

includes Hpricot

Page 24: Treading the Rails with Ruby Shoes
Page 25: Treading the Rails with Ruby Shoes

instant messaging

Page 26: Treading the Rails with Ruby Shoes

the dialogues

conversation: bidirectional & interleaved

approximates soft real-time

two or more clients

today’s lesson: client-server

try this at home: peer-to-peer

Page 27: Treading the Rails with Ruby Shoes

serving packets asynchronouslyrequire 'socket'require 'thread'

class EndPoint attr_accessor :host, :port

def initialize host, port @host, @port = host, port end

def == other other.host == @host && other.port == @port endend

Message = Struct.new(:end_point, :text, :status)

class UDPServer attr_reader :end_point, :ticks, :outbound_messages

OK = 200 UNAUTHORISED = 401 RESOURCE_NOT_FOUND = 403 RESOURCE_CONFLICT = 409

def initialize host, port @end_point = EndPoint.new(host, port) @clients = [] @outbound_messages = [] @ticks = 0 end

def process_request request, peer Message.new peer, "hello", OK end

def start options = {} (@socket = UDPSocket.new).bind(@end_point.host, @end_point.port) @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) @housekeeper = Thread.new(options[:housekeeping_period] || 0.5) do |period| loop do sleep period @ticks += 1 do_housekeeping if @socket end end

@listener = Thread.new(options[:buffer_size] || 4096) do |buffer_size| loop do if streams = select([@socket]) then streams[0].each do |client| message, peer = *client.recvfrom(buffer_size) peer = EndPoint.new(peer[2], peer[1]) @outbound_messages << process_request(message, peer) end @clients.compact! end end end

@despatcher = Thread.new(options[:throttle_period] || 0.01) do |period| loop do if m = @outbound_messages.shift then UDPSocket.open.send(m.text, m.status, m.end_point.host, m.end_point.port) end sleep period end end $stderr.puts "server launched" @running = true loop do stop unless @running sleep 1 end end

def stop @listener.kill sleep 1 while @messages.length > 0 @despatcher.kill @housekeeper.kill @clients.each { |thread| thread.kill } @socket.close @socket = nil exit end

def do_housekeeping endend

Page 28: Treading the Rails with Ruby Shoes

an IRCsome chat serverUser = Struct.new(:peer, :access_time, :password, :presence, :messages)Request = Struct.new(:peer, :user, :command, :subject, :message)

class ChatServer < UDPServer attr_reader :users, :idle_allowance

def initialize address, port, idle_allowance = 300 super address, port @idle_allowance = idle_allowance @users = {} end

def do_housekeeping manage_presence end

def authenticated? request @users[request.user] ? @users[request.user].peer == request.peer : false end

def process_request raw_request, peer tokens = raw_request.match(/^(\w*):?(\w*):?(\w*):?(.*)$/).captures request = Request.new(peer, *tokens) request.command.downcase! message = if authenticated?(request) then do_private_command(request) else do_public_command(request) end Message.new peer, message[0], message[1] end

def do_public_command request answer = case request.command when 'subscribe' unless @users[request.user] @users[request.user] = User.new(request.peer, @ticks, request.subject, :active, []) ["you are now recognised as #{request.user}", OK] else ["that user already exists", RESOURCE_CONFLICT] end else ["you probably need to be logged in for that", UNAUTHORISED] end answer end

def do_private_command request answer = case request.command when 'unsubscribe' @users[request.user] = nil ["you are no longer subscribed", OK] when 'users' [@users.inject("") { |list, user| list + "#{user[0]} : #{user[1].presence}\n" }, OK] when 'presence' ["status: #{request.subject}", @users[request.subject] ? OK : RESOURCE_NOT_FOUND] when 'message' if @users.include?(request.subject) then ["#{request.user.upcase}: #{request.message}", OK] else ["error: #{request.subject.upcase}: recipient unknown", RESOURCE_NOT_FOUND] end else ["are you sure that you intended to do that?", RESOURCE_NOT_FOUND] end answer end

private def manage_presence active_threshold = @idle_allowance / 10 idle_threshold = @idle_allowance / 2 @users.each do |name, details| presence = case @ticks - details.access_time when 0...active_threshold then :active when active_threshold...idle_threshold then :idle when idle_threshold...@idle_allowance then :away else nil end if presence then @users[name].details.presence = presence else @users[name] = nil end end endend

ChatServer.new("localhost", 3000).start

Page 29: Treading the Rails with Ruby Shoes

and its faithful clientsclass UDPClient attr_reader :remote, :status, :messages

OK = 200 UNAUTHORISED = 401 RESOURCE_NOT_FOUND = 403 RESOURCE_CONFLICT = 409

def initialize local_host, local_port @local = EndPoint.new(local_host, local_port) @messages = [] end

def connect remote_host, remote_port, buffer_size = 4096 raise if @socket puts "starting client" @remote = EndPoint.new(remote_host, remote_port) (@socket = UDPSocket.open).bind(@local.host, @local.port) @socket.setsockopt(Socket::SOL_SOCKET, Socket::SO_REUSEADDR, 1) @listener = Thread.new(buffer_size) do |buffer_size| loop do $stdout.puts "checking messages #{buffer_size} bytes at a time" if streams = select([@socket]) then streams[0].each do |server| message, peer = server.recvfrom(buffer_size) peer = EndPoint.new(peer[2], peer[1]) $stdout.puts "#{Time.now} : #{message}" end end end end end

def send message @socket.send(message, 0, @remote.host, @remote.port) end

def receive max_bytes = 512 raise unless @socket message, @status = @socket.recvfrom(max_bytes) message end

def disconnect @listener.kill if @listener @socket.close if @socket @socket = nil endend

class ChatClient attr_reader :connection, :user_name

def initialize local_host, local_port @connection = UDPClient.new(local_host, local_port) end

def send_command *args @connection.send "#{user_name}:#{args.join(':')}" $stdout.puts receive(4096) end

def connect host, port, user_name, password @connection.connect host, port @user_name = user_name send_command :subscribe, password end

def check_presence user send_command :presence, user end

def message user, text send_command :message, user, text end

def disconnect send_command :unsubscribe @connection.disconnect end

def list_users send_command :users endend

chat_client = ChatClient.new("localhost", 3001)chat_client.connect "localhost", 3000, "admin", "my_password"users = chat_client.list_usersadmin_presence = chat_client.check_presence("admin")chat_client.message("admin", "hello!!!!")chat_client.message("admin", "is that you???")sleep 300chat_client.disconnect

Page 30: Treading the Rails with Ruby Shoes

a babel of tongues

system notification services

UNIX talk

IRC

ICQ

SIMPLE: AIM-Yahoo-MSN

Jabber-XMPP

Page 31: Treading the Rails with Ruby Shoes

XMPP: the grand unified theory

eXtensible Messaging & Presence Protocol

decentralised client-server architecture

uses a streaming XML protocol

clients exchange stanzas with server

RFCs: 3920-3, 4854, 4979, 5122

XMPP4R & XMPP4R-simple

Page 32: Treading the Rails with Ruby Shoes

a little jabbering...require 'rubygems'require 'xmpp4r'include Jabber

Jabber.debug = trueclient = Client.new(JID.new('admin@Lenore/home'))client.connectclient.auth('my_password')client.send(Presence.new.set_type(:available))

admin is the node to connect to

Lenore is the server that node belongs to

/home is a resource, allowing the node to connect multiply

Page 33: Treading the Rails with Ruby Shoes

...creates a lot of chatter00:39:53 Debugging mode enabled.00:39:53 RESOLVING: _xmpp-client._tcp.lenore (SRV)00:39:53 CONNECTING: lenore:522200:39:53 SENDING: <stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' to='lenore' xml:lang='en' version='1.0' >00:39:53 RECEIVED: <stream:stream from='lenore' xmlns:stream='http://etherx.jabber.org/streams' id='3654426306' version='1.0' xml:lang='en' xmlns='jabber:client'/>00:39:53 FEATURES: waiting...00:39:53 RECEIVED: <stream:features><starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>DIGEST-MD5</mechanism><mechanism>PLAIN</mechanism></mechanisms><register xmlns='http://jabber.org/features/iq-register'/></stream:features>00:39:53 FEATURES: received00:39:53 FEATURES: waiting finished00:39:53 PROCESSING: <stream:features xmlns='jabber:client'><starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>DIGEST-MD5</mechanism><mechanism>PLAIN</mechanism></mechanisms><register xmlns='http://jabber.org/features/iq-register'/></stream:features> (REXML::Element)00:39:53 SENDING: <starttls xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>00:39:53 RECEIVED: <proceed xmlns='urn:ietf:params:xml:ns:xmpp-tls'/>00:39:53 TLSv1: OpenSSL handshake in progress00:39:53 TLSv1: restarting parser00:39:53 SENDING: <stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' to='lenore' xml:lang='en' version='1.0' >00:39:53 RECEIVED: <stream:stream from='lenore' xmlns:stream='http://etherx.jabber.org/streams' id='1752527447' version='1.0' xml:lang='en' xmlns='jabber:client'/>00:39:53 FEATURES: waiting...00:39:53 RECEIVED: <stream:features><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>DIGEST-MD5</mechanism><mechanism>PLAIN</mechanism></mechanisms><register xmlns='http://jabber.org/features/iq-register'/></stream:features>00:39:53 FEATURES: received00:39:53 FEATURES: waiting finished00:39:53 PROCESSING: <stream:features xmlns='jabber:client'><mechanisms xmlns='urn:ietf:params:xml:ns:xmpp-sasl'><mechanism>DIGEST-MD5</mechanism><mechanism>PLAIN</mechanism></mechanisms><register xmlns='http://jabber.org/features/iq-register'/></stream:features> (REXML::Element)00:39:53 SENDING: <auth mechanism='DIGEST-MD5' xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>00:39:53 RECEIVED: <challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>bm9uY2U9IjMwNTM4MDUzMDAiLHFvcD0iYXV0aCIsY2hhcnNldD11dGYtOCxhbGdvcml0aG09bWQ1LXNlc3M=</challenge>00:39:53 SASL DIGEST-MD5 challenge: "nonce=\"3053805300\",qop=\"auth\",charset=utf-8,algorithm=md5-sess" {"algorithm"=>"md5-sess", "charset"=>"utf-8", "qop"=>"auth", "nonce"=>"3053805300"}00:39:53 SASL DIGEST-MD5 response: response=766365089123dd048b2163a931dc6aae,cnonce="af8d00bb4d8b30921680c9087dcbe97b",digest-uri="xmpp/lenore",username="admin",charset=utf-8,qop=auth,realm="lenore",nonce="3053805300",nc=0000000100:39:53 SENDING: <response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cmVzcG9uc2U9NzY2MzY1MDg5MTIzZGQwNDhiMjE2M2E5MzFkYzZhYWUsY25vbmNlPSJhZjhkMDBiYjRkOGIzMDkyMTY4MGM5MDg3ZGNiZTk3YiIsZGlnZXN0LXVyaT0ieG1wcC9sZW5vcmUiLHVzZXJuYW1lPSJhZG1pbiIsY2hhcnNldD11dGYtOCxxb3A9YXV0aCxyZWFsbT0ibGVub3JlIixub25jZT0iMzA1MzgwNTMwMCIsbmM9MDAwMDAwMDE=</response>00:39:53 RECEIVED: <challenge xmlns='urn:ietf:params:xml:ns:xmpp-sasl'>cnNwYXV0aD1jMzJiNDI2Mzg1ZjYwYzgzYTAxNTg0MTkzZmMxYmI4MA==</challenge>00:39:53 SENDING: <response xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>00:39:53 RECEIVED: <success xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>00:39:53 SENDING: <stream:stream xmlns:stream='http://etherx.jabber.org/streams' xmlns='jabber:client' to='lenore' xml:lang='en' version='1.0' >00:39:53 RECEIVED: <stream:stream from='lenore' xmlns:stream='http://etherx.jabber.org/streams' id='2848792001' version='1.0' xml:lang='en' xmlns='jabber:client'/>00:39:53 RECEIVED: <stream:features><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></stream:features>00:39:53 FEATURES: received00:39:53 SENDING: <iq type='set' id='1827' xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/></iq>00:39:53 PROCESSING: <stream:features xmlns='jabber:client'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'/><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></stream:features> (REXML::Element)00:39:53 RECEIVED: <iq type='result' id='1827'><bind xmlns='urn:ietf:params:xml:ns:xmpp-bind'><jid>admin@lenore/268663681220139593691251</jid></bind></iq>00:39:53 SENDING: <iq type='set' id='2363' xmlns='jabber:client'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>00:39:53 RECEIVED: <iq type='result' id='2363'><session xmlns='urn:ietf:params:xml:ns:xmpp-session'/></iq>00:39:53 SENDING: <presence xmlns='jabber:client'/>

Page 34: Treading the Rails with Ruby Shoes

(not quite) a jabber clientrequire 'xmpp4r'

class User attr_accessor :node, :server, :resource, :message_id

def initialize node, server, resource = nil @node, @server, @resource = node, server, resource @message_id = 0 end

def next_id @message_id += 1 end

def to_s "#{@name}@#{@node}#{@resource ? '/' + @resource : nil}" endend

class JabberController include Jabber attr_reader :user, :debug attr_reader :subscription_requests, :messages

def initialize user, password, debug = false @user, @password, @debug = user, password, debug @subscription_requests = [] @messages = {} end

def connect initial_status = :available @connection = Client.new(JID.new(@user.to_s)) @roster = Roster::Helper.new(@connection) @connection.connect @connection.auth(@password) status = initial_status end

def consume_messages $stdout.puts @messages.shift end

def status= new_status @connection.send Presence.new.set_type(new_status) end

def accept_buddy new_buddy @roster.accept_subscription(new_buddy.to_s) @subscription_requests.delete_if { |user| user == new_buddy.to_s } end

def buddy_status_change former_status, current_status puts "#{current_status.from} is now #{current_status.show}" end

def add_buddy new_buddy @connection.send Presence.new.set_type(:subscribe).set_to(new_buddy.to_s) end

def send recipient, options = {} (message = Message.new(recipient.to_s, options[:plain_text] || '')).set_subject(options[:subject] || '') if options[:xhtml_content] then (wrapper = REXML::Element.new('html')).add_namespace('http://jabber.org/protocol/xhtml-im') (body = REXML::Element.new('body')).add_namespace('http://www.w3.org/1999/xhtml') body.text options[:xhtml_content] wrapper.add body message.add_element wrapper end message.set_type(options[:message_type] || :chat).set_id(user.next_id.to_s) message.set_subject(options[:subject] || '') @connection.send message end

private def initialize_callbacks @connection.add_update_callback do |p| @connection.send p.from, "thanks #{p.from} for accepting my request" if p.ask == :subscribe end @connection.add_message_callback { |m| (@messages[m.from] ||= []) << m.body } @connection.add_presence_callback do |former_status, current_status| buddy_status_change former_status, current_status end @roster.add_subscription_request_callback do |request, presence| @subscription_request << presence.from unless @subscription_requests[:presence.from] end @message_consumer = Thread.new { consume_messages; sleep 0.01 } endend

jabber_connection = JabberController.new(User.new('admin', 'Lenore', 'work'))jabber_connection.connectmessage_options = { :subject => "are you free to chat?", :plain_text => "hey user, it's your nosey admin checking up on you!!!" :xhtml_content => <<-RAW_XHTML <h1>Wow!!! HTML!!!</h1><strong>hey user</strong>,<br /> it's your nosey admin checking up on you!!!" RAW_XHTML }jabber_connection.send User.new('user', 'Lenore', 'home').to_s, message_optionsjabber_connection.subscription_requests.each { |name| jabber_connection.accept_buffy name }

Page 35: Treading the Rails with Ruby Shoes

modern creature comforts

presence

privacy

personalisation?

persistence?

Page 36: Treading the Rails with Ruby Shoes

Rails “does” Privacy(if you get our drift...)

Page 37: Treading the Rails with Ruby Shoes

how to be private on the web

connection & transport layer secrecy

custom crypto over HTTP

key exchange

encryption

user authentication

Page 38: Treading the Rails with Ruby Shoes

history of web security

Protocol Version Date Implementation Status

SSL 1.0 Netscape Unreleased

SSL 2.0 1994 Netscape Now Broken

PCT 1.0 October 1995 Microsoft Now Broken

SSL 3.0 March 1996 Netscape

TLS 1.0 January 1999 RFC 2246

TLS 1.1 April 2006 RFC 4346

TLS 1.2 August 2008 RFC 5246

Page 39: Treading the Rails with Ruby Shoes

how it works in principle

Page 40: Treading the Rails with Ruby Shoes

SSL in practiceCONNECTED(00000003)>>> SSL 2.0 [length 0074], CLIENT-HELLO01 03 01 00 4b 00 00 00 20 00 00 39 00 00 38 00 00 35 00 00 16 00 00 13 00 00 0a 07 00 c0 00 00 33 00 00 32 00 00 2f 03 00 80 00 00 05 00 00 04 01 00 80 00 00 15 00 00 12 00 00 09 06 00 40 00 00 14 00 00 11 00 00 08 00 00 06 04 00 80 00 00 03 02 00 80 15 2a d6 a7 4e 7d f5 60 61 c7 2b 65 35 45 94 18 b5 53 ed 80 d1 bf 3a 7d 86 ea 34 f1 e0 03 24 8d<<< TLS 1.0 Handshake [length 004a], ServerHello02 00 00 46 03 01 48 be a7 92 85 22 b8 1c 0f 51 c5 34 57 a4 15 48 02 f2 5b b8 0e 81 b8 6e b7 d3 82 72 3b 88 d5 34 20 5e c4 97 f9 4a 79 f0 90 c6 0c 8a bf c2 3d 32 56 6b 20 90 e7 92 25 6b d9 3c 68 9e c1 2b 14 74 73 00 05 00<<< TLS 1.0 Handshake [length 065a], Certificate0b 00 06 56 00 06 53 00 03 26 30 82 03 22 30 82 02 8b a0 03 02 01 02 02 10 6e 57 69 0a 10 4f aa ff 81 74 f8 38 8b 08 0d f1 30 0d 06 09 2a 86 48 86 f7 0d 01 01 05 05 00 30 4c 31 0b 30 09 06 03 55 04 06 13 02 5a 41 31 25 30 23 06 03 55 04 0a 13 1c 54 68 61 77 74 65 20 43 6f 6e 73 75 6c 74 69 6e 67 20 28 50 74 79 29 20 4c 74 64 2e 31 16 30 14 06 03 55 04 03 13 0d 54 68 61 77 74 65 20 53 47 43 20 43 41 30 1e 17 0d 30 38 30 35 30 32 31 36 33 32 35 34 5a 17 0d 30 39 30 35 30 32 31 36 33 32 35 34 5a 30 69 31 0b 30 09 06 03 55 04 06 13 02 55 53 31 13 30 11 06 03 55 04 08 13 0a 43 61 6c 69 66 6f 72 6e 69 61 31 16 30 14 06 03 55 04 07 13 0d 4d 6f 75 6e 74 61 69 6e 20 56 69 65 77 31 13 30 11 06 03 55 04 0a 13 0a 47 6f 6f 67 6c 65 20 49 6e 63 31 18 30 16 06 03 55 04 03 13 0f 6d 61 69 6c 2e 67 6f 6f 67 6c 65 2e 63 6f 6d 30 81 9f 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 81 8d 00 30 81 89 02 81 81 00 b9 64 c5 d8 76 41 77 a0 74 49 6e 90 24 8e 57 6f bc 3c a8 8e 34 84 be f1 3b bd 2b b3 10 b1 8a 6a b4 b3 42 f3 ad 33 c0 e3 d0 d8 d2 c8 dd 9a 78 8f 0b 97 55 3a ed cc 6b a2 a9 fe 49 8a 88 77 e4 88 37 0a a0 d7 b4 bc 99 a3 2a 63 65 44 1c ab 89 a5 2f 31 e5 84 84 38 12 14 a3 0e 60 3b 15 fc 3d cb 77 24 48 e4 20 74 14 16 34 b6 ea b8 78 7d 01 b1 14 8e 68 64 ad 8c 48 a8 a5 de 0a 74 4a 86 fe a7 02 03 01 00 01 a3 81 e7 30 81 e4 30 28 06 03 55 1d 25 04 21 30 1f 06 08 2b 06 01 05 05 07 03 01 06 08 2b 06 01 05 05 07 03 02 06 09 60 86 48 01 86 f8 42 04 01 30 36 06 03 55 1d 1f 04 2f 30 2d 30 2b a0 29 a0 27 86 25 68 74 74 70 3a 2f 2f 63 72 6c 2e 74 68 61 77 74 65 2e 63 6f 6d 2f 54 68 61 77 74 65 53 47 43 43 41 2e 63 72 6c 30 72 06 08 2b 06 01 05 05 07 01 01 04 66 30 64 30 22 06 08 2b 06 01 05 05 07 30 01 86 16 68 74 74 70 3a 2f 2f 6f 63 73 70 2e 74 68 61 77 74 65 2e 63 6f 6d 30 3e 06 08 2b 06 01 05 05 07 30 02 86 32 68 74 74 70 3a 2f 2f 77 77 77 2e 74 68 61 77 74 65 2e 63 6f 6d 2f 72 65 70 6f 73 69 74 6f 72 79 2f 54 68 61 77 74 65 5f 53 47 43 5f 43 41 2e 63 72 74 30 0c 06 03 55 1d 13 01 01 ff 04 02 30 00 30 0d 06 09 2a 86 48 86 f7 0d 01 01 05 05 00 03 81 81 00 b1 1c 29 2e 0d 5d 80 24 75 81 80 ca d7 ce 4c 14 6b a4 5c c7 90 15 4b e1 1a a1 7c 79 3f c2 8e 97 6f 7b 3c 8a 56 ec fc e2 04 ae e9 c7 0c 5e 07 0f 41 91 90 37 f1 78 5e e8 3e 43 4d 5e 71 c2 63 45 25 5f 76 f4 79 ab 0a 6a 0a 3f ba 04 59 79 a2 22 83 06 cb de 4a 4a e6 2f 97 73 b3 66 e7 ed 37 53 49 82 9c 2d e0 64 8d 7c 43 2c 71 81 a8 b2 7a 4c d0 89 dd ce 3f 71 b6 c6 e4 98 46 be 87 a6 6b de 00 03 27 30 82 03 23 30 82 02 8c a0 03 02 01 02 02 04 30 00 00 02 30 0d 06 09 2a 86 48 86 f7 0d 01 01 05 05 00 30 5f 31 0b 30 09 06 03 55 04 06 13 02 55 53 31 17 30 15 06 03 55 04 0a 13 0e 56 65 72 69 53 69 67 6e 2c 20 49 6e 63 2e 31 37 30 35 06 03 55 04 0b 13 2e 43 6c 61 73 73 20 33 20 50 75 62 6c 69 63 20 50 72 69 6d 61 72 79 20 43 65 72 74 69 66 69 63 61 74 69 6f 6e 20 41 75 74 68 6f 72 69 74 79 30 1e 17 0d 30 34 30 35 31 33 30 30 30 30 30 30 5a 17 0d 31 34 30 35 31 32 32 33 35 39 35 39 5a 30 4c 31 0b 30 09 06 03 55 04 06 13 02 5a 41 31 25 30 23 06 03 55 04 0a 13 1c 54 68 61 77 74 65 20 43 6f 6e 73 75 6c 74 69 6e 67 20 28 50 74 79 29 20 4c 74 64 2e 31 16 30 14 06 03 55 04 03 13 0d 54 68 61 77 74 65 20 53 47 43 20 43 41 30 81 9f 30 0d 06 09 2a 86 48 86 f7 0d 01 01 01 05 00 03 81 8d 00 30 81 89 02 81 81 00 d4 d3 67 d0 8d 15 7f ae cd 31 fe 7d 1d 91 a1 3f 0b 71 3c ac cc c8 64 fb 63 fc 32 4b 07 94 bd 6f 80 ba 2f e1 04 93 c0 33 fc 09 33 23 e9 0b 74 2b 71 c4 03 c6 d2 cd e2 2f f5 09 63 cd ff 48 a5 00 bf e0 e7 f3 88 b7 2d 32 de 98 36 e6 0a ad 00 7b c4 64 4a 3b 84 75 03 f2 70 92 7d 0e 62 f5 21 ab 69 36 84 31 75 90 f8 bf c7 6c 88 1b 06 95 7c c9 e5 a8 de 75 a1 2c 7a 68 df d5 ca 1c 87 58 60 19 02 03 01 00 01 a3 81 fe 30 81 fb 30 12 06 03 55 1d 13 01 01 ff 04 08 30 06 01 01 ff 02 01 00 30 0b 06 03 55 1d 0f 04 04 03 02 01 06 30 11 06 09 60 86 48 01 86 f8 42 01 01 04 04 03 02 01 06 30 28 06 03 55 1d 11 04 21 30 1f a4 1d 30 1b 31 19 30 17 06 03 55 04 03 13 10 50 72 69 76 61 74 65 4c 61 62 65 6c 33 2d 31 35 30 31 06 03 55 1d 1f 04 2a 30 28 30 26 a0 24 a0 22 86 20 68 74 74 70 3a 2f 2f 63 72 6c 2e 76 65 72 69 73 69 67 6e 2e 63 6f 6d 2f 70 63 61 33 2e 63 72 6c 30 32 06 08 2b 06 01 05 05 07 01 01 04 26 30 24 30 22 06 08 2b 06 01 05 05 07 30 01 86 16 68 74 74 70 3a 2f 2f 6f 63 73 70 2e 74 68 61 77 74 65 2e 63 6f 6d 30 34 06 03 55 1d 25 04 2d 30 2b 06 08 2b 06 01 05 05 07 03 01 06 08 2b 06 01 05 05 07 03 02 06 09 60 86 48 01 86 f8 42 04 01 06 0a 60 86 48 01 86 f8 45 01 08 01 30 0d 06 09 2a 86 48 86 f7 0d 01 01 05 05 00 03 81 81 00 55 ac 63 ea de a1 dd d2 90 5f 9f 0b ce 76 be 13 51 8f 93 d9 05 2b c8 1b 77 4b ad 69 50 a1 ee de dc fd db 07 e9 e8 39 94 dc ab 72 79 2f 06 bf ab 81 70 c4 a8 ed ea 53 34 ed ef 1e 53 d9 06 c7 56 2b d1 5c f4 d1 8a 8e b4 2b b1 37 90 48 08 42 25 c5 3e 8a cb 7f eb 6f 04 d1 6d c5 74 a2 f7 a2 7c 7b 60 3c 77 cd 0e ce 48 02 7f 01 2f b6 9b 37 e0 2a 2a 36 dc d5 85 d6 ac e5 3f 54 6f 96 1e 05 af

<<< TLS 1.0 Handshake [length 0004], ServerHelloDone0e 00 00 00>>> TLS 1.0 Handshake [length 0086], ClientKeyExchange10 00 00 82 00 80 46 75 5c 48 e9 e8 88 71 b5 95 d0 ab 72 1e 03 43 32 0e fd c1 7b d3 e4 92 92 1c 4f d3 38 c9 c1 c4 24 1a f9 b3 dd 4e 29 78 26 91 f2 24 3b 19 6e 8f 3d 93 e5 e2 1d 10 c8 90 fe 03 2f 17 33 61 9b 1f 39 a0 46 36 d6 d0 35 4b d8 c4 19 ed d1 bf d2 02 97 2a c0 70 1f 31 0c 77 55 85 99 69 15 94 c1 88 1c b6 64 72 72 e5 29 95 9c 15 c4 b7 b6 e2 9d 7f 0b b7 12 75 74 ec e0 b2 a8 2c 80 61 48 df 7c 2a>>> TLS 1.0 ChangeCipherSpec [length 0001]01>>> TLS 1.0 Handshake [length 0010], Finished14 00 00 0c 7d c8 23 34 5a b8 34 2b b9 a0 64 3b<<< TLS 1.0 ChangeCipherSpec [length 0001]01<<< TLS 1.0 Handshake [length 0010], Finished14 00 00 0c b8 07 c4 97 ff c0 48 b8 f2 e2 56 05---Certificate chain 0 s:/C=US/ST=California/L=Mountain View/O=Google Inc/CN=mail.google.com i:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA-----BEGIN CERTIFICATE-----...-----END CERTIFICATE----- 1 s:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----Server certificatesubject=/C=US/ST=California/L=Mountain View/O=Google Inc/CN=mail.google.comissuer=/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA---No client certificate CA names sent---New, TLSv1/SSLv3, Cipher is RC4-SHAServer public key is 1024 bitSSL-Session:Protocol : TLSv1Cipher : RC4-SHASession-ID: 5EC497F94A79F090C60C8ABFC23D32566B2090E792256BD93C689EC12B147473Session-ID-ctx: Master-Key: EDAD35941EEB15739B5A9883BA2ACDE6594423F1B0E431568E279C39DE357928CE8A04CCBE67F428963C76E10E7AB1C2Key-Arg : NoneStart Time: 1220454290Timeout : 300 (sec)---

Page 41: Treading the Rails with Ruby Shoes

public and hybrid key crypto

Page 42: Treading the Rails with Ruby Shoes

ruby & cryptography

supports the OpenSSL library

doesn’t address problems in OpenSSL

no support for HSMs

no pure Ruby OpenSSL library

RubyKaigi 2006 presentation

our 2007 RCE presentation

Page 43: Treading the Rails with Ruby Shoes

what follows...

...is a quick recap of last year

public key crypto with RSA

private key crypto with AES

serving hybrid keys on a network...

...and a BackgrounDRb client

because privacy needs good crypto!

Page 44: Treading the Rails with Ruby Shoes

Ruby crypto bootcamprequire 'openssl'require 'base64'include OpenSSL

class String def encode Base64.encode64(self) end

def decode Base64.decode64(self) endend

class RSAKey PATTERNS = { :key => /^key: / }

def initialize base_key = 768 # base_key is either a key in PEM format or a key size in bits @key = create(base_key) end

def create bits = 256 @key = PKey::RSA.new(bits) { print "." } puts end

def public_key @key.public_key.to_pem end

def private_key @key.to_pem end

def key= pem_data @key = create(pem_data) end

def encrypt plain_data @key.public_encrypt(plain_data) end

def decrypt encrypted_data @key.private_decrypt(encrypted_data) endend

class AESDataStore attr_reader :key, :encrypted_text, :initialisation_vector

def initialize key = nil, vector = nil @cipher = OpenSSL::Cipher::Cipher.new("aes-256-cbc") @encrypted_text = "" create_key(key, vector) end

def create_key key = nil, vector = nil @key = (key || OpenSSL::Random.random_bytes(256/8)) @initialisation_vector = (vector || @cipher.random_iv) end

def encrypt data @cipher.encrypt @encrypted_text = run_cipher(data) end

def decrypt @cipher.decrypt run_cipher(@encrypted_text) end

def export_key file_name = "aes-256-cbc.key" (key_file = File.new(file_name, "w")) << "key: #{@key}\n" end

private def run_cipher data @cipher.key = @key @cipher.iv = @initialisation_vector (cipher_text = @cipher.update(data)) << @cipher.final endend

key = RSAKey.newkey.createputs "plain text: #{text = 'something very secret'}"encrypted_text = key.encrypt(text).encodetext == key.decrypt(encrypted_text.decode)

encrypted_data_store = AESDataStore.newencrypted_data_store.create_keyencrypted_data_store. export_keyencrypted_text = encrypted_data_store.encrypt(text).encodetext == encrypted_data_store.decrypt(encrypted_text)

Page 45: Treading the Rails with Ruby Shoes

network key server <=> Railsrequire 'gserver'

class AESServer < GServer attr_reader :aes_keys, :acl, :server_key

def initialize host, port super port, host @server_key = RSAKey.new @aes_keys, @acl = {}, {} end

def create_key key_name @aes_keys[key_name] = AESDataStore.new end

def authorise host, key_name (@acl[key_name] ||= []) << host end

def serve client case client.get_line when "public" client.puts @server_key.public_key.encode when "aes" authorise_action(client, @acl) do |client, key_name| raise unless @aes_keys[key_name] client_key = RSAKey.new(client.get_line.decode) aes_key = @aes_keys[key_name].key client.puts client_key.encrypt(aes_key).encode end end end

private def authorise_action client, acl host = client.peeraddr[3] key_name = client.get_line if acl[key_name].include?(host) then yield client, key_name if block_given? end endend

server = AESServer.new("localhost", 3000)server.create_key "1"server.authorise "::1", "1"server.startserver.join

class HybridKeyServerWorker < BackgrounDRb::Worker::Base def do_work args @client_key ||= RSAKey.new logger.info('requesting keys from server') results.merge! { :request_time => Time.now, :completed => false } load_server_key args.each { |key_name| results[key_name] = get_aes_key(key_name) } results.merge! { :duration] = Time.now - results[:request_time], :completed => true } end

def load_server_key @server_key = nil connect do |server| send "public" results[:server_key] = RSAKey.new(@socket.get_line.decode)) end end

def get_aes_key key_name connect do |server| send_with_key "aes", name results[:socket].get_line.decode rescue nil end end

private def connect raise if results[:socket] begin results[:socket] = TCPSocket.new("localhost", 3000) value = yield(results[:socket]) if block_given? ensure results[:socket].close results[:socket] = nil end end

def send message, *params results[:socket].puts(message, *params) end

def send_with_key message, *params send message, *(params << @client_key.public_key.encode) endend

HybridKeyServerWorker.register

Page 46: Treading the Rails with Ruby Shoes

backgrounDRb?

long-running tasks for Rails via DRb

ticket-oriented architecture

submit task to server

receive ticket

poll for results

supported on UNIX but not Windows

Page 47: Treading the Rails with Ruby Shoes

that’s all well and good but...

where’s our Rails controller?

that’s a very good question

a very good homework question!

because we still have a lot more to cover

Page 48: Treading the Rails with Ruby Shoes

RESTful authentication

don’t complicate session creation

treat it as architectural

be kind to the transport layer

and remember to track user intent

RESTful interfaces rock!

Page 49: Treading the Rails with Ruby Shoes

it’s all in the intentclass ApplicationController < ActionController::Base helper :all

protect_from_forgery # :secret => 'c09358ad74a941856e34447df670931d' before_filter :load_session_data

private def load_session_data if session[:user] then @current_player = User.find(session[:user]) @user_name = @current_user.name logger.info "Session data loaded for #{@user_name}" else @current_user = nil @user_name = "#{request.remote_ip}" logger.info "request from #{@user_name}" end end

def save_intent session[:intent] = { :controller => controller_name, :action => action_name, :name => params[:name], :id => params[:id] } end

def load_intent session[:intent] end

def clear_intent session[:intent] = nil end

def with_intent? intent = session[:intent] intent[:action] || intent[:controller] || intent[:name] || intent[:id] end

def act_on_intent if with_intent? then target = load_intent clear_intent redirect_to target else redirect_to home_path end end

def authenticate unless @current_player save_intent redirect_to login_path and return end end

def authorise rules = { :role => :admin } logger.info "#{@user_name} accessing (#{controller_name}.#{action_name})" if @current_user then authorised = @current_user.check_authorisation if authorised then logger.info "--> AUTHORISATION SUCCEEDED" else logger.warn "--> AUTHORISATION FAILED" request.env["HTTP_REFERER"] ? (redirect_to :back) : (redirect_to home_path) end else authenticate end @is_admin = @current_user.authorised_as?(:admin) if authorised authorised endend

Page 50: Treading the Rails with Ruby Shoes

making session access RESTfulclass SessionController < ApplicationController def index render :action => 'create' end

def create if session[:user] then redirect_to home_path else if request.post? then logger.info "Login Request: #{params[:name]}" begin session[:user] = User.authenticate(params[:name], params[:password]).id rescue Exception => e login_failed else act_on_intent end else logger.warn "Login Request: #{params[:name]} via unsupported HTTP method" login_failed end end end

def destroy session[:user] = nil clear_intent redirect_to home_path end

private def login_failed logger.warn "--> login failed for user #{params[:name]} from (#{request.remote_ip})" flash[:error] = "please check your username and password" session[:user] = nil redirect_to login_path endend

login URI: app/session/create

logout URI: app/session/destroy

Page 51: Treading the Rails with Ruby Shoes

push me, pull you

Page 52: Treading the Rails with Ruby Shoes

the pull of the web

HTTP is request oriented

to get an update, you poll the server

more frequent requests mean

swifter updates

heavier server load

Denial of Service magnet

Page 53: Treading the Rails with Ruby Shoes

what’s the skinny on push?

publisher/subscriber model

lets the server set its own pace

one request can initiate many updates

reduces DoS risks

supports broadcast distribution

Page 54: Treading the Rails with Ruby Shoes

defining the server

many channels

many users

one channel broadcasts to many users

concurrent broadcasts

real-time latencies

Page 55: Treading the Rails with Ruby Shoes

a quick note on “real”-time

non-functionals matter

work with latency not against it

throttle demand, smooth supply

don’t just fail, fail-safe

rewrite early, rewrite often

don’t rely on tests - runtime is king

Page 56: Treading the Rails with Ruby Shoes

concurrency with threads

MRI 1.8 uses green threads

run in the interpreter process

scheduled by the interpreter

MRI 1.9 uses native threads

global interpreter lock!!!

JRuby et al. use native threads

Page 57: Treading the Rails with Ruby Shoes

concurrency with processes

forking processes is platform-specific

easy on UNIX

not available on Windows

Garbage Collector causes runtime cost

copies whole process memory space!!!

Page 58: Treading the Rails with Ruby Shoes

the EventMachine

Ruby networking extension library

fast & scalable C++

runs in a single thread

uses system calls to block on IO sockets

select on UNIX and Windows

epoll on Linux 2.6 kernels

Page 59: Treading the Rails with Ruby Shoes

got a basic push server?require 'rubygems'require 'eventmachine'

User = Struct.new(:host, :port)Request = Struct.new(:user, :command, :host, :port, :channel)

$users ||= {}$channels ||= {}

module Manager def receive_data data tokens = data.match(/^(\w*):?(\w*):?(\w*):?(\w*):?(.*)$/).captures request = Request.new(peer, *tokens) request.command.downcase! if authenticated?(request) then case request.command when 'subscribe' $users[request.user] = User.new(:host, :port) $channels[request.channel] &&= $users[request.user] when 'some other command' # do stuff end end end

def authenticated? request # rules for IP Address filtering, etc. endend

module Broadcaster def receive_data data if channel = $channels[data.strip] then channel.each { |user| UDPSocket.open.send(data, 0, user.host, user.port) } end endend

EventMachine::run do EventMachine::start_server "localhost", 9081, Manager EventMachine::start_server ARGV[0], ARGV[1], Broadcasterend