File size: 9,806 Bytes
245db06
a406c08
245db06
 
 
 
a406c08
245db06
 
 
a406c08
c4c36f8
245db06
 
a406c08
 
245db06
a406c08
 
 
245db06
 
 
a406c08
 
 
245db06
a406c08
 
245db06
 
a406c08
245db06
 
 
 
a406c08
 
 
245db06
a406c08
 
 
 
245db06
a406c08
245db06
 
 
 
 
 
 
 
 
a406c08
245db06
 
a406c08
 
245db06
a406c08
245db06
a406c08
 
245db06
a406c08
 
 
245db06
 
 
a406c08
 
245db06
a406c08
245db06
 
 
 
 
 
 
 
a406c08
 
 
245db06
a406c08
245db06
a406c08
245db06
 
a406c08
 
 
 
 
245db06
a406c08
 
245db06
 
 
a406c08
245db06
 
 
787ee13
245db06
 
 
 
 
 
787ee13
245db06
 
787ee13
245db06
 
 
787ee13
245db06
 
787ee13
245db06
 
787ee13
 
245db06
787ee13
245db06
787ee13
245db06
 
 
 
 
787ee13
 
245db06
787ee13
 
 
245db06
787ee13
 
 
245db06
787ee13
245db06
 
787ee13
 
 
 
245db06
787ee13
245db06
787ee13
 
245db06
787ee13
245db06
 
787ee13
245db06
 
 
787ee13
 
 
 
 
245db06
787ee13
245db06
 
787ee13
245db06
787ee13
 
 
245db06
787ee13
 
245db06
787ee13
245db06
 
 
 
 
 
a406c08
787ee13
 
a406c08
 
 
787ee13
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
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)