@ethernity-cloud/sdk-js 1.6.2 → 1.7.1

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.
Files changed (57) hide show
  1. package/README.md +86 -48
  2. package/cas/config.js +61 -3
  3. package/info.js +14 -5
  4. package/init.js +24 -6
  5. package/network.js +51 -0
  6. package/nodenithy/build/securelock/Dockerfile.tmpl +34 -32
  7. package/nodenithy/build/securelock/scripts/binary-fs-build.sh +7 -6
  8. package/nodenithy/build.mjs +80 -68
  9. package/nodenithy/run/docker-compose-final.yml.tmpl +36 -31
  10. package/nodenithy/run/docker-compose.yml.tmpl +34 -45
  11. package/nodenithy/run/image_registry.js +17 -4
  12. package/nodenithy/run.js +42 -28
  13. package/nodenithy/src/ec_helloworld_example.js +46 -42
  14. package/nodenithy/src/preStart.mjs +41 -51
  15. package/package.json +6 -3
  16. package/publish.js +4 -2
  17. package/pynithy/build/securelock/Dockerfile.base.tpl +13 -0
  18. package/pynithy/build/securelock/Dockerfile.tpl +131 -0
  19. package/pynithy/build/securelock/scripts/binary-fs-build.sh +25 -34
  20. package/pynithy/build/securelock/scripts/pyfreeze.sh +32 -0
  21. package/pynithy/build/securelock/src/{pox.abi → abi/etnyImplementationV2.abi} +1489 -1489
  22. package/pynithy/build/securelock/src/abi/image_registry.abi +1 -0
  23. package/pynithy/build/securelock/src/ecld_state.py +906 -0
  24. package/pynithy/build/securelock/src/esr.abi +1 -0
  25. package/pynithy/build/securelock/src/esr_local.py +267 -0
  26. package/pynithy/build/securelock/src/esr_wallet.py +150 -0
  27. package/pynithy/build/securelock/src/etny_crypto.py +208 -167
  28. package/pynithy/build/securelock/src/etny_exec.py +385 -79
  29. package/pynithy/build/securelock/src/etny_exec_flask.py +535 -0
  30. package/pynithy/build/securelock/src/etny_exec_serv.py +353 -0
  31. package/pynithy/build/securelock/src/etny_session_exec.py +245 -0
  32. package/pynithy/build/securelock/src/get_sgx_report.so +0 -0
  33. package/pynithy/build/securelock/src/image_registry.abi +1 -1
  34. package/pynithy/build/securelock/src/key_generation.py +17 -17
  35. package/pynithy/build/securelock/src/local_api.py +189 -0
  36. package/pynithy/build/securelock/src/models.py +200 -176
  37. package/pynithy/build/securelock/src/securelock.py.tmpl +1037 -731
  38. package/pynithy/build/securelock/src/swift_stream_service.py +206 -186
  39. package/pynithy/build.mjs +139 -89
  40. package/pynithy/run/docker-compose-final.yml.tmpl +58 -51
  41. package/pynithy/run/docker-compose.yml.tmpl +62 -48
  42. package/pynithy/run/etny-securelock-test.yaml.tpl +4 -1
  43. package/pynithy/run/image_registry.js +17 -4
  44. package/pynithy/run.js +44 -28
  45. package/pynithy/src/ec_helloworld_example.js +46 -42
  46. package/pynithy/src/preStart.mjs +41 -51
  47. package/pynithy/src/serverless/Dockerfile.serverless +8 -0
  48. package/pynithy/src/serverless/requirements.txt +6 -0
  49. package/run.js +37 -34
  50. package/nodenithy/build/las/Dockerfile +0 -5
  51. package/nodenithy/src/preStart.js +0 -45
  52. package/pynithy/build/docker-compose.yml +0 -70
  53. package/pynithy/build/las/Dockerfile +0 -11
  54. package/pynithy/build/las/src/etny_entrypoint.sh +0 -6
  55. package/pynithy/build/las/src/las_entrypoint.sh +0 -26
  56. package/pynithy/build/scripts/binary-fs-build.sh +0 -28
  57. package/pynithy/build/securelock/Dockerfile.tmpl +0 -95
