anycable-core 1.4.0 → 1.4.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d56f39fa97e55307e2895bcbdff87e8d9a0b6229ca7baab78ba09e6a384114d
4
- data.tar.gz: 5134a604805b575f1d5e17e4152ca4aae5d4a5ed83ce96687ca02f4cf8b91c63
3
+ metadata.gz: 391832d2af2280297419d7f62853313d8ac005e7ca826654d6669135748cb8e8
4
+ data.tar.gz: 229e4b51b7eaed689f0394445e86ba96cb08889391ae343b810439a727e30c0f
5
5
  SHA512:
6
- metadata.gz: d3c57283d963f8228bad73ba1c5f77d3f58915ad38e8b4e28678e94503794eac162b418f7fb1b31d0b4a30f7557b10cfda95a2af5a3be5ebb7785ff96aa7e5dc
7
- data.tar.gz: 5ec1eee3fc7c16f546c4a8d6d315dd4b04efa4da3633dce5c7467189b126b80ccde5c8fa1c6aa1f40fb96434ee28cf67a7b857a63ab12b8360f1e3498b60d5d3
6
+ metadata.gz: c951c3a248b67b9d481865d4a7c1e3d75f76ca963a8f314c8a4889fe9ab1e28eb889ecd48094c96802a20c9196f0932051167c1bd32b54ebf7d7d95dce71620b
7
+ data.tar.gz: 630c505792b0f374dc6acf7ffa861fcde660687ed7ac8894a0275e9f8d707133dbb9c29c24ed7b0e1dd8650b428a421243d5d1179361421996c850bdc50d0128
data/CHANGELOG.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 1.4.2 (2023-09-05)
6
+
7
+ - Fix parsing sentinel configuration from YAML in Rails. ([@palkan][])
8
+
9
+ ## 1.4.1 (2023-07-24)
10
+
11
+ - Add TLS support for gRPC server. ([@Envek][])
12
+
5
13
  ## 1.4.0 (2023-07-07)
6
14
 
7
15
  - Add HTTP RPC support. ([@palkan][])
@@ -98,7 +98,7 @@ module AnyCable
98
98
  --http-health-path=path Endpoint to serve health checks, default: "/health"
99
99
 
100
100
  REDIS PUB/SUB
101
- --redis-url=url Redis URL for pub/sub, default: REDIS_URL or "redis://localhost:6379/5"
101
+ --redis-url=url Redis URL for pub/sub, default: REDIS_URL or "redis://localhost:6379"
102
102
  --redis-channel=name Redis channel for broadcasting, default: "__anycable__"
103
103
  --redis-sentinels=<...hosts> Redis Sentinel followers addresses (as a comma-separated list), default: nil
104
104
  --redis-tls-verify=yes|no Whether to perform server certificate check in case of rediss:// protocol. Default: yes
@@ -171,7 +171,7 @@ module AnyCable
171
171
  private
172
172
 
173
173
  def parse_sentinel(sentinel)
174
- return sentinel.transform_keys!(&:to_sym) if sentinel.is_a?(Hash)
174
+ return sentinel.to_hash.transform_keys(&:to_sym) if sentinel.respond_to?(:to_hash)
175
175
 
176
176
  uri = URI.parse("redis://#{sentinel}")
177
177
 
@@ -3,6 +3,8 @@
3
3
  AnyCable::Config.attr_config(
4
4
  ### gRPC options
5
5
  rpc_host: "127.0.0.1:50051",
6
+ rpc_tls_cert: nil,
7
+ rpc_tls_key: nil,
6
8
  rpc_pool_size: 30,
7
9
  rpc_max_waiting_requests: 20,
8
10
  rpc_poll_period: 1,
@@ -32,6 +34,7 @@ module AnyCable
32
34
  max_waiting_requests: rpc_max_waiting_requests,
33
35
  poll_period: rpc_poll_period,
34
36
  pool_keep_alive: rpc_pool_keep_alive,
37
+ tls_credentials: tls_credentials,
35
38
  server_args: enhance_grpc_server_args(normalized_grpc_server_args)
36
39
  }
37
40
  end
