Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 34 additions & 9 deletions testlib/src/main/resources/zssdk.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
import hmac
from hashlib import sha1
import datetime
import time

try:
int_types = (int, long)
Expand Down Expand Up @@ -58,7 +57,7 @@ def _exception_safe(func):
def wrap(*args, **kwargs):
try:
func(*args, **kwargs)
except:
except Exception:
print(traceback.format_exc())

return wrap
Expand All @@ -80,6 +79,7 @@ def _http_error(status, body=None):
def _error(code, desc, details):
err = ErrorCode()
err.code = code
err.description = desc
err.desc = desc
err.details = details
return {'error': err}
Expand Down Expand Up @@ -181,7 +181,7 @@ def _check_params(self):
if value is not None and isinstance(value, str) and annotation.max_length and len(value) > annotation.max_length:
raise SdkError('invalid length[%s] of the parameter[%s], the max allowed length is %s' % (len(value), param_name, annotation.max_length))

if value is not None and isinstance(value, str) and annotation.min_length and len(value) > annotation.min_length:
if value is not None and isinstance(value, str) and annotation.min_length and len(value) < annotation.min_length:
raise SdkError('invalid length[%s] of the parameter[%s], the minimal allowed length is %s' % (len(value), param_name, annotation.min_length))

if value is not None and isinstance(value, list) and annotation.non_empty is True and len(value) == 0:
Expand Down Expand Up @@ -235,11 +235,11 @@ def _url(self):
elements.append('/v1')

path = self.PATH.replace('{', '${')
unresolved = re.findall('${(.+?)}', path)
unresolved = re.findall(r'\$\{(.+?)\}', path)
params = self._params()
if unresolved:
for u in unresolved:
if u in params:
if u not in params:
raise SdkError('missing a mandatory parameter[%s]' % u)

path = string.Template(path).substitute(params)
Expand Down Expand Up @@ -362,7 +362,21 @@ def _poll_result(self, rsp, cb):
m = json.loads(rsp.data)
location = m[LOCATION]
if not location:
raise SdkError("Internal Error] the api[%s] is an async API but the server doesn't return the polling location url")
raise SdkError("[Internal Error] the api[%s] is an async API but the server doesn't return the polling location url" % self.PATH)

# Rewrite poll URL to use client-configured hostname:port,
# in case server returns an internal IP unreachable from client
try:
from urllib.parse import urlparse, urlunparse
except ImportError:
from urlparse import urlparse, urlunparse
parsed = urlparse(location)
configured_host = __config__[CONFIG_HOSTNAME]
configured_port = str(__config__[CONFIG_PORT])
if ':' in configured_host and not configured_host.startswith('['):
configured_host = '[%s]' % configured_host
location = urlunparse(parsed._replace(
netloc='%s:%s' % (configured_host, configured_port)))
Comment on lines +373 to +379
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
python - <<'PY'
try:
    from urllib.parse import urlparse, urlunparse
except ImportError:
    from urlparse import urlparse, urlunparse

location = '/v1/api-jobs/uuid'
parsed = urlparse(location)
rewritten = urlunparse(parsed._replace(netloc='127.0.0.1:8080'))

print('input    =', location)
print('rewritten=', rewritten)
print('scheme   =', urlparse(rewritten).scheme or '<empty>')
PY

Repository: MatheMatrix/zstack

Length of output: 154


🏁 Script executed:

find . -name "zssdk.py" -type f

Repository: MatheMatrix/zstack

Length of output: 99


🏁 Script executed:

sed -n '370,385p' ./testlib/src/main/resources/zssdk.py | cat -n

Repository: MatheMatrix/zstack

Length of output: 829


🏁 Script executed:

# 检查更大的上下文,找到函数定义
sed -n '360,390p' ./testlib/src/main/resources/zssdk.py | cat -n

Repository: MatheMatrix/zstack

Length of output: 1621


🏁 Script executed:

# 检查整个文件是否有中文字符
grep -n '[^\x00-\x7F]' ./testlib/src/main/resources/zssdk.py | head -20

Repository: MatheMatrix/zstack

Length of output: 472


🏁 Script executed:

python3 << 'EOF'
import re

with open('./testlib/src/main/resources/zssdk.py', 'r', encoding='utf-8', errors='ignore') as f:
    lines = f.readlines()
    for i, line in enumerate(lines, 1):
        if any(ord(c) > 127 for c in line):
            print(f"Line {i}: {line.rstrip()}")
EOF

Repository: MatheMatrix/zstack

