treading the rails with ruby shoes
DESCRIPTION
RailsConf Europe 2008 presentation exploring Shoes, Asynchronous IP networking, hybrid cryptography and instant messaging in the context of the InterWeb.TRANSCRIPT
Treading the Rails with Ruby Shoes
push, pull and instant messaging
http://slides.games-with-brains.net/
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!
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
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
no one can afford a sense of perspective
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
gossip and confusion
Usenet
Telnet
FTP
MUDs
IRC
the medium is the message
Memex & Hyperlinks
Project Xanadu
Hypercard
Gopher
HTML
the diamond age
how we display the content matters
ActiveX
Java Applets & Flash
DHTML & ECMAScript
CSS
AJAX and Web 2.0
calling all stations: this is Rails
we now live in the application age
database + http = content retrieval
html + scripting = interface experience
retrieval + experience = application
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!
introducing 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!!!
working with shoes
built-in manual
console
exceptions
warn, info, debug
my first calculator
calculator model
numeric display
command buttons
keyboard handling
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
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
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
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
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
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
instant messaging
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
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
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
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
a babel of tongues
system notification services
UNIX talk
IRC
ICQ
SIMPLE: AIM-Yahoo-MSN
Jabber-XMPP
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
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
...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'/>
(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 }
modern creature comforts
presence
privacy
personalisation?
persistence?
Rails “does” Privacy(if you get our drift...)
how to be private on the web
connection & transport layer secrecy
custom crypto over HTTP
key exchange
encryption
user authentication
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
how it works in principle
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)---
public and hybrid key crypto
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
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!
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)
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
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
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
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!
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
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
push me, pull you
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
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
defining the server
many channels
many users
one channel broadcasts to many users
concurrent broadcasts
real-time latencies
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
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
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!!!
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
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