itsi 0.1.8 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Cargo.lock +11 -2
- data/Rakefile +6 -2
- data/crates/itsi_rb_helpers/src/lib.rs +27 -4
- data/crates/itsi_server/Cargo.toml +4 -1
- data/crates/itsi_server/src/lib.rs +74 -1
- data/crates/itsi_server/src/request/itsi_request.rs +32 -11
- data/crates/itsi_server/src/response/itsi_response.rs +14 -4
- data/crates/itsi_server/src/server/bind.rs +16 -12
- data/crates/itsi_server/src/server/itsi_server.rs +146 -95
- data/crates/itsi_server/src/server/listener.rs +10 -10
- data/crates/itsi_server/src/server/process_worker.rs +10 -3
- data/crates/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
- data/crates/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
- data/crates/itsi_server/src/server/signal.rs +4 -0
- data/crates/itsi_server/src/server/thread_worker.rs +55 -24
- data/crates/itsi_server/src/server/tls.rs +11 -8
- data/crates/itsi_tracing/src/lib.rs +18 -1
- data/gems/scheduler/Cargo.lock +12 -12
- data/gems/scheduler/ext/itsi_rb_helpers/src/lib.rs +27 -4
- data/gems/scheduler/ext/itsi_server/Cargo.toml +4 -1
- data/gems/scheduler/ext/itsi_server/src/lib.rs +74 -1
- data/gems/scheduler/ext/itsi_server/src/request/itsi_request.rs +32 -11
- data/gems/scheduler/ext/itsi_server/src/response/itsi_response.rs +14 -4
- data/gems/scheduler/ext/itsi_server/src/server/bind.rs +16 -12
- data/gems/scheduler/ext/itsi_server/src/server/itsi_server.rs +146 -95
- data/gems/scheduler/ext/itsi_server/src/server/listener.rs +10 -10
- data/gems/scheduler/ext/itsi_server/src/server/process_worker.rs +10 -3
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
- data/gems/scheduler/ext/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
- data/gems/scheduler/ext/itsi_server/src/server/signal.rs +4 -0
- data/gems/scheduler/ext/itsi_server/src/server/thread_worker.rs +55 -24
- data/gems/scheduler/ext/itsi_server/src/server/tls.rs +11 -8
- data/gems/scheduler/ext/itsi_tracing/src/lib.rs +18 -1
- data/gems/scheduler/lib/itsi/scheduler/version.rb +1 -1
- data/gems/scheduler/test/test_address_resolve.rb +0 -1
- data/gems/scheduler/test/test_file_io.rb +0 -1
- data/gems/scheduler/test/test_kernel_sleep.rb +3 -4
- data/gems/server/Cargo.lock +11 -2
- data/gems/server/Rakefile +8 -1
- data/gems/server/exe/itsi +53 -23
- data/gems/server/ext/itsi_rb_helpers/src/lib.rs +27 -4
- data/gems/server/ext/itsi_server/Cargo.toml +4 -1
- data/gems/server/ext/itsi_server/src/lib.rs +74 -1
- data/gems/server/ext/itsi_server/src/request/itsi_request.rs +32 -11
- data/gems/server/ext/itsi_server/src/response/itsi_response.rs +14 -4
- data/gems/server/ext/itsi_server/src/server/bind.rs +16 -12
- data/gems/server/ext/itsi_server/src/server/itsi_server.rs +146 -95
- data/gems/server/ext/itsi_server/src/server/listener.rs +10 -10
- data/gems/server/ext/itsi_server/src/server/process_worker.rs +10 -3
- data/gems/server/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +15 -9
- data/gems/server/ext/itsi_server/src/server/serve_strategy/single_mode.rs +134 -115
- data/gems/server/ext/itsi_server/src/server/signal.rs +4 -0
- data/gems/server/ext/itsi_server/src/server/thread_worker.rs +55 -24
- data/gems/server/ext/itsi_server/src/server/tls.rs +11 -8
- data/gems/server/ext/itsi_tracing/src/lib.rs +18 -1
- data/gems/server/lib/itsi/request.rb +29 -21
- data/gems/server/lib/itsi/server/Itsi.rb +127 -0
- data/gems/server/lib/itsi/server/config.rb +36 -0
- data/gems/server/lib/itsi/server/options_dsl.rb +401 -0
- data/gems/server/lib/itsi/server/rack/handler/itsi.rb +18 -7
- data/gems/server/lib/itsi/server/rack_interface.rb +75 -0
- data/gems/server/lib/itsi/server/scheduler_interface.rb +21 -0
- data/gems/server/lib/itsi/server/signal_trap.rb +23 -0
- data/gems/server/lib/itsi/server/version.rb +1 -1
- data/gems/server/lib/itsi/server.rb +71 -101
- data/gems/server/test/helpers/test_helper.rb +30 -0
- data/gems/server/test/test_itsi_server.rb +294 -3
- data/lib/itsi/version.rb +1 -1
- data/location_dsl.rb +381 -0
- data/sandbox/deploy/main.tf +1 -0
- data/sandbox/itsi_itsi_file/Itsi.rb +119 -0
- data/sandbox/itsi_sandbox_async/Gemfile +1 -1
- data/sandbox/itsi_sandbox_rack/Gemfile.lock +2 -2
- data/sandbox/itsi_sandbox_rails/Gemfile.lock +2 -2
- data/tasks.txt +25 -8
- metadata +21 -14
- data/gems/server/lib/itsi/signals.rb +0 -23
- data/gems/server/test/test_helper.rb +0 -7
- /data/gems/server/lib/itsi/{index.html.erb → index.html} +0 -0
@@ -0,0 +1,75 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module RackInterface
|
4
|
+
# Interface to Rack applications.
|
5
|
+
# Here we build the env, and invoke the Rack app's call method.
|
6
|
+
# We then turn the Rack response into something Itsi server understands.
|
7
|
+
def call(app, request)
|
8
|
+
respond request, app.call(request.to_rack_env)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Itsi responses are asynchronous and can be streamed.
|
12
|
+
# Response chunks are sent using response.send_frame
|
13
|
+
# and the response is finished using response.close_write.
|
14
|
+
# If only a single chunk is written, you can use the #send_and_close method.
|
15
|
+
def respond(request, (status, headers, body))
|
16
|
+
response = request.response
|
17
|
+
|
18
|
+
# Don't try and respond if we've been hijacked.
|
19
|
+
# The hijacker is now responsible for this.
|
20
|
+
return if request.hijacked
|
21
|
+
|
22
|
+
# 1. Set Status
|
23
|
+
response.status = status
|
24
|
+
|
25
|
+
# 2. Set Headers
|
26
|
+
body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack")
|
27
|
+
headers.each do |key, value|
|
28
|
+
next response.add_header(key, value) unless value.is_a?(Array)
|
29
|
+
|
30
|
+
value.each do |v|
|
31
|
+
response.add_header(key, v)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# 3. Set Body
|
36
|
+
# As soon as we start setting the response
|
37
|
+
# the server will begin to stream it to the client.
|
38
|
+
|
39
|
+
# If we're partially hijacked or returned a streaming body,
|
40
|
+
# stream this response.
|
41
|
+
|
42
|
+
if body_streamer
|
43
|
+
body_streamer.call(StreamIO.new(response))
|
44
|
+
|
45
|
+
# If we're enumerable with more than one chunk
|
46
|
+
# also stream, otherwise write in a single chunk
|
47
|
+
elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
|
48
|
+
unless body.respond_to?(:each)
|
49
|
+
body = body.to_ary
|
50
|
+
raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
|
51
|
+
end
|
52
|
+
# We offset this iteration intentionally,
|
53
|
+
# to optimize for the case where there's only one chunk.
|
54
|
+
buffer = nil
|
55
|
+
body.each do |part|
|
56
|
+
response.send_frame(buffer.to_s) if buffer
|
57
|
+
buffer = part
|
58
|
+
end
|
59
|
+
|
60
|
+
response.send_and_close(buffer.to_s)
|
61
|
+
else
|
62
|
+
response.send_and_close(body.to_s)
|
63
|
+
end
|
64
|
+
ensure
|
65
|
+
response.close_write
|
66
|
+
body.close if body.respond_to?(:close)
|
67
|
+
end
|
68
|
+
|
69
|
+
# A streaming body is one that responds to #call and not #each.
|
70
|
+
def streaming_body?(body)
|
71
|
+
body.respond_to?(:call) && !body.respond_to?(:each)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module Itsi
|
2
|
+
class Server
|
3
|
+
module SchedulerInterface
|
4
|
+
# Simple wrapper to instantiate a scheduler, start it,
|
5
|
+
# and immediate have it invoke a scheduler proc
|
6
|
+
def start_scheduler_loop(scheduler_class, scheduler_task)
|
7
|
+
scheduler = scheduler_class.new
|
8
|
+
Fiber.set_scheduler(scheduler)
|
9
|
+
[scheduler, Fiber.schedule(&scheduler_task)]
|
10
|
+
end
|
11
|
+
|
12
|
+
# When running in scheduler mode,
|
13
|
+
# each request is wrapped in a Fiber.
|
14
|
+
def schedule(app, request)
|
15
|
+
Fiber.schedule do
|
16
|
+
call(app, request)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Itsi
|
2
|
+
module SignalTrap
|
3
|
+
|
4
|
+
DEFAULT_SIGNALS = ["DEFAULT", "", nil].freeze
|
5
|
+
INTERCEPTED_SIGNALS = ["INT"].freeze
|
6
|
+
|
7
|
+
def trap(signal, *args, &block)
|
8
|
+
unless INTERCEPTED_SIGNALS.include?(signal.to_s) && block.nil? && Itsi::Server.running?
|
9
|
+
return super(signal, *args, &block)
|
10
|
+
end
|
11
|
+
Itsi::Server.reset_signal_handlers
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
[Kernel, Signal].each do |receiver|
|
18
|
+
receiver.singleton_class.prepend(Itsi::SignalTrap)
|
19
|
+
end
|
20
|
+
|
21
|
+
[Object].each do |receiver|
|
22
|
+
receiver.include(Itsi::SignalTrap)
|
23
|
+
end
|
@@ -2,119 +2,89 @@
|
|
2
2
|
|
3
3
|
require_relative "server/version"
|
4
4
|
require_relative "server/itsi_server"
|
5
|
-
require_relative "
|
5
|
+
require_relative "server/rack_interface"
|
6
|
+
require_relative "server/signal_trap"
|
7
|
+
require_relative "server/scheduler_interface"
|
8
|
+
require_relative "server/rack/handler/itsi"
|
9
|
+
require_relative "server/config"
|
6
10
|
require_relative "request"
|
7
11
|
require_relative "stream_io"
|
8
|
-
require_relative "server/rack/handler/itsi"
|
9
|
-
require 'erb'
|
10
12
|
|
11
|
-
|
13
|
+
# When you Run Itsi without a Rack app,
|
14
|
+
# we start a tiny little echo server, just so you can see it in action.
|
15
|
+
DEFAULT_INDEX = IO.read("#{__dir__}/index.html").freeze
|
16
|
+
DEFAULT_BINDS = ["http://0.0.0.0:3000"].freeze
|
17
|
+
DEFAULT_APP = lambda {
|
18
|
+
require "json"
|
19
|
+
require "itsi/scheduler"
|
20
|
+
Itsi.log_warn "No config.ru or Itsi.rb app detected. Running default app."
|
21
|
+
lambda do |env|
|
22
|
+
headers, body = \
|
23
|
+
if env["itsi.response"].json?
|
24
|
+
[
|
25
|
+
{ "Content-Type" => "application/json" },
|
26
|
+
[{ "message" => "You're running on Itsi!", "rack_env" => env,
|
27
|
+
"version" => Itsi::Server::VERSION }.to_json]
|
28
|
+
]
|
29
|
+
else
|
30
|
+
[
|
31
|
+
{ "Content-Type" => "text/html" },
|
32
|
+
[
|
33
|
+
format(
|
34
|
+
DEFAULT_INDEX,
|
35
|
+
REQUEST_METHOD: env["REQUEST_METHOD"],
|
36
|
+
PATH_INFO: env["PATH_INFO"],
|
37
|
+
SERVER_NAME: env["SERVER_NAME"],
|
38
|
+
SERVER_PORT: env["SERVER_PORT"],
|
39
|
+
REMOTE_ADDR: env["REMOTE_ADDR"],
|
40
|
+
HTTP_USER_AGENT: env["HTTP_USER_AGENT"]
|
41
|
+
)
|
42
|
+
]
|
43
|
+
]
|
44
|
+
end
|
45
|
+
[200, headers, body]
|
46
|
+
end
|
47
|
+
}
|
12
48
|
|
13
49
|
module Itsi
|
14
50
|
class Server
|
51
|
+
extend RackInterface
|
52
|
+
extend SchedulerInterface
|
15
53
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
def self.start(
|
21
|
-
app: ->(env){
|
22
|
-
[env['CONTENT_TYPE'], env['HTTP_ACCEPT']].include?('application/json') ?
|
23
|
-
[200, {"Content-Type" => "application/json"}, ["{\"message\": \"You're running on Itsi!\"}"]] :
|
24
|
-
[200, {"Content-Type" => "text/html"}, [
|
25
|
-
DEFAULT_INDEX % {
|
26
|
-
REQUEST_METHOD: env['REQUEST_METHOD'],
|
27
|
-
PATH_INFO: env['PATH_INFO'],
|
28
|
-
SERVER_NAME: env['SERVER_NAME'],
|
29
|
-
SERVER_PORT: env['SERVER_PORT'],
|
30
|
-
REMOTE_ADDR: env['REMOTE_ADDR'],
|
31
|
-
HTTP_USER_AGENT: env['HTTP_USER_AGENT']
|
32
|
-
}
|
33
|
-
]]
|
34
|
-
},
|
35
|
-
binds: ['http://0.0.0.0:3000'],
|
36
|
-
**opts
|
37
|
-
)
|
38
|
-
server = new(app: ->{app}, binds: binds, **opts)
|
39
|
-
@running = true
|
40
|
-
Signal.trap('INT', 'DEFAULT')
|
41
|
-
server.start
|
42
|
-
ensure
|
43
|
-
@running = false
|
44
|
-
end
|
45
|
-
|
46
|
-
def self.call(app, request)
|
47
|
-
respond request, app.call(request.to_env)
|
48
|
-
end
|
49
|
-
|
50
|
-
def self.streaming_body?(body)
|
51
|
-
body.respond_to?(:call) && !body.respond_to?(:each)
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.respond(request, (status, headers, body))
|
55
|
-
response = request.response
|
56
|
-
|
57
|
-
# Don't try and respond if we've been hijacked.
|
58
|
-
# The hijacker is now responsible for this.
|
59
|
-
return if request.hijacked
|
60
|
-
|
61
|
-
# 1. Set Status
|
62
|
-
response.status = status
|
63
|
-
|
64
|
-
# 2. Set Headers
|
65
|
-
headers.each do |key, value|
|
66
|
-
next response.add_header(key, value) unless value.is_a?(Array)
|
67
|
-
|
68
|
-
value.each do |v|
|
69
|
-
response.add_header(key, v)
|
70
|
-
end
|
54
|
+
class << self
|
55
|
+
def running?
|
56
|
+
!!@running
|
71
57
|
end
|
72
58
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
if (body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack"))
|
81
|
-
body_streamer.call(StreamIO.new(response))
|
82
|
-
|
83
|
-
# If we're enumerable with more than one chunk
|
84
|
-
# also stream, otherwise write in a single chunk
|
85
|
-
elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
|
86
|
-
unless body.respond_to?(:each)
|
87
|
-
body = body.to_ary
|
88
|
-
raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
|
89
|
-
end
|
90
|
-
# We offset this iteration intentionally,
|
91
|
-
# to optimize for the case where there's only one chunk.
|
92
|
-
buffer = nil
|
93
|
-
body.each do |part|
|
94
|
-
response.send_frame(buffer.to_s) if buffer
|
95
|
-
buffer = part
|
96
|
-
end
|
97
|
-
|
98
|
-
response.send_and_close(buffer.to_s)
|
99
|
-
else
|
100
|
-
response.send_and_close(body.to_s)
|
59
|
+
def build(
|
60
|
+
app: DEFAULT_APP[],
|
61
|
+
loader: nil,
|
62
|
+
binds: DEFAULT_BINDS,
|
63
|
+
**opts
|
64
|
+
)
|
65
|
+
new(app: loader || -> { app }, binds: binds, **opts)
|
101
66
|
end
|
102
|
-
ensure
|
103
|
-
response.close_write
|
104
|
-
body.close if body.respond_to?(:close)
|
105
|
-
end
|
106
67
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
[scheduler, Fiber.schedule(&scheduler_task)]
|
111
|
-
end
|
68
|
+
def start_in_background_thread(silence: true, **opts)
|
69
|
+
start(background: true, silence: silence, **opts)
|
70
|
+
end
|
112
71
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
72
|
+
def start(background: false, silence: false, **opts)
|
73
|
+
build(**opts).tap do |server|
|
74
|
+
previous_handler = Signal.trap("INT", "DEFAULT")
|
75
|
+
@running = true
|
76
|
+
if background
|
77
|
+
Thread.new do
|
78
|
+
server.start
|
79
|
+
@running = false
|
80
|
+
Signal.trap("INT", previous_handler)
|
81
|
+
end
|
82
|
+
else
|
83
|
+
server.start
|
84
|
+
@running = false
|
85
|
+
Signal.trap("INT", previous_handler)
|
86
|
+
end
|
87
|
+
end
|
118
88
|
end
|
119
89
|
end
|
120
90
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest/reporters"
|
4
|
+
|
5
|
+
ENV['ITSI_LOG'] = 'off'
|
6
|
+
|
7
|
+
require "itsi/server"
|
8
|
+
require "itsi/scheduler"
|
9
|
+
|
10
|
+
Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new
|
11
|
+
|
12
|
+
def free_bind(protocol)
|
13
|
+
server = TCPServer.new("0.0.0.0", 0)
|
14
|
+
port = server.addr[1]
|
15
|
+
server.close
|
16
|
+
"#{protocol}://0.0.0.0:#{port}"
|
17
|
+
end
|
18
|
+
|
19
|
+
def run_app(app, protocol: "http", bind: free_bind(protocol), **opts)
|
20
|
+
server = Itsi::Server.start_in_background_thread(
|
21
|
+
app: app,
|
22
|
+
binds: [bind],
|
23
|
+
**opts
|
24
|
+
)
|
25
|
+
|
26
|
+
sleep 0.005
|
27
|
+
yield URI(bind), server
|
28
|
+
ensure
|
29
|
+
server&.stop
|
30
|
+
end
|
@@ -1,9 +1,300 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
1
|
+
require "socket"
|
2
|
+
require "net/http"
|
3
|
+
require "minitest/autorun"
|
4
4
|
|
5
5
|
class TestItsiServer < Minitest::Test
|
6
6
|
def test_that_it_has_a_version_number
|
7
7
|
refute_nil ::Itsi::Server::VERSION
|
8
8
|
end
|
9
|
+
|
10
|
+
def test_hello_world
|
11
|
+
run_app(lambda do |env|
|
12
|
+
[200, { "Content-Type" => "text/plain" }, ["Hello, World!"]]
|
13
|
+
end) do |uri|
|
14
|
+
assert_equal "Hello, World!", Net::HTTP.get(uri)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_post
|
19
|
+
run_app(lambda do |env|
|
20
|
+
assert_equal env["REQUEST_METHOD"], "POST"
|
21
|
+
assert_equal "data", env["rack.input"].read
|
22
|
+
[200, { "Content-Type" => "text/plain" }, ["Hello, World!"]]
|
23
|
+
end) do |uri|
|
24
|
+
assert_equal "Hello, World!", Net::HTTP.post(uri, "data").body
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_full_hijack
|
29
|
+
run_app(lambda do |env|
|
30
|
+
io = env["rack.hijack"].call
|
31
|
+
io.write("HTTP/1.1 200 Ok\r\n")
|
32
|
+
io.write("Content-Type: text/plain\r\n")
|
33
|
+
io.write("Transfer-Encoding: chunked\r\n")
|
34
|
+
io.write("\r\n")
|
35
|
+
io.write("7\r\n")
|
36
|
+
io.write("Hello, \r\n")
|
37
|
+
io.write("6\r\n")
|
38
|
+
io.write("World!\r\n")
|
39
|
+
io.write("0\r\n\r\n")
|
40
|
+
io.close
|
41
|
+
end) do |uri|
|
42
|
+
assert_equal "Hello, World!", Net::HTTP.get(uri)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_streaming_body
|
47
|
+
run_app(lambda do |env|
|
48
|
+
[200, { "Content-Type" => "text/plain" }, lambda { |stream|
|
49
|
+
stream.write("Hello")
|
50
|
+
stream.write(", World!")
|
51
|
+
stream.close
|
52
|
+
}]
|
53
|
+
end) do |uri|
|
54
|
+
assert_equal "Hello, World!", Net::HTTP.get(uri)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_partial_hijack
|
59
|
+
run_app(lambda do |env|
|
60
|
+
[200, { "Content-Type" => "text/plain", "rack.hijack" => lambda { |stream|
|
61
|
+
stream.write("Hello")
|
62
|
+
stream.write(", World!")
|
63
|
+
stream.close
|
64
|
+
} }, []]
|
65
|
+
end) do |uri|
|
66
|
+
assert_equal "Hello, World!", Net::HTTP.get(uri)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def test_enumerable_body
|
71
|
+
run_app(lambda do |env|
|
72
|
+
[200, { "Content-Type" => "application/json" },
|
73
|
+
%W[one\n two\n three\n]]
|
74
|
+
end) do |uri|
|
75
|
+
assert_equal "one\ntwo\nthree\n", Net::HTTP.get(uri)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
require 'debug'
|
80
|
+
def test_scheduler_non_blocking
|
81
|
+
run_app(
|
82
|
+
lambda do |env|
|
83
|
+
sleep 0.25
|
84
|
+
[200, { "Content-Type" => "text/plain" }, "Response: #{env["PATH_INFO"][1..-1]}"]
|
85
|
+
end,
|
86
|
+
scheduler_class: "Itsi::Scheduler"
|
87
|
+
) do |uri|
|
88
|
+
start_time = Time.now
|
89
|
+
20.times.map do
|
90
|
+
Thread.new do
|
91
|
+
payload = SecureRandom.hex(16)
|
92
|
+
local_uri = uri.dup
|
93
|
+
local_uri.path = "/#{payload}"
|
94
|
+
response = Net::HTTP.start(local_uri.hostname, local_uri.port) do |http|
|
95
|
+
http.request(Net::HTTP::Get.new(local_uri))
|
96
|
+
end
|
97
|
+
assert_equal "Response: #{payload}", response.body
|
98
|
+
end
|
99
|
+
end.each(&:join)
|
100
|
+
assert_in_delta 0.25, Time.now - start_time, 0.5
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def test_query_params
|
105
|
+
run_app(lambda do |env|
|
106
|
+
[200, { "Content-Type" => "text/plain" }, [env["QUERY_STRING"]]]
|
107
|
+
end) do |uri|
|
108
|
+
uri.query = "foo=bar&baz=qux"
|
109
|
+
assert_equal "foo=bar&baz=qux", Net::HTTP.get(uri)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def test_put_request
|
114
|
+
run_app(lambda do |env|
|
115
|
+
body = env["rack.input"].read
|
116
|
+
[200, { "Content-Type" => "text/plain" }, [body]]
|
117
|
+
end) do |uri|
|
118
|
+
uri_obj = URI(uri)
|
119
|
+
req = Net::HTTP::Put.new(uri_obj)
|
120
|
+
req.body = "put data"
|
121
|
+
response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) { |http| http.request(req) }
|
122
|
+
assert_equal "put data", response.body
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_custom_headers
|
127
|
+
run_app(lambda do |env|
|
128
|
+
header = env["HTTP_X_CUSTOM"] || ""
|
129
|
+
[200, { "Content-Type" => "text/plain" }, [header]]
|
130
|
+
end) do |uri|
|
131
|
+
uri_obj = URI(uri)
|
132
|
+
req = Net::HTTP::Get.new(uri_obj)
|
133
|
+
req["X-Custom"] = "custom-value"
|
134
|
+
response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) { |http| http.request(req) }
|
135
|
+
assert_equal "custom-value", response.body
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def test_error_response
|
140
|
+
response = nil
|
141
|
+
capture_subprocess_io do
|
142
|
+
run_app(lambda do |env|
|
143
|
+
raise "Intentional error for testing"
|
144
|
+
end) do |uri|
|
145
|
+
response = Net::HTTP.get_response(uri)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
assert_equal "500", response.code
|
149
|
+
end
|
150
|
+
|
151
|
+
def test_redirect
|
152
|
+
run_app(lambda do |env|
|
153
|
+
[302, { "Location" => "http://example.com" }, []]
|
154
|
+
end) do |uri|
|
155
|
+
response = Net::HTTP.get_response(uri)
|
156
|
+
assert_equal "302", response.code
|
157
|
+
assert_equal "http://example.com", response["location"]
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def test_not_found
|
162
|
+
run_app(lambda do |env|
|
163
|
+
if env["PATH_INFO"] == "/"
|
164
|
+
[200, { "Content-Type" => "text/plain" }, ["Home"]]
|
165
|
+
else
|
166
|
+
[404, { "Content-Type" => "text/plain" }, ["Not Found"]]
|
167
|
+
end
|
168
|
+
end) do |uri|
|
169
|
+
uri.path = "/nonexistent"
|
170
|
+
response = Net::HTTP.get_response(uri)
|
171
|
+
assert_equal "404", response.code
|
172
|
+
assert_equal "Not Found", response.body
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
def test_head_request
|
177
|
+
run_app(lambda do |env|
|
178
|
+
[200, { "Content-Type" => "text/plain", "Content-Length" => "13" }, ["Hello, World!"]]
|
179
|
+
end) do |uri|
|
180
|
+
uri_obj = URI(uri)
|
181
|
+
response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) do |http|
|
182
|
+
http.head("/")
|
183
|
+
end
|
184
|
+
assert_equal "200", response.code
|
185
|
+
assert_empty response.body.to_s
|
186
|
+
assert_equal "13", response["content-length"]
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def test_options_request
|
191
|
+
run_app(lambda do |env|
|
192
|
+
[200, { "Allow" => "GET,POST,OPTIONS", "Content-Type" => "text/plain" }, ["Options Response"]]
|
193
|
+
end) do |uri|
|
194
|
+
uri_obj = URI(uri)
|
195
|
+
req = Net::HTTP::Options.new(uri_obj)
|
196
|
+
response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) { |http| http.request(req) }
|
197
|
+
assert_equal "200", response.code
|
198
|
+
assert_equal "GET,POST,OPTIONS", response["allow"]
|
199
|
+
assert_equal "Options Response", response.body
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_cookie_handling
|
204
|
+
run_app(lambda do |env|
|
205
|
+
[200, { "Content-Type" => "text/plain", "Set-Cookie" => "session=abc123; Path=/" }, ["Cookie Test"]]
|
206
|
+
end) do |uri|
|
207
|
+
response = Net::HTTP.get_response(uri)
|
208
|
+
assert_equal "200", response.code
|
209
|
+
assert_match(/session=abc123/, response["set-cookie"])
|
210
|
+
assert_equal "Cookie Test", response.body
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def test_multiple_headers
|
215
|
+
run_app(lambda do |env|
|
216
|
+
[200, { "Content-Type" => "text/plain", "X-Example" => "one, two, three" }, ["Multiple Headers"]]
|
217
|
+
end) do |uri|
|
218
|
+
response = Net::HTTP.get_response(uri)
|
219
|
+
assert_equal "200", response.code
|
220
|
+
assert_equal "one, two, three", response["x-example"]
|
221
|
+
assert_equal "Multiple Headers", response.body
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def test_large_body
|
226
|
+
large_text = "A" * 10_000
|
227
|
+
run_app(lambda do |env|
|
228
|
+
[200, { "Content-Type" => "text/plain", "Content-Length" => large_text.bytesize.to_s }, [large_text]]
|
229
|
+
end) do |uri|
|
230
|
+
response = Net::HTTP.get_response(uri)
|
231
|
+
assert_equal "200", response.code
|
232
|
+
assert_equal large_text, response.body
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
def test_custom_status_code
|
237
|
+
run_app(lambda do |env|
|
238
|
+
[201, { "Content-Type" => "text/plain" }, ["Created"]]
|
239
|
+
end) do |uri|
|
240
|
+
response = Net::HTTP.get_response(uri)
|
241
|
+
assert_equal "201", response.code
|
242
|
+
assert_equal "Created", response.body
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
def test_empty_body
|
247
|
+
run_app(lambda do |env|
|
248
|
+
[204, { "Content-Type" => "text/plain" }, []]
|
249
|
+
end) do |uri|
|
250
|
+
response = Net::HTTP.get_response(uri)
|
251
|
+
assert_equal "204", response.code
|
252
|
+
assert_nil response.body
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def test_utf8_response
|
257
|
+
utf8_text = "こんにちは世界"
|
258
|
+
run_app(lambda do |env|
|
259
|
+
[200, { "Content-Type" => "text/plain; charset=utf-8" }, [utf8_text]]
|
260
|
+
end) do |uri|
|
261
|
+
response = Net::HTTP.get_response(uri)
|
262
|
+
assert_equal "200", response.code
|
263
|
+
assert_equal utf8_text, response.body.force_encoding("UTF-8")
|
264
|
+
end
|
265
|
+
end
|
266
|
+
|
267
|
+
def test_custom_request_header
|
268
|
+
run_app(lambda do |env|
|
269
|
+
header_value = env["HTTP_X_MY_HEADER"] || ""
|
270
|
+
[200, { "Content-Type" => "text/plain" }, [header_value]]
|
271
|
+
end) do |uri|
|
272
|
+
uri_obj = URI(uri)
|
273
|
+
req = Net::HTTP::Get.new(uri_obj)
|
274
|
+
req["X-My-Header"] = "test-header"
|
275
|
+
response = Net::HTTP.start(uri_obj.hostname, uri_obj.port) { |http| http.request(req) }
|
276
|
+
assert_equal "test-header", response.body
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
def test_url_encoded_query_params
|
281
|
+
run_app(lambda do |env|
|
282
|
+
[200, { "Content-Type" => "text/plain" }, [env["QUERY_STRING"]]]
|
283
|
+
end) do |uri|
|
284
|
+
uri.query = "param=%C3%A9" # %C3%A9 represents 'é'
|
285
|
+
assert_equal "param=%C3%A9", Net::HTTP.get(uri)
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
def test_https
|
290
|
+
run_app(lambda do |env|
|
291
|
+
[200, { "Content-Type" => "text/plain" }, ["Hello, HTTPS!"]]
|
292
|
+
end, protocol: "https") do |uri|
|
293
|
+
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) do |http|
|
294
|
+
http.request(Net::HTTP::Get.new(uri))
|
295
|
+
end
|
296
|
+
assert_equal "200", response.code
|
297
|
+
assert_equal "Hello, HTTPS!", response.body
|
298
|
+
end
|
299
|
+
end
|
9
300
|
end
|
data/lib/itsi/version.rb
CHANGED