Skip to content
This repository has been archived by the owner on Dec 7, 2018. It is now read-only.

Commit

Permalink
Initial support for the full HTTP request cycle
Browse files Browse the repository at this point in the history
  • Loading branch information
tarcieri committed Feb 20, 2012
1 parent 98e7ce7 commit eccf9dd
Show file tree
Hide file tree
Showing 12 changed files with 184 additions and 16 deletions.
4 changes: 4 additions & 0 deletions .rspec
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
--color
--format documentation
--backtrace
--default_path spec
4 changes: 4 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
#!/usr/bin/env rake
require "bundler/gem_tasks"

Dir[File.expand_path("../tasks/**/*.rake", __FILE__)].each { |task| load task }

task :default => :spec
4 changes: 4 additions & 0 deletions lib/reel.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@

require 'reel/connection'
require 'reel/logger'
require 'reel/request'
require 'reel/request_parser'
require 'reel/response'
require 'reel/server'

# A Reel good HTTP server
module Reel
# The method given was not understood
class UnsupportedMethodError < ArgumentError; end
end
23 changes: 20 additions & 3 deletions lib/reel/connection.rb
Original file line number Diff line number Diff line change
@@ -1,15 +1,32 @@
module Reel
# A connection to the HTTP server
class Connection
attr_reader :request

# Attempt to read this much data
BUFFER_SIZE = 4096

def initialize(socket)
@socket = socket
@parser = RequestParser.new
@request = nil
end

def read_header
while data = @socket.readpartial(4096)
@parser << data
def read_request
return if @request

until @parser.headers
@parser << @socket.readpartial(BUFFER_SIZE)
end

@request = Request.new(@parser.http_method, @parser.url, @parser.http_version, @parser.headers)
end

def respond(response)
response.render(@socket)
ensure
# FIXME: Keep-Alive support
@socket.close
end
end
end
17 changes: 17 additions & 0 deletions lib/reel/request.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
module Reel
class Request
attr_accessor :method, :version, :url
METHODS = [:get, :post, :put, :delete, :trace, :options, :connect, :patch]

def initialize(method, url, version = "1.1", headers = {})
@method = method.to_s.downcase.to_sym
raise UnsupportedArgumentError, "unknown method: #{method}" unless METHODS.include? @method

@url, @version, @headers = url, version, headers
end

def [](header)
@headers[header]
end
end
end
33 changes: 29 additions & 4 deletions lib/reel/request_parser.rb
Original file line number Diff line number Diff line change
@@ -1,24 +1,49 @@
module Reel
# Parses incoming HTTP requests
class RequestParser
attr_reader :headers

def initialize
@parser = Http::Parser.new(self)
@headers = nil
@read_body = false
end

def add(data)
@parser << data
end
alias_method :<<, :add

def on_headers_complete(headers)
puts "Got headers: #{headers.inspect}"
def headers?
!!@headers
end

def http_method
@parser.http_method.downcase.to_sym
end

def http_version
@parser.http_version.join(".")
end

def url
@parser.request_url
end

#
# Http::Parser callbacks
#

def on_headers_complete(headers)
@headers = headers
end

def on_body(chunk)
puts "[BODY] #{chunk}"
# FIXME: handle request bodies
end

def on_message_complete
puts "DONE!"
@read_body = true
end
end
end
54 changes: 54 additions & 0 deletions lib/reel/response.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
module Reel
class Response
attr_reader :status

def initialize(status, body = '')
self.status = status
@body = body
@headers = {}

# hax
@version = "HTTP/1.1"
end

# Set the status
def set_status(status, reason = nil)
case status
when Integer
@status = status
@reason = reason
when Symbol
# hax!
if status == :ok
@status = 200
@reason = reason || "OK"
else
raise ArgumentError, "unrecognized status symbol :/"
end
else
raise ArgumentError, "invalid status: #{status}"
end
end
alias_method :status=, :set_status

# Write the response out to the wire
def render(socket)
socket << render_header
socket << @body
end

#######
private
#######

# Convert headers into a string
# FIXME: this should probably be factored elsewhere, SRP and all
def render_header
header = "#{@version} #{@status} #{@reason}\r\n"
header << @headers.map do |header, value|
"#{header}: #{value}"
end.join("\r\n")
header << "\r\n"
end
end
end
13 changes: 5 additions & 8 deletions lib/reel/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ class Server
include Celluloid::IO

def initialize(host, port, &callback)
# What looks at first glance to be a normal TCPServer is in fact an
# "evented" Celluloid::IO::TCPServer
# This is actually an evented Celluloid::IO::TCPServer
@server = TCPServer.new(host, port)
@callback = callback

run!
end

Expand All @@ -17,12 +15,11 @@ def run

def handle_connection(socket)
connection = Connection.new(socket)
connection.read_header
@callback.(connection)
connection.read_request
@callback[connection]
rescue EOFError
# Client disconnected prematurely
ensure
socket.close
# Client disconnected prematurely
# FIXME: should probably do something here
end
end
end
2 changes: 1 addition & 1 deletion reel.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@ Gem::Specification.new do |gem|
gem.add_dependency 'http_parser.rb'

gem.add_development_dependency 'rake'
gem.add_development_dependency 'rspec', '~> 2.7.0'
gem.add_development_dependency 'rspec'
end
26 changes: 26 additions & 0 deletions spec/reel/server_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
require 'spec_helper'
require 'net/http'

describe Reel::Server do
let(:endpoint) { "http://#{example_addr}:#{example_port}#{example_url}" }
let(:response_body) { "ohai thar" }

it "receives HTTP requests and sends responses" do
handler_called = false
handler = proc do |connection|
request = connection.request
request.method.should eq :get
request.version.should eq "1.1"
request.url.should eq example_url

connection.respond Reel::Response.new(:ok, response_body)
handler_called = true
end

with_reel(handler) do
response = Net::HTTP.get URI(endpoint)
response.should eq response_body
handler_called.should be_true
end
end
end
13 changes: 13 additions & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
require 'bundler/setup'
require 'reel'

def example_addr; '127.0.0.1'; end
def example_port; 1234; end
def example_url; "/example"; end

def with_reel(handler)
server = Reel::Server.new(example_addr, example_port, &handler)
yield server
ensure
server.terminate
end
7 changes: 7 additions & 0 deletions tasks/rspec.rake
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require 'rspec/core/rake_task'

RSpec::Core::RakeTask.new

RSpec::Core::RakeTask.new(:rcov) do |task|
task.rcov = true
end

0 comments on commit eccf9dd

Please sign in to comment.