- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
85 lines
3.7 KiB
Python
85 lines
3.7 KiB
Python
"""Deployment-only HTTP prefix adapter; modeling code remains unchanged."""
|
|
import http.client
|
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from urllib.parse import urlsplit
|
|
|
|
PREFIX = '/box-sample'
|
|
UPSTREAM_PORT = 8097
|
|
HOP = {'connection', 'transfer-encoding', 'keep-alive', 'upgrade',
|
|
'proxy-authenticate', 'proxy-authorization', 'te', 'trailer'}
|
|
ROOT_URL = re.compile(rb'''(["'`])/(api/|src/|dxf_rasterizer\.html|model-report\.html)''')
|
|
|
|
|
|
def rewrite(payload):
|
|
return ROOT_URL.sub(lambda m: m[1] + PREFIX.encode() + b'/' + m[2], payload)
|
|
|
|
|
|
class Gateway(BaseHTTPRequestHandler):
|
|
def forward(self):
|
|
parsed = urlsplit(self.path)
|
|
if parsed.path == PREFIX:
|
|
self.send_response(308)
|
|
self.send_header('Location', PREFIX + '/' + ('?' + parsed.query if parsed.query else ''))
|
|
self.send_header('Content-Length', '0')
|
|
self.end_headers()
|
|
return
|
|
if not parsed.path.startswith(PREFIX + '/'):
|
|
self.send_error(404)
|
|
return
|
|
if self.headers.get('Transfer-Encoding'):
|
|
self.send_error(411, 'Content-Length required')
|
|
return
|
|
target = self.path[len(PREFIX):]
|
|
connection = http.client.HTTPConnection('127.0.0.1', UPSTREAM_PORT, timeout=600)
|
|
try:
|
|
size = int(self.headers.get('Content-Length', '0'))
|
|
if size < 0 or size > 512 * 1024 * 1024:
|
|
self.send_error(413)
|
|
return
|
|
headers = {k: v for k, v in self.headers.items()
|
|
if k.lower() not in HOP | {'accept-encoding', 'host'}}
|
|
headers['Host'] = '127.0.0.1:' + str(UPSTREAM_PORT)
|
|
connection.request(self.command, target, self.rfile.read(size) if size else None, headers)
|
|
response = connection.getresponse()
|
|
content_type = response.getheader('Content-Type', '').lower()
|
|
text = any(t in content_type for t in ('json', 'javascript', 'text/html', 'text/css'))
|
|
body = rewrite(response.read()) if text and self.command != 'HEAD' else None
|
|
self.send_response(response.status)
|
|
for key, value in response.getheaders():
|
|
if key.lower() in HOP | {'content-length', 'etag', 'content-md5'}:
|
|
continue
|
|
if key.lower() == 'location' and value.startswith('/') and not value.startswith('//'):
|
|
value = PREFIX + value
|
|
self.send_header(key, value)
|
|
self.send_header('X-DMS-Deployment', 'box-sample')
|
|
self.send_header('Cache-Control', 'no-store')
|
|
if body is not None:
|
|
self.send_header('Content-Length', str(len(body)))
|
|
elif response.getheader('Content-Length') is not None:
|
|
self.send_header('Content-Length', response.getheader('Content-Length'))
|
|
self.end_headers()
|
|
if self.command != 'HEAD':
|
|
if body is not None:
|
|
self.wfile.write(body)
|
|
else:
|
|
while chunk := response.read(65536):
|
|
self.wfile.write(chunk)
|
|
except (ValueError, OSError, http.client.HTTPException):
|
|
self.send_error(502, 'Application not ready')
|
|
finally:
|
|
connection.close()
|
|
|
|
do_GET = do_POST = do_HEAD = do_PUT = do_DELETE = forward
|
|
|
|
|
|
if __name__ == '__main__':
|
|
child = subprocess.Popen([sys.executable, 'src/backend/serve.py', '--host', '127.0.0.1', '--port', str(UPSTREAM_PORT)])
|
|
try:
|
|
ThreadingHTTPServer(('0.0.0.0', 8080), Gateway).serve_forever()
|
|
finally:
|
|
child.terminate()
|
|
child.wait(timeout=15)
|