tp2 0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0722207c7f88a7f070a40e0257c41c557a1e59c7c3dc7768eefc9aed52fd7c04
4
+ data.tar.gz: 80003148244f0278b56cf4fa99f966d9d4cfc88a72b69744c8120e91c70f74e6
5
+ SHA512:
6
+ metadata.gz: 1efa96a8ae2adfd2a4cd46bb0086e927cbe5c54819d68ff2051096130ecbdae89b7ff73501d7c058962bce931471961e74bcc3c53b190f07b83b5ddc6d3a36ec
7
+ data.tar.gz: fd111961e0c152b30cafcf22b718c541c8d977e7bf6bd0219b5a17fc6bcae55b9d95d9b2ed3d123d9711ec0111e56aeb913675c06de235da2a8853b3d08fd739
data/.gitignore ADDED
@@ -0,0 +1,56 @@
1
+ *.gem
2
+ *.rbc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
48
+ # Gemfile.lock
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
data/Gemfile ADDED
@@ -0,0 +1,12 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec name: 'tp2'
4
+
5
+ group :development do
6
+ gem 'listen', '3.9.0'
7
+ gem 'minitest', '5.25.4'
8
+ gem 'rake', '13.2.1'
9
+ gem 'rake-compiler', '1.2.9'
10
+ gem 'simplecov', '0.22.0'
11
+ gem 'yard', '0.9.37'
12
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,64 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ tp2 (0.1)
5
+ http_parser.rb (= 0.8.0)
6
+ qeweney (= 0.21)
7
+ uringmachine (= 0.5.1)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ docile (1.4.1)
13
+ escape_utils (1.3.0)
14
+ ffi (1.17.1-aarch64-linux-gnu)
15
+ ffi (1.17.1-aarch64-linux-musl)
16
+ ffi (1.17.1-arm-linux-gnu)
17
+ ffi (1.17.1-arm-linux-musl)
18
+ ffi (1.17.1-arm64-darwin)
19
+ ffi (1.17.1-x86_64-darwin)
20
+ ffi (1.17.1-x86_64-linux-gnu)
21
+ ffi (1.17.1-x86_64-linux-musl)
22
+ http_parser.rb (0.8.0)
23
+ listen (3.9.0)
24
+ rb-fsevent (~> 0.10, >= 0.10.3)
25
+ rb-inotify (~> 0.9, >= 0.9.10)
26
+ minitest (5.25.4)
27
+ qeweney (0.21)
28
+ escape_utils (= 1.3.0)
29
+ rake (13.2.1)
30
+ rake-compiler (1.2.9)
31
+ rake
32
+ rb-fsevent (0.11.2)
33
+ rb-inotify (0.11.1)
34
+ ffi (~> 1.0)
35
+ simplecov (0.22.0)
36
+ docile (~> 1.1)
37
+ simplecov-html (~> 0.11)
38
+ simplecov_json_formatter (~> 0.1)
39
+ simplecov-html (0.13.1)
40
+ simplecov_json_formatter (0.1.4)
41
+ uringmachine (0.5.1)
42
+ yard (0.9.37)
43
+
44
+ PLATFORMS
45
+ aarch64-linux-gnu
46
+ aarch64-linux-musl
47
+ arm-linux-gnu
48
+ arm-linux-musl
49
+ arm64-darwin
50
+ x86_64-darwin
51
+ x86_64-linux-gnu
52
+ x86_64-linux-musl
53
+
54
+ DEPENDENCIES
55
+ listen (= 3.9.0)
56
+ minitest (= 5.25.4)
57
+ rake (= 13.2.1)
58
+ rake-compiler (= 1.2.9)
59
+ simplecov (= 0.22.0)
60
+ tp2!
61
+ yard (= 0.9.37)
62
+
63
+ BUNDLED WITH
64
+ 2.6.2
data/README.md ADDED
@@ -0,0 +1,5 @@
1
+ # tp2
2
+
3
+ TP2 is an experimental HTTP server based on
4
+ [UringMachine](https://github.com/digital-fabric/uringmachine) and [Qeweney](https://github.com/digital-fabric/qeweney).
5
+
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rake/testtask"
4
+
5
+ task :default => [:test]
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << "test"
9
+ t.test_files = FileList["test/**/test_*.rb"]
10
+ end
11
+
12
+ task :release do
13
+ require_relative './lib/tp2/version'
14
+ version = TP2::VERSION
15
+
16
+ puts 'Building tp2...'
17
+ `gem build tp2.gemspec`
18
+
19
+ puts "Pushing tp2 #{version}..."
20
+ `gem push tp2-#{version}.gem`
21
+
22
+ puts "Cleaning up..."
23
+ `rm *.gem`
24
+ end
data/TODO.md ADDED
@@ -0,0 +1 @@
1
+ - Write some tests.
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/inline'
4
+
5
+ gemfile do
6
+ gem 'tp2', path: '.'
7
+ end
8
+
9
+ require 'tp2'
10
+
11
+ app = ->(req) {
12
+ req.respond('foobar', ':status' => Qeweney::Status::TEAPOT)
13
+ }
14
+
15
+ machine = UM.new
16
+ server = TP2::Server.new(machine, '0.0.0.0', 1234, &app)
17
+
18
+ machine.spin { server.run }
19
+
20
+ main = Fiber.current
21
+ trap('SIGINT') { machine.schedule(main, nil) }
22
+
23
+ puts "Running... (pid: #{Process.pid})"
24
+ STDOUT.flush
25
+ machine.yield # wait for termination
26
+ puts
data/lib/tp2/http.rb ADDED
@@ -0,0 +1,188 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'http/parser'
4
+ require 'qeweney'
5
+
6
+ module TP2
7
+ # Encapsulates a HTTP/1 connection
8
+ class HTTP1Connection # rubocop:disable Metrics/ClassLength
9
+ SEND_FLAGS = UM::MSG_NOSIGNAL | UM::MSG_WAITALL
10
+
11
+ def initialize(machine, fd, bgid, &app) # rubocop:disable Naming/MethodParameterName
12
+ @machine = machine
13
+ @fd = fd
14
+ @bgid = bgid
15
+ @parser = Http::Parser.new(self)
16
+ @app = app
17
+ end
18
+
19
+ def on_headers_complete(headers) # rubocop:disable Metrics/MethodLength
20
+ headers = normalize_headers(headers)
21
+ headers[':path'] = @parser.request_url
22
+ headers[':method'] = @parser.http_method.downcase
23
+ headers[':scheme'] = get_scheme_from_headers(headers)
24
+ headers[':protocol'] = "http/#{@parser.http_major}.#{@parser.http_minor}"
25
+ @request = Qeweney::Request.new(headers, self)
26
+ end
27
+
28
+ def get_scheme_from_headers(headers)
29
+ if (proto = headers['x-forwarded-proto'])
30
+ proto.downcase
31
+ else
32
+ 'http'
33
+ end
34
+ end
35
+
36
+ def normalize_headers(headers)
37
+ headers.each_with_object({}) do |(k, v), h|
38
+ k = k.downcase
39
+ hk = h[k]
40
+ if hk
41
+ hk = h[k] = [hk] unless hk.is_a?(Array)
42
+ v.is_a?(Array) ? hk.concat(v) : hk << v
43
+ else
44
+ h[k] = v
45
+ end
46
+ end
47
+ end
48
+
49
+ def on_body(chunk)
50
+ @request.buffer_body_chunk(chunk)
51
+ end
52
+
53
+ def on_message_complete
54
+ @done = @request.headers[':protocol'] != 'http/1.1'
55
+ @app.call(@request)
56
+ end
57
+
58
+ def run
59
+ @machine.recv_each(@fd, @bgid, 0) do |buf|
60
+ @parser << buf
61
+ break if @done
62
+ end
63
+ rescue SystemCallError
64
+ # do nothing
65
+ ensure
66
+ @machine.close(@fd)
67
+ end
68
+
69
+ # response API
70
+
71
+ CRLF = "\r\n"
72
+ ZERO_CRLF_CRLF = "0\r\n\r\n"
73
+ CRLF_ZERO_CRLF_CRLF = "\r\n0\r\n\r\n"
74
+
75
+ # Sends response including headers and body. Waits for the request to complete
76
+ # if not yet completed. The body is sent using chunked transfer encoding.
77
+ # @param request [Qeweney::Request] HTTP request
78
+ # @param body [String] response body
79
+ # @param headers
80
+ def respond(_request, body, headers)
81
+ formatted_headers = format_headers(headers, body, false)
82
+ # request.tx_incr(formatted_headers.bytesize + (body ? body.bytesize : 0))
83
+ if body
84
+ buf = formatted_headers + body
85
+ @machine.send(@fd, buf, buf.bytesize, SEND_FLAGS)
86
+ # handle_write(formatted_headers + body)
87
+ else
88
+ @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
89
+ end
90
+ end
91
+
92
+ # Sends response headers. If empty_response is truthy, the response status
93
+ # code will default to 204, otherwise to 200.
94
+ # @param request [Qeweney::Request] HTTP request
95
+ # @param headers [Hash] response headers
96
+ # @param empty_response [boolean] whether a response body will be sent
97
+ # @param chunked [boolean] whether to use chunked transfer encoding
98
+ # @return [void]
99
+ def send_headers(request, headers, empty_response: false, chunked: true)
100
+ formatted_headers = format_headers(headers, !empty_response, http1_1?(request) && chunked)
101
+ # request.tx_incr(formatted_headers.bytesize)
102
+ #
103
+ @machine.send(@fd, formatted_headers, formatted_headers.bytesize, SEND_FLAGS)
104
+ end
105
+
106
+ # Sends a response body chunk. If no headers were sent, default headers are
107
+ # sent using #send_headers. if the done option is true(thy), an empty chunk
108
+ # will be sent to signal response completion to the client.
109
+ # @param request [Qeweney::Request] HTTP request
110
+ # @param chunk [String] response body chunk
111
+ # @param done [boolean] whether the response is completed
112
+ # @return [void]
113
+ def send_chunk(_request, chunk, done: false)
114
+ data = +''
115
+ data << "#{chunk.bytesize.to_s(16)}\r\n#{chunk}\r\n" if chunk
116
+ data << "0\r\n\r\n" if done
117
+ return if data.empty?
118
+
119
+ # request.tx_incr(data.bytesize)
120
+ @machine.send(@fd, data, data.bytesize, SEND_FLAGS)
121
+ end
122
+
123
+ # Finishes the response to the current request. If no headers were sent,
124
+ # default headers are sent using #send_headers.
125
+ # @return [void]
126
+ def finish(_request)
127
+ # request.tx_incr(5)
128
+ @machine.send(@fd, ZERO_CRLF_CRLF, ZERO_CRLF_CRLF.bytesize, SEND_FLAGS)
129
+ end
130
+
131
+ def http1_1?(request)
132
+ request.headers[':protocol'] == 'http/1.1'
133
+ end
134
+
135
+ INTERNAL_HEADER_REGEXP = /^:/.freeze
136
+
137
+ # Formats response headers into an array. If empty_response is true(thy),
138
+ # the response status code will default to 204, otherwise to 200.
139
+ # @param headers [Hash] response headers
140
+ # @param body [boolean] whether a response body will be sent
141
+ # @param chunked [boolean] whether to use chunked transfer encoding
142
+ # @return [String] formatted response headers
143
+ def format_headers(headers, body, chunked)
144
+ status = headers[':status']
145
+ status ||= (body ? Qeweney::Status::OK : Qeweney::Status::NO_CONTENT)
146
+ lines = format_status_line(body, status, chunked)
147
+ headers.each do |k, v|
148
+ next if k =~ INTERNAL_HEADER_REGEXP
149
+
150
+ collect_header_lines(lines, k, v)
151
+ end
152
+ lines << CRLF
153
+ lines
154
+ end
155
+
156
+ def format_status_line(body, status, chunked)
157
+ if !body
158
+ empty_status_line(status)
159
+ else
160
+ with_body_status_line(status, body, chunked)
161
+ end
162
+ end
163
+
164
+ def empty_status_line(status)
165
+ if status == 204
166
+ +"HTTP/1.1 #{status}\r\n"
167
+ else
168
+ +"HTTP/1.1 #{status}\r\nContent-Length: 0\r\n"
169
+ end
170
+ end
171
+
172
+ def with_body_status_line(status, body, chunked)
173
+ if chunked
174
+ +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
175
+ else
176
+ +"HTTP/1.1 #{status}\r\nContent-Length: #{body.is_a?(String) ? body.bytesize : body.to_i}\r\n"
177
+ end
178
+ end
179
+
180
+ def collect_header_lines(lines, key, value)
181
+ if value.is_a?(Array)
182
+ value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
183
+ else
184
+ lines << "#{key}: #{value}\r\n"
185
+ end
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ p "request_extensions"
4
+
5
+ class Qeweney::Request
6
+ def serve_io(io, opts)
7
+ # TODO: implement using UM
8
+
9
+ raise NotImplementedError
10
+ end
11
+ end
data/lib/tp2/server.rb ADDED
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'tp2/http'
4
+ require 'tp2/request_extensions'
5
+
6
+ module TP2
7
+ class Server
8
+ def initialize(machine, hostname, port, &app)
9
+ @machine = machine
10
+ @hostname = hostname
11
+ @port = port
12
+ @app = app
13
+ end
14
+
15
+ def setup
16
+ @server_fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
17
+ @machine.setsockopt(@server_fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
18
+ @machine.bind(@server_fd, @hostname, @port)
19
+ @machine.listen(@server_fd, UM::SOMAXCONN)
20
+
21
+ @bgid = @machine.setup_buffer_ring(4096, 1024)
22
+ puts "Listening on #{@hostname}:#{@port}"
23
+ end
24
+
25
+ def run
26
+ setup
27
+ @machine.accept_each(@server_fd) do |fd|
28
+ conn = HTTP1Connection.new(@machine, fd, @bgid, &@app)
29
+ @machine.spin(conn) { it.run }
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,3 @@
1
+ module TP2
2
+ VERSION = '0.1'
3
+ end
data/lib/tp2.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uringmachine'
4
+ require 'tp2/server'
data/tp2.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ require_relative './lib/tp2/version'
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'tp2'
5
+ s.summary = ''
6
+ s.version = TP2::VERSION
7
+ s.licenses = ['MIT']
8
+ s.author = 'Sharon Rosner'
9
+ s.email = 'sharon@noteflakes.com'
10
+ s.files = `git ls-files`.split
11
+ s.homepage = 'https://github.com/noteflakes/tp2'
12
+ s.metadata = {
13
+ 'homepage_uri' => 'https://github.com/noteflakes/tp2',
14
+ 'documentation_uri' => 'https://www.rubydoc.info/gems/tp2',
15
+ 'changelog_uri' => 'https://github.com/noteflakes/tp2/blob/master/CHANGELOG.md'
16
+ }
17
+ s.rdoc_options = ['--title', 'TP2', '--main', 'README.md']
18
+ s.extra_rdoc_files = ['README.md']
19
+ s.require_paths = ['lib']
20
+ s.required_ruby_version = '>= 3.4'
21
+
22
+ s.add_dependency 'http_parser.rb', '0.8.0'
23
+ s.add_dependency 'uringmachine', '0.5.1'
24
+ s.add_dependency 'qeweney', '0.21'
25
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tp2
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ platform: ruby
6
+ authors:
7
+ - Sharon Rosner
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 2025-02-22 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: http_parser.rb
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - '='
17
+ - !ruby/object:Gem::Version
18
+ version: 0.8.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - '='
24
+ - !ruby/object:Gem::Version
25
+ version: 0.8.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: uringmachine
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - '='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.5.1
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - '='
38
+ - !ruby/object:Gem::Version
39
+ version: 0.5.1
40
+ - !ruby/object:Gem::Dependency
41
+ name: qeweney
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - '='
45
+ - !ruby/object:Gem::Version
46
+ version: '0.21'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - '='
52
+ - !ruby/object:Gem::Version
53
+ version: '0.21'
54
+ email: sharon@noteflakes.com
55
+ executables: []
56
+ extensions: []
57
+ extra_rdoc_files:
58
+ - README.md
59
+ files:
60
+ - ".gitignore"
61
+ - Gemfile
62
+ - Gemfile.lock
63
+ - README.md
64
+ - Rakefile
65
+ - TODO.md
66
+ - examples/simple.rb
67
+ - lib/tp2.rb
68
+ - lib/tp2/http.rb
69
+ - lib/tp2/request_extensions.rb
70
+ - lib/tp2/server.rb
71
+ - lib/tp2/version.rb
72
+ - tp2.gemspec
73
+ homepage: https://github.com/noteflakes/tp2
74
+ licenses:
75
+ - MIT
76
+ metadata:
77
+ homepage_uri: https://github.com/noteflakes/tp2
78
+ documentation_uri: https://www.rubydoc.info/gems/tp2
79
+ changelog_uri: https://github.com/noteflakes/tp2/blob/master/CHANGELOG.md
80
+ rdoc_options:
81
+ - "--title"
82
+ - TP2
83
+ - "--main"
84
+ - README.md
85
+ require_paths:
86
+ - lib
87
+ required_ruby_version: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '3.4'
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ requirements: []
98
+ rubygems_version: 3.6.2
99
+ specification_version: 4
100
+ summary: ''
101
+ test_files: []