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,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyTLS
4
+ # 延迟拨号的出站入口;每条流独占租用一个会话,客户端持有会话池。
5
+ # 构造时不拨号;块形式在退出时关闭交给块的资源。
6
+ class Client
7
+ attr_reader :address
8
+
9
+ def initialize(host:, password:, port: 443, tls: nil, min_idle_sessions: 5, idle_timeout: 30,
10
+ idle_check_interval: 30, connect_timeout: 5, handshake_timeout: 5, open_timeout: 5,
11
+ max_sessions: 128, logger: nil, padding: nil, reuse: true)
12
+ raise ConfigurationError, "password must be a nonempty String" unless password.is_a?(String) && !password.empty?
13
+
14
+ @address = Address.new(host: host, port: port)
15
+ raise ConfigurationError, "server port must be positive" unless port.positive?
16
+
17
+ [connect_timeout, handshake_timeout, open_timeout].each do |value|
18
+ Deadline.new(value)
19
+ raise ConfigurationError, "connection timeouts must be positive" unless value&.positive?
20
+ end
21
+ @digest = Digest::SHA256.digest(password).freeze
22
+ @tls = tls || TLSConfig.new
23
+ @connect_timeout = connect_timeout
24
+ @handshake_timeout = handshake_timeout
25
+ @open_timeout = open_timeout
26
+ @logger = logger
27
+ @scheme = padding || Padding::Scheme.default
28
+ @scheme_mutex = Mutex.new
29
+ @pool = SessionPool.new(max_sessions: max_sessions, min_idle_sessions: min_idle_sessions,
30
+ idle_timeout: idle_timeout, idle_check_interval: idle_check_interval,
31
+ reuse: reuse) do |sequence, timeout|
32
+ create_session(sequence, timeout)
33
+ end
34
+ end
35
+
36
+ def self.open(**)
37
+ client = new(**)
38
+ return client unless block_given?
39
+
40
+ begin
41
+ yield client
42
+ ensure
43
+ client.close
44
+ end
45
+ end
46
+
47
+ # 返回的流可能仍未确认目标连接,直到收到 v2 协商结果和 SYNACK。
48
+ # 获取会话和开流共用预算;失败操作不会自动重放。
49
+ def connect(host, port, timeout: nil)
50
+ destination = Address.new(host: host, port: port)
51
+ raise ArgumentError, "destination port must be positive" unless port.positive?
52
+
53
+ deadline = Deadline.new(timeout.nil? ? @open_timeout : timeout)
54
+ session, stream = open_stream(destination, deadline)
55
+ stream.wait_connected(timeout: deadline.remaining) if session.peer_version >= 2
56
+ return stream unless block_given?
57
+
58
+ begin
59
+ yield stream
60
+ ensure
61
+ stream.close
62
+ end
63
+ rescue StandardError
64
+ stream&.close
65
+ raise
66
+ end
67
+
68
+ # 固定目标使用连接模式 UoT;未指定固定目标时,每个包必须提供目标。
69
+ def open_datagrams(destination: nil, timeout: nil)
70
+ deadline = Deadline.new(timeout.nil? ? @open_timeout : timeout)
71
+ session, stream = open_stream(Address.new(host: "sp.v2.udp-over-tcp.arpa", port: 443), deadline)
72
+ connection = UoT::Connection.new(stream, destination: destination, timeout: deadline.remaining)
73
+ stream.wait_connected(timeout: deadline.remaining) if session.peer_version >= 2
74
+ return connection unless block_given?
75
+
76
+ begin
77
+ yield connection
78
+ ensure
79
+ connection.close
80
+ end
81
+ rescue StandardError
82
+ stream&.close
83
+ raise
84
+ end
85
+
86
+ def stats
87
+ @pool.stats
88
+ end
89
+
90
+ def padding_scheme
91
+ @scheme_mutex.synchronize { @scheme }
92
+ end
93
+
94
+ def close
95
+ @pool.close
96
+ end
97
+
98
+ def inspect
99
+ "#<#{self.class} server=#{address.host}:#{address.port}>"
100
+ end
101
+
102
+ private
103
+
104
+ def open_stream(destination, deadline)
105
+ session = @pool.acquire(timeout: deadline.remaining)
106
+ begin
107
+ [session, session.open_stream(destination, timeout: deadline.remaining)]
108
+ rescue StandardError
109
+ session.close
110
+ raise
111
+ end
112
+ end
113
+
114
+ def create_session(sequence, timeout)
115
+ deadline = Deadline.new(timeout)
116
+ socket = Dialer.connect(address, timeout: [deadline.remaining, @connect_timeout].compact.min)
117
+ session = Session.new(socket, tls: @tls, password_digest: @digest, scheme: padding_scheme,
118
+ host: address.host, sequence: sequence, handshake_timeout: @handshake_timeout,
119
+ open_timeout: @open_timeout, logger: @logger,
120
+ on_idle: ->(item) { @pool.release(item) },
121
+ on_close: ->(item) { @pool.remove(item) },
122
+ on_padding: ->(scheme) { @scheme_mutex.synchronize { @scheme = scheme } })
123
+ session.wait_ready(timeout: deadline.remaining)
124
+ rescue StandardError
125
+ session ? session.close : socket&.close
126
+ raise
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module AnyTLS
6
+ # 纯连接 URI 编解码:保留字面加号,校验已知选项,
7
+ # 允许未来扩展查询参数,解析异常不回显凭据。
8
+ module ConnectionURI
9
+ module_function
10
+
11
+ def parse(value)
12
+ raise TypeError, "connection URI must be a String" unless value.is_a?(String)
13
+ raise ConfigurationError, "invalid percent encoding in connection URI" if value.match?(/%(?![0-9a-fA-F]{2})/)
14
+
15
+ uri = URI.parse(value)
16
+ unless uri.scheme == "anytls" && uri.host && ["", "/"].include?(uri.path)
17
+ raise ConfigurationError, "expected an anytls URI with a host and no path"
18
+ end
19
+ raise ConfigurationError, "encode password punctuation in URI user information" if uri.password
20
+
21
+ address = Address.new(host: uri.hostname, port: uri.port || 443)
22
+ raise ConfigurationError, "server port must be positive" if address.port.zero?
23
+
24
+ query = parse_query(uri.query)
25
+ insecure = query.fetch("insecure", "0")
26
+ raise ConfigurationError, "insecure must be 0 or 1" unless %w[0 1].include?(insecure)
27
+
28
+ {
29
+ host: address.host,
30
+ port: address.port,
31
+ password: decode_component(uri.user || "").freeze,
32
+ sni: query["sni"]&.freeze,
33
+ insecure: insecure == "1",
34
+ name: uri.fragment && decode_component(uri.fragment).freeze
35
+ }.freeze
36
+ rescue URI::InvalidURIError, ArgumentError => error
37
+ raise error if error.is_a?(ConfigurationError)
38
+
39
+ raise ConfigurationError, "invalid AnyTLS connection URI", cause: nil
40
+ end
41
+
42
+ def build(host:, password:, port: 443, sni: nil, insecure: false, name: nil)
43
+ address = Address.new(host:, port:)
44
+ raise ConfigurationError, "server port must be positive" if address.port.zero?
45
+ raise TypeError, "password must be a String" unless password.is_a?(String)
46
+ raise ArgumentError, "insecure must be boolean" unless [true, false].include?(insecure)
47
+
48
+ host_text = address.ipv6? ? "[#{address.host}]" : address.host
49
+ result = "anytls://#{encode_component(password)}@#{host_text}:#{port}/"
50
+ query = []
51
+ query << "sni=#{encode_component(sni)}" unless sni.nil?
52
+ query << "insecure=1" if insecure
53
+ result << "?#{query.join('&')}" unless query.empty?
54
+ result << "##{encode_component(name)}" unless name.nil?
55
+ result
56
+ end
57
+
58
+ def redact(value)
59
+ config = parse(value)
60
+ build(**config, password: "REDACTED")
61
+ end
62
+
63
+ def parse_query(query)
64
+ return {} unless query
65
+
66
+ query.split("&").each_with_object({}) do |part, result|
67
+ key, value = part.split("=", 2)
68
+ raise ConfigurationError, "URI query entry requires a value" unless value
69
+
70
+ key = decode_component(key)
71
+ raise ConfigurationError, "duplicate URI query parameter" if result.key?(key)
72
+
73
+ result[key] = decode_component(value)
74
+ end
75
+ end
76
+
77
+ private_class_method :parse_query
78
+
79
+ def decode_component(value)
80
+ decoded = URI::RFC2396_PARSER.unescape(value).force_encoding(Encoding::UTF_8)
81
+ raise ConfigurationError, "URI text must be UTF-8" unless decoded.valid_encoding?
82
+
83
+ decoded
84
+ end
85
+
86
+ private_class_method :decode_component
87
+
88
+ def encode_component(value)
89
+ raise TypeError, "URI component must be a String" unless value.is_a?(String)
90
+
91
+ URI::RFC2396_PARSER.escape(value, /[^a-zA-Z0-9\-._~]/)
92
+ end
93
+
94
+ private_class_method :encode_component
95
+ end
96
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyTLS
4
+ # 排队、等待和 socket 操作共用一个基于单调时钟的绝对时间预算。
5
+ class Deadline
6
+ def self.now
7
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
8
+ end
9
+
10
+ def initialize(seconds = nil)
11
+ unless seconds.nil? || (seconds.is_a?(Numeric) && seconds.finite? && seconds >= 0)
12
+ raise ArgumentError, "timeout must be a finite nonnegative number"
13
+ end
14
+
15
+ @expires_at = seconds && (self.class.now + seconds)
16
+ end
17
+
18
+ def remaining
19
+ return nil unless @expires_at
20
+
21
+ [@expires_at - self.class.now, 0].max
22
+ end
23
+
24
+ def check!
25
+ raise TimeoutError, "operation timed out" if @expires_at && self.class.now >= @expires_at
26
+ end
27
+
28
+ def wait(condition, mutex)
29
+ check!
30
+ condition.wait(mutex, remaining)
31
+ check!
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyTLS
4
+ # 在同一连接预算内解析地址,并交错尝试不同地址族。
5
+ # 成功连接的 TCP socket 交给调用者;其余尝试全部关闭。
6
+ module Dialer
7
+ module_function
8
+
9
+ def connect(address, timeout: 5)
10
+ raise ArgumentError, "destination port must be positive" unless address.port.positive?
11
+
12
+ deadline = Deadline.new(timeout)
13
+ deadline.check!
14
+ addresses = Addrinfo.getaddrinfo(address.host, address.port, nil, :STREAM, timeout: deadline.remaining)
15
+ candidates = interleave_families(addresses.uniq(&:to_sockaddr))
16
+ pending = []
17
+ next_attempt = Deadline.now
18
+ last_error = SocketError.new("no addresses for destination")
19
+ loop do
20
+ deadline.check!
21
+ if !candidates.empty? && (pending.empty? || Deadline.now >= next_attempt)
22
+ endpoint = candidates.shift
23
+ socket = nil
24
+ begin
25
+ socket = Socket.new(endpoint.afamily, Socket::SOCK_STREAM, 0)
26
+ pending << socket
27
+ socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
28
+ result = socket.connect_nonblock(endpoint, exception: false)
29
+ if result.is_a?(Integer) && result.zero?
30
+ pending.delete(socket)
31
+ return socket
32
+ end
33
+ rescue SystemCallError => error
34
+ last_error = error
35
+ pending.delete(socket)
36
+ socket&.close
37
+ end
38
+ next_attempt = Deadline.now + 0.25
39
+ end
40
+ raise last_error if candidates.empty? && pending.empty?
41
+ next if pending.empty?
42
+
43
+ delay = candidates.empty? ? deadline.remaining : [next_attempt - Deadline.now, 0].max
44
+ delay = [delay, deadline.remaining].compact.min
45
+ ready = IO.select(nil, pending, nil, delay)
46
+ next unless ready
47
+
48
+ ready[1].each do |connection|
49
+ status = connection.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR).int
50
+ pending.delete(connection)
51
+ return connection if status.zero?
52
+
53
+ last_error = SystemCallError.new("TCP connect failed", status)
54
+ connection.close
55
+ end
56
+ end
57
+ rescue Errno::ETIMEDOUT, IO::TimeoutError => error
58
+ raise TimeoutError, "TCP connection timed out", cause: error
59
+ ensure
60
+ pending&.each(&:close)
61
+ end
62
+
63
+ def interleave_families(addresses)
64
+ first, other = addresses.partition { |endpoint| endpoint.afamily == addresses.first.afamily }
65
+ first.zip(other).flatten.compact + other.drop(first.length)
66
+ end
67
+
68
+ private_class_method :interleave_families
69
+ end
70
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnyTLS
4
+ # 协议和传输故障继承 Error;配置错误和本地关闭
5
+ # 分别保留 Ruby 的 ArgumentError 与 IOError 捕获契约。
6
+ class Error < StandardError; end
7
+
8
+ class ConfigurationError < ArgumentError; end
9
+
10
+ class ProtocolError < Error; end
11
+
12
+ class AuthenticationError < Error; end
13
+
14
+ class TransportError < Error; end
15
+
16
+ class RemoteError < Error; end
17
+
18
+ class TimeoutError < Error; end
19
+
20
+ class BufferLimitExceeded < Error; end
21
+
22
+ class ClosedError < IOError; end
23
+ end
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+ require "protocol/http1/connection"
5
+
6
+ module AnyTLS
7
+ module Inbound
8
+ # 每个客户端连接独立使用处理器;由 protocol-http1 解析 HTTP 消息边界,
9
+ # 过滤逐跳头,并流式转发普通请求体,避免缓存整条消息。
10
+ # CONNECT 与成功的 Upgrade 将预读字节交给转发器;HTTPS 使用 CONNECT。
11
+ class HTTPProxy
12
+ HOP_HEADERS = %w[connection proxy-connection proxy-authenticate proxy-authorization keep-alive
13
+ te trailer transfer-encoding upgrade content-length].freeze
14
+
15
+ # 在 protocol-http1 消费消息定界头之前完成校验。
16
+ class Connection < ::Protocol::HTTP1::Connection
17
+ attr_reader :declared_content_length
18
+
19
+ def read_headers
20
+ headers = super
21
+ fields = headers.to_a
22
+ lengths = fields.select { |name, _| name.downcase == "content-length" }.map(&:last)
23
+ transfers = fields.select { |name, _| name.downcase == "transfer-encoding" }.map(&:last)
24
+ hosts = fields.count { |name, _| name.downcase == "host" }
25
+ raise ProtocolError, "duplicate Host header" if hosts > 1
26
+ raise ProtocolError, "ambiguous HTTP framing" if lengths.length > 1 || (lengths.any? && transfers.any?)
27
+ if lengths.any? && !lengths.first.match?(/\A[0-9]+\z/)
28
+ raise ProtocolError, "invalid Content-Length"
29
+ end
30
+
31
+ @declared_content_length = lengths.first
32
+ if transfers.any? && transfers.map(&:downcase) != ["chunked"]
33
+ raise ProtocolError, "unsupported Transfer-Encoding"
34
+ end
35
+
36
+ fields.map(&:last).each do |value|
37
+ raise ProtocolError, "invalid HTTP header value" if value.match?(/[\x00-\x08\x0a-\x1f\x7f]/)
38
+ end
39
+ headers
40
+ end
41
+
42
+ def read_request_body(method, headers)
43
+ # 依赖将 Upgrade 请求直接解释为隧道;带消息体时先拒绝,防止
44
+ # Content-Length 或分块定界被丢弃、请求体变成升级后的协议数据。
45
+ if headers["upgrade"] && (headers["transfer-encoding"] ||
46
+ (@declared_content_length && Integer(@declared_content_length, 10).positive?))
47
+ raise ProtocolError, "Upgrade requests with a body are unsupported"
48
+ end
49
+
50
+ super
51
+ end
52
+ end
53
+
54
+ def initialize(client:, logger: nil, handshake_timeout: 5)
55
+ @client = client
56
+ @logger = logger
57
+ @handshake_timeout = handshake_timeout
58
+ end
59
+
60
+ def handle(io)
61
+ connection = Connection.new(io, maximum_line_length: 8192)
62
+ loop do
63
+ @responded = false
64
+ io.timeout = @handshake_timeout
65
+ io.begin_headers!
66
+ request = connection.read_request
67
+ break unless request
68
+
69
+ host, method, target, version, headers, body = request
70
+ raise ProtocolError, "unsupported HTTP version" unless %w[HTTP/1.0 HTTP/1.1].include?(version)
71
+ raise ProtocolError, "HTTP/1.1 requires a Host header" if version == "HTTP/1.1" && host.to_s.empty?
72
+
73
+ io.timeout = nil
74
+ if method == "CONNECT"
75
+ connect(io, connection, target)
76
+ break
77
+ end
78
+
79
+ forward(io, connection, method, target, version, headers, body, host)
80
+ break unless connection.idle?
81
+ end
82
+ rescue BufferLimitExceeded
83
+ error_response(io, 431, "Request Header Fields Too Large")
84
+ rescue TimeoutError
85
+ error_response(io, 504, "Gateway Timeout")
86
+ rescue ProtocolError, ArgumentError, URI::InvalidURIError, ::Protocol::HTTP1::Error
87
+ error_response(io, 400, "Bad Request")
88
+ rescue TransportError, RemoteError, SocketError, SystemCallError, IOError, ::Protocol::HTTP::Error
89
+ error_response(io, 502, "Bad Gateway")
90
+ end
91
+
92
+ private
93
+
94
+ def connect(io, connection, authority)
95
+ parsed = URI.parse("http://#{authority}")
96
+ unless parsed.host && parsed.userinfo.nil? && parsed.path.empty? && parsed.query.nil? &&
97
+ parsed.fragment.nil? && authority.match?(/:\d+\z/) && parsed.port.positive?
98
+ raise ProtocolError, "CONNECT requires host:port"
99
+ end
100
+
101
+ stream = @client.connect(parsed.hostname, parsed.port, timeout: @handshake_timeout)
102
+ io.write("HTTP/1.1 200 Connection Established\r\n\r\n".b)
103
+ @responded = true
104
+ Relay.copy(connection.hijack!, stream)
105
+ ensure
106
+ stream&.close
107
+ end
108
+
109
+ def forward(io, downstream, method, target, version, headers, body, _host)
110
+ uri = URI.parse(target)
111
+ unless uri.is_a?(URI::HTTP) && uri.host && uri.userinfo.nil? && uri.fragment.nil?
112
+ raise ProtocolError, "HTTP proxy requires an absolute HTTP URI"
113
+ end
114
+ # HTTPS 目标使用 CONNECT;若在此加密目标流量,
115
+ # 需要可供 TLS 使用的真实 IO 端点,AnyTLS 虚拟流不具备该能力。
116
+ raise ProtocolError, "use CONNECT for HTTPS destinations" unless uri.scheme == "http"
117
+
118
+ stream = @client.connect(uri.hostname, uri.port, timeout: @handshake_timeout)
119
+ transport = BufferedIO.new(stream)
120
+ upstream = Connection.new(transport, maximum_line_length: 8192)
121
+ upgrade = headers["upgrade"]
122
+ if upgrade && !headers["connection"].to_s.downcase.split(",").map(&:strip).include?("upgrade")
123
+ raise ProtocolError, "Upgrade requires Connection: upgrade"
124
+ end
125
+
126
+ outgoing = filtered_headers(headers, upgrade: !upgrade.nil?)
127
+ outgoing.delete("expect")
128
+ if headers["expect"]
129
+ raise ProtocolError, "unsupported expectation" unless headers["expect"].downcase == "100-continue"
130
+
131
+ downstream.write_interim_response(version, 100, [])
132
+ end
133
+ upstream.write_request(uri.host.include?(":") ? "[#{uri.hostname}]:#{uri.port}" : uri.authority,
134
+ method, uri.request_uri, "HTTP/1.1", outgoing)
135
+ if upgrade
136
+ transport.write("\r\n")
137
+ elsif body
138
+ cancel_upload = false
139
+ upload = Thread.new do
140
+ upstream.write_body("HTTP/1.1", body)
141
+ nil
142
+ rescue StandardError => error
143
+ stream.close unless cancel_upload || stream.closed?
144
+ error
145
+ end
146
+ else
147
+ upstream.write_body("HTTP/1.1", body)
148
+ end
149
+ loop do
150
+ transport.timeout = upload&.alive? ? nil : @handshake_timeout
151
+ transport.begin_headers!
152
+ _response_version, status, reason, response_headers, response_body = upstream.read_response(method)
153
+ transport.timeout = nil
154
+ if status.between?(100, 199) && status != 101
155
+ downstream.write_interim_response(version, status, filtered_headers(response_headers), reason)
156
+ next
157
+ end
158
+
159
+ if upload&.alive?
160
+ # 目标服务可能在读取完整请求体之前拒绝上传。
161
+ # 仅关闭本地客户端 socket 的读取方向,等待上传线程结束,
162
+ # 再交付目标响应并关闭该 HTTP 连接。
163
+ downstream.persistent = false
164
+ cancel_upload = true
165
+ io.close_read
166
+ upload.join
167
+ end
168
+
169
+ if status == 101
170
+ raise ProtocolError, "unexpected HTTP upgrade" unless upgrade
171
+
172
+ downstream.write_response(version, status, filtered_headers(response_headers, upgrade: true), reason)
173
+ io.write("\r\n")
174
+ @responded = true
175
+ Relay.copy(downstream.hijack!, upstream.hijack!)
176
+ else
177
+ response_fields = filtered_headers(response_headers)
178
+ no_body = method == "HEAD" || [204, 304].include?(status)
179
+ if no_body && status != 204 && upstream.declared_content_length
180
+ response_fields.add("content-length", upstream.declared_content_length)
181
+ end
182
+ downstream.write_response(version, status, response_fields, reason)
183
+ @responded = true
184
+ # Upgrade 被拒绝时,请求的隧道体仍未结束。
185
+ downstream.persistent = false if upgrade
186
+ if no_body
187
+ io.write("\r\n")
188
+ downstream.send_end_stream!
189
+ response_body&.close
190
+ else
191
+ downstream.write_body(version, response_body)
192
+ end
193
+ end
194
+ break
195
+ end
196
+ rescue StandardError => error
197
+ upload_error = upload.value if upload && !upload.alive?
198
+ raise(upload_error || error)
199
+ ensure
200
+ stream&.close
201
+ if upload&.alive?
202
+ cancel_upload = true
203
+ io.close_read
204
+ upload.join
205
+ end
206
+ end
207
+
208
+ def filtered_headers(headers, upgrade: false)
209
+ names = headers["connection"].to_s.split(",").map { |name| name.strip.downcase }
210
+ forbidden = HOP_HEADERS + names + ["host"]
211
+ forbidden -= %w[connection upgrade] if upgrade
212
+ ::Protocol::HTTP::Headers.new(headers.to_a.reject { |name, _| forbidden.include?(name.downcase) })
213
+ end
214
+
215
+ def error_response(io, status, reason)
216
+ return if @responded || io.closed?
217
+
218
+ io.write("HTTP/1.1 #{status} #{reason}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n")
219
+ rescue IOError, SystemCallError, Error
220
+ nil
221
+ end
222
+ end
223
+ end
224
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../buffered_io"
4
+ require_relative "../relay"
5
+ require_relative "../uot/connection"
6
+ require_relative "socks4"
7
+ require_relative "socks5"
8
+
9
+ module AnyTLS
10
+ # 本地代理适配层,将应用协议握手转换为 Client API 调用。
11
+ module Inbound
12
+ # 按缓冲区首字节分派协议,保留已预读的后续载荷。
13
+ # 持有接入 socket;关闭活动连接以唤醒等待中的处理线程。
14
+ class Mixed
15
+ def initialize(client:, logger: nil, handshake_timeout: 5)
16
+ @client = client
17
+ @logger = logger
18
+ @handshake_timeout = handshake_timeout
19
+ @lock = Mutex.new
20
+ @connections = {}
21
+ @closed = false
22
+ end
23
+
24
+ def handle(socket)
25
+ @lock.synchronize do
26
+ raise ClosedError, "inbound listener closed" if @closed
27
+
28
+ @connections[socket] = true
29
+ end
30
+ io = BufferedIO.new(socket, timeout: @handshake_timeout)
31
+ handler = case io.peek(1).getbyte(0)
32
+ when 4 then Socks4
33
+ when 5 then Socks5
34
+ else
35
+ require_relative "http_proxy"
36
+ HTTPProxy
37
+ end
38
+ handler.new(client: @client, logger: @logger, handshake_timeout: @handshake_timeout).handle(io)
39
+ rescue IOError, SystemCallError, Error, SocketError => error
40
+ Log.emit(@logger, :debug, "inbound connection ended: #{error.class}")
41
+ ensure
42
+ IOHelpers.close(socket) unless socket.closed?
43
+ @lock.synchronize { @connections.delete(socket) }
44
+ end
45
+
46
+ def close
47
+ connections = @lock.synchronize do
48
+ @closed = true
49
+ @connections.keys
50
+ end
51
+ connections.each do |socket|
52
+ IOHelpers.close(socket) unless socket.closed?
53
+ rescue IOError, SystemCallError
54
+ nil
55
+ end
56
+ nil
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ipaddr"
4
+
5
+ module AnyTLS
6
+ module Inbound
7
+ # 仅支持 SOCKS4/4a CONNECT;以有界 NUL 结尾字符串读取用户标识和域名。
8
+ # 用户标识不作为本地监听器的授权机制。
9
+ class Socks4
10
+ def initialize(client:, logger: nil, handshake_timeout: 5)
11
+ @client = client
12
+ @logger = logger
13
+ @handshake_timeout = handshake_timeout
14
+ end
15
+
16
+ def handle(io)
17
+ header = IOHelpers.read_exact(io, 8, timeout: @handshake_timeout)
18
+ version, command, port = header.unpack("CCn")
19
+ raise ProtocolError, "unsupported SOCKS4 request" unless version == 4 && command == 1 && port.positive?
20
+
21
+ null_string(io) # SOCKS4 用户标识仅为元数据,不用于认证。
22
+ address = header.byteslice(4, 4)
23
+ host = if address.byteslice(0, 3) == "\0\0\0".b && address.getbyte(3).positive?
24
+ null_string(io)
25
+ else
26
+ IPAddr.ntop(address)
27
+ end
28
+ raise ProtocolError, "empty SOCKS4a host" if host.empty?
29
+
30
+ stream = @client.connect(host, port, timeout: @handshake_timeout)
31
+ io.write([0, 90, 0, 0].pack("CCnN"))
32
+ replied = true
33
+ io.timeout = nil
34
+ Relay.copy(io, stream)
35
+ rescue ProtocolError, ArgumentError, TimeoutError, TransportError, RemoteError, SocketError, SystemCallError
36
+ io.write([0, 91, 0, 0].pack("CCnN")) unless replied || io.closed?
37
+ ensure
38
+ stream&.close
39
+ end
40
+
41
+ private
42
+
43
+ def null_string(io)
44
+ value = +"".b
45
+ 256.times do
46
+ byte = IOHelpers.read_exact(io, 1, timeout: @handshake_timeout)
47
+ return value if byte == "\0".b
48
+
49
+ value << byte
50
+ end
51
+ raise ProtocolError, "SOCKS4 string exceeds 255 bytes"
52
+ end
53
+ end
54
+ end
55
+ end