@@ -53,6 +56,17 @@ module AnyCable
53
56
  opts["grpc.max_connection_age_ms"] = rpc_max_connection_age.to_i * 1000
54
57
  opts
55
58
  end
59
+
60
+ def tls_credentials
61
+ cert_path_or_content = rpc_tls_cert # Assign to local variable to make steep happy
62
+ key_path_or_content = rpc_tls_key # Assign to local variable to make steep happy
63
+ return {} if cert_path_or_content.nil? || key_path_or_content.nil?
64
+
65
+ cert = File.exist?(cert_path_or_content) ? File.read(cert_path_or_content) : cert_path_or_content
66
+ pkey = File.exist?(key_path_or_content) ? File.read(key_path_or_content) : key_path_or_content
67
+
68
+ {cert: cert, pkey: pkey}
69
+ end
56
70
  end
57
71
  end
58
72
  end
@@ -62,6 +76,8 @@ AnyCable::Config.prepend AnyCable::GRPC::Config
62
76
  AnyCable::Config.usage <<~TXT
63
77
  GRPC OPTIONS
64
78
  --rpc-host=host Local address to run gRPC server on, default: "127.0.0.1:50051"
79
+ --rpc-tls-cert=path TLS certificate file path or contents in PEM format, default: <none> (TLS disabled)
80
+ --rpc-tls-key=path TLS private key file path or contents in PEM format, default: <none> (TLS disabled)
65
81
  --rpc-pool-size=size gRPC workers pool size, default: 30
66
82
  --rpc-max-waiting-requests=num Max waiting requests queue size, default: 20
67
83
  --rpc-poll-period=seconds Poll period (sec), default: 1
@@ -83,8 +83,9 @@ module AnyCable
83
83
  end
84
84
 
85
85
  def build_server(**options)
86
+ tls_credentials = options.delete(:tls_credentials)
86
87
  ::GRPC::RpcServer.new(**options).tap do |server|
87
- server.add_http2_port(host, :this_port_is_insecure)
88
+ server.add_http2_port(host, server_credentials(**tls_credentials))
88
89
  server.handle(AnyCable::GRPC::Handler)
89
90
  server.handle(build_health_checker)
90
91
  end
@@ -102,6 +103,12 @@ module AnyCable
102
103
  )
103
104
  health_checker
104
105
  end
106
+
107
+ def server_credentials(cert: nil, pkey: nil)
108
+ return :this_port_is_insecure if cert.nil? || pkey.nil?
109
+
110
+ ::GRPC::Core::ServerCredentials.new(nil, [{private_key: pkey, cert_chain: cert}], false)
111
+ end
105
112
  end
106
113
  end
107
114
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  require "anycable/grpc/handler"
4
4
 
5
- require_relative "./health_pb"
6
- require_relative "./health_services_pb"
7
- require_relative "./health_checker"
5
+ require_relative "health_pb"
6
+ require_relative "health_services_pb"
7
+ require_relative "health_checker"
8
8
 
9
9
  module AnyCable
10
10
  module GRPC
@@ -44,6 +44,7 @@ module AnyCable
44
44
  @hostname = host_parts[:hostname]
45
45
  @port = host_parts[:port].to_i
46
46
 
47
+ @tls_credentials = options.delete(:tls_credentials)
47
48
  @grpc_server = build_server(**options)
48
49
  end
49
50
 
@@ -56,7 +57,7 @@ module AnyCable
56
57
 
57
58
  logger.info "RPC server (grpc_kit) is starting..."
58
59
 
59
- @sock = TCPServer.new(hostname, port)
60
+ @sock = build_server_socket
60
61
 
61
62
  server = grpc_server
62
63
 
@@ -66,6 +67,12 @@ module AnyCable
66
67
  server.run(conn)
67
68
  rescue IOError
68
69
  # ignore broken connections
70
+ rescue OpenSSL::SSL::SSLError => ssl_error
71
+ if ssl_error.message.match?(/SSL_read: unexpected eof while reading/i)
72
+ # ignore broken connections
73
+ else
74
+ raise
75
+ end
69
76
  end
70
77
  end
71
78
 
