itsi-server 0.1.1 → 0.1.18

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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/CODE_OF_CONDUCT.md +7 -0
  4. data/Cargo.lock +3937 -0
  5. data/Cargo.toml +7 -0
  6. data/README.md +4 -0
  7. data/Rakefile +8 -1
  8. data/_index.md +6 -0
  9. data/exe/itsi +141 -46
  10. data/ext/itsi_error/Cargo.toml +3 -0
  11. data/ext/itsi_error/src/lib.rs +98 -24
  12. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  13. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  14. data/ext/itsi_error/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  15. data/ext/itsi_error/target/debug/build/rb-sys-49f554618693db24/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  16. data/ext/itsi_error/target/debug/incremental/itsi_error-1mmt5sux7jb0i/s-h510z7m8v9-0bxu7yd.lock +0 -0
  17. data/ext/itsi_error/target/debug/incremental/itsi_error-2vn3jey74oiw0/s-h5113n0e7e-1v5qzs6.lock +0 -0
  18. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510ykifhe-0tbnep2.lock +0 -0
  19. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510yyocpj-0tz7ug7.lock +0 -0
  20. data/ext/itsi_error/target/debug/incremental/itsi_error-37uv9dicz7awp/s-h510z0xc8g-14ol18k.lock +0 -0
  21. data/ext/itsi_error/target/debug/incremental/itsi_error-3g5qf4y7d54uj/s-h5113n0e7d-1trk8on.lock +0 -0
  22. data/ext/itsi_error/target/debug/incremental/itsi_error-3lpfftm45d3e2/s-h510z7m8r3-1pxp20o.lock +0 -0
  23. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510ykifek-1uxasnk.lock +0 -0
  24. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510yyocki-11u37qm.lock +0 -0
  25. data/ext/itsi_error/target/debug/incremental/itsi_error-3o4qownhl3d7n/s-h510z0xc93-0pmy0zm.lock +0 -0
  26. data/ext/itsi_instrument_entry/Cargo.toml +15 -0
  27. data/ext/itsi_instrument_entry/src/lib.rs +31 -0
  28. data/ext/itsi_rb_helpers/Cargo.toml +3 -0
  29. data/ext/itsi_rb_helpers/src/heap_value.rs +139 -0
  30. data/ext/itsi_rb_helpers/src/lib.rs +140 -10
  31. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/common.rs +355 -0
  32. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/dynamic.rs +276 -0
  33. data/ext/itsi_rb_helpers/target/debug/build/clang-sys-da71b0344e568175/out/macros.rs +49 -0
  34. data/ext/itsi_rb_helpers/target/debug/build/rb-sys-eb9ed4ff3a60f995/out/bindings-0.9.110-mri-arm64-darwin23-3.4.2.rs +8865 -0
  35. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-040pxg6yhb3g3/s-h5113n7a1b-03bwlt4.lock +0 -0
  36. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h51113xnh3-1eik1ip.lock +0 -0
  37. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-131g1u4dzkt1a/s-h5111704jj-0g4rj8x.lock +0 -0
  38. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-1q2d3drtxrzs5/s-h5113n79yl-0bxcqc5.lock +0 -0
  39. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h51113xoox-10de2hp.lock +0 -0
  40. data/ext/itsi_rb_helpers/target/debug/incremental/itsi_rb_helpers-374a9h7ovycj0/s-h5111704w7-0vdq7gq.lock +0 -0
  41. data/ext/itsi_scheduler/Cargo.toml +24 -0
  42. data/ext/itsi_scheduler/src/itsi_scheduler/io_helpers.rs +56 -0
  43. data/ext/itsi_scheduler/src/itsi_scheduler/io_waiter.rs +44 -0
  44. data/ext/itsi_scheduler/src/itsi_scheduler/timer.rs +44 -0
  45. data/ext/itsi_scheduler/src/itsi_scheduler.rs +308 -0
  46. data/ext/itsi_scheduler/src/lib.rs +38 -0
  47. data/ext/itsi_server/Cargo.lock +2956 -0
  48. data/ext/itsi_server/Cargo.toml +72 -14
  49. data/ext/itsi_server/extconf.rb +1 -1
  50. data/ext/itsi_server/src/default_responses/html/401.html +68 -0
  51. data/ext/itsi_server/src/default_responses/html/403.html +68 -0
  52. data/ext/itsi_server/src/default_responses/html/404.html +68 -0
  53. data/ext/itsi_server/src/default_responses/html/413.html +71 -0
  54. data/ext/itsi_server/src/default_responses/html/429.html +68 -0
  55. data/ext/itsi_server/src/default_responses/html/500.html +71 -0
  56. data/ext/itsi_server/src/default_responses/html/502.html +71 -0
  57. data/ext/itsi_server/src/default_responses/html/503.html +68 -0
  58. data/ext/itsi_server/src/default_responses/html/504.html +69 -0
  59. data/ext/itsi_server/src/default_responses/html/index.html +238 -0
  60. data/ext/itsi_server/src/default_responses/json/401.json +6 -0
  61. data/ext/itsi_server/src/default_responses/json/403.json +6 -0
  62. data/ext/itsi_server/src/default_responses/json/404.json +6 -0
  63. data/ext/itsi_server/src/default_responses/json/413.json +6 -0
  64. data/ext/itsi_server/src/default_responses/json/429.json +6 -0
  65. data/ext/itsi_server/src/default_responses/json/500.json +6 -0
  66. data/ext/itsi_server/src/default_responses/json/502.json +6 -0
  67. data/ext/itsi_server/src/default_responses/json/503.json +6 -0
  68. data/ext/itsi_server/src/default_responses/json/504.json +6 -0
  69. data/ext/itsi_server/src/default_responses/mod.rs +11 -0
  70. data/ext/itsi_server/src/env.rs +43 -0
  71. data/ext/itsi_server/src/lib.rs +132 -40
  72. data/ext/itsi_server/src/prelude.rs +2 -0
  73. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/big_bytes.rs +109 -0
  74. data/ext/itsi_server/src/ruby_types/itsi_body_proxy/mod.rs +143 -0
  75. data/ext/itsi_server/src/ruby_types/itsi_grpc_call.rs +344 -0
  76. data/ext/itsi_server/src/ruby_types/itsi_grpc_response_stream/mod.rs +264 -0
  77. data/ext/itsi_server/src/ruby_types/itsi_http_request.rs +345 -0
  78. data/ext/itsi_server/src/ruby_types/itsi_http_response.rs +391 -0
  79. data/ext/itsi_server/src/ruby_types/itsi_server/file_watcher.rs +225 -0
  80. data/ext/itsi_server/src/ruby_types/itsi_server/itsi_server_config.rs +375 -0
  81. data/ext/itsi_server/src/ruby_types/itsi_server.rs +83 -0
  82. data/ext/itsi_server/src/ruby_types/mod.rs +48 -0
  83. data/ext/itsi_server/src/server/binds/bind.rs +201 -0
  84. data/ext/itsi_server/src/server/binds/bind_protocol.rs +37 -0
  85. data/ext/itsi_server/src/server/binds/listener.rs +432 -0
  86. data/ext/itsi_server/src/server/binds/mod.rs +4 -0
  87. data/ext/itsi_server/src/server/binds/tls/locked_dir_cache.rs +132 -0
  88. data/ext/itsi_server/src/server/binds/tls.rs +270 -0
  89. data/ext/itsi_server/src/server/byte_frame.rs +32 -0
  90. data/ext/itsi_server/src/server/http_message_types.rs +97 -0
  91. data/ext/itsi_server/src/server/io_stream.rs +105 -0
  92. data/ext/itsi_server/src/server/lifecycle_event.rs +12 -0
  93. data/ext/itsi_server/src/server/middleware_stack/middleware.rs +165 -0
  94. data/ext/itsi_server/src/server/middleware_stack/middlewares/allow_list.rs +56 -0
  95. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_api_key.rs +87 -0
  96. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_basic.rs +86 -0
  97. data/ext/itsi_server/src/server/middleware_stack/middlewares/auth_jwt.rs +285 -0
  98. data/ext/itsi_server/src/server/middleware_stack/middlewares/cache_control.rs +142 -0
  99. data/ext/itsi_server/src/server/middleware_stack/middlewares/compression.rs +289 -0
  100. data/ext/itsi_server/src/server/middleware_stack/middlewares/cors.rs +292 -0
  101. data/ext/itsi_server/src/server/middleware_stack/middlewares/deny_list.rs +55 -0
  102. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response/default_responses.rs +190 -0
  103. data/ext/itsi_server/src/server/middleware_stack/middlewares/error_response.rs +157 -0
  104. data/ext/itsi_server/src/server/middleware_stack/middlewares/etag.rs +195 -0
  105. data/ext/itsi_server/src/server/middleware_stack/middlewares/header_interpretation.rs +82 -0
  106. data/ext/itsi_server/src/server/middleware_stack/middlewares/intrusion_protection.rs +201 -0
  107. data/ext/itsi_server/src/server/middleware_stack/middlewares/log_requests.rs +82 -0
  108. data/ext/itsi_server/src/server/middleware_stack/middlewares/max_body.rs +47 -0
  109. data/ext/itsi_server/src/server/middleware_stack/middlewares/mod.rs +87 -0
  110. data/ext/itsi_server/src/server/middleware_stack/middlewares/proxy.rs +414 -0
  111. data/ext/itsi_server/src/server/middleware_stack/middlewares/rate_limit.rs +131 -0
  112. data/ext/itsi_server/src/server/middleware_stack/middlewares/redirect.rs +76 -0
  113. data/ext/itsi_server/src/server/middleware_stack/middlewares/request_headers.rs +44 -0
  114. data/ext/itsi_server/src/server/middleware_stack/middlewares/response_headers.rs +36 -0
  115. data/ext/itsi_server/src/server/middleware_stack/middlewares/ruby_app.rs +126 -0
  116. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_assets.rs +180 -0
  117. data/ext/itsi_server/src/server/middleware_stack/middlewares/static_response.rs +55 -0
  118. data/ext/itsi_server/src/server/middleware_stack/middlewares/string_rewrite.rs +163 -0
  119. data/ext/itsi_server/src/server/middleware_stack/middlewares/token_source.rs +12 -0
  120. data/ext/itsi_server/src/server/middleware_stack/mod.rs +347 -0
  121. data/ext/itsi_server/src/server/mod.rs +12 -5
  122. data/ext/itsi_server/src/server/process_worker.rs +247 -0
  123. data/ext/itsi_server/src/server/request_job.rs +11 -0
  124. data/ext/itsi_server/src/server/serve_strategy/cluster_mode.rs +342 -0
  125. data/ext/itsi_server/src/server/serve_strategy/mod.rs +30 -0
  126. data/ext/itsi_server/src/server/serve_strategy/single_mode.rs +421 -0
  127. data/ext/itsi_server/src/server/signal.rs +76 -0
  128. data/ext/itsi_server/src/server/size_limited_incoming.rs +101 -0
  129. data/ext/itsi_server/src/server/thread_worker.rs +475 -0
  130. data/ext/itsi_server/src/services/cache_store.rs +74 -0
  131. data/ext/itsi_server/src/services/itsi_http_service.rs +239 -0
  132. data/ext/itsi_server/src/services/mime_types.rs +1416 -0
  133. data/ext/itsi_server/src/services/mod.rs +6 -0
  134. data/ext/itsi_server/src/services/password_hasher.rs +83 -0
  135. data/ext/itsi_server/src/services/rate_limiter.rs +569 -0
  136. data/ext/itsi_server/src/services/static_file_server.rs +1324 -0
  137. data/ext/itsi_tracing/Cargo.toml +5 -0
  138. data/ext/itsi_tracing/src/lib.rs +315 -7
  139. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0994n8rpvvt9m/s-h510hfz1f6-1kbycmq.lock +0 -0
  140. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-0bob7bf4yq34i/s-h5113125h5-0lh4rag.lock +0 -0
  141. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2fcodulrxbbxo/s-h510h2infk-0hp5kjw.lock +0 -0
  142. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2iak63r1woi1l/s-h510h2in4q-0kxfzw1.lock +0 -0
  143. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2kk4qj9gn5dg2/s-h5113124kv-0enwon2.lock +0 -0
  144. data/ext/itsi_tracing/target/debug/incremental/itsi_tracing-2mwo0yas7dtw4/s-h510hfz1ha-1udgpei.lock +0 -0
  145. data/lib/itsi/http_request/response_status_shortcodes.rb +74 -0
  146. data/lib/itsi/http_request.rb +186 -0
  147. data/lib/itsi/http_response.rb +41 -0
  148. data/lib/itsi/passfile.rb +109 -0
  149. data/lib/itsi/server/config/dsl.rb +565 -0
  150. data/lib/itsi/server/config.rb +166 -0
  151. data/lib/itsi/server/default_app/default_app.rb +34 -0
  152. data/lib/itsi/server/default_app/index.html +115 -0
  153. data/lib/itsi/server/default_config/Itsi-rackup.rb +119 -0
  154. data/lib/itsi/server/default_config/Itsi.rb +107 -0
  155. data/lib/itsi/server/grpc/grpc_call.rb +246 -0
  156. data/lib/itsi/server/grpc/grpc_interface.rb +100 -0
  157. data/lib/itsi/server/grpc/reflection/v1/reflection_pb.rb +26 -0
  158. data/lib/itsi/server/grpc/reflection/v1/reflection_services_pb.rb +122 -0
  159. data/lib/itsi/server/rack/handler/itsi.rb +27 -0
  160. data/lib/itsi/server/rack_interface.rb +94 -0
  161. data/lib/itsi/server/route_tester.rb +107 -0
  162. data/lib/itsi/server/scheduler_interface.rb +21 -0
  163. data/lib/itsi/server/scheduler_mode.rb +10 -0
  164. data/lib/itsi/server/signal_trap.rb +29 -0
  165. data/lib/itsi/server/typed_handlers/param_parser.rb +200 -0
  166. data/lib/itsi/server/typed_handlers/source_parser.rb +55 -0
  167. data/lib/itsi/server/typed_handlers.rb +17 -0
  168. data/lib/itsi/server/version.rb +1 -1
  169. data/lib/itsi/server.rb +160 -9
  170. data/lib/itsi/standard_headers.rb +86 -0
  171. data/lib/ruby_lsp/itsi/addon.rb +111 -0
  172. data/lib/shell_completions/completions.rb +26 -0
  173. metadata +182 -25
  174. data/ext/itsi_server/src/request/itsi_request.rs +0 -143
  175. data/ext/itsi_server/src/request/mod.rs +0 -1
  176. data/ext/itsi_server/src/server/bind.rs +0 -138
  177. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.crt +0 -32
  178. data/ext/itsi_server/src/server/itsi_ca/itsi_ca.key +0 -52
  179. data/ext/itsi_server/src/server/itsi_server.rs +0 -182
  180. data/ext/itsi_server/src/server/listener.rs +0 -218
  181. data/ext/itsi_server/src/server/tls.rs +0 -138
  182. data/ext/itsi_server/src/server/transfer_protocol.rs +0 -23
  183. data/ext/itsi_server/src/stream_writer/mod.rs +0 -21
  184. data/lib/itsi/request.rb +0 -39
