plum 0.1.3 → 0.2.0

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.
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