protocol-http 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,92 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require 'async/io/buffer'
22
+
23
+ module Protocol
24
+ module HTTP
25
+ module Body
26
+ # A generic base class for wrapping body instances. Typically you'd override `#read`.
27
+ # The implementation assumes a sequential unbuffered stream of data.
28
+ # def each -> yield(String | nil)
29
+ # def read -> String | nil
30
+ # def join -> String
31
+
32
+ # def finish -> buffer the stream and close it.
33
+ # def close(error = nil) -> close the stream immediately.
34
+ # end
35
+ class Readable
36
+ # The consumer can call stop to signal that the stream output has terminated.
37
+ def close(error = nil)
38
+ end
39
+
40
+ # Will read return any data?
41
+ def empty?
42
+ false
43
+ end
44
+
45
+ def length
46
+ nil
47
+ end
48
+
49
+ # Read the next available chunk.
50
+ def read
51
+ nil
52
+ end
53
+
54
+ # Read all remaining chunks into a buffered body and close the underlying input.
55
+ def finish
56
+ # Internally, this invokes `self.each` which then invokes `self.close`.
57
+ Buffered.for(self)
58
+ end
59
+
60
+ # Enumerate all chunks until finished, then invoke `#close`.
61
+ def each
62
+ while chunk = self.read
63
+ yield chunk
64
+ # chunk.clear
65
+ end
66
+ ensure
67
+ self.close($!)
68
+ end
69
+
70
+ def call(stream)
71
+ # Flushing after every chunk is inefficient, but it's also a safe default.
72
+ self.each do |chunk|
73
+ stream.write(chunk)
74
+ stream.flush
75
+ end
76
+ end
77
+
78
+ # Read all remaining chunks into a single binary string using `#each`.
79
+ def join
80
+ buffer = String.new.force_encoding(Encoding::BINARY)
81
+
82
+ self.each do |chunk|
83
+ buffer << chunk
84
+ chunk.clear
85
+ end
86
+
87
+ return buffer
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,83 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Protocol
22
+ module HTTP
23
+ module Body
24
+ # General operations for interacting with a request or response body.
25
+ module Reader
26
+ # Read chunks from the body.
27
+ # @yield [String] read chunks from the body.
28
+ def each(&block)
29
+ if @body
30
+ @body.each(&block)
31
+ @body = nil
32
+ end
33
+ end
34
+
35
+ # Reads the entire request/response body.
36
+ # @return [String] the entire body as a string.
37
+ def read
38
+ if @body
39
+ buffer = @body.join
40
+ @body = nil
41
+
42
+ return buffer
43
+ end
44
+ end
45
+
46
+ # Gracefully finish reading the body. This will buffer the remainder of the body.
47
+ # @return [Buffered] buffers the entire body.
48
+ def finish
49
+ if @body
50
+ body = @body.finish
51
+ @body = nil
52
+
53
+ return body
54
+ end
55
+ end
56
+
57
+ # Write the body of the response to the given file path.
58
+ def save(path, mode = ::File::WRONLY|::File::CREAT, *args)
59
+ if @body
60
+ ::File.open(path, mode, *args) do |file|
61
+ self.each do |chunk|
62
+ file.write(chunk)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ # Close the connection as quickly as possible. Discards body. May close the underlying connection if necessary to terminate the stream.
69
+ def close(error = nil)
70
+ if @body
71
+ @body.close(error)
72
+ @body = nil
73
+ end
74
+ end
75
+
76
+ # Whether there is a body?
77
+ def body?
78
+ @body and !@body.empty?
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,60 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'wrapper'
22
+
23
+ module Protocol
24
+ module HTTP
25
+ module Body
26
+ # A body which buffers all it's contents as it is `#read`.
27
+ class Rewindable < Wrapper
28
+ def initialize(body)
29
+ super(body)
30
+
31
+ @chunks = []
32
+ @index = 0
33
+ end
34
+
35
+ def read
36
+ if @index < @chunks.count
37
+ chunk = @chunks[@index]
38
+ @index += 1
39
+ else
40
+ if chunk = super
41
+ @chunks << chunk
42
+ @index += 1
43
+ end
44
+ end
45
+
46
+ # We dup them on the way out, so that if someone modifies the string, it won't modify the rewindability.
47
+ return chunk&.dup
48
+ end
49
+
50
+ def rewind
51
+ @index = 0
52
+ end
53
+
54
+ def inspect
55
+ "\#<#{self.class} #{@index}/#{@chunks.count} chunks read>"
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,143 @@
1
+ # Copyright, 2019, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ module Protocol
22
+ module HTTP
23
+ module Body
24
+ # The input stream is an IO-like object which contains the raw HTTP POST data. When applicable, its external encoding must be “ASCII-8BIT” and it must be opened in binary mode, for Ruby 1.9 compatibility. The input stream must respond to gets, each, read and rewind.
25
+ class Stream
26
+ def initialize(input, output)
27
+ @input = input
28
+ @output = output
29
+
30
+ # Will hold remaining data in `#read`.
31
+ @buffer = nil
32
+ @closed = false
33
+ end
34
+
35
+ attr :input
36
+ attr :output
37
+
38
+ # rack.hijack_io must respond to:
39
+ # read, write, read_nonblock, write_nonblock, flush, close, close_read, close_write, closed?
40
+
41
+ # read behaves like IO#read. Its signature is read([length, [buffer]]). If given, length must be a non-negative Integer (>= 0) or nil, and buffer must be a String and may not be nil. If length is given and not nil, then this method reads at most length bytes from the input stream. If length is not given or nil, then this method reads all data until EOF. When EOF is reached, this method returns nil if length is given and not nil, or “” if length is not given or is nil. If buffer is given, then the read data will be placed into buffer instead of a newly created String object.
42
+ # @param length [Integer] the amount of data to read
43
+ # @param buffer [String] the buffer which will receive the data
44
+ # @return a buffer containing the data
45
+ def read(length = nil, buffer = nil)
46
+ buffer ||= Async::IO::Buffer.new
47
+ buffer.clear
48
+
49
+ until buffer.bytesize == length
50
+ @buffer = read_next if @buffer.nil?
51
+ break if @buffer.nil?
52
+
53
+ remaining_length = length - buffer.bytesize if length
54
+
55
+ if remaining_length && remaining_length < @buffer.bytesize
56
+ # We know that we are not going to reuse the original buffer.
57
+ # But byteslice will generate a hidden copy. So let's freeze it first:
58
+ @buffer.freeze
59
+
60
+ buffer << @buffer.byteslice(0, remaining_length)
61
+ @buffer = @buffer.byteslice(remaining_length, @buffer.bytesize)
62
+ else
63
+ buffer << @buffer
64
+ @buffer = nil
65
+ end
66
+ end
67
+
68
+ return nil if buffer.empty? && length && length > 0
69
+
70
+ return buffer
71
+ end
72
+
73
+ def read_nonblock(length, buffer = nil)
74
+ @buffer ||= read_next
75
+ chunk = nil
76
+
77
+ if @buffer.bytesize > length
78
+ chunk = @buffer.byteslice(0, length)
79
+ @buffer = @buffer.byteslice(length, @buffer.bytesize)
80
+ else
81
+ chunk = @buffer
82
+ @buffer = nil
83
+ end
84
+
85
+ if buffer
86
+ buffer.replace(chunk)
87
+ else
88
+ buffer = chunk
89
+ end
90
+
91
+ return buffer
92
+ end
93
+
94
+ def write(buffer)
95
+ @output.write(buffer)
96
+ end
97
+
98
+ alias write_nonblock write
99
+
100
+ def flush
101
+ end
102
+
103
+ def close_read
104
+ @input&.close
105
+ end
106
+
107
+ # close must never be called on the input stream. huh?
108
+ def close_write
109
+ @output&.close
110
+ end
111
+
112
+ # Close the input and output bodies.
113
+ def close
114
+ self.close_read
115
+ self.close_write
116
+ ensure
117
+ @closed = true
118
+ end
119
+
120
+ # Whether the stream has been closed.
121
+ def closed?
122
+ @closed
123
+ end
124
+
125
+ # Whether there are any output chunks remaining?
126
+ def empty?
127
+ @output.empty?
128
+ end
129
+
130
+ private
131
+
132
+ def read_next
133
+ if chunk = @input&.read
134
+ return chunk
135
+ else
136
+ @input = nil
137
+ return nil
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,83 @@
1
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
+ #
3
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ # of this software and associated documentation files (the "Software"), to deal
5
+ # in the Software without restriction, including without limitation the rights
6
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ # copies of the Software, and to permit persons to whom the Software is
8
+ # furnished to do so, subject to the following conditions:
9
+ #
10
+ # The above copyright notice and this permission notice shall be included in
11
+ # all copies or substantial portions of the Software.
12
+ #
13
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ # THE SOFTWARE.
20
+
21
+ require_relative 'wrapper'
22
+
23
+ module Protocol
24
+ module HTTP
25
+ module Body
26
+ # Invokes a callback once the body has finished reading.
27
+ class Streamable < Wrapper
28
+ def self.wrap(message, &block)
29
+ if message and body = message.body
30
+ if remaining = body.length
31
+ remaining = Integer(remaining)
32
+ end
33
+
34
+ message.body = self.new(message.body, block, remaining)
35
+ else
36
+ yield
37
+ end
38
+ end
39
+
40
+ def initialize(body, callback, remaining = nil)
41
+ super(body)
42
+
43
+ @callback = callback
44
+ @remaining = remaining
45
+ end
46
+
47
+ def finish
48
+ if @body
49
+ result = super
50
+
51
+ @callback.call
52
+
53
+ @body = nil
54
+
55
+ return result
56
+ end
57
+ end
58
+
59
+ def close(*)
60
+ if @body
61
+ super
62
+
63
+ @callback.call
64
+
65
+ @body = nil
66
+ end
67
+ end
68
+
69
+ def read
70
+ if chunk = super
71
+ @remaining -= chunk.bytesize if @remaining
72
+ else
73
+ if @remaining and @remaining > 0
74
+ raise EOFError, "Expected #{@remaining} more bytes!"
75
+ end
76
+ end
77
+
78
+ return chunk
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end