plum 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -12
  3. data/circle.yml +27 -0
  4. data/examples/client/large.rb +20 -0
  5. data/examples/client/twitter.rb +51 -0
  6. data/examples/non_tls_server.rb +15 -9
  7. data/examples/static_server.rb +30 -23
  8. data/lib/plum.rb +9 -2
  9. data/lib/plum/client.rb +198 -0
  10. data/lib/plum/client/client_session.rb +91 -0
  11. data/lib/plum/client/connection.rb +19 -0
  12. data/lib/plum/client/legacy_client_session.rb +118 -0
  13. data/lib/plum/client/response.rb +100 -0
  14. data/lib/plum/client/upgrade_client_session.rb +46 -0
  15. data/lib/plum/connection.rb +58 -65
  16. data/lib/plum/connection_utils.rb +1 -1
  17. data/lib/plum/errors.rb +7 -3
  18. data/lib/plum/flow_control.rb +3 -3
  19. data/lib/plum/rack/listener.rb +3 -3
  20. data/lib/plum/rack/server.rb +1 -0
  21. data/lib/plum/rack/session.rb +5 -2
  22. data/lib/plum/server/connection.rb +42 -0
  23. data/lib/plum/{http_connection.rb → server/http_connection.rb} +7 -14
  24. data/lib/plum/{https_connection.rb → server/https_connection.rb} +2 -9
  25. data/lib/plum/stream.rb +54 -24
  26. data/lib/plum/stream_utils.rb +0 -12
  27. data/lib/plum/version.rb +1 -1
  28. data/plum.gemspec +2 -2
  29. data/test/plum/client/test_client.rb +152 -0
  30. data/test/plum/client/test_connection.rb +11 -0
  31. data/test/plum/client/test_legacy_client_session.rb +90 -0
  32. data/test/plum/client/test_response.rb +74 -0
  33. data/test/plum/client/test_upgrade_client_session.rb +45 -0
  34. data/test/plum/connection/test_handle_frame.rb +4 -1
  35. data/test/plum/{test_http_connection.rb → server/test_http_connection.rb} +4 -4
  36. data/test/plum/{test_https_connection.rb → server/test_https_connection.rb} +14 -8
  37. data/test/plum/test_connection.rb +9 -2
  38. data/test/plum/test_connection_utils.rb +9 -0
  39. data/test/plum/test_error.rb +1 -2
  40. data/test/plum/test_frame_factory.rb +37 -0
  41. data/test/plum/test_stream.rb +24 -4
  42. data/test/plum/test_stream_utils.rb +0 -1
  43. data/test/test_helper.rb +5 -2
  44. data/test/utils/assertions.rb +9 -9
  45. data/test/utils/client.rb +19 -0
  46. data/test/utils/server.rb +6 -6
  47. data/test/utils/string_socket.rb +15 -0
  48. metadata +36 -12
