grpc-web 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|