diff options
Diffstat (limited to 'lib/vendor/excon/tests/servers/good.rb')
-rwxr-xr-x | lib/vendor/excon/tests/servers/good.rb | 350 |
1 files changed, 350 insertions, 0 deletions
diff --git a/lib/vendor/excon/tests/servers/good.rb b/lib/vendor/excon/tests/servers/good.rb new file mode 100755 index 0000000..1cb6d7f --- /dev/null +++ b/lib/vendor/excon/tests/servers/good.rb @@ -0,0 +1,350 @@ +#!/usr/bin/env ruby + +require 'eventmachine' +require 'stringio' +require 'uri' +require 'zlib' + +module GoodServer + # This method will be called with each request received. + # + # request = { + # :method => method, + # :uri => URI.parse(uri), + # :headers => {}, + # :body => '' + # } + # + # Each connection to this server is persistent unless the client sends + # "Connection: close" in the request. If a response requires the connection + # to be closed, use `start_response(:persistent => false)`. + def send_response(request) + type, path = request[:uri].path.split('/', 3)[1, 2] + case type + when 'echo' + case path + when 'request' + data = Marshal.dump(request) + start_response + send_data "Content-Length: #{ data.size }\r\n" + send_data "\r\n" + send_data data + + when 'request_count' + (@request_count ||= '0').next! + start_response + send_data "Content-Length: #{ @request_count.size }\r\n" + send_data "Connection: Keep-Alive\r\n" + send_data "\r\n" + send_data @request_count + + when /(content|transfer)-encoded\/?(.*)/ + if (encoding_type = $1) == 'content' + accept_header = 'Accept-Encoding' + encoding_header = 'Content-Encoding' + else + accept_header = 'TE' + encoding_header = 'Transfer-Encoding' + end + chunked = $2 == 'chunked' + + encodings = parse_encodings(request[:headers][accept_header]) + while encoding = encodings.pop + break if ['gzip', 'deflate'].include?(encoding) + end + + case encoding + when 'gzip' + body = request[:body] + if(body.nil? || body.empty?) + body = '' + else + io = (Zlib::GzipWriter.new(StringIO.new) << request[:body]).finish + io.rewind + body = io.read + end + when 'deflate' + # drops the zlib header + deflator = Zlib::Deflate.new(nil, -Zlib::MAX_WBITS) + body = deflator.deflate(request[:body], Zlib::FINISH) + deflator.close + else + body = request[:body] + end + + # simulate server pre/post content encoding + encodings = [ + request[:headers]["#{ encoding_header }-Pre"], + encoding, + request[:headers]["#{ encoding_header }-Post"], + ] + if chunked && encoding_type == 'transfer' + encodings << 'chunked' + end + encodings = encodings.compact.join(', ') + + start_response + # let the test know what the server sent + send_data "#{ encoding_header }-Sent: #{ encodings }\r\n" + send_data "#{ encoding_header }: #{ encodings }\r\n" unless encodings.empty? + if chunked + if encoding_type == 'content' + send_data "Transfer-Encoding: chunked\r\n" + end + send_data "\r\n" + send_data chunks_for(body) + send_data "\r\n" + else + send_data "Content-Length: #{ body.size }\r\n" + send_data "\r\n" + send_data body + end + end + + when 'chunked' + case path + when 'simple' + start_response + send_data "Transfer-Encoding: chunked\r\n" + send_data "\r\n" + # chunk-extension is currently ignored. + # this works because "6; chunk-extension".to_i => "6" + send_data "6; chunk-extension\r\n" + send_data "hello \r\n" + send_data "5; chunk-extension\r\n" + send_data "world\r\n" + send_data "0; chunk-extension\r\n" # last-chunk + send_data "\r\n" + + # merged trailers also support continuations + when 'trailers' + start_response + send_data "Transfer-Encoding: chunked\r\n" + send_data "Test-Header: one, two\r\n" + send_data "\r\n" + send_data chunks_for('hello world') + send_data "Test-Header: three, four,\r\n" + send_data "\tfive, six\r\n" + send_data "\r\n" + end + + when 'content-length' + case path + when 'simple' + start_response + send_data "Content-Length: 11\r\n" + send_data "\r\n" + send_data "hello world" + end + + when 'unknown' + case path + when 'cookies' + start_response(:persistent => false) + send_data "Set-Cookie: one, two\r\n" + send_data "Set-Cookie: three, four\r\n" + send_data "\r\n" + send_data "hello world" + + when 'simple' + start_response(:persistent => false) + send_data "\r\n" + send_data "hello world" + + when 'header_continuation' + start_response(:persistent => false) + send_data "Test-Header: one, two\r\n" + send_data "Test-Header: three, four,\r\n" + send_data " five, six\r\n" + send_data "\r\n" + send_data "hello world" + end + + when 'bad' + # Excon will close these connections due to the errors. + case path + when 'malformed_header' + start_response + send_data "Bad-Header\r\n" # no ':' + send_data "\r\n" + send_data "hello world" + + when 'malformed_header_continuation' + send_data "HTTP/1.1 200 OK\r\n" + send_data " Bad-Header: one, two\r\n" # no previous header + send_data "\r\n" + send_data "hello world" + end + + when 'not-found' + start_response(:status => "404 Not Found") + send_data "Content-Length: 11\r\n" + send_data "\r\n" + send_data "hello world" + end + end + + # Sends response status-line, plus headers common to all responses. + def start_response(opts = {}) + opts = { + :status => '200 OK', + :persistent => @persistent # true unless client sent Connection: close + }.merge!(opts) + + @persistent = opts[:persistent] + send_data "HTTP/1.1 #{ opts[:status] }\r\n" + send_data "Connection: close\r\n" unless @persistent + end + + def post_init + @buffer = StringIO.new + @buffer.set_encoding('BINARY') if @buffer.respond_to?(:set_encoding) + end + + # Receives a String of +data+ sent from the client. + # +data+ may only be a portion of what the client sent. + # The data is buffered, then processed and removed from the buffer + # as data becomes available until the @request is complete. + def receive_data(data) + @buffer.seek(0, IO::SEEK_END) + @buffer.write(data) + + parse_headers unless @request + parse_body if @request + + if @request_complete + send_response(@request) + if @persistent + @request = nil + @request_complete = false + # process remaining buffer for next request + receive_data('') unless @buffer.eof? + else + close_connection(true) + end + end + end + + # Removes the processed portion of the buffer + # by replacing the buffer with it's contents from the current pos. + def sync_buffer + @buffer.string = @buffer.read + end + + def parse_headers + @buffer.rewind + # wait until buffer contains the end of the headers + if /\sHTTP\/\d+\.\d+\r\n.*?\r\n\r\n/m =~ @buffer.read + @buffer.rewind + # For persistent connections, the buffer could start with the + # \r\n chunked-message terminator from the previous request. + # This will discard anything up to the request-line. + until m = /^(\w+)\s(.*)\sHTTP\/\d+\.\d+$/.match(@buffer.readline.chop!); end + method, uri = m[1, 2] + + headers = {} + last_key = nil + until (line = @buffer.readline.chop!).empty? + if !line.lstrip!.nil? + headers[last_key] << ' ' << line.rstrip + else + key, value = line.split(':', 2) + headers[key] = ([headers[key]] << value.strip).compact.join(', ') + last_key = key + end + end + + sync_buffer + + @chunked = headers['Transfer-Encoding'] =~ /chunked/i + @content_length = headers['Content-Length'].to_i + @persistent = headers['Connection'] !~ /close/i + @request = { + :method => method, + :uri => URI.parse(uri), + :headers => headers, + :body => '' + } + end + end + + def parse_body + if @chunked + @buffer.rewind + until @request_complete || @buffer.eof? + unless @chunk_size + # in case buffer only contains a portion of the chunk-size line + if (line = @buffer.readline) =~ /\r\n\z/ + @chunk_size = line.to_i(16) + if @chunk_size > 0 + sync_buffer + else # last-chunk + @buffer.read(2) # the final \r\n may or may not be in the buffer + sync_buffer + @chunk_size = nil + @request_complete = true + end + end + end + if @chunk_size + if @buffer.size >= @chunk_size + 2 + @request[:body] << @buffer.read(@chunk_size + 2).chop! + @chunk_size = nil + sync_buffer + else + break # wait for more data + end + end + end + elsif @content_length > 0 + @buffer.rewind + unless @buffer.eof? # buffer only contained the headers + @request[:body] << @buffer.read(@content_length - @request[:body].size) + sync_buffer + if @request[:body].size == @content_length + @request_complete = true + end + end + else + # no body + @request_complete = true + end + end + + def chunks_for(str) + chunks = '' + str.force_encoding('BINARY') if str.respond_to?(:force_encoding) + chunk_size = str.size / 2 + until (chunk = str.slice!(0, chunk_size)).empty? + chunks << chunk.size.to_s(16) << "\r\n" + chunks << chunk << "\r\n" + end + chunks << "0\r\n" # last-chunk + end + + # only supports a single quality parameter for tokens + def parse_encodings(encodings) + return [] if encodings.nil? + split_header_value(encodings).map do |value| + token, q_val = /^(.*?)(?:;q=(.*))?$/.match(value.strip)[1, 2] + if q_val && q_val.to_f == 0 + nil + else + [token, (q_val || 1).to_f] + end + end.compact.sort_by {|_, q_val| q_val }.map {|token, _| token } + end + + # Splits a header value +str+ according to HTTP specification. + def split_header_value(str) + return [] if str.nil? + str.strip.scan(%r'\G((?:"(?:\\.|[^"])+?"|[^",]+)+) + (?:,\s*|\Z)'xn).flatten + end +end + +EM.run do + EM.start_server("127.0.0.1", 9292, GoodServer) + EM.start_server("::1", 9293, GoodServer) unless RUBY_PLATFORM == 'java' + $stderr.puts "ready" +end |