File size: 21,611 Bytes
6521e91
 
 
 
 
 
 
 
a85d4b0
6521e91
a85d4b0
6521e91
b3d9cff
a85d4b0
 
 
6521e91
 
 
 
 
 
 
 
 
a85d4b0
 
 
 
 
 
 
 
 
 
 
 
 
81c8cf0
a85d4b0
 
 
 
 
 
 
81c8cf0
a85d4b0
 
81c8cf0
a85d4b0
81c8cf0
a85d4b0
 
 
 
 
 
 
 
 
81c8cf0
a85d4b0
 
81c8cf0
a85d4b0
 
 
81c8cf0
a85d4b0
 
81c8cf0
a85d4b0
 
 
 
 
 
 
81c8cf0
a85d4b0
 
81c8cf0
a85d4b0
 
 
 
81c8cf0
a85d4b0
 
 
 
 
 
 
 
 
6521e91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a85d4b0
6521e91
 
a85d4b0
6521e91
 
 
 
 
 
 
 
 
 
 
a85d4b0
6521e91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a85d4b0
6521e91
 
a85d4b0
 
6521e91
81c8cf0
6521e91
 
 
a85d4b0
 
81c8cf0
6521e91
 
 
a85d4b0
6521e91
 
 
a85d4b0
6521e91
 
 
 
 
 
 
 
 
 
 
 
a85d4b0
 
81c8cf0
6521e91
 
 
a85d4b0
6521e91
 
 
 
 
a85d4b0
6521e91
81c8cf0
6521e91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81c8cf0
 
6521e91
 
a85d4b0
 
6521e91
81c8cf0
6521e91
987d7ef
1f9ec6b
a85d4b0
157adb8
 
 
 
 
 
 
 
 
 
1f9ec6b
157adb8
 
 
 
 
 
 
 
 
 
 
 
 
 
a85d4b0
987d7ef
 
 
157adb8
987d7ef
1f9ec6b
987d7ef
 
157adb8
987d7ef
 
6521e91
 
a85d4b0
 
 
1f9ec6b
987d7ef
 
 
 
 
 
 
 
 
 
157adb8
a85d4b0
157adb8
987d7ef
157adb8
a85d4b0
 
 
28dd64f
2baa515
81c8cf0
a85d4b0
28dd64f
157adb8
a85d4b0
987d7ef
a85d4b0
81c8cf0
6521e91
 
 
 
 
 
a85d4b0
 
 
 
 
 
6521e91
a85d4b0
 
 
 
 
 
6521e91
a85d4b0
 
 
 
 
 
6521e91
a85d4b0
 
 
 
81c8cf0
6521e91
a85d4b0
 
2baa515
a85d4b0
 
 
2baa515
157adb8
a85d4b0
2baa515
 
a85d4b0
 
6521e91
a85d4b0
 
 
 
 
 
 
 
 
 
 
 
 
2baa515
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157adb8
2baa515
 
157adb8
2baa515
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
import sys
import io
import traceback
import ast
import time
import json
import os
from contextlib import redirect_stdout, redirect_stderr
from typing import Dict, Any, Optional
import threading
import asyncio
import signal
from concurrent.futures import ThreadPoolExecutor
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, Request
from pydantic import BaseModel
from starlette.responses import HTMLResponse

# Set up proper signal handling for Docker
def signal_handler(signum, frame):
    print(f"Received signal {signum}, shutting down gracefully...")
    sys.exit(0)

signal.signal(signal.SIGTERM, signal_handler)
signal.signal(signal.SIGINT, signal_handler)

# --- Custom IO Redirection for Interactive Execution ---

