ctyun-cli 0.1.0a1__py3-none-any.whl

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.
ctyun_cli/__init__.py ADDED
@@ -0,0 +1,22 @@
1
+ """
2
+ 天翼云CLI工具
3
+ """
4
+
5
+ __version__ = "1.0.0"
6
+ __author__ = "Your Name"
7
+ __email__ = "your.email@example.com"
8
+
9
+ from .client import CTYUNClient, CTYUNAPIError
10
+ from .config.settings import ConfigManager, config
11
+
12
+ __all__ = [
13
+ 'CTYUNClient',
14
+ 'CTYUNAPIError',
15
+ 'ConfigManager',
16
+ 'config'
17
+ ]
18
+
19
+ def main():
20
+ """CLI主入口函数"""
21
+ from .cli.main import cli
22
+ cli()
@@ -0,0 +1,5 @@
1
+ """认证模块"""
2
+
3
+ from .signature import CTYUNAuth
4
+
5
+ __all__ = ['CTYUNAuth']
@@ -0,0 +1,235 @@
1
+ """
2
+ 天翼云EOP签名认证模块
3
+ 实现基于EOP规范的AK/SK签名认证机制
4
+ """
5
+
6
+ import hashlib
7
+ import hmac
8
+ import base64
9
+ import uuid
10
+ from datetime import datetime
11
+ from urllib.parse import quote
12
+ from typing import Dict, Any, Optional
13
+
14
+
15
+ class CTYUNEOPAuth:
16
+ """天翼云EOP签名认证类"""
17
+
18
+ def __init__(self, access_key: str, secret_key: str):
19
+ """
20
+ 初始化认证器
21
+
22
+ Args:
23
+ access_key: 访问密钥(AK)
24
+ secret_key: 密钥(SK)
25
+ """
26
+ self.access_key = access_key
27
+ self.secret_key = secret_key
28
+
29
+ def sign_request(self, method: str, url: str, query_params: Optional[Dict[str, Any]] = None,
30
+ body: Optional[str] = None, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
31
+ """
32
+ 对请求进行签名,返回完整的请求头
33
+
34
+ Args:
35
+ method: HTTP方法
36
+ url: 请求URL
37
+ query_params: 查询参数
38
+ body: 请求体
39
+ extra_headers: 额外的请求头
40
+
41
+ Returns:
42
+ 包含签名的请求头字典
43
+ """
44
+ # 生成必需的请求头
45
+ request_id = str(uuid.uuid4())
46
+ eop_date = self._get_eop_date()
47
+
48
+ # 构建基础请求头
49
+ headers = {
50
+ 'Content-Type': 'application/json',
51
+ 'ctyun-eop-request-id': request_id,
52
+ 'Eop-date': eop_date
53
+ }
54
+
55
+ # 添加额外的请求头
56
+ if extra_headers:
57
+ headers.update(extra_headers)
58
+
59
+ # 步骤一:构造待签名字符串 signature
60
+ signature_string = self._build_signature_string(
61
+ headers, query_params, body
62
+ )
63
+
64
+ # 步骤二:构造动态密钥 kdate
65
+ kdate = self._build_kdate(eop_date)
66
+
67
+ # 步骤三:构造 signature
68
+ signature = self._build_signature(signature_string, kdate)
69
+
70
+ # 步骤四:构造 Eop-Authorization
71
+ eop_authorization = self._build_eop_authorization(signature, headers)
72
+
73
+ # 添加认证头
74
+ headers['Eop-Authorization'] = eop_authorization
75
+
76
+ return headers
77
+
78
+ def _get_eop_date(self) -> str:
79
+ """
80
+ 获取EOP格式的日期时间
81
+ 格式:yyyyMMdd'T'HHmmss'Z'
82
+ 注意:实际传时间为北京东八区UTC+8时间,TZ仅为格式,非UTC时间
83
+
84
+ Returns:
85
+ EOP格式的日期时间字符串
86
+ """
87
+ # 获取当前北京时间(UTC+8)
88
+ now = datetime.now()
89
+ return now.strftime('%Y%m%dT%H%M%SZ')
90
+
91
+ def _build_signature_string(self, headers: Dict[str, str],
92
+ query_params: Optional[Dict[str, Any]] = None,
93
+ body: Optional[str] = None) -> str:
94
+ """
95
+ 构造待签名字符串
96
+ sigture = 需要进行签名的Header排序后的组合列表 + "\n" + encode的query + "\n" + toHex(sha256(原封的body))
97
+
98
+ Args:
99
+ headers: 请求头字典
100
+ query_params: 查询参数
101
+ body: 请求体
102
+
103
+ Returns:
104
+ 待签名字符串
105
+ """
106
+ # 1. 构造需要签名的Header排序后的组合列表
107
+ # EOP强制要求 ctyun-eop-request-id、eop-date 必须进行签名
108
+ signed_header_names = ['ctyun-eop-request-id', 'eop-date']
109
+
110
+ # 按字母顺序排序
111
+ signed_header_names.sort()
112
+
113
+ # 构造 header_name:header_value\n 格式
114
+ header_list = []
115
+ for header_name in signed_header_names:
116
+ # 注意:查找header时不区分大小写,但构造签名字符串时必须用小写
117
+ header_value = None
118
+ for k, v in headers.items():
119
+ if k.lower() == header_name.lower():
120
+ header_value = v
121
+ break
122
+
123
+ if header_value:
124
+ header_list.append(f"{header_name.lower()}:{header_value}\n")
125
+
126
+ header_string = ''.join(header_list)
127
+
128
+ # 2. 构造编码后的query字符串
129
+ query_string = ''
130
+ if query_params:
131
+ # 对参数按key排序
132
+ sorted_params = sorted(query_params.items())
133
+ encoded_params = []
134
+ for key, value in sorted_params:
135
+ # 值需要进行URL编码
136
+ encoded_value = quote(str(value), safe='')
137
+ encoded_params.append(f"{key}={encoded_value}")
138
+ query_string = '&'.join(encoded_params)
139
+
140
+ # 3. 对body进行SHA256摘要并转十六进制
141
+ if body is None or body == '':
142
+ body = ''
143
+ body_hash = hashlib.sha256(body.encode('utf-8')).hexdigest()
144
+
145
+ # 拼接最终的待签名字符串
146
+ # 格式:header_string + "\n" + query_string + "\n" + body_hash
147
+ signature_string = f"{header_string}\n{query_string}\n{body_hash}"
148
+
149
+ return signature_string
150
+
151
+ def _build_kdate(self, eop_date: str) -> bytes:
152
+ """
153
+ 构造动态密钥 kdate
154
+
155
+ 步骤:
156
+ 1. ktime = hmacSHA256(eop_date, sk)
157
+ 2. kAk = hmacSHA256(ak, ktime)
158
+ 3. kdate = hmacSHA256(eop_date的年月日值, kAk)
159
+
160
+ Args:
161
+ eop_date: EOP格式的日期时间
162
+
163
+ Returns:
164
+ 动态密钥 kdate
165
+ """
166
+ # 1. 使用eop_date作为数据,sk作为密钥,算出ktime
167
+ ktime = hmac.new(
168
+ self.secret_key.encode('utf-8'),
169
+ eop_date.encode('utf-8'),
170
+ hashlib.sha256
171
+ ).digest()
172
+
173
+ # 2. 使用ak作为数据,ktime作为密钥,算出kAk
174
+ kAk = hmac.new(
175
+ ktime,
176
+ self.access_key.encode('utf-8'),
177
+ hashlib.sha256
178
+ ).digest()
179
+
180
+ # 3. 使用eop_date的年月日值作为数据,kAk作为密钥,算出kdate
181
+ # eop_date格式:20221107T093029Z,提取年月日:20221107
182
+ date_part = eop_date.split('T')[0]
183
+ kdate = hmac.new(
184
+ kAk,
185
+ date_part.encode('utf-8'),
186
+ hashlib.sha256
187
+ ).digest()
188
+
189
+ return kdate
190
+
191
+ def _build_signature(self, signature_string: str, kdate: bytes) -> str:
192
+ """
193
+ 构造 signature
194
+ 使用kdate作为密钥、signature_string作为数据,进行HMAC-SHA256,然后Base64编码
195
+
196
+ Args:
197
+ signature_string: 待签名字符串
198
+ kdate: 动态密钥
199
+
200
+ Returns:
201
+ Base64编码的签名
202
+ """
203
+ signature_bytes = hmac.new(
204
+ kdate,
205
+ signature_string.encode('utf-8'),
206
+ hashlib.sha256
207
+ ).digest()
208
+
209
+ # Base64编码
210
+ signature = base64.b64encode(signature_bytes).decode('utf-8')
211
+
212
+ return signature
213
+
214
+ def _build_eop_authorization(self, signature: str, headers: Dict[str, str]) -> str:
215
+ """
216
+ 构造 Eop-Authorization 请求头
217
+ 格式:ak Headers=header1;header2 Signature=xxx
218
+
219
+ Args:
220
+ signature: 签名
221
+ headers: 请求头字典
222
+
223
+ Returns:
224
+ Eop-Authorization 字符串
225
+ """
226
+ # 构造 Headers 部分(需要签名的header,按字母排序,用分号分隔)
227
+ signed_header_names = ['ctyun-eop-request-id', 'eop-date']
228
+ signed_header_names.sort()
229
+ headers_part = ';'.join(signed_header_names)
230
+
231
+ # 构造完整的 Eop-Authorization
232
+ # 格式:ak Headers=xxx Signature=xxx
233
+ eop_authorization = f"{self.access_key} Headers={headers_part} Signature={signature}"
234
+
235
+ return eop_authorization
@@ -0,0 +1,225 @@
1
+ """
2
+ 天翼云API签名认证模块
3
+ 实现AK/SK签名认证机制
4
+ """
5
+
6
+ import hashlib
7
+ import hmac
8
+ import time
9
+ import uuid
10
+ from urllib.parse import quote
11
+ from typing import Dict, Any
12
+
13
+
14
+ class CTYUNAuth:
15
+ """天翼云API签名认证类"""
16
+
17
+ def __init__(self, access_key: str, secret_key: str):
18
+ """
19
+ 初始化认证器
20
+
21
+ Args:
22
+ access_key: 访问密钥
23
+ secret_key: 密钥
24
+ """
25
+ self.access_key = access_key
26
+ self.secret_key = secret_key
27
+
28
+ def _create_string_to_sign(self, method: str, uri: str, params: Dict[str, Any],
29
+ headers: Dict[str, str], timestamp: str) -> str:
30
+ """
31
+ 创建待签名字符串
32
+
33
+ Args:
34
+ method: HTTP方法
35
+ uri: 请求URI
36
+ params: 查询参数
37
+ headers: 请求头
38
+ timestamp: 时间戳
39
+
40
+ Returns:
41
+ 待签名字符串
42
+ """
43
+ # 规范化URI
44
+ canonical_uri = quote(uri, safe='/')
45
+
46
+ # 规范化查询字符串
47
+ canonical_query = self._canonicalize_query(params)
48
+
49
+ # 规范化请求头
50
+ canonical_headers, signed_headers = self._canonicalize_headers(headers)
51
+
52
+ # 创建哈希载荷(如果有body)
53
+ payload_hash = hashlib.sha256(''.encode()).hexdigest()
54
+
55
+ # 构建待签名字符串
56
+ string_to_sign = f"{method.upper()}\n{canonical_uri}\n{canonical_query}\n"
57
+ string_to_sign += f"{canonical_headers}\n{signed_headers}\n{payload_hash}"
58
+
59
+ return string_to_sign
60
+
61
+ def _canonicalize_query(self, params: Dict[str, Any]) -> str:
62
+ """
63
+ 规范化查询参数
64
+
65
+ Args:
66
+ params: 查询参数字典
67
+
68
+ Returns:
69
+ 规范化的查询字符串
70
+ """
71
+ if not params:
72
+ return ""
73
+
74
+ # 对参数名进行编码和排序
75
+ encoded_params = []
76
+ for key in sorted(params.keys()):
77
+ encoded_key = quote(str(key), safe='')
78
+ encoded_value = quote(str(params[key]), safe='')
79
+ encoded_params.append(f"{encoded_key}={encoded_value}")
80
+
81
+ return "&".join(encoded_params)
82
+
83
+ def _canonicalize_headers(self, headers: Dict[str, str]) -> tuple:
84
+ """
85
+ 规范化请求头
86
+
87
+ Args:
88
+ headers: 请求头字典
89
+
90
+ Returns:
91
+ (规范化请求头字符串, 已签名头列表)
92
+ """
93
+ # 过滤和规范化请求头
94
+ canonical_headers = {}
95
+ signed_headers = []
96
+
97
+ # 天翼云API要求包含的头
98
+ required_headers = ['host', 'x-ctyun-date', 'x-ctyun-nonce']
99
+
100
+ for key, value in headers.items():
101
+ lower_key = key.lower().strip()
102
+ if lower_key in required_headers or lower_key.startswith('x-ctyun-'):
103
+ canonical_headers[lower_key] = value.strip()
104
+ signed_headers.append(lower_key)
105
+
106
+ # 排序已签名头
107
+ signed_headers.sort()
108
+
109
+ # 构建规范化请求头字符串
110
+ header_lines = []
111
+ for header in signed_headers:
112
+ header_lines.append(f"{header}:{canonical_headers[header]}")
113
+
114
+ canonical_headers_str = "\n".join(header_lines) + "\n"
115
+ signed_headers_str = ";".join(signed_headers)
116
+
117
+ return canonical_headers_str, signed_headers_str
118
+
119
+ def sign_request(self, method: str, uri: str, params: Dict[str, Any] = None,
120
+ headers: Dict[str, str] = None, body: str = None) -> Dict[str, str]:
121
+ """
122
+ 为请求添加签名
123
+
124
+ Args:
125
+ method: HTTP方法
126
+ uri: 请求URI
127
+ params: 查询参数
128
+ headers: 请求头
129
+ body: 请求体
130
+
131
+ Returns:
132
+ 包含签名信息的请求头
133
+ """
134
+ if params is None:
135
+ params = {}
136
+ if headers is None:
137
+ headers = {}
138
+ if body is None:
139
+ body = ""
140
+
141
+ # 生成时间戳和随机数
142
+ timestamp = time.strftime("%Y%m%dT%H%M%SZ", time.gmtime())
143
+ nonce = str(uuid.uuid4()).replace('-', '')
144
+
145
+ # 添加必要的请求头
146
+ headers['X-CTYUN-Date'] = timestamp
147
+ headers['X-CTYUN-Nonce'] = nonce
148
+
149
+ # 创建待签名字符串
150
+ string_to_sign = self._create_string_to_sign(method, uri, params, headers, timestamp)
151
+
152
+ # 计算签名
153
+ signature = self._calculate_signature(string_to_sign, timestamp)
154
+
155
+ # 添加认证头
156
+ auth_headers = {
157
+ 'X-CTYUN-Signature': signature,
158
+ 'X-CTYUN-SignedHeaders': self._get_signed_headers(headers),
159
+ 'X-CTYUN-Date': timestamp,
160
+ 'X-CTYUN-Nonce': nonce,
161
+ 'X-CTYUN-Algorithm': 'CTYUN-HMAC-SHA256'
162
+ }
163
+
164
+ return auth_headers
165
+
166
+ def _calculate_signature(self, string_to_sign: str, timestamp: str) -> str:
167
+ """
168
+ 计算签名
169
+
170
+ Args:
171
+ string_to_sign: 待签名字符串
172
+ timestamp: 时间戳
173
+
174
+ Returns:
175
+ 签名值
176
+ """
177
+ # 创建签名密钥
178
+ date_key = hmac.new(
179
+ f'CTYUN{self.secret_key}'.encode(),
180
+ timestamp[:8].encode(),
181
+ hashlib.sha256
182
+ ).digest()
183
+
184
+ signature_key = hmac.new(
185
+ date_key,
186
+ 'ctyun_request'.encode(),
187
+ hashlib.sha256
188
+ ).digest()
189
+
190
+ # 计算最终签名
191
+ signature = hmac.new(
192
+ signature_key,
193
+ string_to_sign.encode(),
194
+ hashlib.sha256
195
+ ).hexdigest()
196
+
197
+ return signature
198
+
199
+ def _get_signed_headers(self, headers: Dict[str, str]) -> str:
200
+ """
201
+ 获取已签名头列表
202
+
203
+ Args:
204
+ headers: 请求头字典
205
+
206
+ Returns:
207
+ 已签名头列表字符串
208
+ """
209
+ signed_headers = []
210
+ for key in headers:
211
+ lower_key = key.lower()
212
+ if lower_key in ['host', 'x-ctyun-date', 'x-ctyun-nonce'] or lower_key.startswith('x-ctyun-'):
213
+ signed_headers.append(lower_key)
214
+
215
+ signed_headers.sort()
216
+ return ";".join(signed_headers)
217
+
218
+ def get_bearer_token(self) -> str:
219
+ """
220
+ 获取Bearer Token(简化认证方式)
221
+
222
+ Returns:
223
+ Bearer Token字符串
224
+ """
225
+ return f"Bearer {self.access_key}"
@@ -0,0 +1,8 @@
1
+ """
2
+ 天翼云账务服务模块
3
+ 提供账单查询、费用查询等功能
4
+ """
5
+
6
+ from .client import BillingClient
7
+
8
+ __all__ = ['BillingClient']