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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE +21 -0
- data/README.md +143 -0
- data/bin/anytls-client +7 -0
- data/bin/anytls-server +7 -0
- data/docs/ARCHITECTURE.md +105 -0
- data/docs/SOURCES.md +15 -0
- data/docs/VALIDATION.md +100 -0
- data/docs/validation/runtime-ruby34.json +8 -0
- data/docs/validation/runtime-ruby40.json +8 -0
- data/docs/validation/stress-c1.json +25 -0
- data/docs/validation/stress-c128.json +26 -0
- data/docs/validation/stress-c32.json +26 -0
- data/docs/validation/stress-sequential-10000.json +26 -0
- data/lib/anytls/address.rb +53 -0
- data/lib/anytls/buffered_io.rb +185 -0
- data/lib/anytls/byte_buffer.rb +61 -0
- data/lib/anytls/cli/client_listener.rb +91 -0
- data/lib/anytls/cli/options.rb +160 -0
- data/lib/anytls/cli/runner.rb +89 -0
- data/lib/anytls/cli/shutdown.rb +36 -0
- data/lib/anytls/client.rb +129 -0
- data/lib/anytls/connection_uri.rb +96 -0
- data/lib/anytls/deadline.rb +34 -0
- data/lib/anytls/dialer.rb +70 -0
- data/lib/anytls/errors.rb +23 -0
- data/lib/anytls/inbound/http_proxy.rb +224 -0
- data/lib/anytls/inbound/mixed.rb +60 -0
- data/lib/anytls/inbound/socks4.rb +55 -0
- data/lib/anytls/inbound/socks5.rb +138 -0
- data/lib/anytls/io_helpers.rb +87 -0
- data/lib/anytls/log.rb +15 -0
- data/lib/anytls/padding/scheme.rb +121 -0
- data/lib/anytls/padding/writer.rb +55 -0
- data/lib/anytls/protocol/authentication.rb +44 -0
- data/lib/anytls/protocol/decoder.rb +39 -0
- data/lib/anytls/protocol/frame.rb +40 -0
- data/lib/anytls/protocol/settings.rb +53 -0
- data/lib/anytls/protocol/socks_address.rb +59 -0
- data/lib/anytls/relay.rb +49 -0
- data/lib/anytls/server.rb +220 -0
- data/lib/anytls/session.rb +593 -0
- data/lib/anytls/session_pool.rb +166 -0
- data/lib/anytls/stream.rb +151 -0
- data/lib/anytls/tls_config.rb +76 -0
- data/lib/anytls/transport/tls_connection.rb +117 -0
- data/lib/anytls/uot/connection.rb +122 -0
- data/lib/anytls/uot/server.rb +97 -0
- data/lib/anytls/version.rb +6 -0
- data/lib/anytls.rb +40 -0
- 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
|