Aggiunto un log migliore
This commit is contained in:
@@ -29,6 +29,7 @@ TCP_CONNECT_TIMEOUT_SECONDS = float(os.getenv("TCP_CONNECT_TIMEOUT_SECONDS", "1.
|
|||||||
INITIAL_REQUEST_TIMEOUT_SECONDS = float(os.getenv("INITIAL_REQUEST_TIMEOUT_SECONDS", "3"))
|
INITIAL_REQUEST_TIMEOUT_SECONDS = float(os.getenv("INITIAL_REQUEST_TIMEOUT_SECONDS", "3"))
|
||||||
MAX_REQUEST_HEADER_BYTES = int(os.getenv("MAX_REQUEST_HEADER_BYTES", "65536"))
|
MAX_REQUEST_HEADER_BYTES = int(os.getenv("MAX_REQUEST_HEADER_BYTES", "65536"))
|
||||||
GATEWAY_ROUTE_TTL_SECONDS = int(os.getenv("GATEWAY_ROUTE_TTL_SECONDS", "3600"))
|
GATEWAY_ROUTE_TTL_SECONDS = int(os.getenv("GATEWAY_ROUTE_TTL_SECONDS", "3600"))
|
||||||
|
GATEWAY_ROUTE_COOKIE_NAME = "fis_user_route"
|
||||||
|
|
||||||
DEFAULTS: Dict[str, Any] = {
|
DEFAULTS: Dict[str, Any] = {
|
||||||
"container_prefix": "fis-",
|
"container_prefix": "fis-",
|
||||||
@@ -221,15 +222,15 @@ def build_http_error(status_code: int, reason: str, message: str) -> bytes:
|
|||||||
return "\r\n".join(headers).encode("utf-8") + body
|
return "\r\n".join(headers).encode("utf-8") + body
|
||||||
|
|
||||||
|
|
||||||
def build_http_redirect(location: str) -> bytes:
|
def build_http_redirect(location: str, extra_headers: Optional[Iterable[str]] = None) -> bytes:
|
||||||
headers = [
|
headers = [
|
||||||
"HTTP/1.1 302 Found",
|
"HTTP/1.1 302 Found",
|
||||||
f"Location: {location}",
|
f"Location: {location}",
|
||||||
"Content-Length: 0",
|
"Content-Length: 0",
|
||||||
"Connection: close",
|
"Connection: close",
|
||||||
"",
|
|
||||||
"",
|
|
||||||
]
|
]
|
||||||
|
headers.extend(extra_headers or [])
|
||||||
|
headers.extend(["", ""])
|
||||||
return "\r\n".join(headers).encode("utf-8")
|
return "\r\n".join(headers).encode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
@@ -248,6 +249,59 @@ def rewrite_http_request_target(data: bytes, target: str) -> bytes:
|
|||||||
return rewritten_line + data[header_end:]
|
return rewritten_line + data[header_end:]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_http_request(data: bytes):
|
||||||
|
if not data:
|
||||||
|
return None
|
||||||
|
|
||||||
|
header_end = data.find(b"\r\n\r\n")
|
||||||
|
if header_end == -1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
header_block = data[:header_end].decode("iso-8859-1", errors="replace")
|
||||||
|
lines = header_block.split("\r\n")
|
||||||
|
if not lines or len(lines[0].split(" ")) < 3:
|
||||||
|
return None
|
||||||
|
|
||||||
|
method, target, version = lines[0].split(" ", 2)
|
||||||
|
headers = {}
|
||||||
|
for line in lines[1:]:
|
||||||
|
if ":" not in line:
|
||||||
|
continue
|
||||||
|
key, value = line.split(":", 1)
|
||||||
|
headers[key.strip().lower()] = value.strip()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"method": method,
|
||||||
|
"target": target,
|
||||||
|
"version": version,
|
||||||
|
"headers": headers,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def extract_cookie_user(headers: Dict[str, str], known_users: Dict[str, Dict[str, Any]]) -> Optional[Dict[str, Any]]:
|
||||||
|
cookie_header = headers.get("cookie", "")
|
||||||
|
if not cookie_header:
|
||||||
|
return None
|
||||||
|
|
||||||
|
for part in cookie_header.split(";"):
|
||||||
|
key, separator, value = part.strip().partition("=")
|
||||||
|
if separator and key == GATEWAY_ROUTE_COOKIE_NAME:
|
||||||
|
user_id = value.strip()
|
||||||
|
return known_users.get(user_id)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def is_websocket_request(headers: Dict[str, str]) -> bool:
|
||||||
|
return headers.get("upgrade", "").lower() == "websocket"
|
||||||
|
|
||||||
|
|
||||||
|
def extract_request_target(data: bytes) -> Optional[str]:
|
||||||
|
parsed = parse_http_request(data)
|
||||||
|
if not parsed:
|
||||||
|
return None
|
||||||
|
return parsed["target"]
|
||||||
|
|
||||||
|
|
||||||
def remember_gateway_route(client_ip: str, user_id: str) -> None:
|
def remember_gateway_route(client_ip: str, user_id: str) -> None:
|
||||||
if not client_ip:
|
if not client_ip:
|
||||||
return
|
return
|
||||||
@@ -271,25 +325,12 @@ def get_remembered_gateway_user(client_ip: str) -> Optional[Dict[str, Any]]:
|
|||||||
|
|
||||||
|
|
||||||
def extract_user_from_http_request(data: bytes, known_users: Dict[str, Dict[str, Any]]):
|
def extract_user_from_http_request(data: bytes, known_users: Dict[str, Dict[str, Any]]):
|
||||||
if not data:
|
parsed = parse_http_request(data)
|
||||||
return None, data, None
|
if not parsed:
|
||||||
|
return None, data, None, False
|
||||||
|
|
||||||
header_end = data.find(b"\r\n\r\n")
|
target = parsed["target"]
|
||||||
if header_end == -1:
|
headers = parsed["headers"]
|
||||||
return None, data, None
|
|
||||||
|
|
||||||
header_block = data[:header_end].decode("iso-8859-1", errors="replace")
|
|
||||||
lines = header_block.split("\r\n")
|
|
||||||
if not lines or len(lines[0].split(" ")) < 3:
|
|
||||||
return None, data, None
|
|
||||||
|
|
||||||
method, target, version = lines[0].split(" ", 2)
|
|
||||||
headers = {}
|
|
||||||
for line in lines[1:]:
|
|
||||||
if ":" not in line:
|
|
||||||
continue
|
|
||||||
key, value = line.split(":", 1)
|
|
||||||
headers[key.strip().lower()] = value.strip()
|
|
||||||
|
|
||||||
host = headers.get("host", "").split(":", 1)[0].strip().lower()
|
host = headers.get("host", "").split(":", 1)[0].strip().lower()
|
||||||
if host:
|
if host:
|
||||||
@@ -297,7 +338,7 @@ def extract_user_from_http_request(data: bytes, known_users: Dict[str, Dict[str,
|
|||||||
if labels:
|
if labels:
|
||||||
candidate = labels[0]
|
candidate = labels[0]
|
||||||
if candidate in known_users and candidate not in {"localhost", "127", "0"}:
|
if candidate in known_users and candidate not in {"localhost", "127", "0"}:
|
||||||
return known_users[candidate], data, None
|
return known_users[candidate], data, None, False
|
||||||
|
|
||||||
parsed_target = urlsplit(target)
|
parsed_target = urlsplit(target)
|
||||||
if parsed_target.scheme and parsed_target.netloc:
|
if parsed_target.scheme and parsed_target.netloc:
|
||||||
@@ -314,9 +355,13 @@ def extract_user_from_http_request(data: bytes, known_users: Dict[str, Dict[str,
|
|||||||
rewritten_path = "/" + "/".join(segments[1:]) if len(segments) > 1 else "/"
|
rewritten_path = "/" + "/".join(segments[1:]) if len(segments) > 1 else "/"
|
||||||
rewritten_target = f"{rewritten_path}?{query}" if separator else rewritten_path
|
rewritten_target = f"{rewritten_path}?{query}" if separator else rewritten_path
|
||||||
rewritten_data = rewrite_http_request_target(data, rewritten_target)
|
rewritten_data = rewrite_http_request_target(data, rewritten_target)
|
||||||
return known_users[candidate], rewritten_data, None
|
return known_users[candidate], rewritten_data, None, True
|
||||||
|
|
||||||
return None, data, None
|
cookie_user = extract_cookie_user(headers, known_users)
|
||||||
|
if cookie_user:
|
||||||
|
return cookie_user, data, None, False
|
||||||
|
|
||||||
|
return None, data, None, False
|
||||||
|
|
||||||
|
|
||||||
def wait_until_ready(container_name: str) -> bool:
|
def wait_until_ready(container_name: str) -> bool:
|
||||||
@@ -500,7 +545,8 @@ async def handle_gateway_connection(reader, writer):
|
|||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
return
|
return
|
||||||
|
|
||||||
user, rewritten_data, redirect_target = extract_user_from_http_request(initial_data, users_by_id)
|
parsed_request = parse_http_request(initial_data)
|
||||||
|
user, rewritten_data, redirect_target, selected_via_path = extract_user_from_http_request(initial_data, users_by_id)
|
||||||
if user:
|
if user:
|
||||||
remember_gateway_route(client_ip, user["id"])
|
remember_gateway_route(client_ip, user["id"])
|
||||||
|
|
||||||
@@ -511,6 +557,22 @@ async def handle_gateway_connection(reader, writer):
|
|||||||
await writer.wait_closed()
|
await writer.wait_closed()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if user and selected_via_path and parsed_request and not is_websocket_request(parsed_request["headers"]):
|
||||||
|
normalized_target = extract_request_target(rewritten_data) or "/"
|
||||||
|
writer.write(
|
||||||
|
build_http_redirect(
|
||||||
|
normalized_target,
|
||||||
|
extra_headers=[
|
||||||
|
f"Set-Cookie: {GATEWAY_ROUTE_COOKIE_NAME}={user['id']}; Path=/; HttpOnly; SameSite=Lax"
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await writer.drain()
|
||||||
|
writer.close()
|
||||||
|
await writer.wait_closed()
|
||||||
|
log(f"[gateway] Switched {client_ip} to user {user['id']} via path route")
|
||||||
|
return
|
||||||
|
|
||||||
if not user:
|
if not user:
|
||||||
remembered_user = get_remembered_gateway_user(client_ip)
|
remembered_user = get_remembered_gateway_user(client_ip)
|
||||||
if remembered_user:
|
if remembered_user:
|
||||||
|
|||||||
@@ -192,11 +192,12 @@ class ManagerConfigTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
users = {"user2": {"id": "user2"}}
|
users = {"user2": {"id": "user2"}}
|
||||||
|
|
||||||
user, rewritten, redirect_target = self.manager.extract_user_from_http_request(request, users)
|
user, rewritten, redirect_target, selected_via_path = self.manager.extract_user_from_http_request(request, users)
|
||||||
|
|
||||||
self.assertEqual(user["id"], "user2")
|
self.assertEqual(user["id"], "user2")
|
||||||
self.assertEqual(rewritten, request)
|
self.assertEqual(rewritten, request)
|
||||||
self.assertIsNone(redirect_target)
|
self.assertIsNone(redirect_target)
|
||||||
|
self.assertFalse(selected_via_path)
|
||||||
|
|
||||||
def test_extract_user_from_path_rewrites_request_for_direct_proxying(self):
|
def test_extract_user_from_path_rewrites_request_for_direct_proxying(self):
|
||||||
request = (
|
request = (
|
||||||
@@ -207,7 +208,7 @@ class ManagerConfigTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
users = {"user1": {"id": "user1"}}
|
users = {"user1": {"id": "user1"}}
|
||||||
|
|
||||||
user, rewritten, redirect_target = self.manager.extract_user_from_http_request(request, users)
|
user, rewritten, redirect_target, selected_via_path = self.manager.extract_user_from_http_request(request, users)
|
||||||
|
|
||||||
self.assertEqual(user["id"], "user1")
|
self.assertEqual(user["id"], "user1")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -220,6 +221,7 @@ class ManagerConfigTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertIsNone(redirect_target)
|
self.assertIsNone(redirect_target)
|
||||||
|
self.assertTrue(selected_via_path)
|
||||||
|
|
||||||
def test_extract_user_from_bare_user_path_rewrites_to_root(self):
|
def test_extract_user_from_bare_user_path_rewrites_to_root(self):
|
||||||
request = (
|
request = (
|
||||||
@@ -230,7 +232,7 @@ class ManagerConfigTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
users = {"user1": {"id": "user1"}}
|
users = {"user1": {"id": "user1"}}
|
||||||
|
|
||||||
user, rewritten, redirect_target = self.manager.extract_user_from_http_request(request, users)
|
user, rewritten, redirect_target, selected_via_path = self.manager.extract_user_from_http_request(request, users)
|
||||||
|
|
||||||
self.assertEqual(user["id"], "user1")
|
self.assertEqual(user["id"], "user1")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -243,6 +245,7 @@ class ManagerConfigTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertIsNone(redirect_target)
|
self.assertIsNone(redirect_target)
|
||||||
|
self.assertTrue(selected_via_path)
|
||||||
|
|
||||||
def test_extract_user_from_absolute_form_request_rewrites_to_root(self):
|
def test_extract_user_from_absolute_form_request_rewrites_to_root(self):
|
||||||
request = (
|
request = (
|
||||||
@@ -253,7 +256,7 @@ class ManagerConfigTests(unittest.TestCase):
|
|||||||
)
|
)
|
||||||
users = {"user1": {"id": "user1"}}
|
users = {"user1": {"id": "user1"}}
|
||||||
|
|
||||||
user, rewritten, redirect_target = self.manager.extract_user_from_http_request(request, users)
|
user, rewritten, redirect_target, selected_via_path = self.manager.extract_user_from_http_request(request, users)
|
||||||
|
|
||||||
self.assertEqual(user["id"], "user1")
|
self.assertEqual(user["id"], "user1")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
@@ -266,6 +269,24 @@ class ManagerConfigTests(unittest.TestCase):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
self.assertIsNone(redirect_target)
|
self.assertIsNone(redirect_target)
|
||||||
|
self.assertTrue(selected_via_path)
|
||||||
|
|
||||||
|
def test_extract_user_from_cookie_route(self):
|
||||||
|
request = (
|
||||||
|
b"GET / HTTP/1.1\r\n"
|
||||||
|
b"Host: 10.200.200.113:8080\r\n"
|
||||||
|
b"Cookie: fis_user_route=user2\r\n"
|
||||||
|
b"Connection: keep-alive\r\n"
|
||||||
|
b"\r\n"
|
||||||
|
)
|
||||||
|
users = {"user2": {"id": "user2"}}
|
||||||
|
|
||||||
|
user, rewritten, redirect_target, selected_via_path = self.manager.extract_user_from_http_request(request, users)
|
||||||
|
|
||||||
|
self.assertEqual(user["id"], "user2")
|
||||||
|
self.assertEqual(rewritten, request)
|
||||||
|
self.assertIsNone(redirect_target)
|
||||||
|
self.assertFalse(selected_via_path)
|
||||||
|
|
||||||
def test_remembered_gateway_route_returns_user_within_ttl(self):
|
def test_remembered_gateway_route_returns_user_within_ttl(self):
|
||||||
self.manager.users_by_id = {"user1": {"id": "user1"}}
|
self.manager.users_by_id = {"user1": {"id": "user1"}}
|
||||||
|
|||||||
Reference in New Issue
Block a user