From eea95887424d1efe476a1dbf1985780b1d363aab Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 13:47:44 +0200 Subject: [PATCH 01/17] Accept lowercase headers --- lib/nanoserve.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 3e15e1d..c464766 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -125,7 +125,7 @@ module NanoServe def parse_header(str) (@sep = '' && return) if str == '' - 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 From 53988be5ee922fff61dca22d80eb506e3188a363 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 13:48:45 +0200 Subject: [PATCH 02/17] Close listening socket outside of start --- lib/nanoserve.rb | 7 ++++--- test/test_nanoserve.rb | 4 ++++ 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index c464766..cbc098b 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" @@ -38,12 +39,12 @@ module NanoServe yield @thr.join - server.close y end def stop + @srv.close @thr.kill end diff --git a/test/test_nanoserve.rb b/test/test_nanoserve.rb index f5bf694..9bbded2 100644 --- a/test/test_nanoserve.rb +++ b/test/test_nanoserve.rb @@ -24,6 +24,8 @@ class TestNanoServe < MiniTest::Test s.close end + r.stop + assert_equal(uuid, buf) end @@ -50,6 +52,8 @@ class TestNanoServe < MiniTest::Test Net::HTTP.get(uri + "test?uuid=#{uuid}") end + r.stop + assert_equal(uuid, req.first.params['uuid']) end end From 324a05291399a832e10f6e26bfa4f773eb31b1bc Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 13:49:16 +0200 Subject: [PATCH 03/17] Silence double log on client disconnect --- lib/nanoserve.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index cbc098b..1d746bc 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -28,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 From f70c28abd9ef902704f942fac316b35c2c46733d Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 13:49:28 +0200 Subject: [PATCH 04/17] Allow overridable logger --- lib/nanoserve.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 1d746bc..a09058c 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -52,6 +52,10 @@ module NanoServe def logger @logger ||= Logger.new(STDOUT).tap { |l| l.level = Logger::INFO } end + + def logger=(logger) + @logger = logger + end end class HTTPResponder < TCPResponder From c3a69ac1edeb5c5c4c7be4d63517691d3102184a Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 13:57:01 +0200 Subject: [PATCH 05/17] Return after assign in guard clause --- lib/nanoserve.rb | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index a09058c..ba2bedc 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -129,7 +129,10 @@ module NanoServe end def parse_header(str) - (@sep = '' && return) if str == '' + if str == '' + @sep = true + return + end unless (m = str.match(/(?
[A-Za-z][-A-Za-z]*):\s+(?.+)$/)) raise RequestError, "cannot parse header: '#{str}'" From 803d910a3e76f116d933ae6c9aba6ec40151ae83 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 13:57:34 +0200 Subject: [PATCH 06/17] Accumulate in buffer only for debug output --- lib/nanoserve.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index ba2bedc..57bb2ba 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -67,7 +67,7 @@ module NanoServe line = conn.readline break if line.chomp == '' req << line - buf << line + buf << line if logger.debug? end logger.debug "request:\n" + buf.gsub(/^/, ' ') From df5525854e89f9da2fe3fdf1fb9a5b34ae49ba10 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 13:59:29 +0200 Subject: [PATCH 07/17] Factor end of headers processing in --- lib/nanoserve.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 57bb2ba..5410521 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -65,9 +65,9 @@ module NanoServe buf = +'' loop do line = conn.readline - break if line.chomp == '' req << line buf << line if logger.debug? + break if req.headers? end logger.debug "request:\n" + buf.gsub(/^/, ' ') @@ -104,6 +104,10 @@ module NanoServe end end + def headers? + @sep + end + REQ_RE = %r{(?[A-Z]+)\s+(?\S+)\s+(?HTTP/\d+.\d+)$} def parse_request(str) From 5c1717d8ee5fb99d2b083287c77fb056752caa3d Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 14:01:00 +0200 Subject: [PATCH 08/17] Privatise parse methods --- lib/nanoserve.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 5410521..a6b1bd6 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -108,6 +108,8 @@ module NanoServe @sep end + private + REQ_RE = %r{(?[A-Z]+)\s+(?\S+)\s+(?HTTP/\d+.\d+)$} def parse_request(str) From f3aebdcf0f805c5bc3fc38ddaed19ebc8510ec92 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 14:02:57 +0200 Subject: [PATCH 09/17] Normalize header keys to lowercase --- lib/nanoserve.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index a6b1bd6..0fdf317 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -144,7 +144,7 @@ module NanoServe raise RequestError, "cannot parse header: '#{str}'" end - @headers[m[:header]] = m[:value] + @headers[m[:header].downcase] = m[:value] end end From 88d0824dc8910a7007a3a90177774380d4a752f6 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 14:06:07 +0200 Subject: [PATCH 10/17] Handle request body according to Content-Length --- lib/nanoserve.rb | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 0fdf317..3ed9bfa 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -70,6 +70,13 @@ module NanoServe 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' @@ -88,6 +95,7 @@ module NanoServe @http_version = nil @sep = nil @headers = {} + @body = +''.encode('ASCII-8BIT') end def params @@ -100,7 +108,7 @@ module NanoServe elsif @sep.nil? parse_header(line.chomp) else - @body << line + parse_body(line) end end @@ -108,6 +116,14 @@ module NanoServe @sep end + def content_length + @headers['content-length'].to_i + end + + def content_length? + @headers.key?('content-length') + end + private REQ_RE = %r{(?[A-Z]+)\s+(?\S+)\s+(?HTTP/\d+.\d+)$} @@ -146,6 +162,10 @@ module NanoServe @headers[m[:header].downcase] = m[:value] end + + def parse_body(line) + @body << line + end end class Response From ea6406f4b8da40e669076d24cbba090bc77cd9bf Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 14:09:47 +0200 Subject: [PATCH 11/17] Compute response's Content-Length once --- lib/nanoserve.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 3ed9bfa..0b761eb 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -186,7 +186,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 @@ -216,7 +216,7 @@ module NanoServe end def content_length - @content_length ||= body.bytes.count.to_s + @content_length || '0' end def content_type From 8f20ccdd60a0315fb5c11d66e02f33f7e416abb0 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 14:12:00 +0200 Subject: [PATCH 12/17] Release v0.2.0 --- nanoserve.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nanoserve.gemspec b/nanoserve.gemspec index 32af8e4..04d67f1 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.2.0' s.licenses = ['3BSD'] s.summary = 'Listen to one-shot connections' s.authors = ['Loic Nageleisen'] From dc8739c69228de86006cae6e60a34173cd02a369 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 15:34:02 +0200 Subject: [PATCH 13/17] Normalize request method verb --- lib/nanoserve.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 0b761eb..29c32dc 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -139,7 +139,7 @@ module NanoServe end def parse_method(str) - str + str.upcase end def parse_path(str) From dd5001cb7c32bcbda4b5e083c73fc74aa264b36b Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 15:35:55 +0200 Subject: [PATCH 14/17] Implement request body and normalized headers access --- lib/nanoserve.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 29c32dc..5ab4559 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -102,6 +102,14 @@ module NanoServe Hash[*@uri.query.split('&').map { |kv| kv.split('=') }.flatten] end + def body + @body + end + + def [](key) + @headers[key.downcase] + end + def <<(line) if @method.nil? parse_request(line.chomp) From 66a2a206634a496e3bba4173df97624a9fbc92f9 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 15:36:16 +0200 Subject: [PATCH 15/17] Implement request host, path and content_type access --- lib/nanoserve.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 5ab4559..36b3dbc 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -98,6 +98,14 @@ module NanoServe @body = +''.encode('ASCII-8BIT') end + def host + @headers['host'] + end + + def path + @uri.path + end + def params Hash[*@uri.query.split('&').map { |kv| kv.split('=') }.flatten] end @@ -132,6 +140,10 @@ module NanoServe @headers.key?('content-length') end + def content_type + @headers['content-type'] + end + private REQ_RE = %r{(?[A-Z]+)\s+(?\S+)\s+(?HTTP/\d+.\d+)$} From 8a5c956a2431ebcb51b51a0c22535137e123b518 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 15:46:02 +0200 Subject: [PATCH 16/17] Implement form-urlencoded support Query and form return a Hash, folding duplicates with last-key-wins strategy. *_array methods provide the seldom-used duplicate-preserving counterparts. Keep query and form separate, but provide params, which handles both, with form having precedence over query. --- lib/nanoserve.rb | 22 ++++++++++++++++++++- test/test_nanoserve.rb | 44 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/lib/nanoserve.rb b/lib/nanoserve.rb index 36b3dbc..d6573fa 100644 --- a/lib/nanoserve.rb +++ b/lib/nanoserve.rb @@ -106,8 +106,28 @@ module NanoServe @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 diff --git a/test/test_nanoserve.rb b/test/test_nanoserve.rb index 9bbded2..29dd919 100644 --- a/test/test_nanoserve.rb +++ b/test/test_nanoserve.rb @@ -29,7 +29,7 @@ class TestNanoServe < MiniTest::Test 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') @@ -56,4 +56,46 @@ class TestNanoServe < MiniTest::Test 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 From 2edc861a06538d5703a480744f20c8f201722c18 Mon Sep 17 00:00:00 2001 From: Loic Nageleisen Date: Tue, 19 Sep 2017 15:47:44 +0200 Subject: [PATCH 17/17] Release v0.3.0 --- nanoserve.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nanoserve.gemspec b/nanoserve.gemspec index 04d67f1..81c28a2 100644 --- a/nanoserve.gemspec +++ b/nanoserve.gemspec @@ -2,7 +2,7 @@ Gem::Specification.new do |s| s.name = 'nanoserve' - s.version = '0.2.0' + s.version = '0.3.0' s.licenses = ['3BSD'] s.summary = 'Listen to one-shot connections' s.authors = ['Loic Nageleisen']