refactor: file viewer server so it is accessible via localhost without authentication (#7987)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Xingyao Wang 2025-04-21 18:12:06 -04:00 committed by GitHub
parent 1e509a70d4
commit a04024a239
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 126 additions and 54 deletions

View File

@ -21,7 +21,7 @@ from zipfile import ZipFile
from binaryornot.check import is_binary
from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile
from fastapi.exceptions import RequestValidationError
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
from fastapi.responses import FileResponse, JSONResponse
from fastapi.security import APIKeyHeader
from openhands_aci.editor.editor import OHEditor
from openhands_aci.editor.exceptions import ToolError
@ -57,10 +57,11 @@ from openhands.events.observation import (
from openhands.events.serialization import event_from_dict, event_to_dict
from openhands.runtime.browser import browse
from openhands.runtime.browser.browser_env import BrowserEnv
from openhands.runtime.file_viewer_server import start_file_viewer_server
from openhands.runtime.plugins import ALL_PLUGINS, JupyterPlugin, Plugin, VSCodePlugin
from openhands.runtime.utils import find_available_tcp_port
from openhands.runtime.utils.async_bash import AsyncBashSession
from openhands.runtime.utils.bash import BashSession
from openhands.runtime.utils.file_viewer import generate_file_viewer_html
from openhands.runtime.utils.files import insert_lines, read_lines
from openhands.runtime.utils.memory_monitor import MemoryMonitor
from openhands.runtime.utils.runtime_init import init_user_and_working_directory
@ -532,6 +533,7 @@ class ActionExecutor:
if __name__ == '__main__':
logger.warning('Starting Action Execution Server')
parser = argparse.ArgumentParser()
parser.add_argument('port', type=int, help='Port to listen on')
parser.add_argument('--working-dir', type=str, help='Working directory')
@ -550,10 +552,13 @@ if __name__ == '__main__':
# example: python client.py 8000 --working-dir /workspace --plugins JupyterRequirement
args = parser.parse_args()
port_path = '/tmp/oh-server-url'
os.makedirs(os.path.dirname(port_path), exist_ok=True)
with open(port_path, 'w') as f:
f.write(f'http://127.0.0.1:{args.port}')
# Start the file viewer server in a separate thread
logger.info('Starting file viewer server')
_file_viewer_port = find_available_tcp_port(
min_port=args.port + 1, max_port=args.port + 10000
)
server_url, _ = start_file_viewer_server(port=_file_viewer_port)
logger.info(f'File viewer server started at {server_url}')
plugins_to_load: list[Plugin] = []
if args.plugins:
@ -839,53 +844,5 @@ if __name__ == '__main__':
logger.error(f'Error listing files: {e}')
return []
@app.get('/view')
async def view_file(path: str, request: Request):
"""View a file using an embedded viewer.
Args:
path (str): The absolute path of the file to view.
request (Request): The FastAPI request object.
Returns:
HTMLResponse: An HTML page with an appropriate viewer for the file.
"""
# Security check: Only allow requests from localhost
client_host = request.client.host if request.client else None
if client_host not in ['127.0.0.1', 'localhost', '::1']:
logger.warning(f'Unauthorized file view attempt from {client_host}')
return HTMLResponse(
content='<h1>Access Denied</h1><p>This endpoint is only accessible from localhost</p>',
status_code=403,
)
if not os.path.isabs(path):
return HTMLResponse(
content=f'<h1>Error: Path must be absolute</h1><p>{path}</p>',
status_code=400,
)
if not os.path.exists(path):
return HTMLResponse(
content=f'<h1>Error: File not found</h1><p>{path}</p>', status_code=404
)
if os.path.isdir(path):
return HTMLResponse(
content=f'<h1>Error: Path is a directory</h1><p>{path}</p>',
status_code=400,
)
try:
html_content = generate_file_viewer_html(path)
return HTMLResponse(content=html_content)
except Exception as e:
logger.error(f'Error serving file viewer: {str(e)}')
return HTMLResponse(
content=f'<h1>Error viewing file</h1><p>{path}</p><p>{str(e)}</p>',
status_code=500,
)
logger.debug(f'Starting action execution API on port {args.port}')
run(app, host='0.0.0.0', port=args.port)

View File

@ -0,0 +1,115 @@
"""
A tiny, isolated server that provides only the /view endpoint from the action execution server.
This server has no authentication and only listens to localhost traffic.
"""
import os
import threading
from typing import Tuple
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse
from uvicorn import Config, Server
from openhands.core.logger import openhands_logger as logger
from openhands.runtime.utils.file_viewer import generate_file_viewer_html
def create_app() -> FastAPI:
"""Create the FastAPI application."""
app = FastAPI(
title='File Viewer Server', openapi_url=None, docs_url=None, redoc_url=None
)
@app.get('/')
async def root():
"""Root endpoint to check if the server is running."""
return {'status': 'File viewer server is running'}
@app.get('/view')
async def view_file(path: str, request: Request):
"""View a file using an embedded viewer.
Args:
path (str): The absolute path of the file to view.
request (Request): The FastAPI request object.
Returns:
HTMLResponse: An HTML page with an appropriate viewer for the file.
"""
# Security check: Only allow requests from localhost
client_host = request.client.host if request.client else None
if client_host not in ['127.0.0.1', 'localhost', '::1']:
return HTMLResponse(
content='<h1>Access Denied</h1><p>This endpoint is only accessible from localhost</p>',
status_code=403,
)
if not os.path.isabs(path):
return HTMLResponse(
content=f'<h1>Error: Path must be absolute</h1><p>{path}</p>',
status_code=400,
)
if not os.path.exists(path):
return HTMLResponse(
content=f'<h1>Error: File not found</h1><p>{path}</p>', status_code=404
)
if os.path.isdir(path):
return HTMLResponse(
content=f'<h1>Error: Path is a directory</h1><p>{path}</p>',
status_code=400,
)
try:
html_content = generate_file_viewer_html(path)
return HTMLResponse(content=html_content)
except Exception as e:
return HTMLResponse(
content=f'<h1>Error viewing file</h1><p>{path}</p><p>{str(e)}</p>',
status_code=500,
)
return app
def start_file_viewer_server(port: int) -> Tuple[str, threading.Thread]:
"""Start the file viewer server on the specified port or find an available one.
Args:
port (int, optional): The port to bind to. If None, an available port will be found.
Returns:
Tuple[str, threading.Thread]: The server URL and the thread object.
"""
# Save the server URL to a file
server_url = f'http://localhost:{port}'
port_path = '/tmp/oh-server-url'
os.makedirs(os.path.dirname(port_path), exist_ok=True)
with open(port_path, 'w') as f:
f.write(server_url)
logger.info(f'File viewer server URL saved to /tmp/oh-server-url: {server_url}')
logger.info(f'Starting file viewer server on port {port}')
app = create_app()
config = Config(app=app, host='127.0.0.1', port=port, log_level='error')
server = Server(config=config)
# Run the server in a new thread
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
return server_url, thread
if __name__ == '__main__':
url, thread = start_file_viewer_server(port=8000)
# Keep the main thread running
try:
thread.join()
except KeyboardInterrupt:
logger.info('Server stopped')