File size: 4,398 Bytes
9684770
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""
============================================
RUHI-CORE - Built-in Reverse Proxy
Routes traffic to internal service ports
============================================
"""

import httpx
from fastapi import Request, Response
from fastapi.responses import StreamingResponse
from loguru import logger

from core.port_manager import port_manager
from core.process_manager import process_manager


class ReverseProxy:
    """
    Reverse proxy to route traffic from /proxy/{service_name}/
    to the internal port of the running service
    """
    
    def __init__(self):
        self._client = httpx.AsyncClient(
            timeout=30.0,
            follow_redirects=True,
            limits=httpx.Limits(
                max_connections=100,
                max_keepalive_connections=20
            )
        )
        logger.info("🔀 ReverseProxy initialized")
    
    async def proxy_request(self, service_name: str, path: str, request: Request) -> Response:
        """Proxy an incoming request to the target service"""
        
        # Find the service
        service = process_manager.get_service_by_name(service_name)
        if not service:
            return Response(
                content=f'{{"error": "Service \'{service_name}\' not found"}}',
                status_code=404,
                media_type="application/json"
            )
        
        if service.status != "running":
            return Response(
                content=f'{{"error": "Service \'{service_name}\' is not running (status: {service.status})"}}',
                status_code=503,
                media_type="application/json"
            )
        
        if not service.port:
            return Response(
                content=f'{{"error": "Service \'{service_name}\' has no port assigned"}}',
                status_code=503,
                media_type="application/json"
            )
        
        # Build target URL
        target_url = f"http://127.0.0.1:{service.port}/{path}"
        
        # Build headers (forward relevant ones)
        headers = {}
        for key, value in request.headers.items():
            if key.lower() not in ("host", "connection", "content-length", "transfer-encoding"):
                headers[key] = value
        headers["X-Forwarded-For"] = request.client.host
        headers["X-Forwarded-Proto"] = request.url.scheme
        headers["X-Forwarded-Host"] = request.headers.get("host", "")
        headers["X-Real-IP"] = request.client.host
        
        try:
            # Get request body
            body = await request.body()
            
            # Make proxied request
            response = await self._client.request(
                method=request.method,
                url=target_url,
                headers=headers,
                content=body,
                params=dict(request.query_params)
            )
            
            # Build response headers
            resp_headers = dict(response.headers)
            # Remove hop-by-hop headers
            for h in ("connection", "keep-alive", "transfer-encoding", "content-encoding", "content-length"):
                resp_headers.pop(h, None)
            
            return Response(
                content=response.content,
                status_code=response.status_code,
                headers=resp_headers,
                media_type=response.headers.get("content-type")
            )
            
        except httpx.ConnectError:
            return Response(
                content=f'{{"error": "Cannot connect to service \'{service_name}\' on port {service.port}"}}',
                status_code=502,
                media_type="application/json"
            )
        except httpx.TimeoutException:
            return Response(
                content=f'{{"error": "Request to service \'{service_name}\' timed out"}}',
                status_code=504,
                media_type="application/json"
            )
        except Exception as e:
            logger.error(f"Proxy error for {service_name}: {str(e)}")
            return Response(
                content=f'{{"error": "Proxy error: {str(e)}"}}',
                status_code=502,
                media_type="application/json"
            )
    
    async def close(self):
        """Close the HTTP client"""
        await self._client.aclose()


# Global instance
reverse_proxy = ReverseProxy()