Length of output: 44


🏁 Script executed:

python3 << 'EOF'
try:
    from urllib.parse import urlparse, urlunparse
except ImportError:
    from urlparse import urlparse, urlunparse

# Test case 1: relative path
location = '/v1/api-jobs/uuid'
parsed = urlparse(location)

# Current behavior (wrong)
rewritten_old = urlunparse(parsed._replace(netloc='127.0.0.1:8080'))
print("Test 1: Relative path /v1/api-jobs/uuid")
print(f"  Current (wrong):  {rewritten_old}")
print(f"  Scheme:           {urlparse(rewritten_old).scheme or '<empty>'}")

# Proposed fix
scheme = parsed.scheme or 'http'
rewritten_new = urlunparse(parsed._replace(
    scheme=scheme,
    netloc='127.0.0.1:8080'))
print(f"  Proposed (fixed): {rewritten_new}")
print(f"  Scheme:           {urlparse(rewritten_new).scheme or '<empty>'}")

# Test case 2: absolute URL with scheme
print("\nTest 2: Absolute URL with scheme")
location2 = 'http://internal-server:8080/v1/api-jobs/uuid'
parsed2 = urlparse(location2)
scheme2 = parsed2.scheme or 'http'
rewritten2 = urlunparse(parsed2._replace(
    scheme=scheme2,
    netloc='127.0.0.1:8080'))
print(f"  Input:    {location2}")
print(f"  Output:   {rewritten2}")

# Test case 3: absolute URL with https
print("\nTest 3: Absolute URL with https")
location3 = 'https://internal-server:8080/v1/api-jobs/uuid'
parsed3 = urlparse(location3)
scheme3 = parsed3.scheme or 'http'
rewritten3 = urlunparse(parsed3._replace(
    scheme=scheme3,
    netloc='127.0.0.1:8080'))
print(f"  Input:    {location3}")
print(f"  Output:   {rewritten3}")
EOF

Repository: MatheMatrix/zstack

Length of output: 552


相对 Location 会被重写成无 scheme 的 URL

当前只替换 netloc。当服务端返回相对路径(如 /v1/api-jobs/...)时,重写结果可能是 //host:port/...,后续请求在部分客户端会失败。建议在缺少 scheme 时补默认 scheme(与 SDK 当前构造 URL 的 http 一致)。

建议修改
         parsed = urlparse(location)
         configured_host = __config__[CONFIG_HOSTNAME]
         configured_port = str(__config__[CONFIG_PORT])
         if ':' in configured_host and not configured_host.startswith('['):
             configured_host = '[%s]' % configured_host
+        scheme = parsed.scheme or 'http'
         location = urlunparse(parsed._replace(
+            scheme=scheme,
             netloc='%s:%s' % (configured_host, configured_port)))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@testlib/src/main/resources/zssdk.py` around lines 373 - 379, 当前对 `location`
只替换了 `netloc`(使用 parsed/_replace 和 urlunparse),导致当服务端返回相对 Location(如
"/v1/...")时会生成以 "//host:port/..." 的无 scheme URL;请在处理前检查 parsed.scheme(由 urlparse
返回的 parsed 对象),如果为空则设置默认 scheme 为 "http"(与 SDK 其它构造一致),然后再用 parsed._replace
来构造新的
URL;参考标识符:parsed、urlparse、parsed._replace、urlunparse、location、__config__[CONFIG_HOSTNAME]、__config__[CONFIG_PORT]、configured_host、configured_port。


if cb:
# async polling
Expand Down Expand Up @@ -480,11 +494,13 @@ def _uuid():
def _json_http(
uri,
body=None,
headers={},
headers=None,
method='POST',
timeout=120.0
):
pool = urllib3.PoolManager(timeout=timeout, retries=urllib3.util.retry.Retry(15))
if headers is None:
headers = {}
headers.update({'Content-Type': 'application/json', 'Connection': 'close'})

if body is not None and not isinstance(body, str):
Expand All @@ -497,6 +513,15 @@ def _json_http(
else:
rsp = pool.request(method, uri, headers=headers)

print('[Response to %s %s]: status: %s, body: %s' % (method, uri, rsp.status, rsp.data))
return rsp
data = rsp.data
if isinstance(data, bytes):
data = data.decode('utf-8')
print('[Response to %s %s]: status: %s, body: %s' % (method, uri, rsp.status, data))

class _Rsp(object):
pass
r = _Rsp()
r.status = rsp.status
r.data = data
return r