Backend for minirt implemented

This commit is contained in:
Victor Vobis 2026-02-11 12:58:25 +01:00
parent 29d0c42adf
commit 968eecc6cd
10 changed files with 11229374 additions and 172 deletions

Binary file not shown.

View File

@ -1,33 +1,92 @@
import asyncio
import websockets
import aiohttp
import sys
import os
from aiohttp import web
async def proxy_handler(websocket, path):
target_url = await websocket.recv()
from proxy import proxy_ws, proxy_http
async with aiohttp.ClientSession() as session:
async with session.ws_connect(target_url) as target_ws:
async def forward_to_target():
async for msg in websocket:
if isinstance(msg, str):
await target_ws.send_str(msg)
else:
await target_ws.send_bytes(msg)
[_, host, port, file_root, ws_proxy_url] = sys.argv
async def forward_to_client():
async for msg in target_ws:
if msg.type == aiohttp.WSMsgType.TEXT:
await websocket.send(msg.data)
elif msg.type == aiohttp.WSMsgType.BINARY:
await websocket.send(msg.data)
def print_usage():
print("""Usage: python3 ./backend.py <host> <port> <file_root> <ws_proxy_url>""")
await asyncio.gather(
forward_to_target(),
forward_to_client()
)
async def main():
async with websockets.serve(proxy_handler, "localhost", 8765):
await asyncio.Future()
async def handle_service(request: web.Request):
[service, medium, target] = [
request.match_info.get('service'),
request.match_info.get('medium'),
request.match_info.get('target')
]
asyncio.run(main())
if 'vnc' in request.url.path:
print("caught vnc request")
return await proxy_http(request)
if not service or not medium:
return web.json_response({"success": "false", "message": "Missing Serice or Medium"})
if not target:
target = "index.html"
serve_path = f"{file_root}/service/{service}/{medium}/{target}"
print(f"looking for path {serve_path}")
if not os.path.exists(serve_path):
data = { "success": "false", "message": "Not Found" }
return web.json_response(
status=404,
data=data,
)
return web.FileResponse(serve_path)
async def serve_http(request: web.Request):
path_info = request.match_info.get('path_info', '')
print(f"Req: {request} with path: {path_info}")
if '..' in path_info:
data = {"success": "false", "message": "Forbidden"}
return web.json_response(
status=403,
data=data,
content_type='application/json'
)
elif path_info == '':
path_info = "index.html"
serve_path = f"{file_root}/{path_info}"
print(f"looking for path {serve_path}")
if not os.path.exists(serve_path):
data = { "success": "false", "message": "Not Found" }
return web.json_response(
status=404,
data=data,
)
return web.FileResponse(serve_path)
def build_http_server():
app = web.Application()
app.add_routes([
web.get('/{service}/{medium}/{target:.*}', handle_service),
web.get("/{service}/websockify", proxy_ws),
web.get("/{path_info:.*}", serve_http),
])
return app
def main(host, port):
app = build_http_server()
web.run_app(app, host=host, port=int(port))
try:
main(host, port)
except ValueError as e:
print(f"Argv Error: {e}")
print_usage()
except Exception as e:
print("Caught exception: {e}")
print_usage()

80
proxy.py Normal file
View File

@ -0,0 +1,80 @@
import aiohttp
from aiohttp import web
import asyncio
from datetime import datetime
minirt_vnc_host = "localhost"
minirt_vnc_port = 7080
connections = {}
max_duration = 60
async def proxy_http(request):
service_name = request.match_info.get('service')
target = request.match_info.get('target')
if service_name == 'minirt':
target = f"{minirt_vnc_host}:{minirt_vnc_port}/{target}"
elif service_name == 'minishell':
target = ''
print(f"proxying to host {target}")
async with aiohttp.ClientSession() as session:
async with session.request(
request.method,
f"http://{target}",
headers=request.headers,
data=await request.read()
) as resp:
return web.Response(
body=await resp.read(),
status=resp.status,
headers=resp.headers
)
async def proxy_ws(request: web.Request):
service_name = request.match_info.get('service')
if service_name in connections:
return web.Response(text='Service already in use', status=409)
ws = web.WebSocketResponse()
await ws.prepare(request)
connections[service_name] = (ws, datetime.now())
target = ''
if service_name == 'minirt':
target = f"{minirt_vnc_host}:{minirt_vnc_port}"
elif service_name == 'minishell':
target = ''
try:
async with aiohttp.ClientSession() as session:
async with session.ws_connect(f"ws://{target}/websockify") as remote_ws:
async def forward_to_remote():
async for msg in ws:
if msg.type == aiohttp.WSMsgType.BINARY:
await remote_ws.send_bytes(msg.data)
async def forward_to_client():
async for msg in remote_ws:
if msg.type == aiohttp.WSMsgType.BINARY:
await ws.send_bytes(msg.data)
async def check_timeout():
while True:
await asyncio.sleep(10)
elapsed = (datetime.now() - connections[service_name][1]).total_seconds()
if elapsed > max_duration:
await ws.close()
break
await asyncio.gather(forward_to_remote(), forward_to_client(), check_timeout())
finally:
if service_name in connections:
del connections[service_name]
return ws

