orbit-web-framework 1.0.2__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.
- orbit/__init__.py +16 -0
- orbit/core/__init__.py +1 -0
- orbit/core/application.py +289 -0
- orbit/core/middleware.py +236 -0
- orbit/core/request.py +126 -0
- orbit/core/response.py +264 -0
- orbit/core/router.py +183 -0
- orbit/http/__init__.py +1 -0
- orbit/http/parser.py +176 -0
- orbit/http/server.py +387 -0
- orbit/static/__init__.py +1 -0
- orbit/static/handler.py +168 -0
- orbit/template/__init__.py +1 -0
- orbit/template/engine.py +239 -0
- orbit/testing/__init__.py +1 -0
- orbit/testing/client.py +120 -0
- orbit_web_framework-1.0.2.dist-info/METADATA +480 -0
- orbit_web_framework-1.0.2.dist-info/RECORD +21 -0
- orbit_web_framework-1.0.2.dist-info/WHEEL +5 -0
- orbit_web_framework-1.0.2.dist-info/licenses/LICENSE +21 -0
- orbit_web_framework-1.0.2.dist-info/top_level.txt +1 -0
orbit/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Orbit - Lightweight Advanced Python Web Framework
|
|
3
|
+
Pure Standard Library Implementation
|
|
4
|
+
|
|
5
|
+
A production-minded, educational-grade web framework built entirely
|
|
6
|
+
with Python's standard library. No external dependencies.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "1.0.2"
|
|
10
|
+
__author__ = "Orbit Framework Team"
|
|
11
|
+
|
|
12
|
+
from .core.application import Orbit
|
|
13
|
+
from .core.response import Response
|
|
14
|
+
from .core.request import Request
|
|
15
|
+
|
|
16
|
+
__all__ = ['Orbit', 'Response', 'Request']
|
orbit/core/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Core module
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Orbit Application
|
|
3
|
+
Main framework class orchestrating all components
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import logging
|
|
7
|
+
import signal
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Callable, List, Optional, Dict, Any
|
|
10
|
+
from .router import Router, RouterGroup
|
|
11
|
+
from .middleware import MiddlewareChain, Middleware
|
|
12
|
+
from .request import Request
|
|
13
|
+
from .response import Response
|
|
14
|
+
from ..http.server import HTTPServer
|
|
15
|
+
from ..static.handler import StaticFileHandler
|
|
16
|
+
from ..template.engine import TemplateEngine
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Orbit:
|
|
20
|
+
"""
|
|
21
|
+
Main Orbit framework application
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, debug: bool = False):
|
|
25
|
+
"""
|
|
26
|
+
Initialize Orbit application
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
debug: Enable debug mode
|
|
30
|
+
"""
|
|
31
|
+
self.debug = debug
|
|
32
|
+
self.router = Router()
|
|
33
|
+
self.middleware_chain = MiddlewareChain()
|
|
34
|
+
self.server: Optional[HTTPServer] = None
|
|
35
|
+
|
|
36
|
+
# Static file handler (optional)
|
|
37
|
+
self.static_handler: Optional[StaticFileHandler] = None
|
|
38
|
+
|
|
39
|
+
# Template engine (optional)
|
|
40
|
+
self.template_engine: Optional[TemplateEngine] = None
|
|
41
|
+
|
|
42
|
+
# Error handlers
|
|
43
|
+
self.error_handlers: Dict[int, Callable] = {}
|
|
44
|
+
|
|
45
|
+
# Shutdown state
|
|
46
|
+
self._is_shutting_down = False
|
|
47
|
+
|
|
48
|
+
# Setup logging
|
|
49
|
+
self._setup_logging()
|
|
50
|
+
|
|
51
|
+
# Graceful shutdown
|
|
52
|
+
self._setup_signal_handlers()
|
|
53
|
+
|
|
54
|
+
def _setup_logging(self):
|
|
55
|
+
"""Setup logging configuration"""
|
|
56
|
+
level = logging.DEBUG if self.debug else logging.INFO
|
|
57
|
+
|
|
58
|
+
logging.basicConfig(
|
|
59
|
+
level=level,
|
|
60
|
+
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
61
|
+
datefmt='%Y-%m-%d %H:%M:%S'
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
self.logger = logging.getLogger('orbit')
|
|
65
|
+
|
|
66
|
+
def _setup_signal_handlers(self):
|
|
67
|
+
"""Setup signal handlers for graceful shutdown"""
|
|
68
|
+
def signal_handler(sig, frame):
|
|
69
|
+
# Prevent multiple shutdown calls
|
|
70
|
+
if self._is_shutting_down:
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
self._is_shutting_down = True
|
|
74
|
+
self.logger.info("Shutdown signal received")
|
|
75
|
+
|
|
76
|
+
if self.server:
|
|
77
|
+
self.server.stop()
|
|
78
|
+
|
|
79
|
+
sys.exit(0)
|
|
80
|
+
|
|
81
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
82
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
83
|
+
|
|
84
|
+
def route(self, path: str, methods: List[str] = None,
|
|
85
|
+
handler: Callable = None, name: str = None):
|
|
86
|
+
"""
|
|
87
|
+
Register a route
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
path: URL path pattern
|
|
91
|
+
methods: List of HTTP methods
|
|
92
|
+
handler: Handler function
|
|
93
|
+
name: Route name
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
Decorator if handler is None, else None
|
|
97
|
+
"""
|
|
98
|
+
if methods is None:
|
|
99
|
+
methods = ['GET']
|
|
100
|
+
|
|
101
|
+
if handler is None:
|
|
102
|
+
# Return decorator
|
|
103
|
+
def decorator(func: Callable) -> Callable:
|
|
104
|
+
self.router.add_route(path, methods, func, name)
|
|
105
|
+
return func
|
|
106
|
+
return decorator
|
|
107
|
+
else:
|
|
108
|
+
# Direct registration
|
|
109
|
+
self.router.add_route(path, methods, handler, name)
|
|
110
|
+
|
|
111
|
+
def group(self, prefix: str) -> RouterGroup:
|
|
112
|
+
"""
|
|
113
|
+
Create route group with prefix
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
prefix: URL prefix for group
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
RouterGroup instance
|
|
120
|
+
"""
|
|
121
|
+
return RouterGroup(self.router, prefix)
|
|
122
|
+
|
|
123
|
+
def middleware(self, middleware: Middleware):
|
|
124
|
+
"""
|
|
125
|
+
Add middleware to application
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
middleware: Middleware instance
|
|
129
|
+
"""
|
|
130
|
+
self.middleware_chain.add(middleware)
|
|
131
|
+
|
|
132
|
+
def static(self, static_dir: str, url_prefix: str = '/static',
|
|
133
|
+
**kwargs):
|
|
134
|
+
"""
|
|
135
|
+
Enable static file serving
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
static_dir: Directory containing static files
|
|
139
|
+
url_prefix: URL prefix for static files
|
|
140
|
+
**kwargs: Additional StaticFileHandler options
|
|
141
|
+
"""
|
|
142
|
+
self.static_handler = StaticFileHandler(static_dir, url_prefix, **kwargs)
|
|
143
|
+
self.logger.info(f"Static files enabled: {static_dir} -> {url_prefix}")
|
|
144
|
+
|
|
145
|
+
def templates(self, template_dir: str):
|
|
146
|
+
"""
|
|
147
|
+
Enable template engine
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
template_dir: Directory containing templates
|
|
151
|
+
"""
|
|
152
|
+
self.template_engine = TemplateEngine(template_dir)
|
|
153
|
+
self.logger.info(f"Templates enabled: {template_dir}")
|
|
154
|
+
|
|
155
|
+
def render(self, template_name: str, context: Dict[str, Any] = None) -> Response:
|
|
156
|
+
"""
|
|
157
|
+
Render template
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
template_name: Template filename
|
|
161
|
+
context: Template context
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Response with rendered HTML
|
|
165
|
+
"""
|
|
166
|
+
if not self.template_engine:
|
|
167
|
+
raise RuntimeError("Template engine not configured")
|
|
168
|
+
|
|
169
|
+
html = self.template_engine.render(template_name, context)
|
|
170
|
+
return Response.html(html)
|
|
171
|
+
|
|
172
|
+
def error_handler(self, status_code: int):
|
|
173
|
+
"""
|
|
174
|
+
Register error handler decorator
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
status_code: HTTP status code
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Decorator function
|
|
181
|
+
"""
|
|
182
|
+
def decorator(func: Callable) -> Callable:
|
|
183
|
+
self.error_handlers[status_code] = func
|
|
184
|
+
return func
|
|
185
|
+
return decorator
|
|
186
|
+
|
|
187
|
+
def handle_request(self, request: Request) -> Response:
|
|
188
|
+
"""
|
|
189
|
+
Main request handler
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
request: Request object
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
Response object
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
# Pre-request middleware
|
|
199
|
+
early_response = self.middleware_chain.process_request(request)
|
|
200
|
+
if early_response:
|
|
201
|
+
return early_response
|
|
202
|
+
|
|
203
|
+
# Check static files
|
|
204
|
+
if self.static_handler and self.static_handler.is_static_request(request.path):
|
|
205
|
+
response = self.static_handler.serve(request.path)
|
|
206
|
+
else:
|
|
207
|
+
# Route matching
|
|
208
|
+
match = self.router.match(request.path, request.method)
|
|
209
|
+
|
|
210
|
+
if match:
|
|
211
|
+
handler, path_params = match
|
|
212
|
+
request.path_params = path_params
|
|
213
|
+
|
|
214
|
+
# Call handler
|
|
215
|
+
response = handler(request)
|
|
216
|
+
|
|
217
|
+
# Ensure response is Response object
|
|
218
|
+
if not isinstance(response, Response):
|
|
219
|
+
if isinstance(response, dict):
|
|
220
|
+
response = Response.json(response)
|
|
221
|
+
elif isinstance(response, str):
|
|
222
|
+
response = Response.html(response)
|
|
223
|
+
else:
|
|
224
|
+
response = Response(str(response))
|
|
225
|
+
else:
|
|
226
|
+
# 404 Not Found
|
|
227
|
+
if 404 in self.error_handlers:
|
|
228
|
+
response = self.error_handlers[404](request)
|
|
229
|
+
else:
|
|
230
|
+
response = Response.error(404, "Not Found")
|
|
231
|
+
|
|
232
|
+
# Post-response middleware
|
|
233
|
+
response = self.middleware_chain.process_response(request, response)
|
|
234
|
+
|
|
235
|
+
return response
|
|
236
|
+
|
|
237
|
+
except Exception as e:
|
|
238
|
+
self.logger.error(f"Request handling error: {e}", exc_info=True)
|
|
239
|
+
|
|
240
|
+
# Error handler
|
|
241
|
+
if 500 in self.error_handlers:
|
|
242
|
+
return self.error_handlers[500](request, e)
|
|
243
|
+
|
|
244
|
+
# Default error response
|
|
245
|
+
if self.debug:
|
|
246
|
+
import traceback
|
|
247
|
+
error_detail = traceback.format_exc()
|
|
248
|
+
return Response.error(500, f"Internal Server Error: {error_detail}")
|
|
249
|
+
else:
|
|
250
|
+
return Response.error(500, "Internal Server Error")
|
|
251
|
+
|
|
252
|
+
def run(self, host: str = '0.0.0.0', port: int = 8080,
|
|
253
|
+
use_threading: bool = True, **kwargs):
|
|
254
|
+
"""
|
|
255
|
+
Run the application server
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
host: Host address
|
|
259
|
+
port: Port number
|
|
260
|
+
use_threading: Use threading for connections
|
|
261
|
+
**kwargs: Additional server options
|
|
262
|
+
"""
|
|
263
|
+
self.server = HTTPServer(
|
|
264
|
+
host=host,
|
|
265
|
+
port=port,
|
|
266
|
+
request_handler=self.handle_request,
|
|
267
|
+
use_threading=use_threading,
|
|
268
|
+
**kwargs
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
self.logger.info(f"Starting Orbit application in {'DEBUG' if self.debug else 'PRODUCTION'} mode")
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
self.server.start()
|
|
275
|
+
except KeyboardInterrupt:
|
|
276
|
+
self.logger.info("Shutting down...")
|
|
277
|
+
finally:
|
|
278
|
+
if self.server:
|
|
279
|
+
self.server.stop()
|
|
280
|
+
|
|
281
|
+
def test_client(self):
|
|
282
|
+
"""
|
|
283
|
+
Create test client for testing
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
TestClient instance
|
|
287
|
+
"""
|
|
288
|
+
from ..testing.client import TestClient
|
|
289
|
+
return TestClient(self)
|
orbit/core/middleware.py
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Middleware System
|
|
3
|
+
Chain-based middleware for request/response processing
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Callable, List, Optional
|
|
7
|
+
from .request import Request
|
|
8
|
+
from .response import Response
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Middleware:
|
|
12
|
+
"""
|
|
13
|
+
Base middleware class
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def process_request(self, request: Request) -> Optional[Response]:
|
|
17
|
+
"""
|
|
18
|
+
Process request before handler
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
request: Request object
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Response to short-circuit, or None to continue
|
|
25
|
+
"""
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
def process_response(self, request: Request, response: Response) -> Response:
|
|
29
|
+
"""
|
|
30
|
+
Process response after handler
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
request: Request object
|
|
34
|
+
response: Response object
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Modified response
|
|
38
|
+
"""
|
|
39
|
+
return response
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MiddlewareChain:
|
|
43
|
+
"""
|
|
44
|
+
Middleware chain manager
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self):
|
|
48
|
+
self.middlewares: List[Middleware] = []
|
|
49
|
+
|
|
50
|
+
def add(self, middleware: Middleware):
|
|
51
|
+
"""
|
|
52
|
+
Add middleware to chain
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
middleware: Middleware instance
|
|
56
|
+
"""
|
|
57
|
+
self.middlewares.append(middleware)
|
|
58
|
+
|
|
59
|
+
def process_request(self, request: Request) -> Optional[Response]:
|
|
60
|
+
"""
|
|
61
|
+
Run request through middleware chain
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
request: Request object
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
Response if short-circuited, None otherwise
|
|
68
|
+
"""
|
|
69
|
+
for middleware in self.middlewares:
|
|
70
|
+
response = middleware.process_request(request)
|
|
71
|
+
if response is not None:
|
|
72
|
+
return response
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
def process_response(self, request: Request, response: Response) -> Response:
|
|
76
|
+
"""
|
|
77
|
+
Run response through middleware chain (in reverse)
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
request: Request object
|
|
81
|
+
response: Response object
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Processed response
|
|
85
|
+
"""
|
|
86
|
+
for middleware in reversed(self.middlewares):
|
|
87
|
+
response = middleware.process_response(request, response)
|
|
88
|
+
return response
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# Built-in Middlewares
|
|
92
|
+
|
|
93
|
+
class SecurityHeadersMiddleware(Middleware):
|
|
94
|
+
"""Add security headers to responses"""
|
|
95
|
+
|
|
96
|
+
def process_response(self, request: Request, response: Response) -> Response:
|
|
97
|
+
"""Add security headers"""
|
|
98
|
+
response.set_header('X-Content-Type-Options', 'nosniff')
|
|
99
|
+
response.set_header('X-Frame-Options', 'SAMEORIGIN')
|
|
100
|
+
response.set_header('X-XSS-Protection', '1; mode=block')
|
|
101
|
+
return response
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class CORSMiddleware(Middleware):
|
|
105
|
+
"""Handle CORS headers"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, allow_origins: List[str] = None,
|
|
108
|
+
allow_methods: List[str] = None,
|
|
109
|
+
allow_headers: List[str] = None):
|
|
110
|
+
self.allow_origins = allow_origins or ['*']
|
|
111
|
+
self.allow_methods = allow_methods or ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS']
|
|
112
|
+
self.allow_headers = allow_headers or ['Content-Type', 'Authorization']
|
|
113
|
+
|
|
114
|
+
def process_request(self, request: Request) -> Optional[Response]:
|
|
115
|
+
"""Handle preflight requests"""
|
|
116
|
+
if request.method == 'OPTIONS':
|
|
117
|
+
response = Response('', status=204)
|
|
118
|
+
self._add_cors_headers(response)
|
|
119
|
+
return response
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def process_response(self, request: Request, response: Response) -> Response:
|
|
123
|
+
"""Add CORS headers to response"""
|
|
124
|
+
self._add_cors_headers(response)
|
|
125
|
+
return response
|
|
126
|
+
|
|
127
|
+
def _add_cors_headers(self, response: Response):
|
|
128
|
+
"""Add CORS headers"""
|
|
129
|
+
response.set_header('Access-Control-Allow-Origin', ', '.join(self.allow_origins))
|
|
130
|
+
response.set_header('Access-Control-Allow-Methods', ', '.join(self.allow_methods))
|
|
131
|
+
response.set_header('Access-Control-Allow-Headers', ', '.join(self.allow_headers))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class RateLimitMiddleware(Middleware):
|
|
135
|
+
"""
|
|
136
|
+
IP-based rate limiting
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def __init__(self, requests_per_minute: int = 60):
|
|
140
|
+
self.requests_per_minute = requests_per_minute
|
|
141
|
+
self.request_counts: dict = {} # ip -> [(timestamp, count)]
|
|
142
|
+
|
|
143
|
+
# Security logger
|
|
144
|
+
import logging
|
|
145
|
+
self.security_logger = logging.getLogger('orbit.security')
|
|
146
|
+
|
|
147
|
+
def process_request(self, request: Request) -> Optional[Response]:
|
|
148
|
+
"""Check rate limit"""
|
|
149
|
+
import time
|
|
150
|
+
|
|
151
|
+
client_ip = request.client_ip
|
|
152
|
+
if not client_ip:
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
current_time = time.time()
|
|
156
|
+
|
|
157
|
+
# Clean old entries
|
|
158
|
+
if client_ip in self.request_counts:
|
|
159
|
+
self.request_counts[client_ip] = [
|
|
160
|
+
(ts, count) for ts, count in self.request_counts[client_ip]
|
|
161
|
+
if current_time - ts < 60
|
|
162
|
+
]
|
|
163
|
+
else:
|
|
164
|
+
self.request_counts[client_ip] = []
|
|
165
|
+
|
|
166
|
+
# Count requests in last minute
|
|
167
|
+
total_requests = sum(count for ts, count in self.request_counts[client_ip])
|
|
168
|
+
|
|
169
|
+
if total_requests >= self.requests_per_minute:
|
|
170
|
+
# Log rate limit hit
|
|
171
|
+
self.security_logger.warning(
|
|
172
|
+
f"{client_ip} | {request.method} {request.path} | RATE_LIMIT | "
|
|
173
|
+
f"{total_requests}/{self.requests_per_minute} req/min"
|
|
174
|
+
)
|
|
175
|
+
return Response.error(429, 'Rate limit exceeded')
|
|
176
|
+
|
|
177
|
+
# Add current request
|
|
178
|
+
self.request_counts[client_ip].append((current_time, 1))
|
|
179
|
+
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class LoggingMiddleware(Middleware):
|
|
184
|
+
"""Log requests and responses"""
|
|
185
|
+
|
|
186
|
+
def __init__(self, logger):
|
|
187
|
+
self.logger = logger
|
|
188
|
+
|
|
189
|
+
def process_request(self, request: Request) -> Optional[Response]:
|
|
190
|
+
"""Log incoming request"""
|
|
191
|
+
self.logger.info(f"{request.method} {request.path} from {request.client_ip}")
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
def process_response(self, request: Request, response: Response) -> Response:
|
|
195
|
+
"""Log response status"""
|
|
196
|
+
self.logger.info(f"{request.method} {request.path} -> {response.status}")
|
|
197
|
+
return response
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class CSRFProtectionMiddleware(Middleware):
|
|
201
|
+
"""
|
|
202
|
+
Basic CSRF token protection
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
def __init__(self):
|
|
206
|
+
import hashlib
|
|
207
|
+
import secrets
|
|
208
|
+
self.tokens: dict = {} # session_id -> token
|
|
209
|
+
self.secret = secrets.token_hex(32)
|
|
210
|
+
|
|
211
|
+
def generate_token(self, session_id: str) -> str:
|
|
212
|
+
"""Generate CSRF token for session"""
|
|
213
|
+
import hashlib
|
|
214
|
+
import secrets
|
|
215
|
+
|
|
216
|
+
token = secrets.token_hex(32)
|
|
217
|
+
self.tokens[session_id] = token
|
|
218
|
+
return token
|
|
219
|
+
|
|
220
|
+
def process_request(self, request: Request) -> Optional[Response]:
|
|
221
|
+
"""Validate CSRF token for state-changing requests"""
|
|
222
|
+
if request.method in ['POST', 'PUT', 'DELETE', 'PATCH']:
|
|
223
|
+
# Get session ID from cookie (simplified)
|
|
224
|
+
session_id = request.get_header('cookie', '')
|
|
225
|
+
|
|
226
|
+
# Get token from header or form
|
|
227
|
+
token = request.get_header('x-csrf-token')
|
|
228
|
+
if not token and request.is_form():
|
|
229
|
+
token = request.get_form('csrf_token')
|
|
230
|
+
|
|
231
|
+
# Validate token
|
|
232
|
+
if session_id in self.tokens:
|
|
233
|
+
if token != self.tokens[session_id]:
|
|
234
|
+
return Response.error(403, 'CSRF token validation failed')
|
|
235
|
+
|
|
236
|
+
return None
|
orbit/core/request.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request Object
|
|
3
|
+
Represents an HTTP request with parsed data
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Optional, Any
|
|
7
|
+
import json
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Request:
|
|
11
|
+
"""
|
|
12
|
+
HTTP Request object containing all request information
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self, method: str, path: str, version: str,
|
|
16
|
+
headers: Dict[str, str], query_params: Dict[str, list],
|
|
17
|
+
body: bytes, parsed_body: Optional[dict] = None,
|
|
18
|
+
client_address: tuple = None):
|
|
19
|
+
"""
|
|
20
|
+
Initialize Request object
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
method: HTTP method (GET, POST, etc.)
|
|
24
|
+
path: Request path
|
|
25
|
+
version: HTTP version
|
|
26
|
+
headers: Request headers
|
|
27
|
+
query_params: Query string parameters
|
|
28
|
+
body: Raw request body
|
|
29
|
+
parsed_body: Parsed body (JSON or form data)
|
|
30
|
+
client_address: Client IP and port tuple
|
|
31
|
+
"""
|
|
32
|
+
self.method = method
|
|
33
|
+
self.path = path
|
|
34
|
+
self.version = version
|
|
35
|
+
self.headers = headers
|
|
36
|
+
self.query_params = query_params
|
|
37
|
+
self.body = body
|
|
38
|
+
self.parsed_body = parsed_body
|
|
39
|
+
self.client_address = client_address
|
|
40
|
+
|
|
41
|
+
# Path parameters (filled by router)
|
|
42
|
+
self.path_params: Dict[str, str] = {}
|
|
43
|
+
|
|
44
|
+
# Additional context storage
|
|
45
|
+
self.context: Dict[str, Any] = {}
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def client_ip(self) -> Optional[str]:
|
|
49
|
+
"""Get client IP address"""
|
|
50
|
+
if self.client_address:
|
|
51
|
+
return self.client_address[0]
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
def get_header(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
|
55
|
+
"""
|
|
56
|
+
Get header value (case-insensitive)
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
name: Header name
|
|
60
|
+
default: Default value if header not found
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
Header value or default
|
|
64
|
+
"""
|
|
65
|
+
return self.headers.get(name.lower(), default)
|
|
66
|
+
|
|
67
|
+
def get_query(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
|
68
|
+
"""
|
|
69
|
+
Get query parameter value
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
name: Parameter name
|
|
73
|
+
default: Default value if not found
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
Parameter value or default
|
|
77
|
+
"""
|
|
78
|
+
values = self.query_params.get(name, [])
|
|
79
|
+
return values[0] if values else default
|
|
80
|
+
|
|
81
|
+
def get_json(self) -> Optional[dict]:
|
|
82
|
+
"""
|
|
83
|
+
Get parsed JSON body
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Parsed JSON data or None
|
|
87
|
+
"""
|
|
88
|
+
if self.parsed_body and isinstance(self.parsed_body, dict):
|
|
89
|
+
return self.parsed_body
|
|
90
|
+
|
|
91
|
+
# Try to parse if not already parsed
|
|
92
|
+
if self.get_header('content-type', '').startswith('application/json'):
|
|
93
|
+
try:
|
|
94
|
+
return json.loads(self.body.decode('utf-8'))
|
|
95
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
def get_form(self, name: str, default: Optional[str] = None) -> Optional[str]:
|
|
101
|
+
"""
|
|
102
|
+
Get form data value
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
name: Form field name
|
|
106
|
+
default: Default value if not found
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Form field value or default
|
|
110
|
+
"""
|
|
111
|
+
if self.parsed_body and isinstance(self.parsed_body, dict):
|
|
112
|
+
return self.parsed_body.get(name, default)
|
|
113
|
+
return default
|
|
114
|
+
|
|
115
|
+
def is_json(self) -> bool:
|
|
116
|
+
"""Check if request has JSON content type"""
|
|
117
|
+
content_type = self.get_header('content-type', '')
|
|
118
|
+
return 'application/json' in content_type
|
|
119
|
+
|
|
120
|
+
def is_form(self) -> bool:
|
|
121
|
+
"""Check if request has form content type"""
|
|
122
|
+
content_type = self.get_header('content-type', '')
|
|
123
|
+
return 'application/x-www-form-urlencoded' in content_type
|
|
124
|
+
|
|
125
|
+
def __repr__(self) -> str:
|
|
126
|
+
return f"<Request {self.method} {self.path}>"
|