phable 0.1.4__tar.gz
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.
- phable-0.1.4/LICENSE +21 -0
- phable-0.1.4/PKG-INFO +48 -0
- phable-0.1.4/README.rst +34 -0
- phable-0.1.4/phable/__init__.py +0 -0
- phable-0.1.4/phable/scram.py +354 -0
- phable-0.1.4/phable/session.py +34 -0
- phable-0.1.4/phable/utils.py +102 -0
- phable-0.1.4/pyproject.toml +21 -0
phable-0.1.4/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 Rick Jennings
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
phable-0.1.4/PKG-INFO
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: phable
|
|
3
|
+
Version: 0.1.4
|
|
4
|
+
Summary:
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Rick Jennings
|
|
7
|
+
Author-email: rjennings055@gmail.com
|
|
8
|
+
Requires-Python: >=3.10,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Description-Content-Type: text/x-rst
|
|
14
|
+
|
|
15
|
+
Project Haystack
|
|
16
|
+
----------------
|
|
17
|
+
To be defined
|
|
18
|
+
|
|
19
|
+
Installation
|
|
20
|
+
------------
|
|
21
|
+
|
|
22
|
+
Download the project from Pypi:
|
|
23
|
+
|
|
24
|
+
.. code-block:: bash
|
|
25
|
+
|
|
26
|
+
$ pip install proj-haystack
|
|
27
|
+
|
|
28
|
+
Quick start
|
|
29
|
+
-----------
|
|
30
|
+
|
|
31
|
+
The below example shows how to obtain an auth token from a Haystack server.
|
|
32
|
+
|
|
33
|
+
.. code-block:: python
|
|
34
|
+
|
|
35
|
+
from proj_haystack.scram import get_auth_token
|
|
36
|
+
|
|
37
|
+
# define these settings specific for your use case
|
|
38
|
+
host_url = "http://localhost:8080"
|
|
39
|
+
username = "su"
|
|
40
|
+
password = "su"
|
|
41
|
+
|
|
42
|
+
# get the auth token
|
|
43
|
+
auth_token = get_auth_token(host_url, username, password)
|
|
44
|
+
print(f"Here is the auth token: {auth_token}")
|
|
45
|
+
|
|
46
|
+
License
|
|
47
|
+
-------
|
|
48
|
+
To be defined
|
phable-0.1.4/README.rst
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Project Haystack
|
|
2
|
+
----------------
|
|
3
|
+
To be defined
|
|
4
|
+
|
|
5
|
+
Installation
|
|
6
|
+
------------
|
|
7
|
+
|
|
8
|
+
Download the project from Pypi:
|
|
9
|
+
|
|
10
|
+
.. code-block:: bash
|
|
11
|
+
|
|
12
|
+
$ pip install proj-haystack
|
|
13
|
+
|
|
14
|
+
Quick start
|
|
15
|
+
-----------
|
|
16
|
+
|
|
17
|
+
The below example shows how to obtain an auth token from a Haystack server.
|
|
18
|
+
|
|
19
|
+
.. code-block:: python
|
|
20
|
+
|
|
21
|
+
from proj_haystack.scram import get_auth_token
|
|
22
|
+
|
|
23
|
+
# define these settings specific for your use case
|
|
24
|
+
host_url = "http://localhost:8080"
|
|
25
|
+
username = "su"
|
|
26
|
+
password = "su"
|
|
27
|
+
|
|
28
|
+
# get the auth token
|
|
29
|
+
auth_token = get_auth_token(host_url, username, password)
|
|
30
|
+
print(f"Here is the auth token: {auth_token}")
|
|
31
|
+
|
|
32
|
+
License
|
|
33
|
+
-------
|
|
34
|
+
To be defined
|
|
File without changes
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import hmac
|
|
3
|
+
import logging
|
|
4
|
+
import re
|
|
5
|
+
from base64 import urlsafe_b64decode, urlsafe_b64encode
|
|
6
|
+
from binascii import hexlify, unhexlify
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from hashlib import pbkdf2_hmac
|
|
9
|
+
from random import getrandbits
|
|
10
|
+
from time import time_ns
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
from proj_haystack.utils import request
|
|
14
|
+
|
|
15
|
+
from .utils import Response
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
gs2_header = "n,,"
|
|
20
|
+
|
|
21
|
+
# -----------------------------------------------------------------------
|
|
22
|
+
#
|
|
23
|
+
# -----------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_auth_token(host_url: str, username: str, password: str):
|
|
27
|
+
|
|
28
|
+
sc = ScramClient(username, password)
|
|
29
|
+
|
|
30
|
+
# send hello msg & set the response
|
|
31
|
+
hello_resp = request(host_url, headers=sc.get_hello_req())
|
|
32
|
+
sc.set_hello_resp(hello_resp)
|
|
33
|
+
|
|
34
|
+
# send first msg & set the response
|
|
35
|
+
first_resp = request(host_url, headers=sc.get_first_req())
|
|
36
|
+
sc.set_first_resp(first_resp)
|
|
37
|
+
|
|
38
|
+
# send last msg & set the response
|
|
39
|
+
last_resp = request(host_url, headers=sc.get_last_req())
|
|
40
|
+
sc.set_last_resp(last_resp)
|
|
41
|
+
|
|
42
|
+
# return the auth token
|
|
43
|
+
return sc.auth_token
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# -----------------------------------------------------------------------
|
|
47
|
+
#
|
|
48
|
+
# -----------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ScramException(Exception):
|
|
52
|
+
def __init__(self, message: str, server_error: str = None):
|
|
53
|
+
super().__init__(message)
|
|
54
|
+
self.server_error = server_error
|
|
55
|
+
|
|
56
|
+
def __str__(self):
|
|
57
|
+
s_str = "" if self.server_error is None else f": {self.server_error}"
|
|
58
|
+
return super().__str__() + s_str
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# # -----------------------------------------------------------------------
|
|
62
|
+
# # Parsing support - general
|
|
63
|
+
# # -----------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class NotFoundError(Exception):
|
|
68
|
+
help_msg: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ScramClient:
|
|
72
|
+
def __init__(self, username: str, password: str, hash: str = "sha256"):
|
|
73
|
+
if hash not in ["sha100?", "sha256"]:
|
|
74
|
+
raise ScramException(
|
|
75
|
+
"The 'hash' parameter must be a str equal to 'sha256' or 'sha100?'"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
self.username = username
|
|
79
|
+
self.password = password
|
|
80
|
+
self.hash = hash # does the server or the client determine the hash?
|
|
81
|
+
|
|
82
|
+
def get_hello_req(self) -> dict[str, str]:
|
|
83
|
+
"""Return the HTTP headers required for the client's hello message. Note: There is no data required for the client's hello message."""
|
|
84
|
+
headers = {"Authorization": f"HELLO username={to_base64(self.username)}"}
|
|
85
|
+
return headers
|
|
86
|
+
|
|
87
|
+
def set_hello_resp(self, resp: Response) -> None:
|
|
88
|
+
"""Save server's response data as class attributes to be able to get other request messages."""
|
|
89
|
+
auth_msg = fetch_auth_msg(resp.headers.as_string())
|
|
90
|
+
self.handshake_token = fetch_handshake_token(auth_msg)
|
|
91
|
+
self.hash = fetch_hash(
|
|
92
|
+
auth_msg
|
|
93
|
+
) # Should I raise an exception if these don't line up?
|
|
94
|
+
|
|
95
|
+
def get_first_req(self) -> dict[str, str]:
|
|
96
|
+
self.c_nonce: str = gen_nonce()
|
|
97
|
+
self.c1_bare: str = f"n={self.username},r={self.c_nonce}"
|
|
98
|
+
headers = {
|
|
99
|
+
"Authorization": f"scram handshakeToken={self.handshake_token}, hash={self.hash}, data={to_base64(gs2_header+self.c1_bare)}"
|
|
100
|
+
}
|
|
101
|
+
return headers
|
|
102
|
+
|
|
103
|
+
def set_first_resp(self, resp: Response) -> None:
|
|
104
|
+
# can we go straight to _fetch_scram_data?
|
|
105
|
+
auth_msg = fetch_auth_msg(resp.headers.as_string())
|
|
106
|
+
scram_data = fetch_scram_data(auth_msg)
|
|
107
|
+
r, s, i = split_scram_data(scram_data)
|
|
108
|
+
self.s_nonce: str = r
|
|
109
|
+
self.salt: str = s
|
|
110
|
+
self.iter_count: int = i
|
|
111
|
+
|
|
112
|
+
def get_last_req(self):
|
|
113
|
+
# define the client final no proof
|
|
114
|
+
# note: We want to send the s_nonce
|
|
115
|
+
client_final_no_proof = f"c={to_base64('n,,')},r={self.s_nonce}"
|
|
116
|
+
logger.debug(f"client-final-no-proof:\n{client_final_no_proof}\n")
|
|
117
|
+
|
|
118
|
+
# define the auth msg
|
|
119
|
+
auth_msg = f"{self.c1_bare},r={self.s_nonce},s={self.salt},i={self.iter_count},{client_final_no_proof}"
|
|
120
|
+
logger.debug(f"auth-msg:\n{auth_msg}\n")
|
|
121
|
+
|
|
122
|
+
# define the client key
|
|
123
|
+
client_key = hmac.new(
|
|
124
|
+
unhexlify(
|
|
125
|
+
salted_password(
|
|
126
|
+
self.salt,
|
|
127
|
+
self.iter_count,
|
|
128
|
+
self.hash,
|
|
129
|
+
self.password,
|
|
130
|
+
)
|
|
131
|
+
),
|
|
132
|
+
"Client Key".encode("UTF-8"),
|
|
133
|
+
self.hash,
|
|
134
|
+
).hexdigest()
|
|
135
|
+
logger.debug(f"client-key:\n{client_key}\n")
|
|
136
|
+
|
|
137
|
+
# find the stored key
|
|
138
|
+
hashFunc = hashlib.new(self.hash)
|
|
139
|
+
hashFunc.update(unhexlify(client_key))
|
|
140
|
+
stored_key = hashFunc.hexdigest()
|
|
141
|
+
logger.debug(f"stored-key:\n{stored_key}\n")
|
|
142
|
+
|
|
143
|
+
# find the client signature
|
|
144
|
+
client_signature = hmac.new(
|
|
145
|
+
unhexlify(stored_key), auth_msg.encode("utf-8"), self.hash
|
|
146
|
+
).hexdigest()
|
|
147
|
+
logger.debug(f"client-signature:\n{client_signature}\n")
|
|
148
|
+
|
|
149
|
+
# find the client proof
|
|
150
|
+
client_proof = hex(int(client_key, 16) ^ int(client_signature, 16))[2:]
|
|
151
|
+
logger.debug(f"Here is the length of the client proof: {len(client_proof)}")
|
|
152
|
+
|
|
153
|
+
# may need to do some padding before converting the hex to its
|
|
154
|
+
# binary representation
|
|
155
|
+
while len(client_proof) < 64:
|
|
156
|
+
client_proof = "0" + client_proof
|
|
157
|
+
|
|
158
|
+
client_proof_encode = to_base64(unhexlify(client_proof))
|
|
159
|
+
logger.debug(f"client-proof:\n{client_proof}\n")
|
|
160
|
+
|
|
161
|
+
client_final = client_final_no_proof + ",p=" + client_proof_encode
|
|
162
|
+
client_final_base64 = to_base64(client_final)
|
|
163
|
+
|
|
164
|
+
final_msg = (
|
|
165
|
+
f"scram handshaketoken={self.handshake_token},data={client_final_base64}"
|
|
166
|
+
)
|
|
167
|
+
logger.debug(f"Here is the final msg being sent: {final_msg}")
|
|
168
|
+
|
|
169
|
+
headers = {"Authorization": final_msg}
|
|
170
|
+
return headers
|
|
171
|
+
|
|
172
|
+
def set_last_resp(self, resp: Response) -> None:
|
|
173
|
+
self.auth_token = fetch_auth_token(resp.headers.as_string())
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# -
|
|
177
|
+
# define helper funcs used in ScramClient
|
|
178
|
+
# -
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def fetch_auth_msg(resp_msg: str) -> str:
|
|
182
|
+
exclude_msg = "WWW-Authenticate: "
|
|
183
|
+
s = re.search(f"({exclude_msg})[^\n]+", resp_msg)
|
|
184
|
+
|
|
185
|
+
# TODO: add some handling here
|
|
186
|
+
return s.group(0)[len(exclude_msg) :]
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def fetch_scram_data(auth_msg: str) -> str:
|
|
190
|
+
exclude_msg = "scram data="
|
|
191
|
+
s = re.search(f"({exclude_msg})[a-zA-Z0-9]+", auth_msg) # TODO confirm this
|
|
192
|
+
|
|
193
|
+
if s is None:
|
|
194
|
+
raise NotFoundError(f"Acceptable handshake token not found:\n{auth_msg}")
|
|
195
|
+
|
|
196
|
+
return from_base64(s.group(0)[len(exclude_msg) :])
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def split_scram_data(scram_data: str) -> tuple[str, str, int]:
|
|
200
|
+
|
|
201
|
+
# define s_nonce - ASCII characters excluding ','
|
|
202
|
+
r = re.search("(r=)[^,]+", scram_data)
|
|
203
|
+
|
|
204
|
+
# define salt - base 64 encoded str
|
|
205
|
+
s = re.search("(s=)[a-zA-Z0-9+/=]+", scram_data)
|
|
206
|
+
|
|
207
|
+
# define iteration count
|
|
208
|
+
i = re.search("(i=)[0-9]+", scram_data)
|
|
209
|
+
|
|
210
|
+
if r == None or s == None or i == None:
|
|
211
|
+
raise NotFoundError(f"Invalid scram data:\n{scram_data}")
|
|
212
|
+
|
|
213
|
+
s_nonce: str = r.group(0).replace("r=", "")
|
|
214
|
+
salt: str = s.group(0).replace("s=", "")
|
|
215
|
+
iteration_count: int = int(i.group(0).replace("i=", ""))
|
|
216
|
+
|
|
217
|
+
return (s_nonce, salt, iteration_count)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# def _fetch_date_time(date_str: str) -> datetime:
|
|
221
|
+
# format = "%a, %d %b %Y %H:%M:%S %Z"
|
|
222
|
+
# return datetime.strptime(date_str, format)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def fetch_handshake_token(auth_msg: str) -> str:
|
|
226
|
+
exclude_msg = "handshakeToken="
|
|
227
|
+
s = re.search(f"({exclude_msg})[a-zA-Z0-9]+", auth_msg)
|
|
228
|
+
|
|
229
|
+
if s is None:
|
|
230
|
+
raise NotFoundError(f"Acceptable handshake token not found:\n{auth_msg}")
|
|
231
|
+
|
|
232
|
+
return s.group(0)[len(exclude_msg) :]
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def fetch_hash(auth_msg: str) -> str:
|
|
236
|
+
exclude_msg = "hash="
|
|
237
|
+
s = re.search(f"({exclude_msg})(SHA-256)", auth_msg)
|
|
238
|
+
|
|
239
|
+
if s is None:
|
|
240
|
+
raise NotFoundError(f"Acceptable hash method not found:\n{auth_msg}")
|
|
241
|
+
|
|
242
|
+
s_new = s.group(0)[len(exclude_msg) :]
|
|
243
|
+
|
|
244
|
+
if s_new == "SHA-256":
|
|
245
|
+
s_new = "sha256"
|
|
246
|
+
|
|
247
|
+
return s_new
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def fetch_auth_token(headers: str) -> str:
|
|
251
|
+
exclude_msg = "authToken="
|
|
252
|
+
s = re.search(f"({exclude_msg})[^,]+", headers)
|
|
253
|
+
|
|
254
|
+
if s is None:
|
|
255
|
+
raise NotFoundError(f"Acceptable auth token not found:\n{headers}")
|
|
256
|
+
|
|
257
|
+
return s.group(0)[len(exclude_msg) :]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# --------------------------------------------------------------------
|
|
261
|
+
# Nonce & related helper funcs
|
|
262
|
+
# --------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def to_custom_hex(x: int, length: int) -> str:
|
|
266
|
+
"""Convert an integer x to hexadecimal string representation without a prepended '0x' str. Prepend leading zeros as needed to ensure the specified number of nibble characters."""
|
|
267
|
+
|
|
268
|
+
# Convert x to a hexadecimal number
|
|
269
|
+
x_hex = hex(x)
|
|
270
|
+
|
|
271
|
+
# Remove prepended 0x used to describe hex numbers
|
|
272
|
+
x_hex = x_hex.replace("0x", "")
|
|
273
|
+
|
|
274
|
+
# Prepend 0s as needed
|
|
275
|
+
if len(x_hex) < length:
|
|
276
|
+
x_hex = "0" * (length - len(x_hex)) + x_hex
|
|
277
|
+
|
|
278
|
+
return x_hex
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Define nonce random mask for this VM
|
|
282
|
+
nonce_mask: int = getrandbits(64)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def gen_nonce() -> str:
|
|
286
|
+
"""Generate a nonce."""
|
|
287
|
+
# Notes:
|
|
288
|
+
# getrandbits() defines a random 64 bit integer
|
|
289
|
+
# time_ns() defines ticks since the Unix epoch (1 January 1970)
|
|
290
|
+
rand = getrandbits(64)
|
|
291
|
+
ticks = time_ns() ^ nonce_mask ^ rand
|
|
292
|
+
return to_custom_hex(rand, 16) + to_custom_hex(ticks, 16)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# --------------------------------------------------------------------
|
|
296
|
+
# Misc
|
|
297
|
+
# --------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def salted_password(salt: str, iterations: int, hash_func: str, password: str) -> bytes:
|
|
301
|
+
# Need hash_func to be a str here
|
|
302
|
+
dk = pbkdf2_hmac(hash_func, password.encode(), urlsafe_b64decode(salt), iterations)
|
|
303
|
+
encrypt_password = hexlify(dk)
|
|
304
|
+
return encrypt_password
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# --------------------------------------------------------------------
|
|
308
|
+
# Base64uri conversions & related helper funcs
|
|
309
|
+
# --------------------------------------------------------------------
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def to_base64(msg: Union[str, bytes]) -> str:
|
|
313
|
+
"""Encode a str or byte in base64uri format as defined by RFC 4648."""
|
|
314
|
+
|
|
315
|
+
# Convert str inputs to bytes
|
|
316
|
+
if isinstance(msg, str):
|
|
317
|
+
msg = msg.encode("utf-8")
|
|
318
|
+
|
|
319
|
+
# Encode using URL and filesystem-safe alphabet.
|
|
320
|
+
# This means + is encoded as -, and / is encoded as _.
|
|
321
|
+
output = urlsafe_b64encode(msg)
|
|
322
|
+
|
|
323
|
+
# Decode the output as a str
|
|
324
|
+
output = output.decode("utf-8")
|
|
325
|
+
|
|
326
|
+
# Remove padding
|
|
327
|
+
output = output.replace("=", "")
|
|
328
|
+
|
|
329
|
+
return output
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def from_base64(msg: Union[str, bytes]) -> str:
|
|
333
|
+
"""Decode a base64uri encoded str or bytes defined by RFC 4648 into its binary contents. Decode a URI-safe RFC 4648 encoding."""
|
|
334
|
+
|
|
335
|
+
# Convert str inputs to bytes
|
|
336
|
+
if isinstance(msg, str):
|
|
337
|
+
msg = to_padded_bytes(msg)
|
|
338
|
+
|
|
339
|
+
# Decode base64uri
|
|
340
|
+
msg = urlsafe_b64decode(msg)
|
|
341
|
+
|
|
342
|
+
# Decode bytes obj as a str
|
|
343
|
+
msg = msg.decode("utf-8")
|
|
344
|
+
|
|
345
|
+
return msg
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def to_padded_bytes(s: str) -> bytes:
|
|
349
|
+
"""If needed, apply padding to make var s a multiple of 4. Then convert to bytes obj."""
|
|
350
|
+
r = len(s) % 4
|
|
351
|
+
if r != 0:
|
|
352
|
+
s += "=" * (4 - r)
|
|
353
|
+
|
|
354
|
+
return s.encode("utf-8")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from proj_haystack.scram import ScramClient
|
|
2
|
+
from proj_haystack.utils import request
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class HaystackSession:
|
|
6
|
+
"""
|
|
7
|
+
Models the network session to a Project Haystack compliant web
|
|
8
|
+
server from a client's perspective.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, host_url: str, project: str, username: str, password: str):
|
|
12
|
+
self.host_url = host_url
|
|
13
|
+
self.project = project
|
|
14
|
+
self.username = username
|
|
15
|
+
self.password = password
|
|
16
|
+
|
|
17
|
+
# create a scram object
|
|
18
|
+
scram = ScramScheme(host_url, username, password)
|
|
19
|
+
|
|
20
|
+
# try to get the auth token - TODO add a try/catch block here
|
|
21
|
+
self.auth_token = scram.get_auth_token()
|
|
22
|
+
|
|
23
|
+
def exec_query(self, query: str, format: str):
|
|
24
|
+
"""
|
|
25
|
+
An acceptable format is "text/csv"
|
|
26
|
+
"""
|
|
27
|
+
url = f"{self.host_url}/api/{self.project}/{query}"
|
|
28
|
+
header = {
|
|
29
|
+
"Authorization": f"BEARER authToken={self.auth_token}",
|
|
30
|
+
"Accept": format,
|
|
31
|
+
}
|
|
32
|
+
response = requests.get(url, headers=header)
|
|
33
|
+
|
|
34
|
+
return response.text
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import ssl
|
|
3
|
+
import typing
|
|
4
|
+
import urllib.error
|
|
5
|
+
import urllib.parse
|
|
6
|
+
import urllib.request
|
|
7
|
+
from email.message import Message
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Response(typing.NamedTuple):
|
|
11
|
+
"""Container for HTTP response."""
|
|
12
|
+
|
|
13
|
+
body: str
|
|
14
|
+
headers: Message
|
|
15
|
+
status: int
|
|
16
|
+
error_count: int = 0
|
|
17
|
+
|
|
18
|
+
def json(self) -> typing.Any:
|
|
19
|
+
"""
|
|
20
|
+
Decode body's JSON.
|
|
21
|
+
Returns:
|
|
22
|
+
Pythonic representation of the JSON object
|
|
23
|
+
"""
|
|
24
|
+
try:
|
|
25
|
+
output = json.loads(self.body)
|
|
26
|
+
except json.JSONDecodeError:
|
|
27
|
+
output = ""
|
|
28
|
+
return output
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def request(
|
|
32
|
+
url: str,
|
|
33
|
+
data: dict = None,
|
|
34
|
+
params: dict = None,
|
|
35
|
+
headers: dict = None,
|
|
36
|
+
method: str = "GET",
|
|
37
|
+
data_as_json: bool = True,
|
|
38
|
+
error_count: int = 0,
|
|
39
|
+
) -> Response:
|
|
40
|
+
"""
|
|
41
|
+
Perform HTTP request.
|
|
42
|
+
Args:
|
|
43
|
+
url: url to fetch
|
|
44
|
+
data: dict of keys/values to be encoded and submitted
|
|
45
|
+
params: dict of keys/values to be encoded in URL query string
|
|
46
|
+
headers: optional dict of request headers
|
|
47
|
+
method: HTTP method , such as GET or POST
|
|
48
|
+
data_as_json: if True, data will be JSON-encoded
|
|
49
|
+
error_count: optional current count of HTTP errors, to manage recursion
|
|
50
|
+
Raises:
|
|
51
|
+
URLError: if url starts with anything other than "http"
|
|
52
|
+
Returns:
|
|
53
|
+
A dict with headers, body, status code, and, if applicable, object
|
|
54
|
+
rendered from JSON
|
|
55
|
+
"""
|
|
56
|
+
if not url.startswith("http"):
|
|
57
|
+
raise urllib.error.URLError("Incorrect and possibly insecure protocol in url")
|
|
58
|
+
method = method.upper()
|
|
59
|
+
request_data = None
|
|
60
|
+
headers = headers or {}
|
|
61
|
+
data = data or {}
|
|
62
|
+
params = params or {}
|
|
63
|
+
headers = {"Accept": "application/json", **headers}
|
|
64
|
+
|
|
65
|
+
if method == "GET":
|
|
66
|
+
params = {**params, **data}
|
|
67
|
+
data = None
|
|
68
|
+
|
|
69
|
+
if params:
|
|
70
|
+
url += "?" + urllib.parse.urlencode(params, doseq=True, safe="/")
|
|
71
|
+
|
|
72
|
+
if data:
|
|
73
|
+
if data_as_json:
|
|
74
|
+
request_data = json.dumps(data).encode()
|
|
75
|
+
headers["Content-Type"] = "application/json; charset=UTF-8"
|
|
76
|
+
else:
|
|
77
|
+
request_data = urllib.parse.urlencode(data).encode()
|
|
78
|
+
|
|
79
|
+
httprequest = urllib.request.Request(
|
|
80
|
+
url, data=request_data, headers=headers, method=method
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
with urllib.request.urlopen(
|
|
85
|
+
httprequest, context=ssl.create_default_context()
|
|
86
|
+
) as httpresponse:
|
|
87
|
+
response = Response(
|
|
88
|
+
headers=httpresponse.headers,
|
|
89
|
+
status=httpresponse.status,
|
|
90
|
+
body=httpresponse.read().decode(
|
|
91
|
+
httpresponse.headers.get_content_charset("utf-8")
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
except urllib.error.HTTPError as e:
|
|
95
|
+
response = Response(
|
|
96
|
+
body=str(e.reason),
|
|
97
|
+
headers=e.headers,
|
|
98
|
+
status=e.code,
|
|
99
|
+
error_count=error_count + 1,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return response
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "phable"
|
|
3
|
+
version = "0.1.4"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["Rick Jennings <rjennings055@gmail.com>"]
|
|
6
|
+
readme = "README.rst"
|
|
7
|
+
packages = [{include = "phable"}]
|
|
8
|
+
license = "MIT"
|
|
9
|
+
|
|
10
|
+
[tool.poetry.dependencies]
|
|
11
|
+
python = "^3.10"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
[tool.poetry.group.dev.dependencies]
|
|
15
|
+
black = "^23.1.0"
|
|
16
|
+
flake8 = "^6.0.0"
|
|
17
|
+
mypy = "^1.1.1"
|
|
18
|
+
|
|
19
|
+
[build-system]
|
|
20
|
+
requires = ["poetry-core"]
|
|
21
|
+
build-backend = "poetry.core.masonry.api"
|