class WebSocketInputOutput:
    """
    Redirects stdin/stdout/stderr to/from a WebSocket connection.
    Handles interactive input by waiting for messages from the client.
    """
    def __init__(self, websocket: WebSocket):
        self.websocket = websocket
        self.input_queue = asyncio.Queue()
        self.output_buffer = io.StringIO()
        self.error_buffer = io.StringIO()
        self.closed = False
        print("DEBUG: WebSocketInputOutput initialized.")

    async def write(self, s: str):
        """Writes to stdout/stderr and sends to WebSocket."""
        self.output_buffer.write(s)
        try:
            # Send output in chunks or lines to prevent large messages
            await self.websocket.send_text(json.dumps({"type": "output", "content": s}))
            print(f"DEBUG: Sent output chunk: {s.strip()[:100]}...")
        except WebSocketDisconnect:
            self.closed = True
            print("ERROR: WebSocket disconnected during write.")
        except Exception as e:
            print(f"ERROR: Error sending output over WebSocket: {e}")
            self.closed = True

    def flush(self):
        """Flushes the buffer (no-op for now, as we send immediately)."""
        pass

    async def readline(self) -> str:
        """Reads a line from stdin, waiting for input from WebSocket."""
        try:
            print("DEBUG: Sending input_request to client.")
            await self.websocket.send_text(json.dumps({"type": "input_request"}))
            line = await self.input_queue.get() # Wait for input from client
            print(f"DEBUG: Received input from queue: {line.strip()[:50]}...")
            return line
        except WebSocketDisconnect:
            self.closed = True
            print("ERROR: WebSocket disconnected during readline.")
            raise EOFError("Input stream closed due to WebSocket disconnect")
        except Exception as e:
            print(f"ERROR: Error requesting input over WebSocket: {e}")
            self.closed = True
            raise EOFError(f"Input stream error: {e}")

    # For compatibility with sys.stdin, which expects a file-like object
    def _get_input_line(self):
        """Synchronous wrapper for readline for use with exec."""
        try:
            print("DEBUG: Synchronous input wrapper called.")
            return asyncio.run(self.readline())
        except Exception as e:
            print(f"ERROR: Synchronous input wrapper error: {e}")
            raise

    def read(self, n=-1):
        """Reads n characters. For simplicity, we'll treat it as readline."""
        print(f"DEBUG: Read called with n={n}. Delegating to readline.")
        return self._get_input_line() # Or implement more complex buffering

    def __getattr__(self, name):
        """Delegate other attributes if needed, or raise AttributeError."""
        raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")


# --- Python Syntax Validation FSM (unchanged) ---

class PythonSyntaxFSM:
    """Finite State Machine for Python syntax validation"""
    
    def __init__(self):
        self.reset()
        
    def reset(self):
        self.state = 'normal'
        self.bracket_stack = []
        self.in_string = False
        self.string_delimiter = None
        self.line_number = 0
        self.errors = []
        self.warnings = []
        
    def validate_code(self, code: str) -> Dict[str, Any]:
        """Validate Python code using FSM approach"""
        self.reset()
        
        if not code or not code.strip():
            return {
                'valid': False,
                'errors': [{'line': 0, 'message': 'Empty code input', 'type': 'input_error'}],
                'warnings': [],
                'total_issues': 1
            }
        
        lines = code.split('\n')
        
        for i, line in enumerate(lines):
            self.line_number = i + 1
            self.validate_line(line)
            
        # Check for unclosed brackets at end
        if self.bracket_stack:
            self.errors.append({
                'line': self.line_number,
                'type': 'syntax_error',
                'message': f'Unclosed bracket: {self.bracket_stack[-1]}',
                'severity': 'error'
            })
            
        return {
            'valid': len(self.errors) == 0,
            'errors': self.errors,
            'warnings': self.warnings,
            'total_issues': len(self.errors) + len(self.warnings)
        }
    
    def validate_line(self, line: str):
        """Validate a single line using FSM logic"""
        if not line.strip():
            return
            
        # Check indentation
        leading_spaces = len(line) - len(line.lstrip())
        if leading_spaces % 4 != 0 and line.strip():
            self.warnings.append({
                'line': self.line_number,
                'type': 'style_warning',
                'message': 'Inconsistent indentation (PEP 8 recommends 4 spaces)',
                'severity': 'warning'
            })
        
        # Process character by character for brackets and strings
        i = 0
        while i < len(line):
            char = line[i]
            
            # Handle string states
            if self.in_string:
                if char == self.string_delimiter and (i == 0 or line[i-1] != '\\'):
                    self.in_string = False
                    self.string_delimiter = None
                i += 1
                continue
                
            # Handle comment detection
            if char == '#':
                break   # Rest of line is comment
                
            # Handle string detection
            if char in ['"', "'"]:
                # Check for triple quotes
                if i + 2 < len(line) and line[i:i+3] == char * 3:
                    self.string_delimiter = char * 3
                    i += 3
                else:
                    self.string_delimiter = char
                    i += 1
                self.in_string = True
                continue
                
            # Handle brackets
            if char in '([{':
                self.bracket_stack.append(char)
            elif char in ')]}':
                if not self.bracket_stack:
                    self.errors.append({
                        'line': self.line_number,
                        'type': 'syntax_error',
                        'message': f'Unmatched closing bracket: {char}',
                        'severity': 'error'
                    })
                else:
                    expected = {'(': ')', '[': ']', '{': '}'}
                    last_open = self.bracket_stack[-1]
                    if expected[last_open] != char:
                        self.errors.append({
                            'line': self.line_number,
                            'type': 'syntax_error',
                            'message': f'Mismatched brackets: expected {expected[last_open]}, got {char}',
                            'severity': 'error'
                        })
                    else:
                        self.bracket_stack.pop()
                        
            i += 1
        
        # Check Python syntax patterns
        stripped = line.strip()
        control_keywords = ['def ', 'class ', 'if ', 'elif ', 'for ', 'while ', 'try:', 'except']
        
        for keyword in control_keywords:
            if stripped.startswith(keyword):
                if not stripped.endswith(':') and keyword != 'except':
                    self.errors.append({
                        'line': self.line_number,
                        'type': 'syntax_error',
                        'message': f'{keyword.strip()} statement must end with colon',
                        'severity': 'error'
                    })
                break