@@ -0,0 +1,535 @@
1
+ # etny_exec.py
2
+
3
+ import os
4
+ import sys
5
+ import ast
6
+ import json
7
+ import threading
8
+ import time
9
+ import secrets
10
+ import socket
11
+ import logging
12
+ from flask import Flask, request, jsonify
13
+ from functools import wraps
14
+ from flask_limiter import Limiter
15
+ from flask_limiter.util import get_remote_address
16
+ from pyngrok import ngrok
17
+ from concurrent.futures import ThreadPoolExecutor, TimeoutError
18
+ import importlib
19
+
20
+ # ===========================
21
+ # Backend Functions Loading
22
+ # ===========================
23
+
24
+ try:
25
+ import serverless.backend as backend
26
+ except ImportError:
27
+ backend = None
28
+ pass
29
+
30
+ sdkFunctions = {}
31
+ if backend is not None:
32
+ for func in backend.__dict__.keys():
33
+ if func not in backend.__builtins__.keys() and func not in [
34
+ "__file__",
35
+ "__cached__",
36
+ "__builtins__",
37
+ ]:
38
+ sdkFunctions.update({func: backend.__dict__[func]})
39
+
40
+ server_instance = None
41
+
42
+ # ===========================
43
+ # Flask and ngrok Setup
44
+ # ===========================
45
+
46
+ class EthernityEnclaveServer:
47
+
48
+ # Initialize the persistent execution context
49
+ globals_context = {}
50
+
51
+ # Initialize a reentrant threading lock for thread-safe access to the execution context
52
+ execution_lock = threading.RLock()
53
+
54
+ def __init__(self, local_port=5000):
55
+ self.api_key = self.generate_api_key()
56
+ self.local_port = local_port
57
+ self.ngrok_url = None
58
+ self.app = Flask(__name__)
59
+ self.setup_security()
60
+ self.setup_execution_environment()
61
+ self.setup_routes()
62
+ self.server_thread = threading.Thread(target=self.run_flask, daemon=True)
63
+ self.ngrok_thread = threading.Thread(target=self.start_ngrok, daemon=True)
64
+
65
+ def ___etny_result___(self, data):
66
+ return([0, data])
67
+
68
+ def generate_api_key(self):
69
+ """Generates a secure random API key."""
70
+ return secrets.token_hex(16) # Generates a 32-character hexadecimal string
71
+
72
+ def setup_security(self):
73
+ """Sets up authentication and rate limiting."""
74
+ # Authentication Decorator
75
+ def require_api_key(f):
76
+ @wraps(f)
77
+ def decorated(*args, **kwargs):
78
+ key = request.headers.get('x-api-key')
79
+ if not key or key != self.api_key:
80
+ return jsonify({"status": "error", "message": "Unauthorized: Invalid or missing API key."}), 401
81
+ return f(*args, **kwargs)
82
+ return decorated
83
+
84
+ # Rate Limiting using Flask-Limiter
85
+ self.limiter = Limiter(
86
+ key_func=get_remote_address,
87
+ default_limits=["200 per day", "50 per hour"]
88
+ )
89
+
90
+ self.limiter.init_app(self.app)
91
+
92
+ # Attach the authentication decorator to the server instance
93
+ self.require_api_key = require_api_key
94
+
95
+ def setup_execution_environment(self):
96
+ """Initializes the secure execution environment."""
97
+ # Initialize the persistent execution context
98
+ class GlobalContext:
99
+ def __init__(self):
100
+ self.context = {}
101
+ self.lock = threading.RLock()
102
+
103
+ def set(self, name, value, timeout=5):
104
+ acquired = self.lock.acquire(timeout=timeout)
105
+ if not acquired:
106
+ raise RuntimeError(f"Failed to acquire lock to set '{name}'.")
107
+ try:
108
+ self.context[name] = value
109
+ finally:
110
+ self.lock.release()
111
+
112
+ def get(self, name, timeout=5):
113
+ acquired = self.lock.acquire(timeout=timeout)
114
+ if not acquired:
115
+ raise RuntimeError(f"Failed to acquire lock to get '{name}'.")
116
+ try:
117
+ value = self.context.get(name)
118
+ return value
119
+ finally:
120
+ self.lock.release()
121
+
122
+ def update(self, updates, timeout=5):
123
+ acquired = self.lock.acquire(timeout=timeout)
124
+ if not acquired:
125
+ raise RuntimeError("Failed to acquire lock to update global context.")
126
+ try:
127
+ self.context.update(updates)
128
+ finally:
129
+ self.lock.release()
130
+
131
+ self.globals_context = GlobalContext()
132
+
133
+ # Define set_global and get_global functions
134
+ def set_global(name, value):
135
+ """
136
+ Sets a global variable in globals_context.
137
+ Args:
138
+ name (str): The name of the global variable.
139
+ value: The value to assign to the global variable.
140
+ """
141
+ try:
142
+ self.globals_context.set(name, value)
143
+ except RuntimeError as e:
144
+ print("Error setting global variable")
145
+ # Depending on design, you might want to propagate the exception or handle it here
146
+
147
+ def get_global(name):
148
+ """
149
+ Retrieves a global variable from globals_context.
150
+ Args:
151
+ name (str): The name of the global variable.
152
+ Returns:
153
+ The value of the global variable if it exists, else None.
154
+ """
155
+ try:
156
+ return self.globals_context.get(name)
157
+ except RuntimeError as e:
158
+ print("Error getting global variable")
159
+ return None
160
+
161
+ self.set_global = set_global
162
+ self.get_global = get_global
163
+
164
+ # Load backend functions
165
+ self.load_backend_functions()
166
+
167
+ def load_backend_functions(self):
168
+ """Dynamically load or reload backend functions from the 'serverless.backend' module."""
169
+ try:
170
+ import serverless.backend as backend
171
+ importlib.reload(backend) # Reload to get updated functions
172
+
173
+ # Inject set_global and get_global into the backend module's namespace
174
+ backend.set_global = self.set_global
175
+ backend.get_global = self.get_global
176
+
177
+ functions = {
178
+ func: getattr(backend, func)
179
+ for func in dir(backend)
180
+ if callable(getattr(backend, func)) and not func.startswith("__")
181
+ }
182
+
183
+ # Update the global context with backend functions
184
+ self.globals_context.update(functions)
185
+ except ImportError:
186
+ print("backend module not found")
187
+ except Exception as e:
188
+ print("error loading backend functions")
189
+
190
+ def setup_routes(self):
191
+ """Defines Flask routes."""
192
+ @self.app.route('/execute', methods=['POST'])
193
+ @self.require_api_key
194
+ @self.limiter.limit("10 per minute") # Additional rate limit for this endpoint
195
+ def execute():
196
+ """
197
+ Executes user-provided Python code and returns the outputs.
198
+ """
199
+ data = request.get_json()
200
+ if not data:
201
+ return jsonify({"status": "error", "message": "Invalid JSON payload."}), 400
202
+
203
+ payload = data.get('payload')
204
+ input_data = data.get('input_data', None)
205
+
206
+ if not payload:
207
+ return jsonify({"status": "error", "message": "No payload provided."}), 400
208
+
209
+ # Execute the payload
210
+ result = self.Exec(payload, input_data)
211
+
212
+ return jsonify(result)
213
+
214
+ @self.app.route('/reload_backend', methods=['POST'])
215
+ @self.require_api_key
216
+ def reload_backend():
217
+ """
218
+ Reloads backend functions dynamically.
219
+ """
220
+ try:
221
+ self.load_backend_functions()
222
+ return jsonify({"status": "success", "message": "Backend functions reloaded successfully."}), 200
223
+ except Exception as e:
224
+ return jsonify({"status": "error", "message": "Failed to reload backend functions."}), 500
225
+
226
+ @self.app.route('/health', methods=['GET'])
227
+ def health():
228
+ """Health check endpoint."""
229
+ return jsonify({"status": "healthy"}), 200
230
+
231
+ @self.app.route('/list_functions', methods=['GET'])
232
+ @self.require_api_key
233
+ def list_functions():
234
+ """
235
+ Lists all loaded backend functions.
236
+ """
237
+ functions = {name: func.__doc__ for name, func in self.globals_context.context.items() if callable(func)}
238
+ return jsonify({"status": "success", "functions": functions}), 200
239
+
240
+ def run_flask(self):
241
+ """Runs the Flask server."""
242
+ self.app.run(port=self.local_port, debug=False, use_reloader=False)
243
+
244
+ def start_ngrok(self):
245
+ """Starts ngrok tunnel to forward the specified port."""
246
+
247
+ print("Starting ngrok")
248
+ try:
249
+ # Authenticate with ngrok
250
+ print("Setting authentication token")
251
+ auth_token = os.environ.get("ETNY_NGROK_AUTHTOKEN", "2orw7I8y15uYu4ajtlsGwimgAqK_6ke12fFHvBdkWkyYpEyf6")
252
+ print(auth_token)
253
+ ngrok.set_auth_token(auth_token)
254
+ print(ngrok)
255
+ # Forward the port with TLS
256
+ public_url = ngrok.connect(self.local_port, bind_tls=True)
257
+
258
+ print(public_url)
259
+ self.ngrok_url = public_url.public_url
260
+ print(f"ngrok tunnel established at {self.ngrok_url}")
261
+ except Exception as e:
262
+ print("Failed to start ngrok tunnel")
263
+
264
+ def initialize_application(self, payload_data, input_data = None):
265
+ """
266
+ Initializes the EthernityEnclaveServer and executes the initial deployment function.
267
+ """
268
+
269
+ health_url = f"{self.ngrok_url}/health"
270
+ headers = {"x-api-key": self.api_key}
271
+
272
+ for attempt in range(5):
273
+ try:
274
+ health_response = requests.get(health_url, headers=headers)
275
+ if health_response.status_code == 200 and health_response.json().get("status") == "healthy":
276
+ break
277
+ else:
278
+ print(f"Health check failed (Attempt {attempt + 1}/5). Retrying...")
279
+ except Exception as e:
280
+ print(f"Health check error (Attempt {attempt + 1}/5): {e}")
281
+
282
+ time.sleep(1)
283
+
284
+ if health_response.status_code != 200:
285
+ return
286
+
287
+ initial_deployment_payload = payload_data # Adjust as per your deploy function
288
+
289
+ # Step 5: Prepare the request headers
290
+ exec_headers = {
291
+ "Content-Type": "application/json",
292
+ "x-api-key": self.api_key
293
+ }
294
+
295
+ # Step 6: Prepare the JSON data
296
+ exec_data = {
297
+ "payload": initial_deployment_payload,
298
+ "input_data": None
299
+ }
300
+
301
+ # Step 7: Send the POST request to /execute
302
+ execute_url = f"{self.ngrok_url}/execute"
303
+
304
+ print("Executing initial deployment...")
305
+ try:
306
+ exec_response = requests.post(execute_url, headers=exec_headers, json=exec_data)
307
+ if exec_response.status_code == 200:
308
+ exec_result = exec_response.json()
309
+ else:
310
+ print("Response:", exec_response.json())
311
+ except Exception as e:
312
+ print(f"Error executing initial deployment: {e}")
313
+
314
+
315
+ def start(self, payload_data, input_data=None):
316
+ """Starts the Flask server and ngrok tunnel."""
317
+ self.server_thread.start()
318
+ self.ngrok_thread.start()
319
+
320
+ while(self.ngrok_url == None):
321
+ time.sleep(0.1)
322
+
323
+ self.initialize_application(payload_data, input_data)
324
+
325
+
326
+ def Exec(self, payload_data, input_data=None):
327
+ """
328
+ Executes the provided payload within the persistent execution context.
329
+ """
330
+ try:
331
+ if payload_data:
332
+ local_context = {}
333
+ if input_data is not None:
334
+ self.globals_context.set("___etny_data_set___", input_data)
335
+ module = ast.parse(payload_data)
336
+ outputs = []
337
+ for node in module.body:
338
+ if isinstance(node, ast.Expr):
339
+ #print("node:", ast.unparse(node.value))
340
+ #print("ctx:", self.globals_context.context)
341
+ result = self.secure_exec_with_timeout("enty_expr_result = " + ast.unparse(node.value), globals_ctx=self.globals_context, locals_ctx=local_context, timeout=120)
342
+
343
+ #print("ctxa:", self.globals_context.context)
344
+ #print("output=", result['outputs']['enty_expr_result'])
345
+
346
+ outputs.append(result['outputs']['enty_expr_result'])
347
+ else:
348
+ # Handle statements if needed
349
+ #print("ctx:", self.globals_context.context)
350
+ self.secure_exec_with_timeout(ast.Module([node], type_ignores=[]), globals_ctx=self.globals_context, locals_ctx=local_context, timeout=120)
351
+ #print("ctxa:", self.globals_context.context)
352
+
353
+ result_object = {"outputs": outputs}
354
+ return self.___etny_result___(json.dumps(result_object, separators=(",", ":")))
355
+
356
+ except Exception as e:
357
+ return {"status": "error", "message": str(e)}
358
+
359
+ def is_json_serializable(self, obj):
360
+ """Check if the object is JSON serializable."""
361
+ try:
362
+ json.dumps(obj)
363
+ return True
364
+ except (TypeError, OverflowError):
365
+ return False
366
+
367
+ def secure_exec(self, code, globals_ctx=None, locals_ctx=None):
368
+ """
369
+ Executes user-provided code in a restricted environment.
370
+ Only JSON-serializable outputs are returned.
371
+ Exposes set_global and get_global for dynamic global variable management.
372
+ """
373
+ try:
374
+ byte_code = compile_restricted(code, '<string>', 'exec')
375
+
376
+
377
+ exec(byte_code, {
378
+ '__builtins__': safe_builtins,
379
+ 'set_global': self.set_global, # Expose set_global
380
+ 'get_global': self.get_global, # Expose get_global
381
+ **globals_ctx.context
382
+ }, locals_ctx)
383
+
384
+ excluded_vars = {'___etny_data_set___', '___etny_result___'}
385
+
386
+ outputs = {}
387
+ for var, value in locals_ctx.items():
388
+ if var not in excluded_vars and not var.startswith('__'):
389
+ if self.is_json_serializable(value):
390
+ outputs[var] = value
391
+ else:
392
+ outputs[var] = str(value)
393
+
394
+ return {"status": "success", "outputs": outputs}
395
+ except Exception as e:
396
+ return {"status": "error", "message": str(e)}
397
+
398
+ def secure_eval(self, code, globals_ctx=None, locals_ctx=None):
399
+ """
400
+ Executes user-provided code in a restricted environment.
401
+ Only JSON-serializable outputs are returned.
402
+ Exposes set_global and get_global for dynamic global variable management.
403
+ """
404
+ try:
405
+ print("eval:", code)
406
+
407
+ byte_code = compile_restricted(code, '<inline code>', 'eval')
408
+
409
+ result = eval(byte_code, {
410
+ '__builtins__': safe_builtins,
411
+ 'return': self.set_global,
412
+ 'set_global': self.set_global, # Expose set_global
413
+ 'get_global': self.get_global, # Expose get_global
414
+ **globals_ctx.context
415
+ }, locals_ctx)
416
+ print("res:", result)
417
+
418
+ excluded_vars = {'___etny_data_set___', '___etny_result___'}
419
+
420
+ outputs.append(result)
421
+
422
+ return {"status": "success", "outputs": outputs}
423
+ except Exception as e:
424
+ return {"status": "error", "message": str(e)}
425
+
426
+
427
+ def secure_exec_with_timeout(self, code, globals_ctx=None, locals_ctx=None, timeout=15):
428
+ """
429
+ Executes user-provided code in a restricted environment with a timeout.
430
+ Only JSON-serializable outputs are returned.
431
+ """
432
+ with ThreadPoolExecutor(max_workers=1) as executor:
433
+ future = executor.submit(self.secure_exec, code, globals_ctx, locals_ctx)
434
+ try:
435
+ result = future.result(timeout=timeout)
436
+ return result
437
+ except TimeoutError:
438
+ return {"status": "error", "message": "Code execution timed out."}
439
+ except Exception as e:
440
+ return {"status": "error", "message": str(e)}
441
+
442
+ def secure_eval_with_timeout(self, code, globals_ctx=None, locals_ctx=None, timeout=15):
443
+ """
444
+ Executes user-provided code in a restricted environment with a timeout.
445
+ Only JSON-serializable outputs are returned.
446
+ """
447
+ with ThreadPoolExecutor(max_workers=1) as executor:
448
+ future = executor.submit(self.secure_eval, code, globals_ctx, locals_ctx)
449
+ try:
450
+ result = future.result(timeout=timeout)
451
+ print("rt:", result)
452
+ return result
453
+ except TimeoutError:
454
+ return {"status": "error", "message": "Code execution timed out."}
455
+ except Exception as e:
456
+ return {"status": "error", "message": str(e)}
457
+
458
+
459
+ def extract_variables_from_ast(self, payload_data):
460
+ """Extracts variable names assigned in the payload."""
461
+ try:
462
+ parsed = ast.parse(payload_data)
463
+ variables = set()
464
+ for node in ast.walk(parsed):
465
+ if isinstance(node, ast.Assign):
466
+ for target in node.targets:
467
+ if isinstance(target, ast.Name):
468
+ variables.add(target.id)
469
+ elif isinstance(node, (ast.Tuple, ast.List)):
470
+ for elt in node.elts:
471
+ if isinstance(elt, ast.Name):
472
+ variables.add(elt.id)
473
+ return variables
474
+ except Exception as e:
475
+ return set()
476
+
477
+ def execute_server_v4(payload_data, input_data=None, globals=None, locals=None):
478
+ """
479
+ The main ExecServer function that sets up the server.
480
+ It does not execute any tasks directly.
481
+
482
+ Args:
483
+ payload_data (str): The Python code to execute (unused in this setup).
484
+ input_data (any): Optional input data (unused in this setup).
485
+ globals (dict): Global variables.
486
+ locals (dict): Local variables.
487
+
488
+ Returns:
489
+ dict: Information about the server setup.
490
+ """
491
+ global server_instance
492
+ server_lock = threading.Lock()
493
+ with server_lock:
494
+ print("SL")
495
+ if server_instance is None:
496
+ # Initialize and start the EthernityEnclaveServer
497
+ print("SI")
498
+ server_instance = EthernityEnclaveServer(local_port=5000)
499
+ server_instance.start(payload_data, input_data)
500
+ # Wait briefly to ensure ngrok tunnel is established
501
+ time.sleep(2)
502
+ if not server_instance.ngrok_url:
503
+ return {
504
+ "status": "error",
505
+ "message": "Failed to establish ngrok tunnel."
506
+ }
507
+
508
+ # Return server details
509
+ return [ 0, f"{server_instance.ngrok_url}:{server_instance.api_key}"]
510
+
511
+ # ===========================
512
+ # Server Management
513
+ # ===========================
514
+
515
+ def execute_task_v4(payload_data, input_data=None):
516
+ """
517
+ Executes the task by calling the ExecFunction.
518
+
519
+ Args:
520
+ payload_data (str): The Python code to execute.
521
+ input_data (any): Optional input data.
522
+
523
+ Returns:
524
+ dict: Result of the execution.
525
+ """
526
+
527
+ global server_instance
528
+ if server_instance is None:
529
+ # Initialize and start the EthernityEnclaveServer
530
+ server_instance = EthernityEnclaveServer(local_port=5000)
531
+
532
+ return server_instance.Exec(
533
+ payload_data,
534
+ input_data,
535
+ )