diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 3e15e1d..d6573fa 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -11,14 +11,15 @@ module NanoServe @port = port @block = block @thr = nil + @srv = nil end def start(y) - server = TCPServer.new(@port) + @srv = TCPServer.new(@port) @thr = Thread.new do Thread.abort_on_exception = true - conn = server.accept + conn = @srv.accept port, host = conn.peeraddr[1, 2] client = "#{host}:#{port}" logger.debug "#{client}: connected" @@ -27,8 +28,9 @@ module NanoServe @block.call(conn, y) rescue EOFError logger.debug "#{client}: disconnected" - ensure + else logger.debug "#{client}: closed" + ensure conn.close end end @@ -38,18 +40,22 @@ module NanoServe yield @thr.join - server.close y end def stop + @srv.close @thr.kill end def logger @logger ||= Logger.new(STDOUT).tap { |l| l.level = Logger::INFO } end + + def logger=(logger) + @logger = logger + end end class HTTPResponder < TCPResponder @@ -59,11 +65,18 @@ module NanoServe buf = +'' loop do line = conn.readline - break if line.chomp == '' req << line - buf << line + buf << line if logger.debug? + break if req.headers? end logger.debug "request:\n" + buf.gsub(/^/, ' ') + length = 0 + while req.content_length? && length < req.content_length + data = conn.readpartial(1024) + length += data.size + req << data + end + logger.debug "request body: #{length} bytes read" res = Response.new logger.debug 'calling' @@ -82,10 +95,47 @@ module NanoServe @http_version = nil @sep = nil @headers = {} + @body = +''.encode('ASCII-8BIT') + end + + def host + @headers['host'] + end + + def path + @uri.path + end + + def query_array + URI.decode_www_form(@uri.query || '') + end + + def form_array + form? ? URI.decode_www_form(body) : [] + end + + def query + Hash[*query_array.flatten] + end + + def form + Hash[*form_array.flatten] end def params - Hash[*@uri.query.split('&').map { |kv| kv.split('=') }.flatten] + query.merge(form) + end + + def form? + content_type == 'application/x-www-form-urlencoded' + end + + def body + @body + end + + def [](key) + @headers[key.downcase] end def <<(line) @@ -94,10 +144,28 @@ module NanoServe elsif @sep.nil? parse_header(line.chomp) else - @body << line + parse_body(line) end end + def headers? + @sep + end + + def content_length + @headers['content-length'].to_i + end + + def content_length? + @headers.key?('content-length') + end + + def content_type + @headers['content-type'] + end + + private + REQ_RE = %r{(?[A-Z]+)\s+(?\S+)\s+(?HTTP/\d+.\d+)$} def parse_request(str) @@ -111,7 +179,7 @@ module NanoServe end def parse_method(str) - str + str.upcase end def parse_path(str) @@ -123,13 +191,20 @@ module NanoServe end def parse_header(str) - (@sep = '' && return) if str == '' + if str == '' + @sep = true + return + end - unless (m = str.match(/(?
[A-Z][-A-Za-z]*):\s+(?.+)$/)) + unless (m = str.match(/(?
[A-Za-z][-A-Za-z]*):\s+(?.+)$/)) raise RequestError, "cannot parse header: '#{str}'" end - @headers[m[:header]] = m[:value] + @headers[m[:header].downcase] = m[:value] + end + + def parse_body(line) + @body << line end end @@ -151,7 +226,7 @@ module NanoServe end def body=(value) - @body = value.tap { @content_length = body.bytes.count.to_s } + @body = value.tap { @content_length = value.bytes.count.to_s } end def to_s @@ -181,7 +256,7 @@ module NanoServe end def content_length - @content_length ||= body.bytes.count.to_s + @content_length || '0' end def content_type diff --git a/nanoserve.gemspec b/nanoserve.gemspec index 32af8e4..81c28a2 100644 --- a/nanoserve.gemspec +++ b/nanoserve.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = 'nanoserve' - s.version = '0.1.0' + s.version = '0.3.0' s.licenses = ['3BSD'] s.summary = 'Listen to one-shot connections' s.authors = ['Loic Nageleisen'] diff --git a/test/test_nanoserve.rb b/test/test_nanoserve.rb index f5bf694..29dd919 100644 --- a/test/test_nanoserve.rb +++ b/test/test_nanoserve.rb @@ -24,10 +24,12 @@ class TestNanoServe < MiniTest::Test s.close end + r.stop + assert_equal(uuid, buf) end - def test_http_responder + def test_http_responder_get uuid = SecureRandom.uuid.encode('UTF-8') uri = URI('http://localhost:2000') @@ -50,6 +52,50 @@ class TestNanoServe < MiniTest::Test Net::HTTP.get(uri + "test?uuid=#{uuid}") end + r.stop + assert_equal(uuid, req.first.params['uuid']) end + + def test_http_responder_post + uuid = SecureRandom.uuid.encode('UTF-8') + uri = URI('http://localhost:2000') + + r = NanoServe::HTTPResponder.new(uri.host, uri.port) do |res, req, y| + y << req + + res.body = <<-EOS.gsub(/^ {8}/, '') + + + An Example Page + + + Hello World, this is a very simple HTML document. + + + EOS + end + + req = r.start([]) do + Net::HTTP.post_form( + uri + "test?uuid=#{uuid}&p=query", + 'p' => 'form', + 'f' => 'foo', + ) + end + + r.stop + + assert_equal(uuid, req.first.params['uuid']) + assert_equal(uuid, req.first.query['uuid']) + assert_nil(req.first.form['uuid']) + + assert_equal('foo', req.first.params['f']) + assert_nil(req.first.query['f']) + assert_equal('foo', req.first.form['f']) + + assert_equal('form', req.first.params['p']) + assert_equal('query', req.first.query['p']) + assert_equal('form', req.first.form['p']) + end end