import os import httpx # Library HTTP client modern, async-friendly import asyncio from fastapi import FastAPI, Request, HTTPException from fastapi.responses import StreamingResponse, RedirectResponse, PlainTextResponse from fastapi.middleware.cors import CORSMiddleware from dotenv import load_dotenv # Untuk membaca .env jika perlu (opsional) import logging import time # --- Konfigurasi Logging --- logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # --- Konfigurasi Aplikasi --- load_dotenv() # Muat variabel dari .env jika ada (untuk pengembangan lokal) # URL ke file backend_url.txt RAW di GitHub # Ganti YOUR_GITHUB_USERNAME dan YOUR_REPO_NAME GITHUB_RAW_URL_FILE = os.getenv( "GITHUB_RAW_URL_FILE", "https://raw.githubusercontent.com/BagaSept26/education-lab/refs/heads/main/backend_url.txt" ) # Token GitHub PAT jika repo Anda private (tidak direkomendasikan untuk file URL ini) # Jika repo publik, token tidak diperlukan untuk membaca file raw. # GITHUB_TOKEN = os.getenv("GITHUB_PAT_FOR_REDIRECTOR") # Simpan di Secrets HF Space # Interval cache untuk URL backend (dalam detik) BACKEND_URL_CACHE_TTL = int(os.getenv("BACKEND_URL_CACHE_TTL", "60")) # Cache 60 detik # --- State Aplikasi --- app = FastAPI(title="Edication Lab Redirector", version="0.1.0") current_backend_url = None last_url_fetch_time = 0 url_fetch_lock = asyncio.Lock() # --- Konfigurasi CORS untuk HF Space --- # Izinkan request dari Vercel frontend Anda # Ganti dengan URL Vercel Anda yang sebenarnya atau gunakan wildcard yang lebih spesifik origins = [ "http://localhost:5173", # Untuk dev lokal frontend "https://education-lab-bagaseptian.vercel.app", # GANTI INI "https://*.vercel.app", # Lebih umum "https://bagaseptian-edu_lab.hf.space" # URL space ini sendiri jika perlu ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) async def get_latest_backend_url_from_github(): """Mengambil URL backend terbaru dari file di GitHub.""" global current_backend_url, last_url_fetch_time async with url_fetch_lock: # Pastikan hanya satu coroutine yang fetch pada satu waktu # Cek apakah cache masih valid if current_backend_url and (time.time() - last_url_fetch_time) < BACKEND_URL_CACHE_TTL: logger.info(f"Menggunakan URL backend dari cache: {current_backend_url}") return current_backend_url logger.info(f"Mengambil URL backend terbaru dari GitHub: {GITHUB_RAW_URL_FILE}") headers = {} # if GITHUB_TOKEN: # Jika file di repo private # headers["Authorization"] = f"token {GITHUB_TOKEN}" try: async with httpx.AsyncClient(timeout=10.0) as client: response = await client.get(GITHUB_RAW_URL_FILE, headers=headers) response.raise_for_status() # Akan raise error untuk status 4xx/5xx new_url = response.text.strip() if new_url and (new_url.startswith("http://") or new_url.startswith("https://")): if new_url != current_backend_url: logger.info(f"URL backend baru ditemukan: {new_url}") current_backend_url = new_url else: logger.info(f"URL backend tidak berubah: {new_url}") last_url_fetch_time = time.time() return current_backend_url else: logger.error(f"Format URL tidak valid dari GitHub: '{new_url}'") # Jangan update cache jika URL tidak valid, tetap gunakan yang lama (jika ada) return current_backend_url # Atau None jika belum pernah ada yg valid except httpx.HTTPStatusError as e: logger.error(f"HTTP error saat mengambil URL dari GitHub: {e.response.status_code} - {e.response.text}") except httpx.RequestError as e: logger.error(f"Request error saat mengambil URL dari GitHub: {e}") except Exception as e: logger.error(f"Error tidak terduga saat mengambil URL: {e}") # Jika fetch gagal, kembalikan URL lama yang valid (jika ada) untuk sementara waktu # atau jika URL lama sudah terlalu lama, mungkin lebih baik return None if current_backend_url and (time.time() - last_url_fetch_time) < (BACKEND_URL_CACHE_TTL * 5): # Toleransi 5x TTL logger.warning("Gagal mengambil URL baru, menggunakan URL lama dari cache.") return current_backend_url else: # Jika URL lama juga sudah terlalu basi atau tidak ada current_backend_url = None # Reset jika gagal fetch terlalu lama last_url_fetch_time = 0 return None @app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"]) async def proxy_to_backend(request: Request, path: str): """ Menerima semua request dan mem-proxy-nya ke backend Colab. """ backend_url = await get_latest_backend_url_from_github() if not backend_url: logger.error("Tidak ada URL backend yang valid tersedia.") raise HTTPException(status_code=503, detail="Layanan backend sementara tidak tersedia. URL tidak ditemukan.") target_url_path = f"{backend_url}/{path}" if request.url.query: target_url_path += f"?{request.url.query}" logger.info(f"Meneruskan request {request.method} ke: {target_url_path}") # Baca body request jika ada body_bytes = await request.body() headers = dict(request.headers) # Hapus atau modifikasi header tertentu jika perlu (misal, 'host') headers.pop("host", None) headers.pop("Host", None) # Tambahkan header yang mungkin dibutuhkan oleh backend Anda # headers["X-Forwarded-For"] = request.client.host async with httpx.AsyncClient(timeout=300.0) as client: # Timeout panjang untuk model AI try: # Kirim request ke backend rp = await client.request( method=request.method, url=target_url_path, headers=headers, content=body_bytes, # Untuk mengizinkan redirect jika backend mengembalikan 3xx # follow_redirects=False # Kita akan handle redirect secara manual jika perlu ) # Buat respons streaming agar efisien # Beberapa header tidak boleh di-proxy langsung excluded_headers = [ "content-encoding", "Content-Encoding", # Biarkan httpx/FastAPI handle de/compression "transfer-encoding", "Transfer-Encoding", "connection", "Connection" ] response_headers = { k: v for k, v in rp.headers.items() if k.lower() not in excluded_headers } # Pastikan CORS header dari proxy juga benar response_headers["Access-Control-Allow-Origin"] = request.headers.get("origin") or "*" # Atau lebih spesifik response_headers["Access-Control-Allow-Credentials"] = "true" # Jika backend mengembalikan redirect, kita bisa meneruskannya if 300 <= rp.status_code < 400 and "location" in rp.headers: return RedirectResponse(url=rp.headers["location"], status_code=rp.status_code, headers=response_headers) return StreamingResponse( rp.aiter_bytes(), # Stream content dari backend status_code=rp.status_code, media_type=rp.headers.get("content-type"), headers=response_headers ) except httpx.HTTPStatusError as e: logger.error(f"HTTP error dari backend: {e.response.status_code} - {e.response.text}") # Meneruskan status error dari backend return PlainTextResponse(content=e.response.text, status_code=e.response.status_code) except httpx.ConnectError as e: logger.error(f"Tidak dapat terhubung ke backend: {backend_url} - {e}") raise HTTPException(status_code=504, detail=f"Gateway Timeout: Tidak dapat terhubung ke layanan backend Colab di {backend_url}.") except httpx.TimeoutException as e: logger.error(f"Timeout saat menghubungi backend: {backend_url} - {e}") raise HTTPException(status_code=504, detail=f"Gateway Timeout: Waktu habis saat menghubungi layanan backend Colab.") except Exception as e: logger.error(f"Error tidak terduga saat proxying: {e}") raise HTTPException(status_code=500, detail="Internal server error pada proxy.") @app.get("/redirector-status", include_in_schema=False) # Endpoint untuk cek status redirector async def status(): latest_url = await get_latest_backend_url_from_github() # Ambil URL terbaru return { "status": "Redirector Aktif", "pesan": "Proxy bot untuk Edication Lab Backend.", "target_github_url_file": GITHUB_RAW_URL_FILE, "current_resolved_backend_url": latest_url if latest_url else "Belum ada URL backend yang valid ditemukan.", "cache_ttl_seconds": BACKEND_URL_CACHE_TTL, "last_url_fetch_attempt_timestamp": last_url_fetch_time if last_url_fetch_time > 0 else "Belum pernah fetch" } # Jika dijalankan secara lokal (python app.py) # if __name__ == "__main__": # import uvicorn # logger.info("Menjalankan redirector secara lokal di port 8001...") # # Ganti nilai di GITHUB_RAW_URL_FILE dengan URL raw GitHub Anda yang benar sebelum menjalankan lokal # # Contoh: GITHUB_RAW_URL_FILE = "https://raw.githubusercontent.com/NAMA_USER_ANDA/edication-lab/main/backend_url.txt" # uvicorn.run(app, host="0.0.0.0", port=8001)