@@ -0,0 +1,246 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "stringio"
4
+
5
+ module Itsi
6
+ class GrpcCall
7
+ attr_accessor :rpc_desc
8
+
9
+ def input_stream?
10
+ @input_stream ||= @rpc_desc&.input&.is_a?(GRPC::RpcDesc::Stream) || false
11
+ end
12
+
13
+ def output_stream?
14
+ @output_stream ||= @rpc_desc&.output&.is_a?(GRPC::RpcDesc::Stream) || false
15
+ end
16
+
17
+ def input_type
18
+ @input_type ||= input_stream? ? rpc_desc.input.type : rpc_desc.input
19
+ end
20
+
21
+ def output_type
22
+ @output_type ||= output_stream? ? rpc_desc.output.type : rpc_desc.output
23
+ end
24
+
25
+ def reader
26
+ @reader ||= IO.open(stream.reader_fileno, "rb")
27
+ end
28
+
29
+ def close
30
+ if output_stream? && content_type == "application/json"
31
+ stream.write("[") unless @opened
32
+ stream.write("]")
33
+ end
34
+
35
+ @reader&.close
36
+ stream.close
37
+ end
38
+
39
+ def deadline
40
+ return @deadline if defined?(@deadline)
41
+ return @deadline = nil unless timeout
42
+
43
+ @deadline = Time.now + timeout
44
+ end
45
+
46
+ def parse_from_json_stream(json_stream)
47
+ first_char = nil
48
+ loop do
49
+ char = json_stream.read(1)
50
+ break if char.nil?
51
+ if char =~ /\s/
52
+ next
53
+ elsif ["[", ","].include?(char)
54
+ first_char = char
55
+ break
56
+ elsif char == "]"
57
+ return nil
58
+ else
59
+ # If the first non-whitespace character is not '[' or comma, return nil.
60
+ return nil
61
+ end
62
+ end
63
+
64
+ return nil if first_char.nil?
65
+
66
+ # Step 2: Process objects until we hit the end of the JSON stream or array.
67
+ loop do
68
+ # Skip any whitespace or commas preceding an object.
69
+ char = nil
70
+ loop do
71
+ char = json_stream.read(1)
72
+ break if char.nil?
73
+ next if char =~ /\s/
74
+
75
+ break
76
+ end
77
+
78
+ # The next non-whitespace, non-comma character should be the start of an object.
79
+ return nil unless char == "{"
80
+
81
+ # Step 3: Start buffering the JSON object.
82
+ buffer = "{".dup
83
+ stack = ["{"]
84
+ in_string = false
85
+ escape = false
86
+
87
+ while stack.any?
88
+ ch = json_stream.read(1)
89
+ return nil if ch.nil? # premature end of stream
90
+
91
+ buffer << ch
92
+
93
+ if in_string
94
+ if escape
95
+ escape = false
96
+ next
97
+ end
98
+ if ch == "\\"
99
+ escape = true
100
+ elsif ch == '"'
101
+ in_string = false
102
+ end
103
+ elsif ch == '"'
104
+ in_string = true
105
+ elsif ["{", "["].include?(ch)
106
+ stack.push(ch)
107
+ elsif ["}", "]"].include?(ch)
108
+ expected = (ch == "}" ? "{" : "[")
109
+ # Check for matching bracket.
110
+ return nil unless stack.last == expected
111
+
112
+ stack.pop
113
+ end
114
+ end
115
+ # Yield the complete JSON object (as a string).
116
+ return buffer
117
+ end
118
+ end
119
+
120
+ def remote_read
121
+ if content_type == "application/json"
122
+ if input_stream?
123
+ if next_item = parse_from_json_stream(reader)
124
+ input_type.decode_json(next_item)
125
+ end
126
+ else
127
+ input_type.decode_json(reader.read)
128
+ end
129
+ else
130
+ header = reader.read(5)
131
+ return nil if header.nil? || header.bytesize < 5
132
+
133
+ compressed = header.bytes[0] == 1
134
+ length = header[1..4].unpack1("N")
135
+
136
+ data = reader.read(length)
137
+ return nil if data.nil?
138
+
139
+ data = decompress_input(data) if compressed
140
+
141
+ input_type.decode(data)
142
+ end
143
+ end
144
+
145
+ def send_framed_message(message_data, compressed = nil)
146
+ if content_type == "application/json"
147
+ if output_stream?
148
+ if @opened
149
+ stream.write(",\n")
150
+ else
151
+ stream.write("[")
152
+ @opened = true
153
+ end
154
+ end
155
+ message_data = output_type.encode_json(message_data)
156
+ stream.write(message_data)
157
+ else
158
+ message_data = output_type.encode(message_data)
159
+ should_compress = compressed.nil? ? should_compress_output?(message_data.bytesize) : compressed
160
+
161
+ if should_compress
162
+ message_data = compress_output(message_data)
163
+ compressed_flag = 1
164
+ else
165
+ compressed_flag = 0
166
+ end
167
+
168
+ message = [compressed_flag, message_data.bytesize].pack("CN") << message_data
169
+ stream.write(message)
170
+
171
+ @body_written = true
172
+ end
173
+ rescue IOError
174
+ close
175
+ end
176
+
177
+ def remote_send(response)
178
+ send_framed_message(response)
179
+ end
180
+
181
+ def deadline_exceeded?
182
+ @deadline && @deadline <= Time.now
183
+ end
184
+
185
+ def each_remote_read
186
+ return enum_for(:each_remote_read) unless block_given?
187
+
188
+ while (resp = remote_read) || !cancelled?
189
+
190
+ yield resp
191
+ end
192
+ end
193
+
194
+ def bidi_streamer?
195
+ input_stream? && output_stream?
196
+ end
197
+
198
+ def client_streamer?
199
+ input_stream? && !output_stream?
200
+ end
201
+
202
+ def server_streamer?
203
+ !input_stream? && output_stream?
204
+ end
205
+
206
+ def request_response?
207
+ !input_stream? && !output_stream?
208
+ end
209
+
210
+ def send_initial_metadata(kv_pairs)
211
+ add_headers(kv_pairs.transform_values { |v| Array(v) })
212
+ end
213
+
214
+ def send_empty
215
+ remote_send(output_type.new) unless @body_written
216
+ end
217
+
218
+ def send_status(status_code, status_details, trailing_metadata = {})
219
+ trailers = {
220
+ "grpc-status" => status_code.to_s
221
+ }
222
+
223
+ unless status_details.nil? || status_details.empty?
224
+ encoded_message = status_details.gsub(/[ #%&+,:;<=>?@\[\]^`{|}~\\"]/) do |c|
225
+ "%" + c.ord.to_s(16).upcase
226
+ end
227
+ trailers["grpc-message"] = encoded_message
228
+ end
229
+
230
+ trailing_metadata.each do |key, value|
231
+ trailers[key] = \
232
+ if key.to_s.end_with?("-bin")
233
+ Base64.strict_encode64(value.to_s)
234
+ else
235
+ value.to_s
236
+ end
237
+ end
238
+
239
+ send_trailers(trailers)
240
+ end
241
+
242
+ def send_trailers(trailers)
243
+ stream.send_trailers(trailers)
244
+ end
245
+ end
246
+ end
@@ -0,0 +1,100 @@
1
+ module Itsi
2
+ class Server
3
+ class GrpcInterface
4
+ DeadlineExceeded = Class.new(StandardError)
5
+
6
+ attr_accessor :service, :service_class
7
+
8
+ def self.for(service)
9
+ interface = new(service)
10
+ lambda do |request|
11
+ interface.handle_request(request)
12
+ end
13
+ end
14
+
15
+ def self.reflection_for(handlers)
16
+ require_relative "reflection/v1/reflection_services_pb"
17
+ interface = new(Grpc::Reflection::V1::ServerReflection::Service.new(handlers))
18
+ lambda do |request|
19
+ interface.handle_request(request)
20
+ end
21
+ end
22
+
23
+ def initialize(service)
24
+ @service = service
25
+ @service_class = service.class
26
+ @service_class.rpc_descs.transform_keys! do |k|
27
+ k.to_s.gsub(/([a-z])([A-Z])/, "\\1_\\2").downcase.to_sym
28
+ end
29
+ end
30
+
31
+ def handle_request(active_call)
32
+ unless (active_call.rpc_desc = service_class.rpc_descs[active_call.method_name])
33
+ active_call.stream.write("\n")
34
+ active_call.send_status(13, "Method not found")
35
+ active_call.close
36
+ return
37
+ end
38
+
39
+ active_call.send_initial_metadata(
40
+ {
41
+ "grpc-accept-encoding" => "gzip, deflate, identity",
42
+ "content-type" => active_call.json? ? "application/json" : "application/grpc"
43
+ }
44
+ )
45
+
46
+ begin
47
+ if active_call.bidi_streamer?
48
+ handle_bidi_streaming(active_call)
49
+ elsif active_call.client_streamer?
50
+ handle_client_streaming(active_call)
51
+ elsif active_call.server_streamer?
52
+ handle_server_streaming(active_call)
53
+ elsif active_call.request_response?
54
+ handle_unary(active_call)
55
+ end
56
+ active_call.send_status(0, "Success")
57
+ rescue Google::Protobuf::ParseError => e
58
+ active_call.send_empty
59
+ active_call.send_status(3, e.message)
60
+ rescue DeadlineExceeded => e
61
+ active_call.send_empty
62
+ active_call.send_status(4, e.message)
63
+ rescue StandardError => e
64
+ active_call.send_empty
65
+ active_call.send_status(13, e.message)
66
+ end
67
+ rescue StandardError => e
68
+ Itsi.log_warn("Unhandled error in grpc_interface: #{e.message}")
69
+ ensure
70
+ active_call.close
71
+ end
72
+
73
+ private
74
+
75
+ def handle_unary(active_call)
76
+ message = active_call.remote_read
77
+ response = service.send(active_call.method_name, message, active_call)
78
+ active_call.remote_send(response)
79
+ end
80
+
81
+ def handle_client_streaming(active_call)
82
+ response = service.send(active_call.method_name, active_call.each_remote_read, active_call)
83
+ active_call.remote_send(response)
84
+ end
85
+
86
+ def handle_server_streaming(active_call)
87
+ message = active_call.remote_read
88
+ service.send(active_call.method_name, message, active_call) do |response|
89
+ active_call.remote_send(response)
90
+ end
91
+ end
92
+
93
+ def handle_bidi_streaming(active_call)
94
+ service.send(active_call.method_name, active_call.each_remote_read, active_call) do |response|
95
+ active_call.remote_send(response)
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
+ # source: reflection.proto
4
+
5
+ require 'google/protobuf'
6
+ require "google/protobuf/descriptor_pb"
7
+
8
+ descriptor_data = "\n\x10reflection.proto\x12\x12grpc.reflection.v1\"\x85\x02\n\x17ServerReflectionRequest\x12\x0c\n\x04host\x18\x01 \x01(\t\x12\x1a\n\x10\x66ile_by_filename\x18\x03 \x01(\tH\x00\x12 \n\x16\x66ile_containing_symbol\x18\x04 \x01(\tH\x00\x12I\n\x19\x66ile_containing_extension\x18\x05 \x01(\x0b\x32$.grpc.reflection.v1.ExtensionRequestH\x00\x12\'\n\x1d\x61ll_extension_numbers_of_type\x18\x06 \x01(\tH\x00\x12\x17\n\rlist_services\x18\x07 \x01(\tH\x00\x42\x11\n\x0fmessage_request\"E\n\x10\x45xtensionRequest\x12\x17\n\x0f\x63ontaining_type\x18\x01 \x01(\t\x12\x18\n\x10\x65xtension_number\x18\x02 \x01(\x05\"\xb8\x03\n\x18ServerReflectionResponse\x12\x12\n\nvalid_host\x18\x01 \x01(\t\x12\x45\n\x10original_request\x18\x02 \x01(\x0b\x32+.grpc.reflection.v1.ServerReflectionRequest\x12N\n\x18\x66ile_descriptor_response\x18\x04 \x01(\x0b\x32*.grpc.reflection.v1.FileDescriptorResponseH\x00\x12U\n\x1e\x61ll_extension_numbers_response\x18\x05 \x01(\x0b\x32+.grpc.reflection.v1.ExtensionNumberResponseH\x00\x12I\n\x16list_services_response\x18\x06 \x01(\x0b\x32\'.grpc.reflection.v1.ListServiceResponseH\x00\x12;\n\x0e\x65rror_response\x18\x07 \x01(\x0b\x32!.grpc.reflection.v1.ErrorResponseH\x00\x42\x12\n\x10message_response\"7\n\x16\x46ileDescriptorResponse\x12\x1d\n\x15\x66ile_descriptor_proto\x18\x01 \x03(\x0c\"K\n\x17\x45xtensionNumberResponse\x12\x16\n\x0e\x62\x61se_type_name\x18\x01 \x01(\t\x12\x18\n\x10\x65xtension_number\x18\x02 \x03(\x05\"K\n\x13ListServiceResponse\x12\x34\n\x07service\x18\x01 \x03(\x0b\x32#.grpc.reflection.v1.ServiceResponse\"\x1f\n\x0fServiceResponse\x12\x0c\n\x04name\x18\x01 \x01(\t\":\n\rErrorResponse\x12\x12\n\nerror_code\x18\x01 \x01(\x05\x12\x15\n\rerror_message\x18\x02 \x01(\t2\x89\x01\n\x10ServerReflection\x12u\n\x14ServerReflectionInfo\x12+.grpc.reflection.v1.ServerReflectionRequest\x1a,.grpc.reflection.v1.ServerReflectionResponse(\x01\x30\x01\x42\x66\n\x15io.grpc.reflection.v1B\x15ServerReflectionProtoP\x01Z4google.golang.org/grpc/reflection/grpc_reflection_v1b\x06proto3"
9
+
10
+ pool = Google::Protobuf::DescriptorPool.generated_pool
11
+ pool.add_serialized_file(descriptor_data)
12
+
13
+ module Grpc
14
+ module Reflection
15
+ module V1
16
+ ServerReflectionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ServerReflectionRequest").msgclass
17
+ ExtensionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ExtensionRequest").msgclass
18
+ ServerReflectionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ServerReflectionResponse").msgclass
19
+ FileDescriptorResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.FileDescriptorResponse").msgclass
20
+ ExtensionNumberResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ExtensionNumberResponse").msgclass
21
+ ListServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ListServiceResponse").msgclass
22
+ ServiceResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ServiceResponse").msgclass
23
+ ErrorResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("grpc.reflection.v1.ErrorResponse").msgclass
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,122 @@
1
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # Source: reflection.proto for package 'grpc.reflection.v1'
3
+ # Original file comments:
4
+ # Copyright 2016 The gRPC Authors
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ # Service exported by server reflection. A more complete description of how
19
+ # server reflection works can be found at
20
+ # https://github.com/grpc/grpc/blob/master/doc/server-reflection.md
21
+ #
22
+ # The canonical version of this proto can be found at
23
+ # https://github.com/grpc/grpc-proto/blob/master/grpc/reflection/v1/reflection.proto
24
+ #
25
+
26
+ require "grpc"
27
+ require_relative "reflection_pb"
28
+
29
+ module Grpc
30
+ module Reflection
31
+ module V1
32
+ module ServerReflection
33
+ class Service
34
+ include ::GRPC::GenericService
35
+
36
+ self.marshal_class_method = :encode
37
+ self.unmarshal_class_method = :decode
38
+ self.service_name = "grpc.reflection.v1.ServerReflection"
39
+
40
+ # The reflection service is structured as a bidirectional stream, ensuring
41
+ # all related requests go to a single server.
42
+ rpc :ServerReflectionInfo, stream(::Grpc::Reflection::V1::ServerReflectionRequest),
43
+ stream(::Grpc::Reflection::V1::ServerReflectionResponse)
44
+
45
+ def initialize(handlers)
46
+ @handlers = handlers
47
+ super()
48
+ end
49
+
50
+ def server_reflection_info(req, _unused_call)
51
+ req.each do |request|
52
+ res = Grpc::Reflection::V1::ServerReflectionResponse.new
53
+
54
+ if !request.list_services.empty?
55
+ res.list_services_response = Grpc::Reflection::V1::ListServiceResponse.new(service: list_services_response)
56
+ elsif !request.file_containing_symbol.empty?
57
+ res.file_descriptor_response = Grpc::Reflection::V1::FileDescriptorResponse.new(
58
+ file_descriptor_proto: [
59
+ Google::Protobuf::FileDescriptorProto.encode(Google::Protobuf::DescriptorPool.generated_pool.lookup(request.file_containing_symbol).file_descriptor.to_proto)
60
+ ]
61
+ )
62
+ elsif !request.file_by_filename.empty?
63
+ # Handle file_by_filename requests
64
+ file_descriptor = find_file_descriptor_by_filename(request.file_by_filename)
65
+ if file_descriptor
66
+ res.file_descriptor_response = Grpc::Reflection::V1::FileDescriptorResponse.new(
67
+ file_descriptor_proto: [Google::Protobuf::FileDescriptorProto.encode(file_descriptor)]
68
+ )
69
+ else
70
+ res.error_response = Grpc::Reflection::V1::ErrorResponse.new(
71
+ error_code: 5, # NOT_FOUND
72
+ error_message: "File not found: #{request.file_by_filename}"
73
+ )
74
+ end
75
+ end
76
+ yield res
77
+ # We can loop here if running in Fiber mode, but for best compatibility with blocking IO modes
78
+ # we'll close the connection and force the client to reconnect force
79
+ # subsequent reflection requests
80
+ break
81
+ end
82
+ end
83
+
84
+ def find_file_descriptor_by_filename(filename)
85
+ # First try direct lookup in the pool
86
+
87
+ descriptor = Google::Protobuf::DescriptorPool.generated_pool.lookup(filename)&.file_descriptor
88
+ return descriptor.to_proto if descriptor
89
+
90
+ proto_name = convert_file_path_to_proto_name(filename)
91
+ descriptor = Google::Protobuf::DescriptorPool.generated_pool.lookup(proto_name)&.file_descriptor
92
+ return descriptor.to_proto if descriptor
93
+
94
+ nil
95
+ end
96
+
97
+ def convert_file_path_to_proto_name(file_path)
98
+ # Remove .proto extension
99
+ file_path = file_path.sub(/\.proto$/, "")
100
+
101
+ # Split path into parts
102
+ parts = file_path.split("/")
103
+
104
+ # Convert last part to PascalCase (e.g., money -> Money)
105
+ parts[-1] = parts[-1].split("_").map(&:capitalize).join
106
+
107
+ # Join with dots
108
+ parts.join(".")
109
+ end
110
+
111
+ def list_services_response
112
+ @list_services_response ||= @handlers.map(&:class).map(&:service_name).map do |name|
113
+ Grpc::Reflection::V1::ServiceResponse.new(name: name)
114
+ end
115
+ end
116
+ end
117
+
118
+ Stub = Service.rpc_stub_class
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,27 @@
1
+ return unless defined?(::Rackup::Handler) || defined?(Rack::Handler)
2
+
3
+ module Rack
4
+ module Handler
5
+ module Itsi
6
+ def self.run(app, options = {})
7
+ host = options.fetch(:host, "127.0.0.1")
8
+ port = options.fetch(:Port, 3001)
9
+ Itsi::Server.start(
10
+ {
11
+ app: app,
12
+ binds: ["http://#{host}:#{port}"]
13
+ },
14
+ Itsi::Server::Config.config_file_path
15
+ )
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ if defined?(Rackup)
22
+ ::Rackup::Handler.register("itsi", Rack::Handler::Itsi)
23
+ ::Rackup::Handler.register("Itsi", Rack::Handler::Itsi)
24
+ elsif defined?(Rack)
25
+ ::Rack::Handler.register("itsi", Rack::Handler::Itsi)
26
+ ::Rack::Handler.register("Itsi", Rack::Handler::Itsi)
27
+ end
@@ -0,0 +1,94 @@
1
+ module Itsi
2
+ class Server
3
+ module RackInterface
4
+
5
+ # Builds a handler proc that is compatible with Rack applications.
6
+ def self.for(app)
7
+ require "rack"
8
+ if app.is_a?(String)
9
+ dir = File.expand_path(File.dirname(app))
10
+ Dir.chdir(dir) do
11
+ loaded_app = ::Rack::Builder.parse_file(app)
12
+ app = loaded_app.is_a?(Array) ? loaded_app.first : loaded_app
13
+ end
14
+ end
15
+ lambda do |request|
16
+ Server.respond(request, app.call(request.to_rack_env))
17
+ end
18
+ end
19
+
20
+ # Interface to Rack applications.
21
+ # Here we build the env, and invoke the Rack app's call method.
22
+ # We then turn the Rack response into something Itsi server understands.
23
+ def call(app, request)
24
+ respond request, app.call(request.to_rack_env)
25
+ end
26
+
27
+ # Itsi responses are asynchronous and can be streamed.
28
+ # Response chunks are sent using response.send_frame
29
+ # and the response is finished using response.close_write.
30
+ # If only a single chunk is written, you can use the #send_and_close method.
31
+ def respond(request, (status, headers, body))
32
+ response = request.response
33
+
34
+ # Don't try and respond if we've been hijacked.
35
+ # The hijacker is now responsible for this.
36
+ return if request.hijacked
37
+
38
+ # 1. Set Status
39
+ response.status = status
40
+
41
+ # 2. Set Headers
42
+ body_streamer = streaming_body?(body) ? body : headers.delete("rack.hijack")
43
+ headers.each do |key, value|
44
+ unless value.is_a?(Array)
45
+ response[key] = value
46
+ next
47
+ end
48
+
49
+ value.each do |v|
50
+ response[key] = v
51
+ end
52
+ end
53
+
54
+ # 3. Set Body
55
+ # As soon as we start setting the response
56
+ # the server will begin to stream it to the client.
57
+
58
+ # If we're partially hijacked or returned a streaming body,
59
+ # stream this response.
60
+
61
+ if body_streamer
62
+ body_streamer.call(response)
63
+
64
+ # If we're enumerable with more than one chunk
65
+ # also stream, otherwise write in a single chunk
66
+ elsif body.respond_to?(:each) || body.respond_to?(:to_ary)
67
+ unless body.respond_to?(:each)
68
+ body = body.to_ary
69
+ raise "Body #to_ary didn't return an array" unless body.is_a?(Array)
70
+ end
71
+ # We offset this iteration intentionally,
72
+ # to optimize for the case where there's only one chunk.
73
+ buffer = nil
74
+ body.each do |part|
75
+ response << buffer.to_s if buffer
76
+ buffer = part
77
+ end
78
+
79
+ response.send_and_close(buffer.to_s)
80
+ else
81
+ response.send_and_close(body.to_s)
82
+ end
83
+ ensure
84
+ response.close_write
85
+ body.close if body.respond_to?(:close)
86
+ end
87
+
88
+ # A streaming body is one that responds to #call and not #each.
89
+ def streaming_body?(body)
90
+ body.respond_to?(:call) && !body.respond_to?(:each)
91
+ end
92
+ end
93
+ end
94
+ end