grpc-web 1.0.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.
- checksums.yaml +7 -0
- data/lib/grpc-web.rb +3 -0
- data/lib/grpc_web.rb +16 -0
- data/lib/grpc_web/client.rb +35 -0
- data/lib/grpc_web/client_executor.rb +81 -0
- data/lib/grpc_web/content_types.rb +19 -0
- data/lib/grpc_web/error_callback.rb +24 -0
- data/lib/grpc_web/grpc_request_processor.rb +51 -0
- data/lib/grpc_web/grpc_web_request.rb +11 -0
- data/lib/grpc_web/grpc_web_response.rb +5 -0
- data/lib/grpc_web/message_frame.rb +31 -0
- data/lib/grpc_web/message_framing.rb +50 -0
- data/lib/grpc_web/message_serialization.rb +83 -0
- data/lib/grpc_web/rack_app.rb +77 -0
- data/lib/grpc_web/rack_handler.rb +83 -0
- data/lib/grpc_web/text_coder.rb +30 -0
- data/lib/grpc_web/version.rb +5 -0
- metadata +207 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 1e933d5fb31c8a64203e667339b03fe89a5b1bcfb8ddecd66c0271bef8d60f0e
|
4
|
+
data.tar.gz: af6dca14b9a5ba0ada4c91f1c8cf534f408cf8be77180b2553d5418491b38908
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1e06359b9f0097781bf63e28b87e0c12746ab6c3e1a80b1d02a971420931f2f62d5d49b78b1629b70ae204e7ed9c5502346176661c4eff5fabcaec724665c602
|
7
|
+
data.tar.gz: 18cd177d9f793d9006ee23f1a28be8879971db60a1e970d7d77e90bb193ed324279d8de3f1bdd7c58a13e31359e9b95596a54a94f57fe7a2ca18b82930ae3141
|
data/lib/grpc-web.rb
ADDED
data/lib/grpc_web.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'grpc_web/version'
|
4
|
+
require 'grpc_web/rack_app'
|
5
|
+
|
6
|
+
module GRPCWeb
|
7
|
+
class << self
|
8
|
+
def rack_app
|
9
|
+
@rack_app ||= ::GRPCWeb::RackApp.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def handle(service_or_class, &lazy_init_block)
|
13
|
+
rack_app.handle(service_or_class, &lazy_init_block)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'uri'
|
4
|
+
require 'grpc_web/client_executor'
|
5
|
+
|
6
|
+
module GRPCWeb
|
7
|
+
class Client
|
8
|
+
attr_reader :base_url, :service_interface
|
9
|
+
|
10
|
+
def initialize(base_url, service_interface)
|
11
|
+
self.base_url = base_url
|
12
|
+
self.service_interface = service_interface
|
13
|
+
|
14
|
+
service_interface.rpc_descs.each do |rpc_method, rpc_desc|
|
15
|
+
define_rpc_method(rpc_method, rpc_desc)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_writer :base_url, :service_interface
|
22
|
+
|
23
|
+
def define_rpc_method(rpc_method, rpc_desc)
|
24
|
+
ruby_method = ::GRPC::GenericService.underscore(rpc_method.to_s).to_sym
|
25
|
+
define_singleton_method(ruby_method) do |params = {}|
|
26
|
+
uri = endpoint_uri(rpc_desc)
|
27
|
+
::GRPCWeb::ClientExecutor.request(uri, rpc_desc, params)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def endpoint_uri(rpc_desc)
|
32
|
+
URI(File.join(base_url, service_interface.service_name, rpc_desc.name.to_s))
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'net/http'
|
5
|
+
|
6
|
+
require 'grpc/errors'
|
7
|
+
require 'grpc_web/content_types'
|
8
|
+
require 'grpc_web/message_framing'
|
9
|
+
|
10
|
+
module GRPCWeb::ClientExecutor
|
11
|
+
class << self
|
12
|
+
include ::GRPCWeb::ContentTypes
|
13
|
+
|
14
|
+
GRPC_STATUS_HEADER = 'grpc-status'
|
15
|
+
GRPC_MESSAGE_HEADER = 'grpc-message'
|
16
|
+
|
17
|
+
def request(uri, rpc_desc, params = {})
|
18
|
+
req_proto = rpc_desc.input.new(params)
|
19
|
+
request_body = ::GRPCWeb::MessageFraming.frame_content(req_proto.to_proto)
|
20
|
+
|
21
|
+
resp = post_request(uri, request_body)
|
22
|
+
resp_body = handle_response(resp)
|
23
|
+
rpc_desc.output.decode(resp_body)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def request_headers
|
29
|
+
{
|
30
|
+
'Accept' => PROTO_CONTENT_TYPE,
|
31
|
+
'Content-Type' => PROTO_CONTENT_TYPE,
|
32
|
+
}
|
33
|
+
end
|
34
|
+
|
35
|
+
def post_request(uri, request_body)
|
36
|
+
request = Net::HTTP::Post.new(uri, request_headers)
|
37
|
+
request.body = request_body
|
38
|
+
if uri.userinfo
|
39
|
+
request.basic_auth uri.user, uri.password
|
40
|
+
end
|
41
|
+
|
42
|
+
Net::HTTP.start(uri.hostname, uri.port) do |http|
|
43
|
+
http.use_ssl = (uri.scheme == 'https')
|
44
|
+
http.request(request)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_response(resp)
|
49
|
+
unless resp.is_a?(Net::HTTPSuccess)
|
50
|
+
raise "Received #{resp.code} #{resp.message} response: #{resp.body}"
|
51
|
+
end
|
52
|
+
|
53
|
+
frames = ::GRPCWeb::MessageFraming.unframe_content(resp.body)
|
54
|
+
header_frame = frames.find(&:header?)
|
55
|
+
headers = parse_headers(header_frame.body) if header_frame
|
56
|
+
raise_on_error(headers)
|
57
|
+
frames.find(&:payload?).body
|
58
|
+
end
|
59
|
+
|
60
|
+
def parse_headers(header_str)
|
61
|
+
headers = {}
|
62
|
+
lines = header_str.split(/\r?\n/)
|
63
|
+
lines.each do |line|
|
64
|
+
key, value = line.split(':', 2)
|
65
|
+
headers[key] = value
|
66
|
+
end
|
67
|
+
headers
|
68
|
+
end
|
69
|
+
|
70
|
+
def raise_on_error(headers)
|
71
|
+
return unless headers
|
72
|
+
|
73
|
+
status_str = headers[GRPC_STATUS_HEADER]
|
74
|
+
status_code = status_str.to_i if status_str && status_str == status_str.to_i.to_s
|
75
|
+
|
76
|
+
if status_code && status_code != 0
|
77
|
+
raise ::GRPC::BadStatus.new_status_exception(status_code, headers[GRPC_MESSAGE_HEADER])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GRPCWeb::ContentTypes
|
4
|
+
DEFAULT_CONTENT_TYPE = 'application/grpc-web'
|
5
|
+
PROTO_CONTENT_TYPE = 'application/grpc-web+proto'
|
6
|
+
JSON_CONTENT_TYPE = 'application/grpc-web+json'
|
7
|
+
TEXT_CONTENT_TYPE = 'application/grpc-web-text'
|
8
|
+
TEXT_PROTO_CONTENT_TYPE = 'application/grpc-web-text+proto'
|
9
|
+
|
10
|
+
BASE64_CONTENT_TYPES = [TEXT_CONTENT_TYPE, TEXT_PROTO_CONTENT_TYPE].freeze
|
11
|
+
ALL_CONTENT_TYPES = [
|
12
|
+
DEFAULT_CONTENT_TYPE,
|
13
|
+
PROTO_CONTENT_TYPE,
|
14
|
+
JSON_CONTENT_TYPE,
|
15
|
+
TEXT_CONTENT_TYPE,
|
16
|
+
TEXT_PROTO_CONTENT_TYPE,
|
17
|
+
].freeze
|
18
|
+
ANY_CONTENT_TYPES = ['*/*', ''].freeze
|
19
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GRPCWeb
|
4
|
+
class << self
|
5
|
+
NOOP_ON_ERROR = proc { |ex, service, service_method| }
|
6
|
+
|
7
|
+
def on_error(&block)
|
8
|
+
if block_given?
|
9
|
+
|
10
|
+
unless block.parameters.length == 3
|
11
|
+
raise ArgumentError, 'callback must accept (exception, service, service_method)'
|
12
|
+
end
|
13
|
+
|
14
|
+
self.on_error_callback = block
|
15
|
+
else
|
16
|
+
on_error_callback || NOOP_ON_ERROR
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_accessor :on_error_callback
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'grpc_web/content_types'
|
4
|
+
require 'grpc_web/error_callback'
|
5
|
+
require 'grpc_web/grpc_web_response'
|
6
|
+
require 'grpc_web/message_framing'
|
7
|
+
require 'grpc_web/message_serialization'
|
8
|
+
require 'grpc_web/text_coder'
|
9
|
+
|
10
|
+
module GRPCWeb::GRPCRequestProcessor
|
11
|
+
class << self
|
12
|
+
include ::GRPCWeb::ContentTypes
|
13
|
+
|
14
|
+
def process(grpc_web_request)
|
15
|
+
text_coder = ::GRPCWeb::TextCoder
|
16
|
+
framing = ::GRPCWeb::MessageFraming
|
17
|
+
serialization = ::GRPCWeb::MessageSerialization
|
18
|
+
|
19
|
+
grpc_web_request = text_coder.decode_request(grpc_web_request)
|
20
|
+
grpc_web_request = framing.unframe_request(grpc_web_request)
|
21
|
+
grpc_web_request = serialization.deserialize_request(grpc_web_request)
|
22
|
+
grpc_web_response = execute_request(grpc_web_request)
|
23
|
+
grpc_web_response = serialization.serialize_response(grpc_web_response)
|
24
|
+
grpc_web_response = framing.frame_response(grpc_web_response)
|
25
|
+
text_coder.encode_response(grpc_web_response)
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def execute_request(request)
|
31
|
+
service_method = ::GRPC::GenericService.underscore(request.service_method.to_s)
|
32
|
+
|
33
|
+
begin
|
34
|
+
response = request.service.send(service_method, request.body)
|
35
|
+
rescue StandardError => e
|
36
|
+
::GRPCWeb.on_error.call(e, request.service, request.service_method)
|
37
|
+
response = e # Return exception as body if one is raised
|
38
|
+
end
|
39
|
+
::GRPCWeb::GRPCWebResponse.new(response_content_type(request), response)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Use Accept header value if specified, otherwise use request content type
|
43
|
+
def response_content_type(request)
|
44
|
+
if request.accept.nil? || ANY_CONTENT_TYPES.include?(request.accept)
|
45
|
+
request.content_type
|
46
|
+
else
|
47
|
+
request.accept
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GRPCWeb
|
4
|
+
class MessageFrame
|
5
|
+
PAYLOAD_FRAME_TYPE = 0 # String: "\x00"
|
6
|
+
HEADER_FRAME_TYPE = 128 # String: "\x80"
|
7
|
+
|
8
|
+
def self.payload_frame(body)
|
9
|
+
new(PAYLOAD_FRAME_TYPE, body)
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.header_frame(body)
|
13
|
+
new(HEADER_FRAME_TYPE, body)
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_accessor :frame_type, :body
|
17
|
+
|
18
|
+
def initialize(frame_type, body)
|
19
|
+
self.frame_type = frame_type
|
20
|
+
self.body = body
|
21
|
+
end
|
22
|
+
|
23
|
+
def payload?
|
24
|
+
frame_type == PAYLOAD_FRAME_TYPE
|
25
|
+
end
|
26
|
+
|
27
|
+
def header?
|
28
|
+
frame_type == HEADER_FRAME_TYPE
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'grpc_web/grpc_web_response'
|
4
|
+
require 'grpc_web/grpc_web_request'
|
5
|
+
require 'grpc_web/message_frame'
|
6
|
+
|
7
|
+
module GRPCWeb
|
8
|
+
# GRPC Web uses a simple 5 byte framing scheme. The first byte represents
|
9
|
+
# flags indicating what type of frame this is. The next 4 bytes indicate the
|
10
|
+
# byte length of the frame body.
|
11
|
+
module MessageFraming
|
12
|
+
class << self
|
13
|
+
def unframe_request(request)
|
14
|
+
frames = unframe_content(request.body)
|
15
|
+
::GRPCWeb::GRPCWebRequest.new(
|
16
|
+
request.service, request.service_method, request.content_type, request.accept, frames,
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
def frame_response(response)
|
21
|
+
framed = response.body.map do |frame|
|
22
|
+
frame_content(frame.body, frame.frame_type)
|
23
|
+
end.join
|
24
|
+
::GRPCWeb::GRPCWebResponse.new(response.content_type, framed)
|
25
|
+
end
|
26
|
+
|
27
|
+
def frame_content(content, frame_type = ::GRPCWeb::MessageFrame::PAYLOAD_FRAME_TYPE)
|
28
|
+
length_bytes = [content.bytesize].pack('N')
|
29
|
+
"#{frame_type.chr}#{length_bytes}#{content}"
|
30
|
+
end
|
31
|
+
|
32
|
+
def unframe_content(content)
|
33
|
+
frames = []
|
34
|
+
remaining_content = content
|
35
|
+
until remaining_content.empty?
|
36
|
+
msg_length = remaining_content[1..4].unpack1('N')
|
37
|
+
raise 'Invalid message length' if msg_length <= 0
|
38
|
+
|
39
|
+
frame_end = 5 + msg_length
|
40
|
+
frames << ::GRPCWeb::MessageFrame.new(
|
41
|
+
remaining_content[0].bytes[0],
|
42
|
+
remaining_content[5...frame_end],
|
43
|
+
)
|
44
|
+
remaining_content = remaining_content[frame_end..-1]
|
45
|
+
end
|
46
|
+
frames
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'grpc/core/status_codes'
|
4
|
+
require 'grpc/errors'
|
5
|
+
require 'grpc_web/content_types'
|
6
|
+
require 'grpc_web/grpc_web_response'
|
7
|
+
require 'grpc_web/grpc_web_request'
|
8
|
+
require 'grpc_web/message_frame'
|
9
|
+
|
10
|
+
module GRPCWeb::MessageSerialization
|
11
|
+
class << self
|
12
|
+
include ::GRPCWeb::ContentTypes
|
13
|
+
|
14
|
+
def deserialize_request(request)
|
15
|
+
service_class = request.service.class
|
16
|
+
request_proto_class = service_class.rpc_descs[request.service_method.to_sym].input
|
17
|
+
payload_frame = request.body.find(&:payload?)
|
18
|
+
|
19
|
+
if request.content_type == JSON_CONTENT_TYPE
|
20
|
+
request_proto = request_proto_class.decode_json(payload_frame.body)
|
21
|
+
else
|
22
|
+
request_proto = request_proto_class.decode(payload_frame.body)
|
23
|
+
end
|
24
|
+
|
25
|
+
::GRPCWeb::GRPCWebRequest.new(
|
26
|
+
request.service,
|
27
|
+
request.service_method,
|
28
|
+
request.content_type,
|
29
|
+
request.accept,
|
30
|
+
request_proto,
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
34
|
+
def serialize_response(response)
|
35
|
+
if response.body.is_a?(Exception)
|
36
|
+
serialize_error_response(response)
|
37
|
+
else
|
38
|
+
serialize_success_response(response)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def serialize_success_response(response)
|
45
|
+
if response.content_type == JSON_CONTENT_TYPE
|
46
|
+
payload = response.body.to_json
|
47
|
+
else
|
48
|
+
payload = response.body.to_proto
|
49
|
+
end
|
50
|
+
|
51
|
+
header_str = generate_headers(::GRPC::Core::StatusCodes::OK, 'OK')
|
52
|
+
payload_frame = ::GRPCWeb::MessageFrame.payload_frame(payload)
|
53
|
+
header_frame = ::GRPCWeb::MessageFrame.header_frame(header_str)
|
54
|
+
|
55
|
+
::GRPCWeb::GRPCWebResponse.new(response.content_type, [payload_frame, header_frame])
|
56
|
+
end
|
57
|
+
|
58
|
+
def serialize_error_response(response)
|
59
|
+
ex = response.body
|
60
|
+
if ex.is_a?(::GRPC::BadStatus)
|
61
|
+
header_str = generate_headers(ex.code, ex.details)
|
62
|
+
else
|
63
|
+
header_str = generate_headers(
|
64
|
+
::GRPC::Core::StatusCodes::UNKNOWN,
|
65
|
+
"#{ex.class}: #{ex.message}",
|
66
|
+
)
|
67
|
+
end
|
68
|
+
header_frame = ::GRPCWeb::MessageFrame.header_frame(header_str)
|
69
|
+
::GRPCWeb::GRPCWebResponse.new(response.content_type, [header_frame])
|
70
|
+
end
|
71
|
+
|
72
|
+
# If needed, trailers can be appended to the response as a 2nd
|
73
|
+
# base64 encoded string with independent framing.
|
74
|
+
def generate_headers(status, message)
|
75
|
+
header_str = [
|
76
|
+
"grpc-status:#{status}",
|
77
|
+
"grpc-message:#{message}",
|
78
|
+
'x-grpc-web:1',
|
79
|
+
nil, # for trailing newline
|
80
|
+
].join("\r\n")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/builder'
|
4
|
+
require 'grpc_web/rack_handler'
|
5
|
+
|
6
|
+
module GRPCWeb
|
7
|
+
class RackApp < ::Rack::Builder
|
8
|
+
# Can be given a service class, an instance of a service class, or a
|
9
|
+
# service interface class with a block to lazily initialize the service.
|
10
|
+
#
|
11
|
+
# Example 1:
|
12
|
+
# app.handle(TestHelloService)
|
13
|
+
#
|
14
|
+
# Example 2:
|
15
|
+
# app.handle(TestHelloService.new)
|
16
|
+
#
|
17
|
+
# Example 3:
|
18
|
+
# app.handle(HelloService::Service) do
|
19
|
+
# require 'test_hello_service'
|
20
|
+
# TestHelloService.new
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
def handle(service_or_class, &lazy_init_block)
|
24
|
+
service_class = service_or_class.is_a?(Class) ? service_or_class : service_or_class.class
|
25
|
+
validate_service_class(service_class)
|
26
|
+
service_config = lazy_init_block || service_or_class
|
27
|
+
|
28
|
+
service_class.rpc_descs.keys.each do |service_method|
|
29
|
+
add_service_method_to_app(service_class.service_name, service_config, service_method)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def validate_service_class(clazz)
|
36
|
+
unless clazz.include?(::GRPC::GenericService)
|
37
|
+
raise(ArgumentError, "#{clazz} must 'include GenericService'")
|
38
|
+
end
|
39
|
+
if clazz.rpc_descs.size.zero?
|
40
|
+
raise(ArgumentError, "#{clazz} should specify some rpc descriptions")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Map a path with Rack::Builder corresponding to the service method
|
45
|
+
def add_service_method_to_app(service_name, service_config, service_method)
|
46
|
+
map("/#{service_name}/#{service_method}") do
|
47
|
+
run(RouteHandler.new(service_config, service_method))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class RouteHandler
|
52
|
+
def initialize(service_config, service_method)
|
53
|
+
self.service_config = service_config
|
54
|
+
self.service_method = service_method
|
55
|
+
end
|
56
|
+
|
57
|
+
def call(env)
|
58
|
+
::GRPCWeb::RackHandler.call(service, service_method, env)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
attr_accessor :service_config, :service_method
|
64
|
+
|
65
|
+
def service
|
66
|
+
case service_config
|
67
|
+
when Proc
|
68
|
+
service_config.call
|
69
|
+
when Class
|
70
|
+
service_config.new
|
71
|
+
else
|
72
|
+
service_config
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rack/request'
|
4
|
+
require 'grpc_web/content_types'
|
5
|
+
require 'grpc_web/error_callback'
|
6
|
+
require 'grpc_web/grpc_request_processor'
|
7
|
+
require 'grpc_web/grpc_web_request'
|
8
|
+
|
9
|
+
module GRPCWeb
|
10
|
+
module RackHandler
|
11
|
+
NOT_FOUND = 404
|
12
|
+
UNSUPPORTED_MEDIA_TYPE = 415
|
13
|
+
INTERNAL_SERVER_ERROR = 500
|
14
|
+
POST = 'POST'
|
15
|
+
ACCEPT_HEADER = 'HTTP_ACCEPT'
|
16
|
+
|
17
|
+
class << self
|
18
|
+
include ::GRPCWeb::ContentTypes
|
19
|
+
|
20
|
+
def call(service, service_method, env)
|
21
|
+
rack_request = Rack::Request.new(env)
|
22
|
+
return not_found_response(rack_request.path) unless post?(rack_request)
|
23
|
+
return unsupported_media_type_response unless valid_content_types?(rack_request)
|
24
|
+
|
25
|
+
request_format = rack_request.content_type
|
26
|
+
accept = rack_request.get_header(ACCEPT_HEADER)
|
27
|
+
body = rack_request.body.read
|
28
|
+
request = GRPCWebRequest.new(service, service_method, request_format, accept, body)
|
29
|
+
response = GRPCRequestProcessor.process(request)
|
30
|
+
|
31
|
+
[200, { 'Content-Type' => response.content_type }, [response.body]]
|
32
|
+
rescue Google::Protobuf::ParseError => e
|
33
|
+
invalid_response(e.message)
|
34
|
+
rescue StandardError => e
|
35
|
+
::GRPCWeb.on_error.call(e, service, service_method)
|
36
|
+
error_response
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def post?(rack_request)
|
42
|
+
rack_request.request_method == POST
|
43
|
+
end
|
44
|
+
|
45
|
+
def valid_content_types?(rack_request)
|
46
|
+
return false unless ALL_CONTENT_TYPES.include?(rack_request.content_type)
|
47
|
+
|
48
|
+
accept = rack_request.get_header(ACCEPT_HEADER)
|
49
|
+
return true if ANY_CONTENT_TYPES.include?(accept)
|
50
|
+
|
51
|
+
ALL_CONTENT_TYPES.include?(accept)
|
52
|
+
end
|
53
|
+
|
54
|
+
def not_found_response(path)
|
55
|
+
[
|
56
|
+
NOT_FOUND,
|
57
|
+
{ 'Content-Type' => 'text/plain', 'X-Cascade' => 'pass' },
|
58
|
+
["Not Found: #{path}"],
|
59
|
+
]
|
60
|
+
end
|
61
|
+
|
62
|
+
def unsupported_media_type_response
|
63
|
+
[
|
64
|
+
UNSUPPORTED_MEDIA_TYPE,
|
65
|
+
{ 'Content-Type' => 'text/plain' },
|
66
|
+
['Unsupported Media Type: Invalid Content-Type or Accept header'],
|
67
|
+
]
|
68
|
+
end
|
69
|
+
|
70
|
+
def invalid_response(message)
|
71
|
+
[422, { 'Content-Type' => 'text/plain' }, ["Invalid request format: #{message}"]]
|
72
|
+
end
|
73
|
+
|
74
|
+
def error_response
|
75
|
+
[
|
76
|
+
INTERNAL_SERVER_ERROR,
|
77
|
+
{ 'Content-Type' => 'text/plain' },
|
78
|
+
['Request failed with an unexpected error.'],
|
79
|
+
]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'base64'
|
4
|
+
require 'grpc_web/content_types'
|
5
|
+
require 'grpc_web/grpc_web_response'
|
6
|
+
require 'grpc_web/grpc_web_request'
|
7
|
+
|
8
|
+
module GRPCWeb::TextCoder
|
9
|
+
class << self
|
10
|
+
include ::GRPCWeb::ContentTypes
|
11
|
+
|
12
|
+
def decode_request(request)
|
13
|
+
return request unless BASE64_CONTENT_TYPES.include?(request.content_type)
|
14
|
+
|
15
|
+
# Body can be several base64 "chunks" concatenated together
|
16
|
+
base64_chunks = request.body.scan(%r{[a-zA-Z0-9+/]+={0,2}})
|
17
|
+
decoded = base64_chunks.map { |chunk| Base64.decode64(chunk) }.join
|
18
|
+
::GRPCWeb::GRPCWebRequest.new(
|
19
|
+
request.service, request.service_method, request.content_type, request.accept, decoded,
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
def encode_response(response)
|
24
|
+
return response unless BASE64_CONTENT_TYPES.include?(response.content_type)
|
25
|
+
|
26
|
+
encoded = Base64.strict_encode64(response.body)
|
27
|
+
::GRPCWeb::GRPCWebResponse.new(response.content_type, encoded)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: grpc-web
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- James Shkolnik
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2020-02-10 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: grpc
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rack
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: 1.6.0
|
34
|
+
- - "<"
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '3.0'
|
37
|
+
type: :runtime
|
38
|
+
prerelease: false
|
39
|
+
version_requirements: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: 1.6.0
|
44
|
+
- - "<"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '3.0'
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: apparition
|
49
|
+
requirement: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
- !ruby/object:Gem::Dependency
|
62
|
+
name: pry-byebug
|
63
|
+
requirement: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - ">="
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
type: :development
|
69
|
+
prerelease: false
|
70
|
+
version_requirements: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
- !ruby/object:Gem::Dependency
|
76
|
+
name: rack-cors
|
77
|
+
requirement: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
type: :development
|
83
|
+
prerelease: false
|
84
|
+
version_requirements: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
- !ruby/object:Gem::Dependency
|
90
|
+
name: rake
|
91
|
+
requirement: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - ">="
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
type: :development
|
97
|
+
prerelease: false
|
98
|
+
version_requirements: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - ">="
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0'
|
103
|
+
- !ruby/object:Gem::Dependency
|
104
|
+
name: rspec
|
105
|
+
requirement: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3.3'
|
110
|
+
type: :development
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '3.3'
|
117
|
+
- !ruby/object:Gem::Dependency
|
118
|
+
name: rubocop
|
119
|
+
requirement: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 0.79.0
|
124
|
+
type: :development
|
125
|
+
prerelease: false
|
126
|
+
version_requirements: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 0.79.0
|
131
|
+
- !ruby/object:Gem::Dependency
|
132
|
+
name: rubocop-rspec
|
133
|
+
requirement: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - ">="
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: '0'
|
138
|
+
type: :development
|
139
|
+
prerelease: false
|
140
|
+
version_requirements: !ruby/object:Gem::Requirement
|
141
|
+
requirements:
|
142
|
+
- - ">="
|
143
|
+
- !ruby/object:Gem::Version
|
144
|
+
version: '0'
|
145
|
+
- !ruby/object:Gem::Dependency
|
146
|
+
name: simplecov
|
147
|
+
requirement: !ruby/object:Gem::Requirement
|
148
|
+
requirements:
|
149
|
+
- - ">="
|
150
|
+
- !ruby/object:Gem::Version
|
151
|
+
version: '0'
|
152
|
+
type: :development
|
153
|
+
prerelease: false
|
154
|
+
version_requirements: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
description: Host gRPC-Web endpoints for Ruby gRPC services in a Rack or Rails app(over
|
160
|
+
HTTP/1.1). Client included.
|
161
|
+
email:
|
162
|
+
- js@gusto.com
|
163
|
+
executables: []
|
164
|
+
extensions: []
|
165
|
+
extra_rdoc_files: []
|
166
|
+
files:
|
167
|
+
- lib/grpc-web.rb
|
168
|
+
- lib/grpc_web.rb
|
169
|
+
- lib/grpc_web/client.rb
|
170
|
+
- lib/grpc_web/client_executor.rb
|
171
|
+
- lib/grpc_web/content_types.rb
|
172
|
+
- lib/grpc_web/error_callback.rb
|
173
|
+
- lib/grpc_web/grpc_request_processor.rb
|
174
|
+
- lib/grpc_web/grpc_web_request.rb
|
175
|
+
- lib/grpc_web/grpc_web_response.rb
|
176
|
+
- lib/grpc_web/message_frame.rb
|
177
|
+
- lib/grpc_web/message_framing.rb
|
178
|
+
- lib/grpc_web/message_serialization.rb
|
179
|
+
- lib/grpc_web/rack_app.rb
|
180
|
+
- lib/grpc_web/rack_handler.rb
|
181
|
+
- lib/grpc_web/text_coder.rb
|
182
|
+
- lib/grpc_web/version.rb
|
183
|
+
homepage: https://github.com/gusto/grpc-web-ruby
|
184
|
+
licenses:
|
185
|
+
- MIT
|
186
|
+
metadata: {}
|
187
|
+
post_install_message:
|
188
|
+
rdoc_options: []
|
189
|
+
require_paths:
|
190
|
+
- lib
|
191
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
192
|
+
requirements:
|
193
|
+
- - ">="
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
version: '0'
|
196
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
197
|
+
requirements:
|
198
|
+
- - ">="
|
199
|
+
- !ruby/object:Gem::Version
|
200
|
+
version: '0'
|
201
|
+
requirements: []
|
202
|
+
rubygems_version: 3.0.3
|
203
|
+
signing_key:
|
204
|
+
specification_version: 4
|
205
|
+
summary: Host gRPC-Web endpoints for Ruby gRPC services in a Rack or Rails app(over
|
206
|
+
HTTP/1.1). Client included.
|
207
|
+
test_files: []
|