import os import time import socket import logging from urllib.parse import urljoin import requests from flask import Flask, request, Response TARGET_HOST = os.getenv("TARGET_HOST", "192.168.1.11") TARGET_PORT = int(os.getenv("TARGET_PORT", "8080")) TARGET_MAC = os.getenv("TARGET_MAC", "") LISTEN_PORT = int(os.getenv("LISTEN_PORT", "3012")) WAKE_TIMEOUT = int(os.getenv("WAKE_TIMEOUT", "60")) CHECK_INTERVAL = int(os.getenv("CHECK_INTERVAL", "5")) BROADCAST_IP = os.getenv("BROADCAST_IP", "192.168.1.255") # Endpoint de sante de la cible : Ollama = /api/version, Jean = /health HEALTH_PATH = os.getenv("HEALTH_PATH", "/health") logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s") log = logging.getLogger(__name__) app = Flask(__name__) def send_wol(mac_address: str): mac = mac_address.replace(":", "").replace("-", "") if len(mac) != 12: raise ValueError("MAC invalide: %s" % mac_address) data = bytes.fromhex("FF" * 6 + mac * 16) with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: s.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) for i in range(3): s.sendto(data, (BROADCAST_IP, 9)) log.info(f"[WoL] Paquet magic #{i+1} envoye -> MAC={mac_address} via {BROADCAST_IP}:9") def target_alive() -> bool: try: url = f"http://{TARGET_HOST}:{TARGET_PORT}{HEALTH_PATH}" r = requests.get(url, timeout=2) return r.ok except Exception: return False def wait_for_target(): if target_alive(): log.info(f"[WoL] Cible deja UP ({TARGET_HOST}:{TARGET_PORT}{HEALTH_PATH})") return if not TARGET_MAC: log.warning("[WoL] Pas de TARGET_MAC, skip WoL, attente directe") else: log.info(f"[WoL] Cible DOWN -> envoi WoL vers {TARGET_MAC}") send_wol(TARGET_MAC) deadline = time.time() + WAKE_TIMEOUT while time.time() < deadline: remaining = int(deadline - time.time()) if target_alive(): log.info(f"[WoL] Cible UP apres reveil (reste {remaining}s)") return log.info(f"[WoL] En attente de la cible... ({remaining}s restantes)") time.sleep(CHECK_INTERVAL) raise RuntimeError(f"Cible ne repond pas apres {WAKE_TIMEOUT}s") def proxy_request(path: str): wait_for_target() base = f"http://{TARGET_HOST}:{TARGET_PORT}" upstream_url = urljoin(base + "/", path) headers = dict(request.headers) headers.pop("Host", None) refused_msg = None try: resp = requests.request( method=request.method, url=upstream_url, headers=headers, data=request.get_data(), cookies=request.cookies, params=request.args, allow_redirects=False, stream=True, timeout=None, ) except Exception as e: refused_msg = str(e) if "Connection refused" in refused_msg and not refused_msg.endswith(" apres WoL"): # Le service n est pas encore pret (ex: Jean qui charge son modele) -> attendre + retry log.warning(f"[Proxy] connexion refusee, attente {WAKE_TIMEOUT}s puis retry") deadline = time.time() + WAKE_TIMEOUT while time.time() < deadline: time.sleep(CHECK_INTERVAL) try: resp = requests.request( method=request.method, url=upstream_url, headers=headers, data=request.get_data(), params=request.args, allow_redirects=False, stream=True, timeout=None, ) break except Exception as e2: log.info(f"[Proxy] retry en cours... ({int(deadline-time.time())}s)") else: return Response(refused_msg, status=502) else: return Response(refused_msg, status=502) excluded = {"content-encoding", "content-length", "transfer-encoding", "connection"} response_headers = [ (name, value) for name, value in resp.headers.items() if name.lower() not in excluded ] return Response( resp.raw, status=resp.status_code, headers=response_headers, direct_passthrough=True, ) @app.route("/", defaults={"path": ""}, methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) @app.route("/", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]) def handle(path): return proxy_request(path) if __name__ == "__main__": app.run(host="0.0.0.0", port=LISTEN_PORT, threaded=True)