onewayswp 0.1.0__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.
- onewayswp/__init__.py +36 -0
- onewayswp/_core.py +220 -0
- onewayswp/admin.py +187 -0
- onewayswp/client.py +245 -0
- onewayswp/exceptions.py +80 -0
- onewayswp/info.py +69 -0
- onewayswp/lib/android-arm64/libonewayswp_core.so +0 -0
- onewayswp/lib/android-armv7/libonewayswp_core.so +0 -0
- onewayswp/lib/android-x64/libonewayswp_core.so +0 -0
- onewayswp/lib/android-x86/libonewayswp_core.so +0 -0
- onewayswp/lib/linux-arm64/libonewayswp_core.so +0 -0
- onewayswp/lib/linux-armv7/libonewayswp_core.so +0 -0
- onewayswp/lib/linux-x64/libonewayswp_core.so +0 -0
- onewayswp/lib/macos-arm64/libonewayswp_core.dylib +0 -0
- onewayswp/lib/macos-x64/libonewayswp_core.dylib +0 -0
- onewayswp/lib/windows-arm64/onewayswp_core.dll +0 -0
- onewayswp/lib/windows-x64/onewayswp_core.dll +0 -0
- onewayswp-0.1.0.dist-info/METADATA +1094 -0
- onewayswp-0.1.0.dist-info/RECORD +23 -0
- onewayswp-0.1.0.dist-info/WHEEL +5 -0
- onewayswp-0.1.0.dist-info/licenses/LICENSE +21 -0
- onewayswp-0.1.0.dist-info/licenses/license.swpb +0 -0
- onewayswp-0.1.0.dist-info/top_level.txt +1 -0
onewayswp/__init__.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from .admin import Admin
|
|
2
|
+
from .client import Client
|
|
3
|
+
from .info import info
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
OnewaySWPError,
|
|
6
|
+
InvalidTokenError,
|
|
7
|
+
ExpiredError,
|
|
8
|
+
ClockRollbackError,
|
|
9
|
+
SecurityError,
|
|
10
|
+
InvalidKeyError,
|
|
11
|
+
CorruptedFileError,
|
|
12
|
+
InsufficientCreditError,
|
|
13
|
+
FileLockedError,
|
|
14
|
+
PlatformNotSupportedError,
|
|
15
|
+
CoreLoadError,
|
|
16
|
+
NoActiveSessionError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
__all__ = [
|
|
21
|
+
"Admin",
|
|
22
|
+
"Client",
|
|
23
|
+
"info",
|
|
24
|
+
"OnewaySWPError",
|
|
25
|
+
"InvalidTokenError",
|
|
26
|
+
"ExpiredError",
|
|
27
|
+
"ClockRollbackError",
|
|
28
|
+
"SecurityError",
|
|
29
|
+
"InvalidKeyError",
|
|
30
|
+
"CorruptedFileError",
|
|
31
|
+
"InsufficientCreditError",
|
|
32
|
+
"FileLockedError",
|
|
33
|
+
"PlatformNotSupportedError",
|
|
34
|
+
"CoreLoadError",
|
|
35
|
+
"NoActiveSessionError",
|
|
36
|
+
]
|
onewayswp/_core.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import ctypes
|
|
2
|
+
import json
|
|
3
|
+
import platform
|
|
4
|
+
import time
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from ctypes import c_char_p, c_void_p, c_int64
|
|
7
|
+
|
|
8
|
+
from .exceptions import (
|
|
9
|
+
PlatformNotSupportedError,
|
|
10
|
+
CoreLoadError,
|
|
11
|
+
FileLockedError,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
SUPPORTED_PLATFORMS = {
|
|
16
|
+
("windows", "x64"): "onewayswp_core.dll",
|
|
17
|
+
("windows", "arm64"): "onewayswp_core.dll",
|
|
18
|
+
("windows", "x86"): "onewayswp_core.dll",
|
|
19
|
+
("linux", "x64"): "libonewayswp_core.so",
|
|
20
|
+
("linux", "arm64"): "libonewayswp_core.so",
|
|
21
|
+
("linux", "armv7"): "libonewayswp_core.so",
|
|
22
|
+
("macos", "x64"): "libonewayswp_core.dylib",
|
|
23
|
+
("macos", "arm64"): "libonewayswp_core.dylib",
|
|
24
|
+
("android", "arm64"): "libonewayswp_core.so",
|
|
25
|
+
("android", "armv7"): "libonewayswp_core.so",
|
|
26
|
+
("android", "x64"): "libonewayswp_core.so",
|
|
27
|
+
("android", "x86"): "libonewayswp_core.so",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def detect_platform() -> tuple:
|
|
32
|
+
system = platform.system().lower()
|
|
33
|
+
machine = platform.machine().lower()
|
|
34
|
+
|
|
35
|
+
if system == "windows":
|
|
36
|
+
os_name = "windows"
|
|
37
|
+
elif system == "darwin":
|
|
38
|
+
os_name = "macos"
|
|
39
|
+
elif system == "linux":
|
|
40
|
+
if "android" in platform.platform().lower():
|
|
41
|
+
os_name = "android"
|
|
42
|
+
else:
|
|
43
|
+
os_name = "linux"
|
|
44
|
+
else:
|
|
45
|
+
os_name = system
|
|
46
|
+
|
|
47
|
+
if machine in ("x86_64", "amd64"):
|
|
48
|
+
arch = "x64"
|
|
49
|
+
elif machine in ("aarch64", "arm64"):
|
|
50
|
+
arch = "arm64"
|
|
51
|
+
elif machine.startswith("armv7") or machine == "armv7l":
|
|
52
|
+
arch = "armv7"
|
|
53
|
+
elif machine in ("i386", "i686", "x86"):
|
|
54
|
+
arch = "x86"
|
|
55
|
+
else:
|
|
56
|
+
arch = machine
|
|
57
|
+
|
|
58
|
+
return os_name, arch
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def find_library() -> str:
|
|
62
|
+
os_name, arch = detect_platform()
|
|
63
|
+
key = (os_name, arch)
|
|
64
|
+
|
|
65
|
+
if key not in SUPPORTED_PLATFORMS:
|
|
66
|
+
supported = ", ".join(f"{o}-{a}" for o, a in sorted(SUPPORTED_PLATFORMS))
|
|
67
|
+
raise PlatformNotSupportedError(
|
|
68
|
+
f"Platform '{os_name}-{arch}' is not supported. Supported: {supported}"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
lib_name = SUPPORTED_PLATFORMS[key]
|
|
72
|
+
base = Path(__file__).parent / "lib"
|
|
73
|
+
specific = base / f"{os_name}-{arch}" / lib_name
|
|
74
|
+
|
|
75
|
+
if not specific.exists():
|
|
76
|
+
raise CoreLoadError(
|
|
77
|
+
f"Binary not found for {os_name}-{arch}. Expected at: {specific}"
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return str(specific)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class CoreLib:
|
|
84
|
+
_instance = None
|
|
85
|
+
|
|
86
|
+
def __new__(cls):
|
|
87
|
+
if cls._instance is None:
|
|
88
|
+
cls._instance = super().__new__(cls)
|
|
89
|
+
cls._instance._load()
|
|
90
|
+
return cls._instance
|
|
91
|
+
|
|
92
|
+
def _load(self) -> None:
|
|
93
|
+
lib_path = find_library()
|
|
94
|
+
|
|
95
|
+
try:
|
|
96
|
+
loader = ctypes.WinDLL if platform.system() == "Windows" else ctypes.CDLL
|
|
97
|
+
self.lib = loader(lib_path)
|
|
98
|
+
except OSError as e:
|
|
99
|
+
raise CoreLoadError(f"Failed to load core library: {e}")
|
|
100
|
+
|
|
101
|
+
self.lib.owswp_version.restype = c_void_p
|
|
102
|
+
self.lib.owswp_version.argtypes = []
|
|
103
|
+
|
|
104
|
+
self.lib.owswp_info.restype = c_void_p
|
|
105
|
+
self.lib.owswp_info.argtypes = []
|
|
106
|
+
|
|
107
|
+
self.lib.owswp_generate_key_pair.restype = c_void_p
|
|
108
|
+
self.lib.owswp_generate_key_pair.argtypes = []
|
|
109
|
+
|
|
110
|
+
self.lib.owswp_validate_key_pair.restype = c_void_p
|
|
111
|
+
self.lib.owswp_validate_key_pair.argtypes = [c_char_p, c_char_p]
|
|
112
|
+
|
|
113
|
+
self.lib.owswp_create_license.restype = c_void_p
|
|
114
|
+
self.lib.owswp_create_license.argtypes = [c_char_p, c_char_p, c_char_p]
|
|
115
|
+
|
|
116
|
+
self.lib.owswp_validate_key.restype = c_void_p
|
|
117
|
+
self.lib.owswp_validate_key.argtypes = [c_char_p, c_char_p, c_char_p]
|
|
118
|
+
|
|
119
|
+
self.lib.owswp_inspect_key.restype = c_void_p
|
|
120
|
+
self.lib.owswp_inspect_key.argtypes = [c_char_p, c_char_p]
|
|
121
|
+
|
|
122
|
+
self.lib.owswp_protection_verify.restype = c_void_p
|
|
123
|
+
self.lib.owswp_protection_verify.argtypes = [c_char_p, c_char_p]
|
|
124
|
+
|
|
125
|
+
self.lib.owswp_decrement_credit.restype = c_void_p
|
|
126
|
+
self.lib.owswp_decrement_credit.argtypes = [c_char_p, c_char_p, c_int64]
|
|
127
|
+
|
|
128
|
+
self.lib.owswp_start_session.restype = c_void_p
|
|
129
|
+
self.lib.owswp_start_session.argtypes = [c_char_p, c_char_p]
|
|
130
|
+
|
|
131
|
+
self.lib.owswp_stop_session.restype = c_void_p
|
|
132
|
+
self.lib.owswp_stop_session.argtypes = [c_char_p, c_char_p]
|
|
133
|
+
|
|
134
|
+
self.lib.owswp_export_state.restype = c_void_p
|
|
135
|
+
self.lib.owswp_export_state.argtypes = [c_char_p, c_char_p]
|
|
136
|
+
|
|
137
|
+
self.lib.owswp_import_state.restype = c_void_p
|
|
138
|
+
self.lib.owswp_import_state.argtypes = [c_char_p, c_char_p, c_char_p]
|
|
139
|
+
|
|
140
|
+
self.lib.owswp_free_string.restype = None
|
|
141
|
+
self.lib.owswp_free_string.argtypes = [c_void_p]
|
|
142
|
+
|
|
143
|
+
def _call(self, func, *args) -> dict:
|
|
144
|
+
encoded = [a.encode("utf-8") if isinstance(a, str) else a for a in args]
|
|
145
|
+
ptr = func(*encoded)
|
|
146
|
+
|
|
147
|
+
if not ptr:
|
|
148
|
+
raise CoreLoadError("Core returned null pointer")
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
raw = ctypes.cast(ptr, c_char_p).value
|
|
152
|
+
if raw is None:
|
|
153
|
+
raise CoreLoadError("Core returned empty response")
|
|
154
|
+
return json.loads(raw.decode("utf-8"))
|
|
155
|
+
except json.JSONDecodeError as e:
|
|
156
|
+
raise CoreLoadError(f"Invalid JSON from core: {e}")
|
|
157
|
+
finally:
|
|
158
|
+
self.lib.owswp_free_string(ptr)
|
|
159
|
+
|
|
160
|
+
def version(self) -> dict:
|
|
161
|
+
return self._call(self.lib.owswp_version)
|
|
162
|
+
|
|
163
|
+
def info(self) -> dict:
|
|
164
|
+
return self._call(self.lib.owswp_info)
|
|
165
|
+
|
|
166
|
+
def generate_key_pair(self) -> dict:
|
|
167
|
+
return self._call(self.lib.owswp_generate_key_pair)
|
|
168
|
+
|
|
169
|
+
def validate_key_pair(self, secret: str, public: str) -> dict:
|
|
170
|
+
return self._call(self.lib.owswp_validate_key_pair, secret, public)
|
|
171
|
+
|
|
172
|
+
def create_license(self, secret: str, license_type: str, payload_json: str) -> dict:
|
|
173
|
+
return self._call(self.lib.owswp_create_license, secret, license_type, payload_json)
|
|
174
|
+
|
|
175
|
+
def validate_key(self, token: str, public: str, swpb_path: str) -> dict:
|
|
176
|
+
return self._call(self.lib.owswp_validate_key, token, public, swpb_path)
|
|
177
|
+
|
|
178
|
+
def inspect_key(self, token: str, public: str) -> dict:
|
|
179
|
+
return self._call(self.lib.owswp_inspect_key, token, public)
|
|
180
|
+
|
|
181
|
+
def protection_verify(self, public: str, swpb_path: str) -> dict:
|
|
182
|
+
return self._call(self.lib.owswp_protection_verify, public, swpb_path)
|
|
183
|
+
|
|
184
|
+
def decrement_credit(self, public: str, swpb_path: str, amount: int) -> dict:
|
|
185
|
+
return self._call(self.lib.owswp_decrement_credit, public, swpb_path, amount)
|
|
186
|
+
|
|
187
|
+
def start_session(self, public: str, swpb_path: str) -> dict:
|
|
188
|
+
return self._call(self.lib.owswp_start_session, public, swpb_path)
|
|
189
|
+
|
|
190
|
+
def stop_session(self, public: str, swpb_path: str) -> dict:
|
|
191
|
+
return self._call(self.lib.owswp_stop_session, public, swpb_path)
|
|
192
|
+
|
|
193
|
+
def export_state(self, public: str, swpb_path: str) -> dict:
|
|
194
|
+
return self._call(self.lib.owswp_export_state, public, swpb_path)
|
|
195
|
+
|
|
196
|
+
def import_state(self, public: str, swpb_path: str, data: str) -> dict:
|
|
197
|
+
return self._call(self.lib.owswp_import_state, public, swpb_path, data)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
_core_instance = None
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def get_core() -> CoreLib:
|
|
204
|
+
global _core_instance
|
|
205
|
+
if _core_instance is None:
|
|
206
|
+
_core_instance = CoreLib()
|
|
207
|
+
return _core_instance
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def retry_on_locked(func, *args, retries: int = 3, delay: float = 0.1, **kwargs):
|
|
211
|
+
last_error = None
|
|
212
|
+
for attempt in range(retries):
|
|
213
|
+
try:
|
|
214
|
+
return func(*args, **kwargs)
|
|
215
|
+
except FileLockedError as e:
|
|
216
|
+
last_error = e
|
|
217
|
+
time.sleep(delay * (2 ** attempt))
|
|
218
|
+
if last_error:
|
|
219
|
+
raise last_error
|
|
220
|
+
raise FileLockedError("File locked")
|
onewayswp/admin.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Optional, Tuple
|
|
4
|
+
|
|
5
|
+
from ._core import get_core, detect_platform
|
|
6
|
+
from .exceptions import (
|
|
7
|
+
OnewaySWPError,
|
|
8
|
+
SecurityError,
|
|
9
|
+
InvalidKeyError,
|
|
10
|
+
raise_from_response,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
SECRET_FILE = "secret.key"
|
|
15
|
+
PUBLIC_FILE = "public.key"
|
|
16
|
+
DEFAULT_DIR = Path.home() / ".owswp"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Admin:
|
|
20
|
+
def __init__(self, key_dir: Optional[str] = None) -> None:
|
|
21
|
+
self.key_dir: Path = Path(key_dir).expanduser() if key_dir else DEFAULT_DIR
|
|
22
|
+
self.secret_key: Optional[str] = None
|
|
23
|
+
self.public_key: Optional[str] = None
|
|
24
|
+
self._core = get_core()
|
|
25
|
+
|
|
26
|
+
def keys_exist(self, key_dir: Optional[str] = None) -> bool:
|
|
27
|
+
check_dir = Path(key_dir).expanduser() if key_dir else self.key_dir
|
|
28
|
+
secret_path = check_dir / SECRET_FILE
|
|
29
|
+
public_path = check_dir / PUBLIC_FILE
|
|
30
|
+
return secret_path.exists() and public_path.exists()
|
|
31
|
+
|
|
32
|
+
def generate_keys(
|
|
33
|
+
self,
|
|
34
|
+
auto_save: bool = True,
|
|
35
|
+
file_path: Optional[str] = None,
|
|
36
|
+
) -> Tuple[str, str]:
|
|
37
|
+
response = self._core.generate_key_pair()
|
|
38
|
+
if not response.get("status"):
|
|
39
|
+
raise_from_response(response)
|
|
40
|
+
|
|
41
|
+
self.secret_key = response.get("secret_key")
|
|
42
|
+
self.public_key = response.get("public_key")
|
|
43
|
+
|
|
44
|
+
if auto_save:
|
|
45
|
+
save_dir = Path(file_path).expanduser() if file_path else self.key_dir
|
|
46
|
+
self.save_keys(target_dir=str(save_dir))
|
|
47
|
+
|
|
48
|
+
return self.secret_key, self.public_key
|
|
49
|
+
|
|
50
|
+
def save_keys(self, target_dir: Optional[str] = None) -> None:
|
|
51
|
+
if not self.secret_key or not self.public_key:
|
|
52
|
+
raise OnewaySWPError("No keys to save. Call generate_keys() first.")
|
|
53
|
+
|
|
54
|
+
save_dir = Path(target_dir).expanduser() if target_dir else self.key_dir
|
|
55
|
+
save_dir.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
|
|
57
|
+
secret_path = save_dir / SECRET_FILE
|
|
58
|
+
public_path = save_dir / PUBLIC_FILE
|
|
59
|
+
|
|
60
|
+
secret_path.write_text(self.secret_key)
|
|
61
|
+
public_path.write_text(self.public_key)
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
secret_path.chmod(0o600)
|
|
65
|
+
except (OSError, NotImplementedError):
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
def load_keys(
|
|
69
|
+
self,
|
|
70
|
+
key_dir: Optional[str] = None,
|
|
71
|
+
validate: bool = True,
|
|
72
|
+
auto_generate: bool = False,
|
|
73
|
+
) -> Tuple[str, str]:
|
|
74
|
+
load_dir = Path(key_dir).expanduser() if key_dir else self.key_dir
|
|
75
|
+
secret_path = load_dir / SECRET_FILE
|
|
76
|
+
public_path = load_dir / PUBLIC_FILE
|
|
77
|
+
|
|
78
|
+
if not secret_path.exists() or not public_path.exists():
|
|
79
|
+
if auto_generate:
|
|
80
|
+
self.key_dir = load_dir
|
|
81
|
+
self.generate_keys(auto_save=True, file_path=str(load_dir))
|
|
82
|
+
if validate:
|
|
83
|
+
self.validate_keys()
|
|
84
|
+
return self.secret_key, self.public_key
|
|
85
|
+
raise InvalidKeyError(f"Key files not found in {load_dir}")
|
|
86
|
+
|
|
87
|
+
self.secret_key = secret_path.read_text().strip()
|
|
88
|
+
self.public_key = public_path.read_text().strip()
|
|
89
|
+
|
|
90
|
+
if validate:
|
|
91
|
+
self.validate_keys()
|
|
92
|
+
|
|
93
|
+
return self.secret_key, self.public_key
|
|
94
|
+
|
|
95
|
+
def set_keys(
|
|
96
|
+
self,
|
|
97
|
+
secret_key: str,
|
|
98
|
+
public_key: str,
|
|
99
|
+
validate: bool = True,
|
|
100
|
+
) -> None:
|
|
101
|
+
if not secret_key or not public_key:
|
|
102
|
+
raise InvalidKeyError("Both secret_key and public_key required")
|
|
103
|
+
|
|
104
|
+
self.secret_key = secret_key.strip()
|
|
105
|
+
self.public_key = public_key.strip()
|
|
106
|
+
|
|
107
|
+
if validate:
|
|
108
|
+
self.validate_keys()
|
|
109
|
+
|
|
110
|
+
def validate_keys(self) -> bool:
|
|
111
|
+
if not self.secret_key or not self.public_key:
|
|
112
|
+
raise InvalidKeyError("Keys not loaded")
|
|
113
|
+
|
|
114
|
+
response = self._core.validate_key_pair(self.secret_key, self.public_key)
|
|
115
|
+
if not response.get("status"):
|
|
116
|
+
raise SecurityError(
|
|
117
|
+
response.get("message", "Key mismatch"),
|
|
118
|
+
code=response.get("code"),
|
|
119
|
+
)
|
|
120
|
+
return True
|
|
121
|
+
|
|
122
|
+
def create_license(
|
|
123
|
+
self,
|
|
124
|
+
type: str = "date",
|
|
125
|
+
expires: Optional[str] = None,
|
|
126
|
+
credits: Optional[int] = None,
|
|
127
|
+
duration_seconds: Optional[int] = None,
|
|
128
|
+
hours: Optional[float] = None,
|
|
129
|
+
additional_data: Optional[dict] = None,
|
|
130
|
+
) -> dict:
|
|
131
|
+
if not self.secret_key:
|
|
132
|
+
raise OnewaySWPError(
|
|
133
|
+
"Secret key not loaded. Call load_keys() or generate_keys()."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
if type not in ("date", "credit", "duration"):
|
|
137
|
+
raise OnewaySWPError(
|
|
138
|
+
f"Invalid type '{type}'. Must be: date, credit, duration"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
if type == "date" and not expires:
|
|
142
|
+
raise OnewaySWPError("type='date' requires 'expires' (YYYY-MM-DD)")
|
|
143
|
+
|
|
144
|
+
if type == "credit" and credits is None:
|
|
145
|
+
raise OnewaySWPError("type='credit' requires 'credits'")
|
|
146
|
+
|
|
147
|
+
if type == "duration" and duration_seconds is None and hours is None:
|
|
148
|
+
raise OnewaySWPError(
|
|
149
|
+
"type='duration' requires 'duration_seconds' or 'hours'"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
payload: dict = {}
|
|
153
|
+
if expires:
|
|
154
|
+
payload["expires"] = expires
|
|
155
|
+
if credits is not None:
|
|
156
|
+
payload["credits"] = credits
|
|
157
|
+
if duration_seconds is not None:
|
|
158
|
+
payload["duration_seconds"] = duration_seconds
|
|
159
|
+
elif hours is not None:
|
|
160
|
+
payload["duration_seconds"] = int(hours * 3600)
|
|
161
|
+
if additional_data:
|
|
162
|
+
payload["additional_data"] = additional_data
|
|
163
|
+
|
|
164
|
+
response = self._core.create_license(
|
|
165
|
+
self.secret_key,
|
|
166
|
+
type,
|
|
167
|
+
json.dumps(payload),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
if not response.get("status"):
|
|
171
|
+
raise_from_response(response)
|
|
172
|
+
|
|
173
|
+
return response
|
|
174
|
+
|
|
175
|
+
def regenerate_keys(self, key_dir: Optional[str] = None) -> Tuple[str, str]:
|
|
176
|
+
target_dir = Path(key_dir).expanduser() if key_dir else self.key_dir
|
|
177
|
+
return self.generate_keys(auto_save=True, file_path=str(target_dir))
|
|
178
|
+
|
|
179
|
+
def platform(self) -> dict:
|
|
180
|
+
os_name, arch = detect_platform()
|
|
181
|
+
return {"os": os_name, "arch": arch}
|
|
182
|
+
|
|
183
|
+
def version(self) -> str:
|
|
184
|
+
response = self._core.version()
|
|
185
|
+
if not response.get("status"):
|
|
186
|
+
raise_from_response(response)
|
|
187
|
+
return response.get("version", "")
|
onewayswp/client.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
from ._core import get_core, retry_on_locked
|
|
5
|
+
from .exceptions import (
|
|
6
|
+
OnewaySWPError,
|
|
7
|
+
SecurityError,
|
|
8
|
+
InvalidKeyError,
|
|
9
|
+
InvalidTokenError,
|
|
10
|
+
raise_from_response,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Client:
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
data_path: Optional[str] = None,
|
|
18
|
+
extra_paths: Optional[List[str]] = None,
|
|
19
|
+
) -> None:
|
|
20
|
+
self.data_path: Path = (
|
|
21
|
+
Path(data_path).expanduser()
|
|
22
|
+
if data_path
|
|
23
|
+
else Path.cwd() / "license.swpb"
|
|
24
|
+
)
|
|
25
|
+
self.extra_paths: List[Path] = (
|
|
26
|
+
[Path(p).expanduser() for p in extra_paths] if extra_paths else []
|
|
27
|
+
)
|
|
28
|
+
self.public_key: Optional[str] = None
|
|
29
|
+
self._last_status: Optional[dict] = None
|
|
30
|
+
self._core = get_core()
|
|
31
|
+
|
|
32
|
+
def open_key(self, path: str) -> None:
|
|
33
|
+
p = Path(path).expanduser()
|
|
34
|
+
|
|
35
|
+
if p.is_dir():
|
|
36
|
+
p = p / "public.key"
|
|
37
|
+
|
|
38
|
+
if not p.exists():
|
|
39
|
+
raise InvalidKeyError(f"Key file not found: {p}")
|
|
40
|
+
|
|
41
|
+
content = p.read_text().strip()
|
|
42
|
+
self.set_public_key(content)
|
|
43
|
+
|
|
44
|
+
def set_public_key(self, key: str) -> None:
|
|
45
|
+
if not key:
|
|
46
|
+
raise InvalidKeyError("Public key is empty")
|
|
47
|
+
|
|
48
|
+
key = key.strip()
|
|
49
|
+
|
|
50
|
+
if key.startswith("OWSWP-sec-"):
|
|
51
|
+
raise SecurityError(
|
|
52
|
+
"Security error: you provided a secret key. "
|
|
53
|
+
"Client must use public key only."
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
if not key.startswith("OWSWP-pub-"):
|
|
57
|
+
raise InvalidKeyError(
|
|
58
|
+
"Invalid key format. Must start with 'OWSWP-pub-'"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
self.public_key = key
|
|
62
|
+
|
|
63
|
+
def is_active(self) -> bool:
|
|
64
|
+
if not self.public_key:
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
if not self.data_path.exists():
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
result = self.protection_verify()
|
|
72
|
+
return result.get("status", False)
|
|
73
|
+
except Exception:
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def validate_key(self, token: str) -> dict:
|
|
77
|
+
if not self.public_key:
|
|
78
|
+
raise InvalidKeyError(
|
|
79
|
+
"Public key not set. Call open_key() or set_public_key()."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if not token or not isinstance(token, str):
|
|
83
|
+
raise OnewaySWPError("Token is required and must be a string")
|
|
84
|
+
|
|
85
|
+
token = token.strip()
|
|
86
|
+
|
|
87
|
+
if not token.startswith("OWSWP-"):
|
|
88
|
+
raise InvalidTokenError(
|
|
89
|
+
"Invalid token format. Must start with 'OWSWP-'"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
self.data_path.parent.mkdir(parents=True, exist_ok=True)
|
|
94
|
+
except OSError as e:
|
|
95
|
+
raise OnewaySWPError(f"Cannot create directory: {e}")
|
|
96
|
+
|
|
97
|
+
response = retry_on_locked(
|
|
98
|
+
self._core.validate_key,
|
|
99
|
+
token,
|
|
100
|
+
self.public_key,
|
|
101
|
+
str(self.data_path),
|
|
102
|
+
)
|
|
103
|
+
self._last_status = response
|
|
104
|
+
return response
|
|
105
|
+
|
|
106
|
+
def inspect_key(self, token: str) -> dict:
|
|
107
|
+
if not self.public_key:
|
|
108
|
+
raise InvalidKeyError("Public key not set.")
|
|
109
|
+
|
|
110
|
+
if not token or not isinstance(token, str):
|
|
111
|
+
raise OnewaySWPError("Token is required and must be a string")
|
|
112
|
+
|
|
113
|
+
response = self._core.inspect_key(token.strip(), self.public_key)
|
|
114
|
+
return response
|
|
115
|
+
|
|
116
|
+
def protection_verify(self) -> dict:
|
|
117
|
+
if not self.public_key:
|
|
118
|
+
raise InvalidKeyError("Public key not set.")
|
|
119
|
+
|
|
120
|
+
if not self.data_path.exists():
|
|
121
|
+
return {
|
|
122
|
+
"status": False,
|
|
123
|
+
"code": "NO_STATE_FILE",
|
|
124
|
+
"message": f"State file not found: {self.data_path}",
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
response = retry_on_locked(
|
|
128
|
+
self._core.protection_verify,
|
|
129
|
+
self.public_key,
|
|
130
|
+
str(self.data_path),
|
|
131
|
+
)
|
|
132
|
+
self._last_status = response
|
|
133
|
+
return response
|
|
134
|
+
|
|
135
|
+
def use_credit(self, amount: int = 1) -> dict:
|
|
136
|
+
if not self.public_key:
|
|
137
|
+
raise InvalidKeyError("Public key not set.")
|
|
138
|
+
|
|
139
|
+
if not self.data_path.exists():
|
|
140
|
+
raise OnewaySWPError("State file not found. Validate key first.")
|
|
141
|
+
|
|
142
|
+
if amount <= 0:
|
|
143
|
+
raise OnewaySWPError("Amount must be greater than zero")
|
|
144
|
+
|
|
145
|
+
response = retry_on_locked(
|
|
146
|
+
self._core.decrement_credit,
|
|
147
|
+
self.public_key,
|
|
148
|
+
str(self.data_path),
|
|
149
|
+
amount,
|
|
150
|
+
)
|
|
151
|
+
self._last_status = response
|
|
152
|
+
return response
|
|
153
|
+
|
|
154
|
+
def session(self):
|
|
155
|
+
return _SessionContext(self)
|
|
156
|
+
|
|
157
|
+
def _start_session(self) -> dict:
|
|
158
|
+
if not self.public_key:
|
|
159
|
+
raise InvalidKeyError("Public key not set.")
|
|
160
|
+
if not self.data_path.exists():
|
|
161
|
+
raise OnewaySWPError("State file not found. Validate key first.")
|
|
162
|
+
return self._core.start_session(self.public_key, str(self.data_path))
|
|
163
|
+
|
|
164
|
+
def _stop_session(self) -> dict:
|
|
165
|
+
if not self.public_key:
|
|
166
|
+
raise InvalidKeyError("Public key not set.")
|
|
167
|
+
if not self.data_path.exists():
|
|
168
|
+
raise OnewaySWPError("State file not found. Validate key first.")
|
|
169
|
+
return self._core.stop_session(self.public_key, str(self.data_path))
|
|
170
|
+
|
|
171
|
+
def export_state(self) -> dict:
|
|
172
|
+
if not self.public_key:
|
|
173
|
+
raise InvalidKeyError("Public key not set.")
|
|
174
|
+
if not self.data_path.exists():
|
|
175
|
+
raise OnewaySWPError("State file not found.")
|
|
176
|
+
return self._core.export_state(self.public_key, str(self.data_path))
|
|
177
|
+
|
|
178
|
+
def import_state(self, data) -> dict:
|
|
179
|
+
if not self.public_key:
|
|
180
|
+
raise InvalidKeyError("Public key not set.")
|
|
181
|
+
|
|
182
|
+
try:
|
|
183
|
+
self.data_path.parent.mkdir(parents=True, exist_ok=True)
|
|
184
|
+
except OSError as e:
|
|
185
|
+
raise OnewaySWPError(f"Cannot create directory: {e}")
|
|
186
|
+
|
|
187
|
+
if isinstance(data, dict):
|
|
188
|
+
import json as _json
|
|
189
|
+
data = _json.dumps(data)
|
|
190
|
+
|
|
191
|
+
response = self._core.import_state(
|
|
192
|
+
self.public_key,
|
|
193
|
+
str(self.data_path),
|
|
194
|
+
data,
|
|
195
|
+
)
|
|
196
|
+
self._last_status = response
|
|
197
|
+
return response
|
|
198
|
+
|
|
199
|
+
def status(self) -> dict:
|
|
200
|
+
if self._last_status:
|
|
201
|
+
return self._last_status
|
|
202
|
+
|
|
203
|
+
if not self.data_path.exists():
|
|
204
|
+
return {
|
|
205
|
+
"status": False,
|
|
206
|
+
"code": "NO_STATE_FILE",
|
|
207
|
+
"message": "No status available",
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return self.protection_verify()
|
|
211
|
+
|
|
212
|
+
def state_path(self) -> str:
|
|
213
|
+
return str(self.data_path)
|
|
214
|
+
|
|
215
|
+
def reset(self) -> None:
|
|
216
|
+
if self.data_path.exists():
|
|
217
|
+
self.data_path.unlink()
|
|
218
|
+
self._last_status = None
|
|
219
|
+
|
|
220
|
+
def version(self) -> str:
|
|
221
|
+
response = self._core.version()
|
|
222
|
+
if not response.get("status"):
|
|
223
|
+
raise_from_response(response)
|
|
224
|
+
return response.get("version", "")
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
class _SessionContext:
|
|
228
|
+
def __init__(self, client: Client) -> None:
|
|
229
|
+
self.client = client
|
|
230
|
+
self._started = False
|
|
231
|
+
|
|
232
|
+
def __enter__(self) -> Client:
|
|
233
|
+
result = self.client._start_session()
|
|
234
|
+
if not result.get("status"):
|
|
235
|
+
raise_from_response(result)
|
|
236
|
+
self._started = True
|
|
237
|
+
return self.client
|
|
238
|
+
|
|
239
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> bool:
|
|
240
|
+
if self._started:
|
|
241
|
+
try:
|
|
242
|
+
self.client._stop_session()
|
|
243
|
+
except Exception:
|
|
244
|
+
pass
|
|
245
|
+
return False
|