@@ -81,13 +88,18 @@ module AnyCable
81
88
 
82
89
  loop do
83
90
  sock = TCPSocket.new(hostname, port, connect_timeout: 1)
91
+ if @tls_credentials&.any?
92
+ sock = OpenSSL::SSL::SSLSocket.new(sock, tls_context)
93
+ sock.sync_close = true
94
+ sock.connect
95
+ end
84
96
  stub = ::Grpc::Health::V1::Health::Stub.new(sock)
85
97
  stub.check(::Grpc::Health::V1::HealthCheckRequest.new)
86
98
  sock.close
87
99
  break
88
- rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError
100
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, SocketError, OpenSSL::SSL::SSLError => e
89
101
  timeout -= 1
90
- raise "Server is not responding" if timeout.zero?
102
+ raise "Server is not responding: #{e}" if timeout.zero?
91
103
  end
92
104
  end
93
105
 
@@ -125,6 +137,20 @@ module AnyCable
125
137
  @logger ||= AnyCable.logger
126
138
  end
127
139
 
140
+ def build_server_socket
141
+ tcp_server = TCPServer.new(hostname, port)
142
+ return tcp_server unless @tls_credentials&.any?
143
+
144
+ OpenSSL::SSL::SSLServer.new(tcp_server, tls_context)
145
+ end
146
+
147
+ def tls_context
148
+ OpenSSL::SSL::SSLContext.new.tap do |tls_context|
149
+ tls_context.cert = OpenSSL::X509::Certificate.new(@tls_credentials.fetch(:cert))
150
+ tls_context.key = OpenSSL::PKey.read(@tls_credentials.fetch(:pkey))
151
+ end
152
+ end
153
+
128
154
  def build_server(**options)
129
155
  pool_size = options[:pool_size]
130
156
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AnyCable
4
- VERSION = "1.4.0"
4
+ VERSION = "1.4.2"
5
5
  end
@@ -68,7 +68,7 @@ module AnyCable
68
68
 
69
69
  private
70
70
 
71
- def parse_sentinel: ((String | Hash[untyped, untyped]) sentinel) -> Hash[Symbol, untyped]
71
+ def parse_sentinel: (untyped sentinel) -> Hash[Symbol, untyped]
72
72
  def load_presets: () -> void
73
73
  def detect_presets: () -> Array[String]
74
74
  def write_preset: (Symbol, untyped, preset: String) -> void
@@ -3,6 +3,10 @@ module AnyCable
3
3
  interface _Config
4
4
  def rpc_host: () -> String
5
5
  def rpc_host=: (String) -> void
6
+ def rpc_tls_cert: () -> String?
7
+ def rpc_tls_cert=: (String?) -> void
8
+ def rpc_tls_key: () -> String?
9
+ def rpc_tls_key=: (String?) -> void
6
10
  def rpc_pool_size: () -> Integer
7
11
  def rpc_pool_size=: (Integer) -> void
8
12
  def rpc_max_waiting_requests: () -> Integer
@@ -28,11 +32,13 @@ module AnyCable
28
32
  max_waiting_requests: Integer,
29
33
  poll_period: Numeric,
30
34
  pool_keep_alive: Numeric,
35
+ tls_credentials: Hash[Symbol, String],
31
36
  server_args: Hash[String, untyped]
32
37
  }
33
38
 
34
39
  def normalized_grpc_server_args: () -> Hash[String, untyped]
35
40
  def enhance_grpc_server_args: (Hash[String, untyped]) -> Hash[String, untyped]
41
+ def tls_credentials: () -> Hash[Symbol, String]
36
42
  end
37
43
  end
38
44
  end
@@ -15,6 +15,7 @@ module AnyCable
15
15
  def logger: () -> Logger
16
16
  def build_server: (**untyped options) -> untyped
17
17
  def build_health_checker: () -> untyped
18
+ def server_credentials: (?cert: String?, ?pkey: String?) -> (::GRPC::Core::ServerCredentials | :this_port_is_insecure)
18
19
  end
19
20
  end
20
21
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: anycable-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - palkan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-07-08 00:00:00.000000000 Z
11
+ date: 2023-09-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: anyway_config