@@ -0,0 +1,152 @@
1
+ require "test_helper"
2
+
3
+ using Plum::BinaryString
4
+ class ClientTest < Minitest::Test
5
+ def test_request_sync
6
+ server_thread = start_tls_server
7
+ client = Client.start("127.0.0.1", LISTEN_PORT, https: true, verify_mode: OpenSSL::SSL::VERIFY_NONE)
8
+ res1 = client.put!("/", "aaa", headers: { "header" => "ccc" })
9
+ assert_equal("PUTcccaaa", res1.body)
10
+ client.close
11
+ ensure
12
+ server_thread.join if server_thread
13
+ end
14
+
15
+ def test_request_async
16
+ res2 = nil
17
+ client = nil
18
+ server_thread = start_tls_server
19
+ Client.start("127.0.0.1", LISTEN_PORT, https: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) { |c|
20
+ client = c
21
+ res1 = client.request({ ":path" => "/", ":method" => "GET", ":scheme" => "https", "header" => "ccc" }, nil) { |res1|
22
+ assert(res1.headers)
23
+ }
24
+ assert_nil(res1.headers)
25
+
26
+ res2 = client.get("/", headers: { "header" => "ccc" })
27
+ assert_nil(res2.headers)
28
+ }
29
+ assert(res2.headers)
30
+ assert_equal("GETccc", res2.body)
31
+ ensure
32
+ server_thread.join if server_thread
33
+ end
34
+
35
+ def test_verify
36
+ client = nil
37
+ server_thread = start_tls_server
38
+ assert_raises(OpenSSL::SSL::SSLError) {
39
+ client = Client.start("127.0.0.1", LISTEN_PORT, https: true, verify_mode: OpenSSL::SSL::VERIFY_PEER)
40
+ }
41
+ ensure
42
+ server_thread.join if server_thread
43
+ end
44
+
45
+ def test_raise_error_sync
46
+ client = nil
47
+ server_thread = start_tls_server
48
+ Client.start("127.0.0.1", LISTEN_PORT, https: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) { |c|
49
+ client = c
50
+ assert_raises(LocalConnectionError) {
51
+ client.get!("/connection_error")
52
+ }
53
+ }
54
+ ensure
55
+ server_thread.join if server_thread
56
+ end
57
+
58
+ def test_raise_error_async_seq_resume
59
+ server_thread = start_tls_server
60
+ client = Client.start("127.0.0.1", LISTEN_PORT, https: true, verify_mode: OpenSSL::SSL::VERIFY_NONE)
61
+ res = client.get("/error_in_data")
62
+ assert_raises(LocalConnectionError) {
63
+ client.resume(res)
64
+ }
65
+ client.close
66
+ ensure
67
+ server_thread.join if server_thread
68
+ end
69
+
70
+ def test_raise_error_async_block
71
+ client = nil
72
+ server_thread = start_tls_server
73
+ assert_raises(LocalConnectionError) {
74
+ Client.start("127.0.0.1", LISTEN_PORT, https: true, verify_mode: OpenSSL::SSL::VERIFY_NONE) { |c|
75
+ client = c
76
+ client.get("/connection_error") { |res| flunk "success??" }
77
+ } # resume
78
+ }
79
+ ensure
80
+ server_thread.join if server_thread
81
+ end
82
+
83
+ def test_session_socket_http2_https
84
+ sock = StringSocket.new
85
+ client = Client.start(sock, nil, http2: true, scheme: "https")
86
+ assert(client.session.class == ClientSession)
87
+ end
88
+
89
+ def test_session_socket_http2_http
90
+ sock = StringSocket.new("HTTP/1.1 100\r\n\r\n")
91
+ client = Client.start(sock, nil, http2: true, scheme: "http")
92
+ assert(client.session.class == UpgradeClientSession)
93
+ end
94
+
95
+ def test_session_socket_http1
96
+ sock = StringSocket.new
97
+ client = Client.start(sock, nil, http2: false)
98
+ assert(client.session.class == LegacyClientSession)
99
+ end
100
+
101
+ private
102
+ def start_tls_server(&block)
103
+ ctx = OpenSSL::SSL::SSLContext.new
104
+ ctx.alpn_select_cb = -> protocols { "h2" }
105
+ ctx.cert = TLS_CERT
106
+ ctx.key = TLS_KEY
107
+ tcp_server = TCPServer.new("127.0.0.1", LISTEN_PORT)
108
+ ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
109
+
110
+ server_thread = Thread.new {
111
+ plum = nil
112
+ begin
113
+ Timeout.timeout(1) {
114
+ sock = ssl_server.accept
115
+ plum = HTTPSServerConnection.new(sock)
116
+
117
+ plum.on(:stream) { |stream|
118
+ headers = data = nil
119
+ stream.on(:headers) { |h|
120
+ headers = h.to_h }
121
+ stream.on(:data) { |d|
122
+ data = d }
123
+ stream.on(:end_stream) {
124
+ case headers[":path"]
125
+ when "/connection_error"
126
+ plum.goaway(:protocol_error)
127
+ when "/error_in_data"
128
+ stream.send_headers({ ":status" => 200 }, end_stream: false)
129
+ stream.send_data("a", end_stream: false)
130
+ raise ExampleError, "example error"
131
+ else
132
+ stream.send_headers({ ":status" => 200 }, end_stream: false)
133
+ stream.send_data(headers.to_h[":method"] + headers.to_h["header"].to_s + data.to_s, end_stream: true)
134
+ end } }
135
+
136
+ yield plum if block_given?
137
+
138
+ while !sock.closed? && !sock.eof?
139
+ plum << sock.readpartial(1024)
140
+ end
141
+ }
142
+ rescue OpenSSL::SSL::SSLError
143
+ rescue Timeout::Error
144
+ flunk "server timeout"
145
+ rescue ExampleError => e
146
+ plum.goaway(:internal_error) if plum
147
+ ensure
148
+ tcp_server.close
149
+ end
150
+ }
151
+ end
152
+ end
@@ -0,0 +1,11 @@
1
+ require "test_helper"
2
+
3
+ using Plum::BinaryString
4
+ class ClientConnectionTest < Minitest::Test
5
+ def test_open_stream
6
+ con = open_client_connection
7
+ stream = con.open_stream
8
+ assert(stream.id % 2 == 1, "Stream ID is not odd")
9
+ assert_equal(:idle, stream.state)
10
+ end
11
+ end
@@ -0,0 +1,90 @@
1
+ require "test_helper"
2
+
3
+ using Plum::BinaryString
4
+ class LegacyClientSessionTest < Minitest::Test
5
+ def test_empty?
6
+ io = StringIO.new
7
+ session = LegacyClientSession.new(io, Client::DEFAULT_CONFIG)
8
+ assert(session.empty?)
9
+ res = session.request({}, "aa", {})
10
+ assert(!session.empty?)
11
+ io.string << "HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n"
12
+ session.succ
13
+ assert(res.finished?)
14
+ assert(session.empty?)
15
+ end
16
+
17
+ def test_close_fails_req
18
+ session = LegacyClientSession.new(StringIO.new, Client::DEFAULT_CONFIG)
19
+ res = session.request({}, nil, {})
20
+ assert(!res.failed?)
21
+ session.close
22
+ assert(res.failed?)
23
+ end
24
+
25
+ def test_fail
26
+ io = StringIO.new
27
+ session = LegacyClientSession.new(io, Client::DEFAULT_CONFIG)
28
+ res = session.request({}, "aa", {})
29
+ assert_raises {
30
+ session.succ
31
+ }
32
+ assert(!res.finished?)
33
+ assert(res.failed?)
34
+ end
35
+
36
+ def test_request
37
+ io = StringIO.new
38
+ session = LegacyClientSession.new(io, Client::DEFAULT_CONFIG.merge(hostname: "aa"))
39
+ res = session.request({ ":method" => "GET", ":path" => "/aa" }, "aa", {})
40
+ assert_equal("GET /aa HTTP/1.1\r\nhost: aa\r\ntransfer-encoding: chunked\r\n\r\n2\r\naa\r\n", io.string)
41
+ io.string << "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\naaa"
42
+ session.succ until res.finished?
43
+ assert(res.finished?)
44
+ assert_equal("aaa", res.body)
45
+ assert_equal({ ":status" => "200", "content-length" => "3" }, res.headers)
46
+ end
47
+
48
+ def test_chunked_chunked_string
49
+ io = StringIO.new
50
+ session = LegacyClientSession.new(io, Client::DEFAULT_CONFIG.merge(hostname: "hostname"))
51
+ res = session.request({ ":method" => "GET", ":path" => "/aa" }, "a" * 1025, {})
52
+ assert_equal(<<-EOR, io.string)
53
+ GET /aa HTTP/1.1\r
54
+ host: hostname\r
55
+ transfer-encoding: chunked\r
56
+ \r
57
+ 401\r
58
+ #{"a"*1025}\r
59
+ EOR
60
+ end
61
+
62
+ def test_chunked_chunked_io
63
+ io = StringIO.new
64
+ session = LegacyClientSession.new(io, Client::DEFAULT_CONFIG.merge(hostname: "hostname"))
65
+ res = session.request({ ":method" => "GET", ":path" => "/aa" }, StringIO.new("a" * 1025), {})
66
+ assert_equal(<<-EOR, io.string)
67
+ GET /aa HTTP/1.1\r
68
+ host: hostname\r
69
+ transfer-encoding: chunked\r
70
+ \r
71
+ 400\r
72
+ #{"a"*1024}\r
73
+ 1\r
74
+ a\r
75
+ EOR
76
+ end
77
+
78
+ def test_chunked_sized
79
+ io = StringIO.new
80
+ session = LegacyClientSession.new(io, Client::DEFAULT_CONFIG.merge(hostname: "hostname"))
81
+ res = session.request({ ":method" => "GET", ":path" => "/aa", "content-length" => 1025 }, StringIO.new("a" * 1025), {})
82
+ assert_equal((<<-EOR).chomp, io.string)
83
+ GET /aa HTTP/1.1\r
84
+ content-length: 1025\r
85
+ host: hostname\r
86
+ \r
87
+ #{"a"*1025}
88
+ EOR
89
+ end
90
+ end
@@ -0,0 +1,74 @@
1
+ require "test_helper"
2
+
3
+ using Plum::BinaryString
4
+ class ResponseTest < Minitest::Test
5
+ def test_finished
6
+ resp = Response.new
7
+ assert_equal(false, resp.finished?)
8
+ resp._finish
9
+ assert_equal(true, resp.finished?)
10
+ end
11
+
12
+ def test_fail
13
+ resp = Response.new
14
+ resp._fail
15
+ assert(true, resp.failed?)
16
+ end
17
+
18
+ def test_status
19
+ resp = Response.new
20
+ resp._headers([
21
+ [":status", "200"]
22
+ ])
23
+ assert_equal("200", resp.status)
24
+ end
25
+
26
+ def test_headers
27
+ resp = Response.new
28
+ resp._headers([
29
+ [":status", "200"],
30
+ ["header", "abc"]
31
+ ])
32
+ assert_equal("abc", resp[:HEADER])
33
+ end
34
+
35
+ def test_body
36
+ resp = Response.new
37
+ resp._chunk("a")
38
+ resp._chunk("b")
39
+ resp._finish
40
+ assert_equal("ab", resp.body)
41
+ end
42
+
43
+ def test_body_not_finished
44
+ resp = Response.new
45
+ resp._chunk("a")
46
+ resp._chunk("b")
47
+ assert_raises { # TODO
48
+ resp.body
49
+ }
50
+ end
51
+
52
+ def test_on_chunk
53
+ resp = Response.new
54
+ res = []
55
+ resp._chunk("a")
56
+ resp._chunk("b")
57
+ resp._finish
58
+ resp.on_chunk { |chunk| res << chunk }
59
+ assert_equal(["a", "b"], res)
60
+ resp._chunk("c")
61
+ assert_equal(["a", "b", "c"], res)
62
+ end
63
+
64
+ def test_on_finish
65
+ resp = Response.new
66
+ ran = false
67
+ resp.on_finish { ran = true }
68
+ resp._finish
69
+ assert(ran)
70
+ ran = false
71
+ resp.on_finish { ran = true }
72
+ assert(ran)
73
+ end
74
+ end
@@ -0,0 +1,45 @@
1
+ require "test_helper"
2
+
3
+ using Plum::BinaryString
4
+ class UpgradeClientSessionTest < Minitest::Test
5
+ def test_empty?
6
+ sock = StringSocket.new("HTTP/1.1 101\r\n\r\n")
7
+ session = UpgradeClientSession.new(sock, Client::DEFAULT_CONFIG)
8
+ assert(sock.wio.string.start_with?("OPTIONS * HTTP/1.1\r\n"), "sends options request")
9
+ assert(session.empty?)
10
+ end
11
+
12
+ def test_close
13
+ sock = StringSocket.new("HTTP/1.1 101\r\n\r\n")
14
+ session = UpgradeClientSession.new(sock, Client::DEFAULT_CONFIG)
15
+ res = session.request({}, nil, {})
16
+ assert(!res.failed?)
17
+ session.close
18
+ assert(res.failed?)
19
+ end
20
+
21
+ def test_request
22
+ sock = StringSocket.new("HTTP/1.1 101\r\n\r\n")
23
+ session = UpgradeClientSession.new(sock, Client::DEFAULT_CONFIG)
24
+ sock.rio.string << Frame.settings().assemble
25
+ sock.rio.string << Frame.settings(:ack).assemble
26
+ res = session.request({ ":method" => "GET", ":path" => "/aa" }, "aa", {})
27
+ sock.rio.string << Frame.headers(3, HPACK::Encoder.new(3).encode(":status" => "200", "content-length" => "3"), :end_headers).assemble
28
+ sock.rio.string << Frame.data(3, "aaa", :end_stream).assemble
29
+ session.succ until res.finished?
30
+ assert(res.finished?)
31
+ assert_equal("aaa", res.body)
32
+ assert_equal({ ":status" => "200", "content-length" => "3" }, res.headers)
33
+ end
34
+
35
+ def test_request_legacy
36
+ sock = StringSocket.new("HTTP/1.1 200\r\nContent-Length: 0\r\n\r\n")
37
+ session = UpgradeClientSession.new(sock, Client::DEFAULT_CONFIG)
38
+ res = session.request({ ":method" => "GET", ":path" => "/aa" }, "aa", {})
39
+ sock.rio.string << "HTTP/1.1 200 OK\r\nContent-Length: 3\r\n\r\naaaHTTP/1.1 400\r\nnext-response"
40
+ session.succ until res.finished?
41
+ assert(res.finished?)
42
+ assert_equal("aaa", res.body)
43
+ assert_equal({ ":status" => "200", "content-length" => "3" }, res.headers)
44
+ end
45
+ end
@@ -62,7 +62,10 @@ class ServerConnectionHandleFrameTest < Minitest::Test
62
62
  def test_server_handle_goaway_reply
