Skip to content

Commit 16bb869

Browse files
fix: Refactor host handling in Node class and add utility functions for URL formatting
1 parent 18a56f7 commit 16bb869

File tree

7 files changed

+108
-9
lines changed

7 files changed

+108
-9
lines changed

PasarGuardNodeBridge/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,10 @@
1212
- Extensible with custom metadata via the `extra` argument
1313
1414
Author: PasarGuard
15-
Version: 0.3.3
15+
Version: 0.3.4
1616
"""
1717

18-
__version__ = "0.3.3"
18+
__version__ = "0.3.4"
1919
__author__ = "PasarGuard"
2020

2121

PasarGuardNodeBridge/grpclib.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from PasarGuardNodeBridge.common import service_grpc
1212
from PasarGuardNodeBridge.common import service_pb2 as service
1313
from PasarGuardNodeBridge.controller import Health, NodeAPIError
14-
from PasarGuardNodeBridge.utils import grpc_to_http_status
14+
from PasarGuardNodeBridge.utils import format_host_for_url, grpc_to_http_status
1515

1616

1717
class Node(PasarGuardNode):
@@ -28,11 +28,18 @@ def __init__(
2828
default_timeout: int = 10,
2929
internal_timeout: int = 15,
3030
):
31-
service_url = f"https://{address.strip('/')}:{api_port}/"
31+
host_for_url = format_host_for_url(address)
32+
service_url = f"https://{host_for_url}:{api_port}/"
33+
print(f"Service URL: {service_url}")
3234
super().__init__(server_ca, api_key, service_url, name, extra, logger, default_timeout, internal_timeout)
3335

3436
try:
35-
self.channel = Channel(host=address, port=port, ssl=self.ctx, config=Configuration(_keepalive_timeout=10))
37+
self.channel = Channel(
38+
host=address,
39+
port=port,
40+
ssl=self.ctx,
41+
config=Configuration(_keepalive_timeout=10),
42+
)
3643
self._client = service_grpc.NodeServiceStub(self.channel)
3744
self._metadata = {"x-api-key": api_key}
3845
except Exception as e:

PasarGuardNodeBridge/rest.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from PasarGuardNodeBridge.abstract_node import PasarGuardNode
1010
from PasarGuardNodeBridge.common import service_pb2 as service
1111
from PasarGuardNodeBridge.controller import Health, NodeAPIError
12+
from PasarGuardNodeBridge.utils import format_host_for_url
1213

1314

1415
class Node(PasarGuardNode):
@@ -25,10 +26,11 @@ def __init__(
2526
default_timeout: int = 10,
2627
internal_timeout: int = 15,
2728
):
28-
service_url = f"https://{address.strip('/')}:{api_port}/"
29+
host_for_url = format_host_for_url(address)
30+
service_url = f"https://{host_for_url}:{api_port}/"
2931
super().__init__(server_ca, api_key, service_url, name, extra, logger, default_timeout, internal_timeout)
3032

31-
url = f"https://{address.strip('/')}:{port}/"
33+
url = f"https://{host_for_url}:{port}/"
3234
self._client = httpx.AsyncClient(
3335
http2=True,
3436
verify=self.ctx,

PasarGuardNodeBridge/utils.py

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from grpclib.const import Status
22
from http import HTTPStatus
3+
from ipaddress import ip_address
34

45
from PasarGuardNodeBridge.common.service_pb2 import User, Proxy, Vmess, Vless, Trojan, Shadowsocks
56

@@ -46,3 +47,90 @@ def grpc_to_http_status(grpc_status: Status) -> int:
4647
Status.DATA_LOSS: HTTPStatus.INTERNAL_SERVER_ERROR.value,
4748
}
4849
return mapping.get(grpc_status, HTTPStatus.INTERNAL_SERVER_ERROR.value)
50+
51+
52+
def normalize_host(address: str) -> str:
53+
"""
54+
Normalize a host/address string for socket connections by stripping
55+
whitespace, schemes, paths, surrounding brackets, and ports.
56+
"""
57+
raw = address.strip()
58+
if not raw:
59+
return ""
60+
61+
# Drop scheme if provided
62+
if "://" in raw:
63+
raw = raw.split("://", 1)[1]
64+
65+
# Normalize slashes and remove path/query/fragment
66+
raw = raw.lstrip("/")
67+
for sep in ("#", "?"):
68+
if sep in raw:
69+
raw = raw.split(sep, 1)[0]
70+
if "/" in raw:
71+
raw = raw.split("/", 1)[0]
72+
73+
# Drop optional credentials (user:pass@host)
74+
if "@" in raw:
75+
raw = raw.rsplit("@", 1)[1]
76+
77+
host = raw
78+
79+
# Bracketed IPv6 literal
80+
if host.startswith("["):
81+
end = host.find("]")
82+
if end != -1:
83+
host = host[1:end]
84+
else:
85+
host = host[1:]
86+
87+
# Pure IP literal
88+
try:
89+
parsed_ip = ip_address(host)
90+
return parsed_ip.compressed
91+
except ValueError:
92+
pass
93+
94+
# IPv6 literal with inline port but without brackets
95+
if host.count(":") >= 2:
96+
maybe_host, maybe_port = host.rsplit(":", 1)
97+
if maybe_port.isdigit():
98+
try:
99+
parsed_ip = ip_address(maybe_host)
100+
return parsed_ip.compressed
101+
except ValueError:
102+
pass
103+
# Looks IPv6-like; return as-is without trying to strip further
104+
return host
105+
106+
# Hostname/IPv4 with port (single colon)
107+
if ":" in host:
108+
maybe_host, maybe_port = host.rsplit(":", 1)
109+
if maybe_port.isdigit():
110+
host = maybe_host
111+
112+
return host
113+
114+
115+
def format_host_for_url(address: str) -> str:
116+
"""
117+
Format a host/address for inclusion in an HTTP URL.
118+
119+
- IPv6 addresses are wrapped in brackets.
120+
- Hosts with multiple colons (IPv6-like) are bracketed to avoid port confusion.
121+
- IPv4/hostnames are returned unchanged (after normalization).
122+
"""
123+
host = normalize_host(address)
124+
if not host:
125+
return ""
126+
try:
127+
parsed = ip_address(host)
128+
if parsed.version == 6:
129+
return f"[{parsed.compressed}]"
130+
return parsed.compressed
131+
except ValueError:
132+
# If it looks like an IPv6 literal (multiple colons) but ipaddress failed,
133+
# still bracket it so URL parsing won't treat parts as a port.
134+
if host.count(":") >= 2:
135+
return f"[{host}]"
136+
return host

examples/example.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
address = "172.27.158.135"
66
port = 2096
7+
api_port = 2097
78
server_ca_file = "certs/ssl_cert.pem"
89
config_file = "config/xray.json"
910
api_key = "d04d8680-942d-4365-992f-9f482275691d"
@@ -24,6 +25,7 @@ async def main():
2425
connection=Bridge.NodeType.grpc,
2526
address=address,
2627
port=port,
28+
api_port=api_port,
2729
server_ca=server_ca_content,
2830
api_key=api_key,
2931
extra={"id": 1},

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "pasarguard-node-bridge"
3-
version = "0.3.3"
3+
version = "0.3.4"
44
description = "python package to connect your project with PasarGuard node go"
55
url = "https://github.com/PasarGuard/node_bridge_py"
66
keywords = [

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)