Networking with Ruby

As part of a networks course, we had to write 2 programs that would talk to one another over a TCP connection. The first program was the Broadcaster. It has to communicate with the client browser, serving a couple of HTML files and receiving the responses accordingly. The second program was the Bank. It has to receive a connection from the Broadcaster and maintains the client’s credit details. Once the client makes a payment on the Broadcaster’s website, we would have to check if the bank has the client’s records. And if it’s available with enough available credit, we make the deduction and send the client the files that were requested.

We could use any programming language that we wanted. I took this opportunity to use Ruby. It was my first time writing a networked application in Ruby and I had to look up how sockets in Ruby worked. We couldn’t make use of any third party gems.

Broadcaster write up

We were provided with HTML files to serve to our client’s browser and I had to open a session with the browser, read the HTML file, send it across the network with the appropriate HTTP headers. It was a pretty interesting experience. With each response from the client’s browser, I had to perform different actions. Opening a new connection to the bank was interesting as I had to write my own messaging format that could be easily parsed by the Bank.

Parsing the messages from the client’s browser and the Bank was made easy with regular expressions.

Bank write up

Reading the messaging format was quite easy as I built the format around how the data was presented. With each connection and message, I made use of Ruby’s regular expressions to match the different requests.

Broadcaster implementation

#!/usr/bin/ruby -w

require 'socket'

unless ARGV.length == 3
   $stderr.puts "Usage: #{$0} BROADCASTER_PORT BANK_IP BANK_PORT"
   exit 1
end

broadcasterPort = ARGV[0].to_i
bankIP = ARGV[1]
bankPort = ARGV[2].to_i

puts "BROADCAST_PORT: #{broadcasterPort}"
puts "BANK_IP: #{bankIP}"
puts "BANK_PORT: #{bankPort}"

if broadcasterPort < 1024
   $stderr.puts "Port number should not be less than 1024"
   exit 1
end

server = TCPServer.new(broadcasterPort)

count = 0
prices = Hash.new
File.open("price.txt", 'r').each_line do |line|
   count += 1
   next if count == 1
   show, price = line.chomp.split("\t")
   show = show.gsub(/\s/,"")
   prices.store(show,price)
end
puts "----- Initial Hash -----"
prices.each do |key,data|
   puts "#{key}:#{data}"
end
puts "------------------------"

puts "Starting up Broadcaster..."

def Print200 session, f
   session.print "HTTP/1.0 200 OK\nContent-Type: text/html\n\n"
   PrintOut session, f
end

def Print404 session, f
   session.print "HTTP/1.0 404 FILE NOT FOUND\nContent-Type: text/html\n\n"
   PrintOut session, f
end

def PrintOut session, f
   f.each do |line|
      session.print "#{line}\n"
   end
end

