anycable-core 1.4.0 → 1.4.2

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 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