View File

@ -85,24 +85,17 @@
for providing the web integration, their stuff is really great!
</p>
<p class="leading-relaxed text-gray-700 max-w-prose">
Before you start, you will need to request the password by clicking the button below.
There two ways to run the app: either directly in your browser with the to WASM compiled program, or over a live noVNC connection to a running instance on the server. The raytracer itself is multithreaded, but since JS/WASM runs on a single thread the wasm version is very slow, and so i scaled down the resolution for that version in order to have it not run at -5 FPS.
</p>
<div class="flex gap-4">
<button id="minirt-password-btn" class="flex p-2 w-full bg-neutral-100/50 place-items-center justify-center rounded-md">
Get Password
</button>
<div id="minirt-password-display" class="flex w-full h-[5dvh] place-items-center justify-center"></div>
</div>
<div class="flex max-w-prose h-[20dvh] justify-center place-items-center">
<button id="minirt-start-button" class="flex p-4 bg-neutral-100/50 place-items-center rounded-md">
Start Raytracer
</button>
<a href='/minirt/wasm/miniRT.html' class="flex p-4 bg-neutral-100/50 place-items-center rounded-md">
Wasm Version (Raylib)
</a>
<a href='/minirt/vnc/vnc_lite.html?path=minirt/websockify&scale=resize' class="flex p-4 bg-neutral-100/50 place-items-center rounded-md">
VNC Version (noVNC)
</a>
<div>
<div id="minirt-canvas-background" style="display: none" class="minirt-canvas-background">
<button id="canvas-close-btn" class="absolute top-[16px] right-[16px] text-white">Close</button>
</div>
</div>
</div>
<script type="module" src="/js/minirt.js"></script>
</body>
</html>

View File

@ -1,130 +0,0 @@
//
// !!! DISCLAIMER !!!
// This is the default template that comes with noVNC, all credit goes to them
//
import { config } from '/config.js';
import RFB from '/vnc/core/rfb.js';
// RFB holds the API to connect and communicate with a VNC server
function closeMinirtFrame() {
const screen = document.getElementById('screen');
if (screen)
screen.remove();
const canvas_background = document.getElementById('minirt-canvas-background');
canvas_background.style = 'display: none';
}
document.addEventListener('keydown', function(event) {
if (event.key === 'Escape') {
closeMinirtFrame();
}
});
const get_passwd = document.getElementById('minirt-password-btn');
get_passwd.addEventListener('click', function() {
try {
fetch(config.BASE_URL + 'minirt/password')
.then(resp => {
if (!resp.ok) {
throw new Error("Failed to fetch password");
}
return resp.json();
})
.then(data => {
const passwd_display = document.getElementById('minirt-password-display');
passwd_display.textContent = `Password: ${data.password}`;
});
} catch (e) {
console.error(e);
}
})
const close_button = document.getElementById('canvas-close-btn');
close_button.addEventListener('click', closeMinirtFrame);
const minirt_button = document.getElementById('minirt-start-button');
minirt_button.addEventListener('click', function() {
// const url = config.BASE_URL + 'minirt/vnc';
const canvas_background = document.getElementById('minirt-canvas-background');
canvas_background.style = '';
const screen = document.createElement('div');
screen.id = 'screen';
canvas_background.appendChild(screen);
let rfb;
let desktopName;
// When this function is called, the server requires
// credentials to authenticate
function credentialsAreRequired(e) {
const password = prompt("Password required:");
if (password)
rfb.sendCredentials({ password: password });
}
// When this function is called we have received
// a desktop name from the server
function updateDesktopName(e) {
desktopName = e.detail.name;
}
// This function extracts the value of one variable from the
// query string. If the variable isn't defined in the URL
// it returns the default value instead.
function readQueryVariable(name, defaultValue) {
// A URL with a query parameter can look like this:
// https://www.example.com?myqueryparam=myvalue
//
// Note that we use location.href instead of location.search
// because Firefox < 53 has a bug w.r.t location.search
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = document.location.href.match(re);
if (match) {
// We have to decode the URL since want the cleartext value
return decodeURIComponent(match[1]);
}
return defaultValue;
}
// Read parameters specified in the URL query string
// By default, use the host and port of server that served this file
const host = readQueryVariable('host', window.location.hostname);
let port = readQueryVariable('port', window.location.port);
const password = readQueryVariable('password');
const path = readQueryVariable('path', 'websockify');
// | | | | | |
// | | | Connect | | |
// v v v v v v
// Build the websocket URL used to connect
let url;
if (window.location.protocol === "https:") {
url = 'wss';
} else {
url = 'ws';
}
url += '://' + host;
if(port) {
url += ':' + port;
}
url += '/' + path;
// Creating a new RFB object will start a new connection
rfb = new RFB(screen, url,
{ credentials: { password: password } });
// Add listeners to important events from the RFB module
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
rfb.addEventListener("desktopname", updateDesktopName);
// Set parameters that can be changed on an active connection
rfb.viewOnly = readQueryVariable('view_only', false);
rfb.scaleViewport = readQueryVariable('scale', false);
});

View File

@ -1 +0,0 @@
{ "password": "1312" }

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.