63
63
  open_server_connection {|con|
64
64
  assert_no_error {
65
- con << Frame.goaway(1234, :stream_closed).assemble
65
+ begin
66
+ con << Frame.goaway(1, :stream_closed).assemble
67
+ rescue LocalHTTPError
68
+ end
66
69
  }
67
70
  assert_equal(:goaway, sent_frames.last.type)
68
71
  }
@@ -5,7 +5,7 @@ using Plum::BinaryString
5
5
  class HTTPConnectionNegotiationTest < Minitest::Test
6
6
  ## with Prior Knowledge (same as over TLS)
7
7
  def test_server_must_raise_cprotocol_error_non_settings_after_magic
8
- con = HTTPConnection.new(StringIO.new)
8
+ con = HTTPServerConnection.new(StringIO.new)
9
9
  con << Connection::CLIENT_CONNECTION_PREFACE
10
10
  assert_connection_error(:protocol_error) {
11
11
  con << Frame.new(type: :window_update, stream_id: 0, payload: "".push_uint32(1)).assemble
@@ -14,7 +14,7 @@ class HTTPConnectionNegotiationTest < Minitest::Test
14
14
 
15
15
  def test_server_accept_fragmented_magic
16
16
  magic = Connection::CLIENT_CONNECTION_PREFACE
17
- con = HTTPConnection.new(StringIO.new)
17
+ con = HTTPServerConnection.new(StringIO.new)
18
18
  assert_no_error {
19
19
  con << magic[0...5]
20
20
  con << magic[5..-1]
@@ -25,7 +25,7 @@ class HTTPConnectionNegotiationTest < Minitest::Test
25
25
  ## with HTTP/1.1 Upgrade
26
26
  def test_server_accept_upgrade
27
27
  io = StringIO.new
28
- con = HTTPConnection.new(io)
28
+ con = HTTPServerConnection.new(io)
29
29
  heads = nil
30
30
  con.on(:headers) {|_, _h| heads = _h.to_h }
31
31
  req = "GET / HTTP/1.1\r\n" <<
@@ -47,7 +47,7 @@ class HTTPConnectionNegotiationTest < Minitest::Test
47
47
 
48
48
  def test_server_deny_non_upgrade
49
49
  io = StringIO.new
50
- con = HTTPConnection.new(io)
50
+ con = HTTPServerConnection.new(io)
51
51
  req = "GET / HTTP/1.1\r\n" <<
52
52
  "Host: rhe.jp\r\n" <<
53
53
  "User-Agent: nya\r\n" <<
@@ -4,21 +4,21 @@ using Plum::BinaryString
4
4
 
5
5
  class HTTPSConnectionNegotiationTest < Minitest::Test
6
6
  def test_server_must_raise_cprotocol_error_invalid_magic_short
7
- con = HTTPSConnection.new(StringIO.new)
7
+ con = HTTPSServerConnection.new(StringIO.new)
8
8
  assert_connection_error(:protocol_error) {
9
9
  con << "HELLO"
10
10
  }
11
11
  end
12
12
 
13
13
  def test_server_must_raise_cprotocol_error_invalid_magic_long
14
- con = HTTPSConnection.new(StringIO.new)
14
+ con = HTTPSServerConnection.new(StringIO.new)
15
15
  assert_connection_error(:protocol_error) {
16
16
  con << ("HELLO" * 100) # over 24
17
17
  }
18
18
  end
19
19
 
20
20
  def test_server_must_raise_cprotocol_error_non_settings_after_magic
21
- con = HTTPSConnection.new(StringIO.new)
21
+ con = HTTPSServerConnection.new(StringIO.new)
22
22
  con << Connection::CLIENT_CONNECTION_PREFACE
23
23
  assert_connection_error(:protocol_error) {
24
24
  con << Frame.new(type: :window_update, stream_id: 0, payload: "".push_uint32(1)).assemble
@@ -27,7 +27,7 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
27
27
 
28
28
  def test_server_accept_fragmented_magic
29
29
  magic = Connection::CLIENT_CONNECTION_PREFACE
30
- con = HTTPSConnection.new(StringIO.new)
30
+ con = HTTPSServerConnection.new(StringIO.new)
31
31
  assert_no_error {
32
32
  con << magic[0...5]
33
33
  con << magic[5..-1]
@@ -40,8 +40,8 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
40
40
 
41
41
  ctx = OpenSSL::SSL::SSLContext.new
42
42
  ctx.alpn_select_cb = -> protocols { "h2" }
43
- ctx.cert = OpenSSL::X509::Certificate.new File.read(File.expand_path("../../server.crt", __FILE__))
44
- ctx.key = OpenSSL::PKey::RSA.new File.read(File.expand_path("../../server.key", __FILE__))
43
+ ctx.cert = TLS_CERT
44
+ ctx.key = TLS_KEY
45
45
  tcp_server = TCPServer.new("127.0.0.1", LISTEN_PORT)
46
46
  ssl_server = OpenSSL::SSL::SSLServer.new(tcp_server, ctx)
47
47
 
@@ -49,14 +49,18 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
49
49
  begin
50
50
  Timeout.timeout(3) {
51
51
  sock = ssl_server.accept
52
- plum = HTTPSConnection.new(sock)
52
+ plum = HTTPSServerConnection.new(sock)
53
53
  assert_connection_error(:inadequate_security) {
54
54
  run = true
55
- plum.run
55
+ while !sock.closed? && !sock.eof?
56
+ plum << sock.readpartial(1024)
57
+ end
56
58
  }
57
59
  }
58
60
  rescue Timeout::Error
59
61
  flunk "server timeout"
62
+ rescue => e
63
+ flunk e
60
64
  ensure
61
65
  tcp_server.close
62
66
  end
@@ -73,6 +77,8 @@ class HTTPSConnectionNegotiationTest < Minitest::Test
73
77
  ssl.write Connection::CLIENT_CONNECTION_PREFACE
74
78
  ssl.write Frame.settings.assemble
75
79
  sleep
80
+ rescue => e
81
+ flunk e
76
82
  ensure
77
83
  sock.close
78
84
  end