anytls 0.1.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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE +21 -0
  4. data/README.md +143 -0
  5. data/bin/anytls-client +7 -0
  6. data/bin/anytls-server +7 -0
  7. data/docs/ARCHITECTURE.md +105 -0
  8. data/docs/SOURCES.md +15 -0
  9. data/docs/VALIDATION.md +100 -0
  10. data/docs/validation/runtime-ruby34.json +8 -0
  11. data/docs/validation/runtime-ruby40.json +8 -0
  12. data/docs/validation/stress-c1.json +25 -0
  13. data/docs/validation/stress-c128.json +26 -0
  14. data/docs/validation/stress-c32.json +26 -0
  15. data/docs/validation/stress-sequential-10000.json +26 -0
  16. data/lib/anytls/address.rb +53 -0
  17. data/lib/anytls/buffered_io.rb +185 -0
  18. data/lib/anytls/byte_buffer.rb +61 -0
  19. data/lib/anytls/cli/client_listener.rb +91 -0
  20. data/lib/anytls/cli/options.rb +160 -0
  21. data/lib/anytls/cli/runner.rb +89 -0
  22. data/lib/anytls/cli/shutdown.rb +36 -0
  23. data/lib/anytls/client.rb +129 -0
  24. data/lib/anytls/connection_uri.rb +96 -0
  25. data/lib/anytls/deadline.rb +34 -0
  26. data/lib/anytls/dialer.rb +70 -0
  27. data/lib/anytls/errors.rb +23 -0
  28. data/lib/anytls/inbound/http_proxy.rb +224 -0
  29. data/lib/anytls/inbound/mixed.rb +60 -0
  30. data/lib/anytls/inbound/socks4.rb +55 -0
  31. data/lib/anytls/inbound/socks5.rb +138 -0
  32. data/lib/anytls/io_helpers.rb +87 -0
  33. data/lib/anytls/log.rb +15 -0
  34. data/lib/anytls/padding/scheme.rb +121 -0
  35. data/lib/anytls/padding/writer.rb +55 -0
  36. data/lib/anytls/protocol/authentication.rb +44 -0
  37. data/lib/anytls/protocol/decoder.rb +39 -0
  38. data/lib/anytls/protocol/frame.rb +40 -0
  39. data/lib/anytls/protocol/settings.rb +53 -0
  40. data/lib/anytls/protocol/socks_address.rb +59 -0
  41. data/lib/anytls/relay.rb +49 -0
  42. data/lib/anytls/server.rb +220 -0
  43. data/lib/anytls/session.rb +593 -0
  44. data/lib/anytls/session_pool.rb +166 -0
  45. data/lib/anytls/stream.rb +151 -0
  46. data/lib/anytls/tls_config.rb +76 -0
  47. data/lib/anytls/transport/tls_connection.rb +117 -0
  48. data/lib/anytls/uot/connection.rb +122 -0
  49. data/lib/anytls/uot/server.rb +97 -0
  50. data/lib/anytls/version.rb +6 -0
  51. data/lib/anytls.rb +40 -0
  52. metadata +203 -0
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyTLS
4
+ # 握手解析器和转发器共用的小型预读缓冲;不暴露 to_io,
5
+ # 防止 IO 优化绕过已缓冲的字节。
6
+ class BufferedIO
7
+ attr_reader :timeout
8
+
9
+ def initialize(io, timeout: nil)
10
+ @io = io
11
+ self.timeout = timeout
12
+ @buffer = +"".b
13
+ @line_budget = nil
14
+ @track_trailers = false
15
+ end
16
+
17
+ # 在协议阶段边界重新设定绝对读取预算;握手完成后清除预算,
18
+ # 允许已建立的隧道持续空闲。
19
+ def timeout=(value)
20
+ @timeout = value
21
+ @deadline = value && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + value)
22
+ end
23
+
24
+ def peek(length)
25
+ fill(length)
26
+ @buffer.byteslice(0, length)
27
+ end
28
+
29
+ def read(length = nil, outbuf = nil, timeout: @timeout)
30
+ raise ArgumentError, "negative length" if length&.negative?
31
+
32
+ data = +"".b
33
+ if length
34
+ data << readpartial(length - data.bytesize, timeout: timeout) while data.bytesize < length
35
+ else
36
+ loop { data << readpartial(16_384, timeout: timeout) }
37
+ end
38
+ outbuf ? outbuf.replace(data) : data
39
+ rescue EOFError
40
+ if data.empty? && length != 0
41
+ outbuf&.clear
42
+ return nil
43
+ end
44
+
45
+ outbuf ? outbuf.replace(data) : data
46
+ end
47
+
48
+ def readpartial(length, outbuf = nil, timeout: @timeout)
49
+ raise ArgumentError, "negative length" if length.negative?
50
+ return outbuf ? outbuf.replace("") : +"".b if length.zero?
51
+
52
+ data = if @buffer.empty?
53
+ read_underlying(length, timeout)
54
+ else
55
+ @buffer.slice!(0, length)
56
+ end
57
+ raise EOFError if data.nil? || data.empty?
58
+
59
+ outbuf ? outbuf.replace(data) : data
60
+ rescue EOFError
61
+ outbuf&.clear
62
+ raise
63
+ end
64
+
65
+ def gets(separator = "\n", limit = 8192)
66
+ raise ArgumentError, "separator must not be empty" if separator.nil? || separator.empty?
67
+
68
+ loop do
69
+ if (index = @buffer.index(separator))
70
+ size = index + separator.bytesize
71
+ raise BufferLimitExceeded, "HTTP line exceeds limit" if size > limit
72
+
73
+ return account_line(@buffer.slice!(0, size))
74
+ end
75
+ raise BufferLimitExceeded, "HTTP line exceeds limit" if @buffer.bytesize >= limit
76
+
77
+ @buffer << read_underlying([4096, limit - @buffer.bytesize].min, @timeout)
78
+ end
79
+ rescue EOFError
80
+ return nil if @buffer.empty?
81
+
82
+ raise ProtocolError, "truncated line"
83
+ end
84
+
85
+ # 同时限制单行、头部总字节数和头部数量;读取终止 chunk 后,
86
+ # 为分块传输的尾部字段建立独立预算。
87
+ def begin_headers!
88
+ @line_budget = [0, 0]
89
+ @track_trailers = true
90
+ end
91
+
92
+ def write(bytes, timeout: @timeout)
93
+ IOHelpers.write_all(@io, bytes, timeout: timeout)
94
+ bytes.bytesize
95
+ end
96
+
97
+ def flush
98
+ @io.flush if @io.respond_to?(:flush)
99
+ self
100
+ end
101
+
102
+ def close
103
+ IOHelpers.close(@io) unless @io.closed?
104
+ rescue IOError, SystemCallError
105
+ nil
106
+ end
107
+
108
+ def closed? = @io.closed?
109
+
110
+ # 本地 TCP socket 可停止上传读取,同时继续返回 HTTP 错误。
111
+ # AnyTLS 逻辑流只支持整体关闭。
112
+ def close_read
113
+ if @io.is_a?(BasicSocket)
114
+ @io.shutdown(Socket::SHUT_RD)
115
+ else
116
+ close
117
+ end
118
+ rescue IOError, SystemCallError
119
+ nil
120
+ end
121
+
122
+ def close_write
123
+ if @io.is_a?(IO) && @io.respond_to?(:close_write)
124
+ @io.close_write
125
+ else
126
+ raise ProtocolError, "AnyTLS streams do not support half-close"
127
+ end
128
+ end
129
+
130
+ def peeraddr = @io.peeraddr
131
+
132
+ def addr = @io.addr
133
+
134
+ private
135
+
136
+ def fill(length)
137
+ @buffer << read_underlying([length - @buffer.bytesize, 4096].max, @timeout) while @buffer.bytesize < length
138
+ end
139
+
140
+ def read_underlying(length, timeout)
141
+ if @deadline
142
+ remaining = @deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
143
+ raise TimeoutError, "read timed out" unless remaining.positive?
144
+
145
+ timeout = timeout ? [timeout, remaining].min : remaining
146
+ end
147
+ unless @io.respond_to?(:read_nonblock)
148
+ return @io.readpartial(length, timeout: timeout)
149
+ end
150
+
151
+ deadline = timeout && (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout)
152
+ loop do
153
+ result = @io.read_nonblock(length, exception: false)
154
+ case result
155
+ when :wait_readable, :wait_writable
156
+ remaining = deadline && (deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC))
157
+ raise TimeoutError, "read timed out" if remaining && remaining <= 0
158
+
159
+ ready = IO.select(result == :wait_readable ? [@io] : nil, result == :wait_writable ? [@io] : nil, nil,
160
+ remaining)
161
+ raise TimeoutError, "read timed out" unless ready
162
+ when nil
163
+ raise EOFError
164
+ else
165
+ return result
166
+ end
167
+ end
168
+ end
169
+
170
+ def account_line(line)
171
+ if @line_budget
172
+ @line_budget[0] += line.bytesize
173
+ @line_budget[1] += 1 unless line == "\r\n"
174
+ if @line_budget[0] > 65_536 || @line_budget[1] > 101
175
+ raise BufferLimitExceeded, "HTTP headers exceed limit"
176
+ end
177
+
178
+ @line_budget = nil if line == "\r\n"
179
+ elsif @track_trailers && line.match?(/\A0+(?:;[^\r\n]*)?\r\n\z/)
180
+ @line_budget = [0, 1] # 尾部字段没有请求行或状态行。
181
+ end
182
+ line
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyTLS
4
+ # 有界二进制接收队列;生产者不阻塞,读取者可等待数据。
5
+ # 远端结束后先读完缓冲再报告 EOF 或异常;本地关闭则丢弃缓冲。
6
+ class ByteBuffer
7
+ def initialize(limit: 262_144)
8
+ @limit = limit
9
+ @bytes = String.new(encoding: Encoding::BINARY)
10
+ @mutex = Mutex.new
11
+ @changed = ConditionVariable.new
12
+ @finished = false
13
+ end
14
+
15
+ def size
16
+ @mutex.synchronize { @bytes.bytesize }
17
+ end
18
+
19
+ def inspect
20
+ "#<#{self.class} bytes=#{size} limit=#{@limit}>"
21
+ end
22
+
23
+ def push(bytes)
24
+ @mutex.synchronize do
25
+ return if @finished
26
+ raise BufferLimitExceeded, "stream receive buffer limit exceeded" if @bytes.bytesize + bytes.bytesize > @limit
27
+
28
+ @bytes << bytes
29
+ @changed.broadcast
30
+ end
31
+ end
32
+
33
+ def readpartial(length, deadline:)
34
+ raise ArgumentError, "length must be positive" unless length.is_a?(Integer) && length.positive?
35
+
36
+ @mutex.synchronize do
37
+ deadline.wait(@changed, @mutex) while @bytes.empty? && !@finished
38
+ if @bytes.empty?
39
+ raise @error if @error
40
+
41
+ raise EOFError, "end of stream"
42
+ end
43
+ count = [length, @bytes.bytesize].min
44
+ result = @bytes.byteslice(0, count)
45
+ @bytes = @bytes.byteslice(count..) || String.new(encoding: Encoding::BINARY)
46
+ result
47
+ end
48
+ end
49
+
50
+ def finish(error = nil, discard: false)
51
+ @mutex.synchronize do
52
+ unless @finished
53
+ @error = error
54
+ @finished = true
55
+ end
56
+ @bytes.clear if discard
57
+ @changed.broadcast
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module AnyTLS
6
+ module CLI
7
+ # 有界本地 TCP 接入;回收工作线程,并允许资源耗尽后恢复接入。
8
+ # 持有监听器和混合代理适配器;停止时也关闭客户端会话池。
9
+ class ClientListener
10
+ def initialize(client:, options:, logger:)
11
+ @client = client
12
+ @options = options
13
+ @logger = logger
14
+ @mixed = Inbound::Mixed.new(client: client, logger: logger,
15
+ handshake_timeout: options.fetch(:handshake_timeout))
16
+ @listener = TCPServer.new(options.fetch(:listen_host), options.fetch(:listen_port))
17
+ @workers = {}
18
+ @completed = Queue.new
19
+ end
20
+
21
+ def run(shutdown)
22
+ address = @listener.local_address
23
+ @logger.info("mixed proxy listening on #{address.ip_address}:#{address.ip_port}")
24
+ loop do
25
+ reap_workers
26
+ ready = IO.select([@listener, shutdown.reader], nil, nil, 0.1)&.first
27
+ next unless ready
28
+ break if ready.include?(shutdown.reader)
29
+
30
+ accept_client(shutdown)
31
+ end
32
+ ensure
33
+ close
34
+ end
35
+
36
+ def close
37
+ @listener.close unless @listener.closed?
38
+ @mixed.close
39
+ @client.close
40
+ @workers.each_key { |socket| socket.close unless socket.closed? }
41
+ deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + @options.fetch(:grace)
42
+ @workers.each_value do |thread|
43
+ remaining = [deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC), 0].max
44
+ raise IOError, "proxy worker did not stop" unless thread.join(remaining)
45
+
46
+ thread.value
47
+ end
48
+ @workers.clear
49
+ end
50
+
51
+ private
52
+
53
+ def accept_client(shutdown)
54
+ socket = @listener.accept_nonblock(exception: false)
55
+ return if socket == :wait_readable
56
+
57
+ if @workers.size >= @options.fetch(:max_connections)
58
+ socket.close
59
+ return
60
+ end
61
+ start_worker(socket)
62
+ @resource_limited = false
63
+ rescue Errno::EMFILE, Errno::ENFILE, ThreadError => error
64
+ socket.close if socket.is_a?(IO) && !socket.closed?
65
+ @logger.warn("proxy admission paused (#{error.class})") unless @resource_limited
66
+ @resource_limited = true
67
+ IO.select([shutdown.reader], nil, nil, 0.05)
68
+ end
69
+
70
+ def start_worker(socket)
71
+ @workers[socket] = Thread.new do
72
+ @mixed.handle(socket)
73
+ rescue StandardError => error
74
+ @logger.warn("proxy connection failed (#{error.class})")
75
+ ensure
76
+ socket.close unless socket.closed?
77
+ @completed << socket
78
+ end
79
+ end
80
+
81
+ def reap_workers
82
+ loop do
83
+ socket = @completed.pop(true)
84
+ @workers.delete(socket)&.join
85
+ rescue ThreadError
86
+ break
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require "logger"
5
+
6
+ module AnyTLS
7
+ # 命令行编排层;核心对象不读取进程参数或 ENV。
8
+ module CLI
9
+ # 解析兼容及当前选项,按优先级读取显式参数、URI 和环境变量中的凭据。
10
+ # 监听前完成校验;帮助和版本查询无需凭据,也不启动网络 I/O。
11
+ class Options
12
+ LEGACY_FLAGS = { "-sni" => "--sni", "-dr" => "--disable-reuse",
13
+ "-padding-scheme" => "--padding-scheme" }.freeze
14
+ VALUE_FLAGS = %w[-l --listen -p --password --password-file --connect-timeout --handshake-timeout
15
+ --max-sessions --grace --padding-scheme --log-level -s --server --sni --ca-file
16
+ -m --min-idle-sessions --open-timeout --max-connections --cert --key --max-streams].freeze
17
+
18
+ attr_reader :values, :parser
19
+
20
+ def initialize(mode, env: ENV)
21
+ @mode = mode
22
+ @env = env
23
+ @values = { listen: mode == :client ? "127.0.0.1:1080" : "0.0.0.0:8443",
24
+ min_idle_sessions: 5, reuse: true, connect_timeout: 5.0,
25
+ handshake_timeout: 5.0, open_timeout: 5.0, max_sessions: 128,
26
+ max_connections: 256, max_streams: 256, grace: 5.0 }
27
+ @parser = OptionParser.new
28
+ configure
29
+ end
30
+
31
+ def parse(argv)
32
+ args = normalize_legacy(argv)
33
+ parser.parse!(args)
34
+ raise ArgumentError, "unexpected positional arguments" unless args.empty?
35
+ return values if values[:help] || values[:version]
36
+
37
+ validate!
38
+ values
39
+ end
40
+
41
+ private
42
+
43
+ def normalize_legacy(argv)
44
+ value_follows = false
45
+ argv.map do |argument|
46
+ if value_follows
47
+ value_follows = false
48
+ next argument
49
+ end
50
+ flag, value = argument.split("=", 2)
51
+ flag = LEGACY_FLAGS.fetch(flag, flag)
52
+ value_follows = value.nil? && VALUE_FLAGS.include?(flag)
53
+ value.nil? ? flag : "#{flag}=#{value}"
54
+ end
55
+ end
56
+
57
+ def configure
58
+ parser.banner = "Usage: anytls-#{@mode} [options]"
59
+ parser.on("-l", "--listen ADDRESS", "Listen address (bracket IPv6)") { |v| values[:listen] = v }
60
+ parser.on("-p", "--password PASSWORD", "Password; prefer a file or ANYTLS_PASSWORD") do |v|
61
+ values[:password_argument] = v
62
+ end
63
+ parser.on("--password-file PATH", "Read password from a private file") { |v| values[:password_file] = v }
64
+ parser.on("--connect-timeout SECONDS", Float) { |v| values[:connect_timeout] = v }
65
+ parser.on("--handshake-timeout SECONDS", Float) { |v| values[:handshake_timeout] = v }
66
+ parser.on("--max-sessions COUNT", Integer) { |v| values[:max_sessions] = v }
67
+ parser.on("--grace SECONDS", Float, "Shutdown grace period") { |v| values[:grace] = v }
68
+ parser.on("--padding-scheme PATH") { |v| values[:padding_file] = v }
69
+ parser.on("--log-level LEVEL", %w[debug info warn error fatal]) { |v| values[:log_level] = v }
70
+ @mode == :client ? configure_client : configure_server
71
+ parser.on("-h", "--help", "Show this help") { values[:help] = true }
72
+ parser.on("-v", "--version", "Show version") { values[:version] = true }
73
+ end
74
+
75
+ def configure_client
76
+ parser.on("-s", "--server ADDRESS", "Server host:port or anytls:// URI") { |v| values[:server] = v }
77
+ parser.on("--sni HOST") { |v| values[:sni] = v }
78
+ parser.on("--ca-file PATH") { |v| values[:ca_file] = v }
79
+ parser.on("--insecure", "Explicitly disable TLS certificate verification") { values[:insecure] = true }
80
+ parser.on("-m", "--min-idle-sessions COUNT", Integer) { |v| values[:min_idle_sessions] = v }
81
+ parser.on("--disable-reuse", "Use a fresh session for every connection") { values[:reuse] = false }
82
+ parser.on("--open-timeout SECONDS", Float) { |v| values[:open_timeout] = v }
83
+ parser.on("--max-connections COUNT", Integer) { |v| values[:max_connections] = v }
84
+ end
85
+
86
+ def configure_server
87
+ parser.on("--cert PATH", "PEM certificate chain") { |v| values[:cert] = v }
88
+ parser.on("--key PATH", "PEM private key") { |v| values[:key] = v }
89
+ parser.on("--self-signed", "Generate an ephemeral development certificate") { values[:self_signed] = true }
90
+ parser.on("--max-streams COUNT", Integer) { |v| values[:max_streams] = v }
91
+ end
92
+
93
+ def validate!
94
+ values[:listen_host], values[:listen_port] = parse_endpoint(values[:listen], allow_zero: true)
95
+ parse_server if @mode == :client
96
+ values[:password] = resolve_password
97
+ raise ArgumentError, "password is required" if values[:password].nil? || values[:password].empty?
98
+
99
+ %i[connect_timeout handshake_timeout open_timeout grace].each do |key|
100
+ value = values.fetch(key)
101
+ raise ArgumentError, "#{key} must be positive and finite" unless value.finite? && value.positive?
102
+ end
103
+ %i[max_sessions max_connections max_streams].each do |key|
104
+ raise ArgumentError, "#{key} must be positive" unless values.fetch(key).positive?
105
+ end
106
+ if @mode == :client && values[:min_idle_sessions].negative?
107
+ raise ArgumentError, "min_idle_sessions must be nonnegative"
108
+ end
109
+
110
+ validate_certificate! if @mode == :server
111
+ values[:log_level] ||= %w[debug info warn error fatal].include?(@env["LOG_LEVEL"]) ? @env["LOG_LEVEL"] : "info"
112
+ end
113
+
114
+ def parse_server
115
+ server = values[:server]
116
+ raise ArgumentError, "server is required" unless server
117
+
118
+ if server.start_with?("anytls://")
119
+ uri = ConnectionURI.parse(server)
120
+ values[:host], values[:port] = uri.values_at(:host, :port)
121
+ values[:uri_password] = uri[:password]
122
+ values[:sni] ||= uri[:sni]
123
+ values[:insecure] = uri[:insecure] unless values.key?(:insecure)
124
+ else
125
+ values[:host], values[:port] = parse_endpoint(server)
126
+ end
127
+ end
128
+
129
+ def parse_endpoint(value, allow_zero: false)
130
+ match = /\A(?:\[([^\]]+)\]|([^:\s]+)):(\d+)\z/.match(value)
131
+ raise ArgumentError, "expected host:port or [IPv6]:port" unless match
132
+
133
+ port = Integer(match[3], 10)
134
+ raise ArgumentError, "port is out of range" unless port.between?(allow_zero ? 0 : 1, 65_535)
135
+
136
+ [match[1] || match[2], port]
137
+ end
138
+
139
+ def resolve_password
140
+ if values[:password_argument] && values[:password_file]
141
+ raise ArgumentError, "choose password or password-file"
142
+ end
143
+
144
+ if values[:password_file]
145
+ File.binread(values[:password_file]).sub(/\r?\n\z/, "")
146
+ else
147
+ values[:password_argument] || values[:uri_password] || @env["ANYTLS_PASSWORD"]
148
+ end
149
+ end
150
+
151
+ def validate_certificate!
152
+ if values[:self_signed]
153
+ raise ArgumentError, "self-signed cannot be combined with cert/key" if values[:cert] || values[:key]
154
+ elsif !values[:cert] || !values[:key]
155
+ raise ArgumentError, "server requires cert and key, or explicit self-signed"
156
+ end
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "options"
4
+ require_relative "shutdown"
5
+ require_relative "client_listener"
6
+
7
+ module AnyTLS
8
+ module CLI
9
+ # 进程入口:创建资源、等待停止,并返回脱敏诊断与退出码。
10
+ # 退出码为 0(成功)、2(参数错误)、1(运行失败)。
11
+ class Runner
12
+ def self.run(mode, argv = ARGV, env: ENV, stdout: $stdout, stderr: $stderr)
13
+ new(mode, argv, env: env, stdout: stdout, stderr: stderr).run
14
+ end
15
+
16
+ def initialize(mode, argv, env:, stdout:, stderr:)
17
+ @mode, @argv, @env, @stdout, @stderr = mode, argv, env, stdout, stderr
18
+ end
19
+
20
+ def run
21
+ options = Options.new(@mode, env: @env)
22
+ values = options.parse(@argv)
23
+ if values[:help]
24
+ @stdout.puts(options.parser)
25
+ return 0
26
+ end
27
+ if values[:version]
28
+ @stdout.puts(AnyTLS::VERSION)
29
+ return 0
30
+ end
31
+ logger = Logger.new(@stderr, level: values.fetch(:log_level))
32
+ logger.formatter = ->(severity, _time, _name, message) { "#{severity}: #{message}\n" }
33
+ shutdown = Shutdown.new.install
34
+ @mode == :client ? run_client(values, logger, shutdown) : run_server(values, logger, shutdown)
35
+ 0
36
+ rescue OptionParser::ParseError, ArgumentError
37
+ @stderr.puts("error: invalid options; use --help")
38
+ 2
39
+ rescue StandardError => error
40
+ @stderr.puts("error: #{@mode} failed (#{error.class})")
41
+ 1
42
+ ensure
43
+ shutdown&.close
44
+ end
45
+
46
+ private
47
+
48
+ def padding(values)
49
+ return unless values[:padding_file]
50
+
51
+ Padding::Scheme.new(File.binread(values[:padding_file]))
52
+ end
53
+
54
+ def run_client(values, logger, shutdown)
55
+ tls = TLSConfig.new(ca_file: values[:ca_file], sni: values[:sni], insecure: values.fetch(:insecure, false))
56
+ client = Client.new(host: values.fetch(:host), port: values.fetch(:port), password: values.fetch(:password),
57
+ tls: tls, min_idle_sessions: values.fetch(:min_idle_sessions), reuse: values.fetch(:reuse),
58
+ connect_timeout: values.fetch(:connect_timeout),
59
+ handshake_timeout: values.fetch(:handshake_timeout),
60
+ open_timeout: values.fetch(:open_timeout),
61
+ max_sessions: values.fetch(:max_sessions), logger: logger, padding: padding(values))
62
+ listener = ClientListener.new(client: client, options: values, logger: logger)
63
+ listener.run(shutdown)
64
+ ensure
65
+ client&.close
66
+ end
67
+
68
+ def run_server(values, logger, shutdown)
69
+ tls = TLSConfig.new(server: true, cert: values[:cert], key: values[:key],
70
+ self_signed: values.fetch(:self_signed, false))
71
+ server = Server.new(host: values.fetch(:listen_host), port: values.fetch(:listen_port),
72
+ password: values.fetch(:password), tls: tls, padding: padding(values),
73
+ connect_timeout: values.fetch(:connect_timeout),
74
+ handshake_timeout: values.fetch(:handshake_timeout),
75
+ max_sessions: values.fetch(:max_sessions), max_streams: values.fetch(:max_streams),
76
+ logger: logger)
77
+ worker = Thread.new { server.run }
78
+ loop do
79
+ break unless worker.alive?
80
+ break if IO.select([shutdown.reader], nil, nil, 0.1)
81
+ end
82
+ server.stop(grace: values.fetch(:grace))
83
+ worker.value
84
+ ensure
85
+ server&.stop(grace: values.fetch(:grace))
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "socket"
4
+
5
+ module AnyTLS
6
+ module CLI
7
+ # 信号处理器仅写入唤醒 socket;由正常线程执行停止流程。
8
+ # 安装者持有 socket 两端,关闭时恢复原信号处理器。
9
+ class Shutdown
10
+ attr_reader :reader
11
+
12
+ def initialize
13
+ @reader, @writer = Socket.pair(:UNIX, :STREAM, 0)
14
+ @previous = {}
15
+ end
16
+
17
+ def install
18
+ %w[INT TERM].each do |signal|
19
+ @previous[signal] = Signal.trap(signal) do
20
+ @writer.write_nonblock("x", exception: false)
21
+ rescue IOError, SystemCallError
22
+ nil
23
+ end
24
+ end
25
+ self
26
+ end
27
+
28
+ def close
29
+ @previous.each { |signal, handler| Signal.trap(signal, handler) }
30
+ @previous.clear
31
+ @reader.close unless @reader.closed?
32
+ @writer.close unless @writer.closed?
33
+ end
34
+ end
35
+ end
36
+ end