Permalink
Browse files

Initial support for the full HTTP request cycle

  • Loading branch information...
1 parent 98e7ce7 commit eccf9ddf6d437bdcbbbf118c4ce9e6717632787e @tarcieri tarcieri committed Feb 20, 2012
Showing with 184 additions and 16 deletions.
  1. +4 −0 .rspec
  2. +4 −0 Rakefile
  3. +4 −0 lib/reel.rb
  4. +20 −3 lib/reel/connection.rb
  5. +17 −0 lib/reel/request.rb
  6. +29 −4 lib/reel/request_parser.rb
  7. +54 −0 lib/reel/response.rb
  8. +5 −8 lib/reel/server.rb
  9. +1 −1 reel.gemspec
  10. +26 −0 spec/reel/server_spec.rb
  11. +13 −0 spec/spec_helper.rb
  12. +7 −0 tasks/rspec.rake
View
4 .rspec
@@ -0,0 +1,4 @@
+--color
+--format documentation
+--backtrace
+--default_path spec
View
@@ -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
View
@@ -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
View
@@ -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
View
@@ -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
View
@@ -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
View
@@ -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
View
@@ -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
@@ -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
View
@@ -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
View
@@ -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
View
@@ -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
View
@@ -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.