# --- Secure Python Executor (modified for WebSockets) ---

class SecurePythonExecutor:
    """Secure Python code executor with sandboxing and timeout, now interactive."""
    
    def __init__(self, timeout: int = 10):
        self.timeout = timeout
        self.restricted_modules = {
            'os', 'sys', 'subprocess', 'socket', 'urllib', 'requests',
            'shutil', 'glob', 'pickle', 'marshal', 'shelve', 'dbm',
            'sqlite3', 'threading', 'multiprocessing', 'ctypes', 'importlib',
            'builtins', '__builtin__', 'imp', 'zipimport'
        }
        
        # Create safe builtins
        # IMPORTANT: 'input' is now included and will be redirected!
        self.safe_builtins = {
            'print': print,
            'len': len,
            'range': range,
            'str': str,
            'int': int,
            'float': float,
            'bool': bool,
            'list': list,
            'dict': dict,
            'tuple': tuple,
            'set': set,
            'frozenset': frozenset,
            'abs': abs,
            'max': max,
            'min': min,
            'sum': sum,
            'sorted': sorted,
            'reversed': reversed,
            'enumerate': enumerate,
            'zip': zip,
            'map': map,
            'filter': filter,
            'all': all,
            'any': any,
            'type': type,
            'isinstance': isinstance,
            'hasattr': hasattr,
            'getattr': getattr,
            'setattr': setattr,
            'delattr': delattr,
            'round': round,
            'pow': pow,
            'divmod': divmod,
            'chr': chr,
            'ord': ord,
            'hex': hex,
            'oct': oct,
            'bin': bin,
            'format': format,
            'repr': repr,
            'ascii': ascii,
            'iter': iter,
            'next': next,
            'slice': slice,
            'callable': callable,
            'id': id,
            'hash': hash,
            'vars': vars,
            'dir': dir,
            'help': help,
            'Exception': Exception,
            'ValueError': ValueError,
            'TypeError': TypeError,
            'IndexError': IndexError,
            'KeyError': KeyError,
            'AttributeError': AttributeError,
            'NameError': NameError,
            'ZeroDivisionError': ZeroDivisionError,
            'input': input # Now included for interactive use
        }
        
    async def execute_code(self, code: str, websocket_io: WebSocketInputOutput) -> Dict[str, Any]:
        """Execute Python code in a secure environment with WebSocket IO."""
        start_time = time.time()
        print(f"DEBUG: execute_code called for client: {websocket_io.websocket.client}")
        
        # Input validation
        if not code or not code.strip():
            error_msg = 'No code provided'
            await websocket_io.write(f"Error: {error_msg}\n")
            print(f"DEBUG: No code provided for execution. Client: {websocket_io.websocket.client}")
            return {
                'success': False,
                'output': '',
                'error': error_msg,
                'execution_time': 0,
                'validation': {
                    'valid': False,
                    'errors': [{'message': error_msg, 'line': 0}],
                    'warnings': [],
                    'total_issues': 1
                }
            }
        
        # Pre-execution validation
        fsm = PythonSyntaxFSM()
        validation_result = fsm.validate_code(code)
        
        # Check for restricted imports
        restricted_check = self.check_restricted_imports(code)
        if not restricted_check['allowed']:
            error_msg = f"Security violation: Restricted module '{restricted_check['module']}' is not allowed"
            await websocket_io.write(f"Error: {error_msg}\n")
            print(f"DEBUG: Restricted import detected: {restricted_check['module']}. Client: {websocket_io.websocket.client}")
            return {
                'success': False,
                'output': '',
                'error': error_msg,
                'execution_time': 0,
                'validation': validation_result
            }
        
        # Execute code with timeout
        result = await self.run_with_timeout(code, websocket_io) # Pass websocket_io
        execution_time = time.time() - start_time
        print(f"DEBUG: Code execution finished. Success: {result['success']}, Client: {websocket_io.websocket.client}")
        
        return {
            'success': result['success'],
            'output': result['output'],
            'error': result['error'],
            'execution_time': round(execution_time, 3),
            'validation': validation_result
        }
    
    def check_restricted_imports(self, code: str) -> Dict[str, Any]:
        """Check for restricted module imports"""
        try:
            tree = ast.parse(code)
            for node in ast.walk(tree):
                if isinstance(node, ast.Import):
                    for alias in node.names:
                        if alias.name.split('.')[0] in self.restricted_modules:
                            return {'allowed': False, 'module': alias.name}
                elif isinstance(node, ast.ImportFrom):
                    if node.module and node.module.split('.')[0] in self.restricted_modules:
                        return {'allowed': False, 'module': node.module}
            return {'allowed': True, 'module': None}
        except Exception as e:
            print(f"WARNING: AST parsing failed for restricted import check: {e}")
            return {'allowed': True, 'module': None}   # If parsing fails, let execution handle it
    
    async def run_with_timeout(self, code: str, websocket_io: WebSocketInputOutput) -> Dict[str, Any]:
        """Run code with timeout protection, interacting via WebSocketIO."""
        result = {'success': False, 'output': '', 'error': ''}
        print(f"DEBUG: run_with_timeout called. Client: {websocket_io.websocket.client}")
        
        # Define the synchronous target function to be run in the executor thread
        def _execute_in_thread_test2(): # Using Test 2 to test sys redirection
            nonlocal result
            original_stdin = sys.stdin
            original_stdout = sys.stdout
            original_stderr = sys.stderr
            try:
                sys.stdin = websocket_io
                sys.stdout = websocket_io
                sys.stderr = websocket_io
                print(f"DEBUG: Executor thread '{threading.current_thread().name}' started. IO redirected.")
                print("DEBUG: This should now go via websocket_io.write!")
                
                # Test print to new sys.stdout
                print("Hello from redirected stdout!")
                
                result['success'] = True
            except Exception as e:
                error_msg = f"{type(e).__name__}: {str(e)}\n{traceback.format_exc()}"
                print(f"ERROR: Exception in _execute_in_thread_test2 during sys redirection test: {error_msg}")
                websocket_io.error_buffer.write(error_msg)
                result['success'] = False
            finally:
                sys.stdin = original_stdin
                sys.stdout = original_stdout
                sys.stderr = original_stderr
                print(f"DEBUG: Executor thread '{threading.current_thread().name}' finished. IO restored.")


        # Use the event loop's default ThreadPoolExecutor to run the synchronous code
        loop = asyncio.get_running_loop()
        try:
            print(f"DEBUG: Scheduling _execute_in_thread_test2 in executor with timeout {self.timeout}s.")
            await asyncio.wait_for(
                loop.run_in_executor(None, _execute_in_thread_test2), # Call _execute_in_thread_test2
                timeout=self.timeout
            )
            print("DEBUG: _execute_in_thread_test2 completed via executor.")
        except asyncio.TimeoutError:
            print(f"ERROR: Code execution timed out after {self.timeout} seconds in run_in_executor.")
            result['error'] = f"Code execution timed out after {self.timeout} seconds"
            result['success'] = False
            try:
                await websocket_io.write(f"Error: {result['error']}\n")
            except Exception:
                pass
        except Exception as e:
            print(f"ERROR: Unexpected exception from executor: {e}")
            result['error'] = f"Unexpected executor error: {type(e).__name__}: {str(e)}"
            result['success'] = False
            try:
                await websocket_io.write(f"Error: {result['error']}\n")
            except Exception:
                pass

        # After execution (or timeout), collect output
        if result['success']:
            result['output'] = websocket_io.output_buffer.getvalue()
        if websocket_io.error_buffer.getvalue():
            result['error'] = result['error'] + "\n" + websocket_io.error_buffer.getvalue() if result['error'] else websocket_io.error_buffer.getvalue()
            result['success'] = False
        
        # Send a final message to the client indicating execution is complete
        try:
            print(f"DEBUG: Attempting to send execution_complete message (success={result['success']}).")
            await websocket_io.websocket.send_text(json.dumps({"type": "execution_complete", "result": result}))
            print("DEBUG: execution_complete message sent.")
        except WebSocketDisconnect:
            print("DEBUG: WebSocketDisconnect when sending execution_complete.")
            pass
        except Exception as e:
            print(f"ERROR: Error sending execution_complete message: {e}")

        print(f"DEBUG: run_with_timeout returning result for client: {websocket_io.websocket.client}")
        return result