while session = server.accept
   puts "Connection from #{session.peeraddr[2]} at #{session.peeraddr[3]}"
   request = ""
   recvLength = 1024
   while (tmp = session.recv(recvLength))
      request += tmp
      break if tmp.length < recvLength
   end
   headerField = Hash.new
   if request.match(/^(GET|POST) (.+) HTTP\/1.[01]\s*$/)
      method, url = request.split(" ")
      if method.match(/GET/)
         url = url.gsub("/", "./")
         file = "#{url}"
         if File.directory?(file)
            file = "index.html"
         end
         if file.match(/.html/)
            if File.exist?(file)
               f = File.open(file,'r')
               Print200 session, f
               f.close
            else
               f = File.open("404.html",'r')
               Print404 session, f
               f.close
            end
         elsif file.match(/downloads.zip/)
            if File.exist?(file)
               session.print "HTTP/1.0 200 OK\nContent-Type: application/zip\n\n"
               data = IO.read("downloads.zip")
               session.print data
               system("rm downloads.zip")
            end
         else
            f = File.open("404.html",'r')
            Print404 session, f
            f.close
         end
      elsif method.match(/POST/)
         totalPrice = 0
         parameters = ""
         params = Hash.new
         parameters = request.match(/((\w)+=(\d)+&)+.* /).to_s
         parameters = parameters.gsub("&","\n")
         parameters.each_line do |line|
            key, data = line.chomp.split("=")
            params.store(key,data)
         end
         if params['service'] == "CD"
            prices.each_key do |key|
               num = params["#{key}"].to_i
               priceOf = prices["#{key}"].to_i
               totalPrice += num.abs * priceOf
            end
            totalPrice += 10
         else
            prices.each_key do |key|
               num = params["#{key}"].to_i
               priceOf = prices["#{key}"].to_i
               totalPrice += num * priceOf
            end
         end
         puts "Connecting to Bank"
         client = TCPSocket.new("#{bankIP}", bankPort)
         client.puts "Broadcaster Connected..."
         puts client.gets
         puts "PURCHASE #{params["userfirst"]}-#{params["userfamily"]}-#{params["cardno"]} #{totalPrice}"
         client.puts "PURCHASE #{params["userfirst"]}-#{params["userfamily"]}-#{params["cardno"]} #{totalPrice}"
         response = client.gets
         puts response
         client.close
         puts "Bank Connection Closed."
         if response.match(/BANK (\d+) ([ \w]+)/)
            errorCode, errorMsg = response.chomp.gsub("BANK ","").split(" ")
            errorCode = errorCode.to_i
            if errorCode == 0
               f = File.open("insufficient.html",'r')
               Print200 session, f
               f.close
            end
            if errorCode == 2
               f = File.open("nouser.html",'r')
               Print200 session, f
               f.close
            end
            if errorCode == 1
               if params['service'] == "CD"
                  f = File.open("CD-Approved.html",'r')
                  Print200 session, f
                  f.close
               else
                  params.each do |key,data|
                     if prices.has_key?("#{key}")
                        if data.to_i >= 1
                           system("zip downloads.zip shows/#{key.downcase}.txt")
                        end
                     end
                  end
                  f = File.open("approved.html", 'r')
                  Print200 session, f
                  f.close
               end
            end
         else
            session.print  "HTTP/1.0 400 BAD REQUEST\nContent-Type: text/plain\n\nBAD REQUEST\n"
         end
      end
   end
   session.close
   puts "Session Closed"
end
server.close

Bank implementation

#!/usr/bin/ruby -w

require 'socket'

unless ARGV.length == 1
   $stderr.puts "Usage: #{$0} BANK_PORT"
   exit 1
end

port = ARGV[0].to_i
puts "BANK_PORT: #{port}"

if port < 1024
   $stderr.puts "Port number should not be less than 1024"
   exit 1
end

fileName = "credit.txt"
count = 0
accountBalance = Hash.new
File.open(fileName, 'r').each_line do |line|
   count += 1
   next if count == 1
   firstName, lastName, cardNumber, availableCredit = line.chomp.split("\t")
   accountBalance.store "#{firstName}-#{lastName}-#{cardNumber}", availableCredit
end

puts "----- Initial Hash -----"
accountBalance.each do |key,data|
   print "#{key} #{data}\n"
end
puts "------------------------"

def UpdateBalance user, price, fileName
   firstName, lastName, cardNumber = user.split("-")
   f = File.open fileName, 'r'
   w = File.open fileName, 'r+'
   puts "----- #{fileName} -----"
   f.each_line do |line|
      if line.match /#{firstName}\t#{lastName}\t#{cardNumber}/
         firstName, lastName, cardNumber, balance = line.chomp.split("\t")
         balance = balance.to_i
         balance -= price.to_i
         puts "#{firstName}\t#{lastName}\t#{cardNumber}\t#{balance}"
         w.write "#{firstName}\t#{lastName}\t#{cardNumber}\t#{balance}\n"
      else
         puts "#{line}"
         w.write "#{line}"
      end
   end
   puts "------- UPDATED --------"
   f.close
   w.close
end

server = TCPServer.new port
puts "Starting up Bank..."

while session = server.accept
   puts "Connection from #{session.peeraddr[2]} at #{session.peeraddr[3]}"
   session.puts "Bank Connected..."
   request = session.gets
   puts request
   request = session.gets
   puts request
   if request.match(/^PURCHASE (.+) (\d+)$/)
      user, price = request.gsub("PURCHASE ","").split(" ")
      puts price
      if accountBalance.has_key?("#{user}")
         if price.to_i > accountBalance[user].to_i
            session.puts "BANK 0 INSUFFICIENT FUNDS"
            puts "BANK 0 INSUFFICIENT FUNDS"
         else
            session.puts "BANK 1 PURCHASE APPROVED"
            puts "BANK 1 PURCHASE APPROVED"
            UpdateBalance user, price, fileName
         end
      else
         session.puts "BANK 2 NO RECORD"
         puts "BANK 2 NO RECORD"
      end
   end
   session.close
   puts "Broadcaster Connection Closed."
end
server.close

Stanley Tan
@stnly