ruby/test/net/http/utils.rb
Hiroshi SHIBATA 82e3312493 [ruby/net-http] Fixed test case for default content-type.
I changed content-type of request to "application/octet-stream" if request didn't have
content-type.

fc5870d2ac
2025-06-11 03:35:12 +00:00

360 lines
9.2 KiB
Ruby

# frozen_string_literal: false
require 'socket'
require 'openssl'
module TestNetHTTPUtils
class Forbidden < StandardError; end
class HTTPServer
def initialize(config, &block)
@config = config
@server = TCPServer.new(@config['host'], 0)
@port = @server.addr[1]
@procs = {}
if @config['ssl_enable']
context = OpenSSL::SSL::SSLContext.new
context.cert = @config['ssl_certificate']
context.key = @config['ssl_private_key']
context.tmp_dh_callback = @config['ssl_tmp_dh_callback']
@ssl_server = OpenSSL::SSL::SSLServer.new(@server, context)
end
@block = block
end
def start
@thread = Thread.new do
loop do
socket = (@ssl_server || @server).accept
run(socket)
rescue
ensure
socket&.close
end
ensure
(@ssl_server || @server).close
end
end
def run(socket)
handle_request(socket)
end
def shutdown
@thread&.kill
@thread&.join
end
def mount(path, proc)
@procs[path] = proc
end
def mount_proc(path, &block)
mount(path, block.to_proc)
end
def handle_request(socket)
request_line = socket.gets
return if request_line.nil? || request_line.strip.empty?
method, path, _version = request_line.split
headers = {}
while (line = socket.gets)
break if line.strip.empty?
key, value = line.split(': ', 2)
headers[key] = value.strip
end
if headers['Expect'] == '100-continue'
socket.write "HTTP/1.1 100 Continue\r\n\r\n"
end
# Set default Content-Type if not provided
if !headers['Content-Type'] && (method == 'POST' || method == 'PUT' || method == 'PATCH')
headers['Content-Type'] = 'application/octet-stream'
end
req = Request.new(method, path, headers, socket)
if @procs.key?(req.path) || @procs.key?("#{req.path}/")
proc = @procs[req.path] || @procs["#{req.path}/"]
res = Response.new(socket)
begin
proc.call(req, res)
rescue Forbidden
res.status = 403
end
res.finish
else
@block.call(method, path, headers, socket)
end
end
def port
@port
end
class Request
attr_reader :method, :path, :headers, :query, :body
def initialize(method, path, headers, socket)
@method = method
@path, @query = parse_path_and_query(path)
@headers = headers
@socket = socket
if method == 'POST' && (@path == '/continue' || @headers['Content-Type'].include?('multipart/form-data'))
if @headers['Transfer-Encoding'] == 'chunked'
@body = read_chunked_body
else
@body = read_body
end
@query = @body.split('&').each_with_object({}) do |pair, hash|
key, value = pair.split('=')
hash[key] = value
end if @body && @body.include?('=')
end
end
def [](key)
@headers[key.downcase]
end
def []=(key, value)
@headers[key.downcase] = value
end
def continue
@socket.write "HTTP\/1.1 100 continue\r\n\r\n"
end
def remote_ip
@socket.peeraddr[3]
end
def peeraddr
@socket.peeraddr
end
private
def parse_path_and_query(path)
path, query_string = path.split('?', 2)
query = {}
if query_string
query_string.split('&').each do |pair|
key, value = pair.split('=', 2)
query[key] = value
end
end
[path, query]
end
def read_body
content_length = @headers['Content-Length']&.to_i
return unless content_length && content_length > 0
@socket.read(content_length)
end
def read_chunked_body
body = ""
while (chunk_size = @socket.gets.strip.to_i(16)) > 0
body << @socket.read(chunk_size)
@socket.read(2) # read \r\n after each chunk
end
body
end
end
class Response
attr_accessor :body, :headers, :status, :chunked, :cookies
def initialize(client)
@client = client
@body = ""
@headers = {}
@status = 200
@chunked = false
@cookies = []
end
def [](key)
@headers[key.downcase]
end
def []=(key, value)
@headers[key.downcase] = value
end
def write_chunk(chunk)
return unless @chunked
@client.write("#{chunk.bytesize.to_s(16)}\r\n")
@client.write("#{chunk}\r\n")
end
def finish
@client.write build_response_headers
if @chunked
write_chunk(@body)
@client.write "0\r\n\r\n"
else
@client.write @body
end
end
private
def build_response_headers
response = "HTTP/1.1 #{@status} #{status_message(@status)}\r\n"
if @chunked
@headers['Transfer-Encoding'] = 'chunked'
else
@headers['Content-Length'] = @body.bytesize.to_s
end
@headers.each do |key, value|
response << "#{key}: #{value}\r\n"
end
@cookies.each do |cookie|
response << "Set-Cookie: #{cookie}\r\n"
end
response << "\r\n"
response
end
def status_message(code)
case code
when 200 then 'OK'
when 301 then 'Moved Permanently'
when 403 then 'Forbidden'
else 'Unknown'
end
end
end
end
def start(&block)
new().start(&block)
end
def new
klass = Net::HTTP::Proxy(config('proxy_host'), config('proxy_port'))
http = klass.new(config('host'), config('port'))
http.set_debug_output logfile
http
end
def config(key)
@config ||= self.class::CONFIG
@config[key]
end
def logfile
$stderr if $DEBUG
end
def setup
spawn_server
end
def teardown
sleep 0.5 if @config['ssl_enable']
if @server
@server.shutdown
end
@log_tester.call(@log) if @log_tester
Net::HTTP.version_1_2
end
def spawn_server
@log = []
@log_tester = lambda {|log| assert_equal([], log) }
@config = self.class::CONFIG
@server = HTTPServer.new(@config) do |method, path, headers, socket|
@log << "DEBUG accept: #{@config['host']}:#{socket.addr[1]}" if @logger_level == :debug
case method
when 'HEAD'
handle_head(path, headers, socket)
when 'GET'
handle_get(path, headers, socket)
when 'POST'
handle_post(path, headers, socket)
when 'PATCH'
handle_patch(path, headers, socket)
else
socket.print "HTTP/1.1 405 Method Not Allowed\r\nContent-Length: 0\r\n\r\n"
end
end
@server.start
@config['port'] = @server.port
end
def handle_head(path, headers, socket)
if headers['Accept'] != '*/*'
content_type = headers['Accept']
else
content_type = $test_net_http_data_type
end
response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{$test_net_http_data.bytesize}"
socket.print(response)
end
def handle_get(path, headers, socket)
if headers['Accept'] != '*/*'
content_type = headers['Accept']
else
content_type = $test_net_http_data_type
end
response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{$test_net_http_data.bytesize}\r\n\r\n#{$test_net_http_data}"
socket.print(response)
end
def handle_post(path, headers, socket)
body = socket.read(headers['Content-Length'].to_i)
scheme = headers['X-Request-Scheme'] || 'http'
host = @config['host']
port = socket.addr[1]
content_type = headers['Content-Type'] || 'application/octet-stream'
charset = parse_content_type(content_type)[1]
path = "#{scheme}://#{host}:#{port}#{path}"
path = path.encode(charset) if charset
response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\nX-request-uri: #{path}\r\n\r\n#{body}"
socket.print(response)
end
def handle_patch(path, headers, socket)
body = socket.read(headers['Content-Length'].to_i)
content_type = headers['Content-Type'] || 'application/octet-stream'
response = "HTTP/1.1 200 OK\r\nContent-Type: #{content_type}\r\nContent-Length: #{body.bytesize}\r\n\r\n#{body}"
socket.print(response)
end
def parse_content_type(content_type)
return [nil, nil] unless content_type
type, *params = content_type.split(';').map(&:strip)
charset = params.find { |param| param.start_with?('charset=') }
charset = charset.split('=', 2).last if charset
[type, charset]
end
$test_net_http = nil
$test_net_http_data = (0...256).to_a.map { |i| i.chr }.join('') * 64
$test_net_http_data.force_encoding("ASCII-8BIT")
$test_net_http_data_type = 'application/octet-stream'
def self.clean_http_proxy_env
orig = {
'http_proxy' => ENV['http_proxy'],
'http_proxy_user' => ENV['http_proxy_user'],
'http_proxy_pass' => ENV['http_proxy_pass'],
'no_proxy' => ENV['no_proxy'],
}
orig.each_key do |key|
ENV.delete key
end
yield
ensure
orig.each do |key, value|
ENV[key] = value
end
end
end