# Initialize the executor
executor = SecurePythonExecutor(timeout=10)

# Initialize FastAPI app
app = FastAPI(
    title="Interactive Python Code Executor API",
    description="A secure WebSocket API for executing Python code interactively.",
    version="1.0.0",
)

# Serve the HTML frontend
@app.get("/", response_class=HTMLResponse)
async def get_index():
    """Serves the interactive frontend."""
    with open("index.html", "r") as f:
        return HTMLResponse(content=f.read())

# WebSocket endpoint for interactive execution
@app.websocket("/ws")
async def websocket_endpoint(websocket: WebSocket):
    await websocket.accept()
    websocket_io = WebSocketInputOutput(websocket)
    print(f"WebSocket connection established: {websocket.client}")

    try:
        while True:
            message = await websocket.receive_text()
            data = json.loads(message)
            print(f"DEBUG: Received message from {websocket.client}: type={data['type']}")

            if data["type"] == "code":
                code = data["content"]
                print(f"Received code from {websocket_io.websocket.client}: {code[:50]}...")
                asyncio.create_task(executor.execute_code(code, websocket_io))
            elif data["type"] == "input":
                user_input = data["content"]
                print(f"Received input from {websocket_io.websocket.client}: {user_input.strip()[:50]}...")
                await websocket_io.input_queue.put(user_input + "\n")
            elif data["type"] == "ping":
                await websocket_io.websocket.send_text(json.dumps({"type": "pong"}))
                print(f"DEBUG: Sent pong to {websocket_io.websocket.client}")
            else:
                await websocket_io.write(f"Unknown message type: {data['type']}\n")

    except WebSocketDisconnect:
        print(f"WebSocket disconnected: {websocket.client}")
    except json.JSONDecodeError:
        print(f"Received invalid JSON from {websocket.client}: {message}")
    except Exception as e:
        print(f"WebSocket error for {websocket.client}: {e}")
    finally:
        websocket_io.closed = True
        while not websocket_io.input_queue.empty():
            try:
                websocket_io.input_queue.get_nowait()
            except asyncio.QueueEmpty:
                pass
        print(f"WebSocket connection closed for {websocket_io.websocket.client}")

if __name__ == "__main__":
    import uvicorn
    print("Starting Interactive Python Code Executor API (FastAPI)...")
    print(f"Python version: {sys.version}")
    print(f"Working directory: {os.getcwd()}")
    
    server_name = os.getenv('FASTAPI_SERVER_NAME', '0.0.0.0')
    server_port = int(os.getenv('FASTAPI_SERVER_PORT', 7860))
    
    print(f"Starting server on {server_name}:{server_port}")
    
    try:
        uvicorn.run(
            "app:app",
            host=server_name,
            port=server_port,
            reload=False,
            log_level="info"
        )
    except Exception as e:
        print(f"Failed to start server: {e}")
        sys.exit(1)