Files
syn-chat-bot/.venv/lib/python3.9/site-packages/urllib3_future/contrib/resolver/utils.py
Hyungi Ahn c2257d3a86 fix: 포트 충돌 회피 — note_bridge 8098, intent_service 8099
Jellyfin(8096), OrbStack(8097) 포트 충돌으로 변경.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 13:53:55 +09:00

323 lines
8.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import base64
import binascii
import socket
import struct
import typing
if typing.TYPE_CHECKING:
class HttpsRecord(typing.TypedDict):
priority: int
target: str
alpn: list[str]
ipv4hint: list[str]
ipv6hint: list[str]
echconfig: list[str]
def inet4_ntoa(address: bytes) -> str:
"""
Convert an IPv4 address from bytes to str.
"""
if len(address) != 4:
raise ValueError(
f"IPv4 addresses are 4 bytes long, got {len(address)} byte(s) instead"
)
return "%u.%u.%u.%u" % (address[0], address[1], address[2], address[3])
def inet6_ntoa(address: bytes) -> str:
"""
Convert an IPv6 address from bytes to str.
"""
if len(address) != 16:
raise ValueError(
f"IPv6 addresses are 16 bytes long, got {len(address)} byte(s) instead"
)
hex = binascii.hexlify(address)
chunks = []
i = 0
length = len(hex)
while i < length:
chunk = hex[i : i + 4].decode().lstrip("0") or "0"
chunks.append(chunk)
i += 4
# Compress the longest subsequence of 0-value chunks to ::
best_start = 0
best_len = 0
start = -1
last_was_zero = False
for i in range(8):
if chunks[i] != "0":
if last_was_zero:
end = i
current_len = end - start
if current_len > best_len:
best_start = start
best_len = current_len
last_was_zero = False
elif not last_was_zero:
start = i
last_was_zero = True
if last_was_zero:
end = 8
current_len = end - start
if current_len > best_len:
best_start = start
best_len = current_len
if best_len > 1:
if best_start == 0 and (best_len == 6 or best_len == 5 and chunks[5] == "ffff"):
# We have an embedded IPv4 address
if best_len == 6:
prefix = "::"
else:
prefix = "::ffff:"
thex = prefix + inet4_ntoa(address[12:])
else:
thex = (
":".join(chunks[:best_start])
+ "::"
+ ":".join(chunks[best_start + best_len :])
)
else:
thex = ":".join(chunks)
return thex
def packet_fragment(payload: bytes, *identifiers: bytes) -> tuple[bytes, ...]:
results = []
offset = 0
start_packet_idx = []
lead_identifier = None
for identifier in identifiers:
idx = payload[:12].find(identifier)
if idx == -1:
continue
if idx != 0:
offset = idx
start_packet_idx.append(idx - offset)
lead_identifier = identifier
break
for identifier in identifiers:
if identifier == lead_identifier:
continue
if offset == 0:
idx = payload.find(b"\x02" + identifier)
else:
idx = payload.find(identifier)
if idx == -1:
continue
start_packet_idx.append(idx - offset)
if not start_packet_idx:
raise ValueError(
"no identifiable dns message emerged from given payload. "
"this should not happen at all. networking issue?"
)
if len(start_packet_idx) == 1:
return (payload,)
start_packet_idx = sorted(start_packet_idx)
previous_idx = None
for idx in start_packet_idx:
if previous_idx is None:
previous_idx = idx
continue
results.append(payload[previous_idx:idx])
previous_idx = idx
results.append(payload[previous_idx:])
return tuple(results)
def is_ipv4(addr: str) -> bool:
try:
socket.inet_aton(addr)
return True
except OSError:
return False
def is_ipv6(addr: str) -> bool:
try:
socket.inet_pton(socket.AF_INET6, addr)
return True
except OSError:
return False
def validate_length_of(hostname: str) -> None:
"""RFC 1035 impose a limit on a domain name length. We verify it there."""
if len(hostname.strip(".")) > 253:
raise UnicodeError("hostname to resolve exceed 253 characters")
elif any([len(_) > 63 for _ in hostname.split(".")]):
raise UnicodeError("at least one label to resolve exceed 63 characters")
def rfc1035_should_read(payload: bytes) -> bool:
if not payload:
return False
if len(payload) <= 2:
return True
cursor = payload
while True:
expected_size: int = struct.unpack("!H", cursor[:2])[0]
if len(cursor[2:]) == expected_size:
return False
elif len(cursor[2:]) < expected_size:
return True
cursor = cursor[2 + expected_size :]
def rfc1035_unpack(payload: bytes) -> tuple[bytes, ...]:
cursor = payload
packets = []
while cursor:
expected_size: int = struct.unpack("!H", cursor[:2])[0]
packets.append(cursor[2 : 2 + expected_size])
cursor = cursor[2 + expected_size :]
return tuple(packets)
def rfc1035_pack(message: bytes) -> bytes:
return struct.pack("!H", len(message)) + message
def read_name(data: bytes, offset: int) -> tuple[str, int]:
"""
Read a DNSencoded name (with compression pointers) from data[offset:].
Returns (name, new_offset).
"""
labels = []
while True:
length = data[offset]
# compression pointer?
if length & 0xC0 == 0xC0:
pointer = struct.unpack_from("!H", data, offset)[0] & 0x3FFF
subname, _ = read_name(data, pointer)
labels.append(subname)
offset += 2
break
if length == 0:
offset += 1
break
offset += 1
labels.append(data[offset : offset + length].decode())
offset += length
return ".".join(labels), offset
def parse_echconfigs(buf: bytes) -> list[str]:
"""
buf is the raw bytes of the ECHConfig vector:
- 2-byte total length, then for each:
- 2-byte cfg length + that many bytes of cfg
We return a list of Base64 strings (one per config).
"""
if len(buf) < 2:
return []
off = 2
total = struct.unpack_from("!H", buf, 0)[0]
end = 2 + total
out = []
while off + 2 <= end:
cfg_len = struct.unpack_from("!H", buf, off)[0]
off += 2
cfg = buf[off : off + cfg_len]
off += cfg_len
out.append(base64.b64encode(cfg).decode())
return out
def parse_https_rdata(rdata: bytes) -> HttpsRecord:
"""
Parse the RDATA of an SVCB/HTTPS record.
Returns a dict with keys: priority, target, alpn, ipv4hint, ipv6hint, echconfig.
"""
off = 0
priority = struct.unpack_from("!H", rdata, off)[0]
off += 2
target, off = read_name(rdata, off)
# pull out all the key/value params
params = {}
while off + 4 <= len(rdata):
key, length = struct.unpack_from("!HH", rdata, off)
off += 4
params[key] = rdata[off : off + length]
off += length
# decode ALPN (key=1), IPv4 (4), IPv6 (6), ECHConfig (5)
def parse_alpn(buf: bytes) -> list[str]:
out = []
i: int = 0
while i < len(buf):
ln = buf[i]
out.append(buf[i + 1 : i + 1 + ln].decode())
i += 1 + ln
return out
alpn: list[str] = parse_alpn(params.get(1, b""))
ipv4 = [
inet4_ntoa(params[4][i : i + 4]) for i in range(0, len(params.get(4, b"")), 4)
]
ipv6 = [
inet6_ntoa(params[6][i : i + 16]) for i in range(0, len(params.get(6, b"")), 16)
]
echconfs = parse_echconfigs(params.get(5, b""))
return {
"priority": priority,
"target": target or ".", # empty name → root
"alpn": alpn,
"ipv4hint": ipv4,
"ipv6hint": ipv6,
"echconfig": echconfs,
}
__all__ = (
"inet4_ntoa",
"inet6_ntoa",
"packet_fragment",
"is_ipv4",
"is_ipv6",
"validate_length_of",
"rfc1035_pack",
"rfc1035_unpack",
"rfc1035_should_read",
"parse_https_rdata",
)