istox_gruf 2.7.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +246 -0
- data/CODE_OF_CONDUCT.md +46 -0
- data/README.md +544 -0
- data/bin/gruf +29 -0
- data/lib/gruf.rb +50 -0
- data/lib/gruf/cli/executor.rb +99 -0
- data/lib/gruf/client.rb +217 -0
- data/lib/gruf/client/error.rb +66 -0
- data/lib/gruf/client/error_factory.rb +105 -0
- data/lib/gruf/configuration.rb +137 -0
- data/lib/gruf/controllers/base.rb +102 -0
- data/lib/gruf/controllers/request.rb +121 -0
- data/lib/gruf/controllers/service_binder.rb +117 -0
- data/lib/gruf/error.rb +230 -0
- data/lib/gruf/errors/debug_info.rb +56 -0
- data/lib/gruf/errors/field.rb +56 -0
- data/lib/gruf/errors/helpers.rb +44 -0
- data/lib/gruf/hooks/base.rb +34 -0
- data/lib/gruf/hooks/executor.rb +47 -0
- data/lib/gruf/hooks/registry.rb +159 -0
- data/lib/gruf/instrumentable_grpc_server.rb +64 -0
- data/lib/gruf/integrations/rails/railtie.rb +10 -0
- data/lib/gruf/interceptors/active_record/connection_reset.rb +48 -0
- data/lib/gruf/interceptors/authentication/basic.rb +87 -0
- data/lib/gruf/interceptors/base.rb +53 -0
- data/lib/gruf/interceptors/client_interceptor.rb +125 -0
- data/lib/gruf/interceptors/context.rb +56 -0
- data/lib/gruf/interceptors/instrumentation/output_metadata_timer.rb +61 -0
- data/lib/gruf/interceptors/instrumentation/request_logging/formatters/base.rb +41 -0
- data/lib/gruf/interceptors/instrumentation/request_logging/formatters/logstash.rb +43 -0
- data/lib/gruf/interceptors/instrumentation/request_logging/formatters/plain.rb +48 -0
- data/lib/gruf/interceptors/instrumentation/request_logging/interceptor.rb +225 -0
- data/lib/gruf/interceptors/instrumentation/statsd.rb +82 -0
- data/lib/gruf/interceptors/registry.rb +161 -0
- data/lib/gruf/interceptors/server_interceptor.rb +34 -0
- data/lib/gruf/interceptors/timer.rb +85 -0
- data/lib/gruf/loggable.rb +30 -0
- data/lib/gruf/logging.rb +53 -0
- data/lib/gruf/outbound/request_context.rb +71 -0
- data/lib/gruf/response.rb +71 -0
- data/lib/gruf/serializers/errors/base.rb +57 -0
- data/lib/gruf/serializers/errors/json.rb +43 -0
- data/lib/gruf/server.rb +294 -0
- data/lib/gruf/synchronized_client.rb +97 -0
- data/lib/gruf/timer.rb +78 -0
- data/lib/gruf/version.rb +20 -0
- metadata +203 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
6
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
7
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
8
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
11
|
+
# Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
14
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
15
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
16
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
17
|
+
#
|
18
|
+
module Gruf
|
19
|
+
module Errors
|
20
|
+
##
|
21
|
+
# Helper module for standardizing error interaction
|
22
|
+
#
|
23
|
+
module Helpers
|
24
|
+
delegate :add_field_error, :set_debug_info, :has_field_errors?, to: :error
|
25
|
+
|
26
|
+
##
|
27
|
+
# Will issue a GRPC BadStatus exception, with a code based on the code passed.
|
28
|
+
#
|
29
|
+
# @param [Symbol] error_code The network error code that maps to gRPC status codes
|
30
|
+
# @param [Symbol] app_code The application-specific code for the error
|
31
|
+
# @param [String] message (Optional) A detail message about the error
|
32
|
+
# @param [Hash] metadata (Optional) Any metadata to inject into the trailing metadata for the response
|
33
|
+
#
|
34
|
+
def fail!(error_code, app_code = nil, message = '', metadata = {})
|
35
|
+
e = error
|
36
|
+
e.code = error_code.to_sym
|
37
|
+
e.app_code = app_code ? app_code.to_sym : e.code
|
38
|
+
e.message = message.to_s
|
39
|
+
e.metadata = metadata
|
40
|
+
e.fail!(request.active_call)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
6
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
7
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
8
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
11
|
+
# Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
14
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
15
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
16
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
17
|
+
#
|
18
|
+
module Gruf
|
19
|
+
module Hooks
|
20
|
+
##
|
21
|
+
# Base class for a hook that allows execution at various points of Gruf server processes
|
22
|
+
#
|
23
|
+
class Base
|
24
|
+
include Gruf::Loggable
|
25
|
+
|
26
|
+
##
|
27
|
+
# @param [Hash] options
|
28
|
+
#
|
29
|
+
def initialize(options: nil)
|
30
|
+
@options = options || {}
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
6
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
7
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
8
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
11
|
+
# Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
14
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
15
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
16
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
17
|
+
#
|
18
|
+
module Gruf
|
19
|
+
module Hooks
|
20
|
+
##
|
21
|
+
# Base class for a hook that allows execution at various points of gRPC server processes
|
22
|
+
#
|
23
|
+
class Executor
|
24
|
+
include Gruf::Loggable
|
25
|
+
|
26
|
+
def initialize(hooks: nil)
|
27
|
+
@hooks = hooks || Gruf.hooks&.prepare || []
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Execute a hook point for each registered hook in the registry
|
32
|
+
#
|
33
|
+
# @param [Symbol] name
|
34
|
+
# @param [Hash] arguments
|
35
|
+
#
|
36
|
+
def call(name, arguments = {})
|
37
|
+
name = name.to_sym
|
38
|
+
|
39
|
+
@hooks.each do |hook|
|
40
|
+
next unless hook.respond_to?(name)
|
41
|
+
|
42
|
+
hook.send(name, arguments)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
6
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
7
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
8
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
11
|
+
# Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
14
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
15
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
16
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
17
|
+
#
|
18
|
+
module Gruf
|
19
|
+
module Hooks
|
20
|
+
##
|
21
|
+
# Handles registration of hooks
|
22
|
+
#
|
23
|
+
class Registry
|
24
|
+
class HookNotFoundError < StandardError; end
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
@registry = []
|
28
|
+
end
|
29
|
+
|
30
|
+
##
|
31
|
+
# Add a hook to the registry
|
32
|
+
#
|
33
|
+
# @param [Class] hook_class The class of the hook to add
|
34
|
+
# @param [Hash] options A hash of options to pass into the hook during initialization
|
35
|
+
#
|
36
|
+
def use(hook_class, options = {})
|
37
|
+
hooks_mutex do
|
38
|
+
@registry << {
|
39
|
+
klass: hook_class,
|
40
|
+
options: options
|
41
|
+
}
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
##
|
46
|
+
# Remove a hook from the registry
|
47
|
+
#
|
48
|
+
# @param [Class] hook_class The hook class to remove
|
49
|
+
# @raise [HookNotFoundError] if the hook is not found
|
50
|
+
#
|
51
|
+
def remove(hook_class)
|
52
|
+
pos = @registry.find_index { |opts| opts.fetch(:klass, '') == hook_class }
|
53
|
+
raise HookNotFoundError if pos.nil?
|
54
|
+
|
55
|
+
@registry.delete_at(pos)
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Insert a hook before another specified hook
|
60
|
+
#
|
61
|
+
# @param [Class] before_class The hook to insert before
|
62
|
+
# @param [Class] hook_class The class of the hook to add
|
63
|
+
# @param [Hash] options A hash of options to pass into the hook during initialization
|
64
|
+
# @raise [HookNotFoundError] if the before hook is not found
|
65
|
+
#
|
66
|
+
def insert_before(before_class, hook_class, options = {})
|
67
|
+
hooks_mutex do
|
68
|
+
pos = @registry.find_index { |opts| opts.fetch(:klass, '') == before_class }
|
69
|
+
raise HookNotFoundError if pos.nil?
|
70
|
+
|
71
|
+
@registry.insert(
|
72
|
+
pos,
|
73
|
+
klass: hook_class,
|
74
|
+
options: options
|
75
|
+
)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
##
|
80
|
+
# Insert a hook after another specified hook
|
81
|
+
#
|
82
|
+
# @param [Class] after_class The hook to insert after
|
83
|
+
# @param [Class] hook_class The class of the hook to add
|
84
|
+
# @param [Hash] options A hash of options to pass into the hook during initialization
|
85
|
+
# @raise [HookNotFoundError] if the after hook is not found
|
86
|
+
#
|
87
|
+
def insert_after(after_class, hook_class, options = {})
|
88
|
+
hooks_mutex do
|
89
|
+
pos = @registry.find_index { |opts| opts.fetch(:klass, '') == after_class }
|
90
|
+
raise HookNotFoundError if pos.nil?
|
91
|
+
|
92
|
+
@registry.insert(
|
93
|
+
(pos + 1),
|
94
|
+
klass: hook_class,
|
95
|
+
options: options
|
96
|
+
)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
##
|
101
|
+
# Return a list of the hook classes in the registry in their execution order
|
102
|
+
#
|
103
|
+
# @return [Array<Class>]
|
104
|
+
#
|
105
|
+
def list
|
106
|
+
hooks_mutex do
|
107
|
+
@registry.map { |h| h[:klass] }
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
##
|
112
|
+
# Lazily load and return all hooks for the given request
|
113
|
+
#
|
114
|
+
# @return [Array<Gruf::Hooks::Base>]
|
115
|
+
#
|
116
|
+
def prepare
|
117
|
+
is = []
|
118
|
+
hooks_mutex do
|
119
|
+
@registry.each do |o|
|
120
|
+
is << o[:klass].new(options: o[:options])
|
121
|
+
end
|
122
|
+
end
|
123
|
+
is
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Clear the registry
|
128
|
+
#
|
129
|
+
def clear
|
130
|
+
hooks_mutex do
|
131
|
+
@registry = []
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# @return [Integer] The number of hooks currently loaded
|
137
|
+
#
|
138
|
+
def count
|
139
|
+
hooks_mutex do
|
140
|
+
@registry ||= []
|
141
|
+
@registry.count
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
##
|
148
|
+
# Handle mutations to the hook registry in a thread-safe manner
|
149
|
+
#
|
150
|
+
def hooks_mutex(&block)
|
151
|
+
@hooks_mutex ||= begin
|
152
|
+
require 'monitor'
|
153
|
+
Monitor.new
|
154
|
+
end
|
155
|
+
@hooks_mutex.synchronize(&block)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Gruf
|
4
|
+
##
|
5
|
+
# A subclass of GRPC::RpcServer that can be used for enhanced monitoring
|
6
|
+
# of thread pool. Note that since we are reaching into the internals of
|
7
|
+
# GRPC::RpcServer, we need to watch the evolution of that class.
|
8
|
+
#
|
9
|
+
class InstrumentableGrpcServer < GRPC::RpcServer
|
10
|
+
##
|
11
|
+
# Add an event_listener_proc that, if supplied, will be called
|
12
|
+
# when interesting events happen in the server.
|
13
|
+
#
|
14
|
+
def initialize(pool_size: DEFAULT_POOL_SIZE,
|
15
|
+
max_waiting_requests: DEFAULT_MAX_WAITING_REQUESTS,
|
16
|
+
poll_period: DEFAULT_POLL_PERIOD,
|
17
|
+
pool_keep_alive: Pool::DEFAULT_KEEP_ALIVE,
|
18
|
+
connect_md_proc: nil,
|
19
|
+
server_args: {},
|
20
|
+
interceptors: [],
|
21
|
+
event_listener_proc: nil)
|
22
|
+
# Call the base class initializer
|
23
|
+
super(
|
24
|
+
pool_size: pool_size,
|
25
|
+
max_waiting_requests: max_waiting_requests,
|
26
|
+
poll_period: poll_period,
|
27
|
+
pool_keep_alive: pool_keep_alive,
|
28
|
+
connect_md_proc: connect_md_proc,
|
29
|
+
server_args: server_args,
|
30
|
+
interceptors: interceptors
|
31
|
+
)
|
32
|
+
|
33
|
+
# Save event listener for later
|
34
|
+
@event_listener_proc = event_listener_proc
|
35
|
+
end
|
36
|
+
|
37
|
+
##
|
38
|
+
# Notify the event listener of something interesting
|
39
|
+
#
|
40
|
+
def notify(event)
|
41
|
+
return if @event_listener_proc.nil? || !@event_listener_proc.respond_to?(:call)
|
42
|
+
|
43
|
+
@event_listener_proc.call(event)
|
44
|
+
end
|
45
|
+
|
46
|
+
##
|
47
|
+
# Hook into the thread pool availability check for monitoring
|
48
|
+
#
|
49
|
+
def available?(an_rpc)
|
50
|
+
super.tap do |obj|
|
51
|
+
notify(:thread_pool_exhausted) unless obj
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
##
|
56
|
+
# Hook into the method implementation check for monitoring
|
57
|
+
#
|
58
|
+
def implemented?(an_rpc)
|
59
|
+
super.tap do |obj|
|
60
|
+
notify(:unimplemented) unless obj
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
6
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
7
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
8
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
11
|
+
# Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
14
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
15
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
16
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
17
|
+
#
|
18
|
+
module Gruf
|
19
|
+
module Interceptors
|
20
|
+
module ActiveRecord
|
21
|
+
##
|
22
|
+
# Resets the ActiveRecord connection to maintain accurate connected state in the thread pool
|
23
|
+
#
|
24
|
+
class ConnectionReset < ::Gruf::Interceptors::ServerInterceptor
|
25
|
+
##
|
26
|
+
# Reset any ActiveRecord connections after a gRPC service is called. Because of the way gRPC manages its
|
27
|
+
# connection pool, we need to ensure that this is done to properly
|
28
|
+
#
|
29
|
+
def call
|
30
|
+
::ActiveRecord::Base.establish_connection if enabled? && !::ActiveRecord::Base.connection.active?
|
31
|
+
|
32
|
+
yield
|
33
|
+
ensure
|
34
|
+
::ActiveRecord::Base.clear_active_connections! if enabled?
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
##
|
40
|
+
# @return [Boolean] If AR is loaded, we can enable this hook safely
|
41
|
+
#
|
42
|
+
def enabled?
|
43
|
+
defined?(::ActiveRecord::Base)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Copyright (c) 2017-present, BigCommerce Pty. Ltd. All rights reserved
|
4
|
+
#
|
5
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
|
6
|
+
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
|
7
|
+
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit
|
8
|
+
# persons to whom the Software is furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
|
11
|
+
# Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
|
14
|
+
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
15
|
+
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
|
16
|
+
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
17
|
+
#
|
18
|
+
require 'base64'
|
19
|
+
|
20
|
+
module Gruf
|
21
|
+
module Interceptors
|
22
|
+
module Authentication
|
23
|
+
##
|
24
|
+
# Handles basic authentication for gRPC requests
|
25
|
+
#
|
26
|
+
class Basic < Gruf::Interceptors::ServerInterceptor
|
27
|
+
##
|
28
|
+
# Validate authentication
|
29
|
+
#
|
30
|
+
def call
|
31
|
+
fail!(:unauthenticated, :unauthenticated) unless bypass? || valid?
|
32
|
+
yield
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
##
|
38
|
+
# @return [Boolean] If this method is in the excluded list, bypass
|
39
|
+
#
|
40
|
+
def bypass?
|
41
|
+
options.fetch(:excluded_methods, []).include?(request.method_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
##
|
45
|
+
# @return [Boolean] True if the basic authentication was valid
|
46
|
+
#
|
47
|
+
def valid?
|
48
|
+
server_credentials.any? do |cred|
|
49
|
+
username = cred.fetch(:username, '').to_s
|
50
|
+
password = cred.fetch(:password, '').to_s
|
51
|
+
if username.empty?
|
52
|
+
request_password == password
|
53
|
+
else
|
54
|
+
request_credentials == "#{username}:#{password}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
##
|
60
|
+
# @return [Array<Hash>] An array of valid server credentials for this service
|
61
|
+
#
|
62
|
+
def server_credentials
|
63
|
+
options.fetch(:credentials, [])
|
64
|
+
end
|
65
|
+
|
66
|
+
##
|
67
|
+
# @return [String] The decoded request credentials
|
68
|
+
#
|
69
|
+
def request_credentials
|
70
|
+
credentials = if request.active_call.respond_to?(:metadata)
|
71
|
+
request.active_call.metadata['authorization'].to_s
|
72
|
+
else
|
73
|
+
''
|
74
|
+
end
|
75
|
+
Base64.decode64(credentials.to_s.gsub('Basic ', '').strip)
|
76
|
+
end
|
77
|
+
|
78
|
+
##
|
79
|
+
# @return [String] The decoded request password
|
80
|
+
#
|
81
|
+
def request_password
|
82
|
+
@request_password ||= request_credentials.split(':').last
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|