diff --git a/backend/main.go b/backend/main.go index d38cfa7..90aed7a 100644 --- a/backend/main.go +++ b/backend/main.go @@ -8,6 +8,7 @@ import ( "log" "net/http" "net/url" + "os" "regexp" "strconv" "sync" @@ -34,8 +35,37 @@ var ( sessionCache = make(map[string]SessionEntry) cacheMutex sync.RWMutex SESSION_TTL = 15 * time.Minute + workerSecret = os.Getenv("CF_WORKER_SECRET") ) +func setWorkerSecret(req *http.Request) { + if workerSecret != "" { + req.Header.Set("X-Worker-Secret", workerSecret) + } +} + +// proxyURL rewrites a Microsoft URL to go through the CF Worker when CF_WORKER_URL is set. +// The Worker receives the original host via ?host= and forwards the request from Cloudflare's edge. +func proxyURL(msURL string) string { + workerBase := os.Getenv("CF_WORKER_URL") + if workerBase == "" { + return msURL + } + parsed, err := url.Parse(msURL) + if err != nil { + return msURL + } + workerParsed, err := url.Parse(workerBase) + if err != nil { + return msURL + } + q := parsed.Query() + q.Set("host", parsed.Hostname()) + workerParsed.Path = parsed.Path + workerParsed.RawQuery = q.Encode() + return workerParsed.String() +} + // Map product ID to referer URL func getReferer(productId string) string { id, err := strconv.Atoi(productId) @@ -60,8 +90,9 @@ func setupSession() (string, error) { q1 := url.Values{} q1.Set("org_id", ORG_ID) q1.Set("session_id", sessionID) - req1, _ := http.NewRequest("GET", "https://vlscppe.microsoft.com/tags?"+q1.Encode(), nil) + req1, _ := http.NewRequest("GET", proxyURL("https://vlscppe.microsoft.com/tags?"+q1.Encode()), nil) req1.Header.Set("User-Agent", UA) + setWorkerSecret(req1) client.Do(req1) // Ignore errors intentionally // Step 2: Fetch tracking JS @@ -69,8 +100,9 @@ func setupSession() (string, error) { q2.Set("instanceId", CUSTOMER_ID) q2.Set("PageId", "si") q2.Set("session_id", sessionID) - req2, _ := http.NewRequest("GET", "https://ov-df.microsoft.com/mdt.js?"+q2.Encode(), nil) + req2, _ := http.NewRequest("GET", proxyURL("https://ov-df.microsoft.com/mdt.js?"+q2.Encode()), nil) req2.Header.Set("User-Agent", UA) + setWorkerSecret(req2) resp2, err := client.Do(req2) if err != nil { return sessionID, nil // Proceed anyway @@ -100,10 +132,11 @@ func setupSession() (string, error) { q3.Set("w", wVal) q3.Set("mdt", fmt.Sprintf("%d", mdt)) q3.Set("rticks", rtVal) - fpURL := "https://ov-df.microsoft.com/?" + q3.Encode() + fpURL := proxyURL("https://ov-df.microsoft.com/?" + q3.Encode()) req3, _ := http.NewRequest("GET", fpURL, nil) req3.Header.Set("User-Agent", UA) + setWorkerSecret(req3) client.Do(req3) } @@ -190,7 +223,7 @@ func handleSkuInfo(w http.ResponseWriter, r *http.Request) { skuQ.Set("friendlyFileName", "undefined") skuQ.Set("Locale", LOCALE) skuQ.Set("sessionID", sessionID) - reqURL := "https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?" + skuQ.Encode() + reqURL := proxyURL("https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?" + skuQ.Encode()) client := &http.Client{Timeout: 15 * time.Second} var finalData map[string]interface{} @@ -204,6 +237,7 @@ func handleSkuInfo(w http.ResponseWriter, r *http.Request) { req.Header.Set("User-Agent", UA) req.Header.Set("Referer", getReferer(productID)) req.Header.Set("Accept", "application/json") + setWorkerSecret(req) resp, err := client.Do(req) if err != nil { @@ -303,13 +337,14 @@ func handleProxy(w http.ResponseWriter, r *http.Request) { warmupQ.Set("friendlyFileName", "undefined") warmupQ.Set("Locale", LOCALE) warmupQ.Set("sessionID", sessionID) - warmupURL := "https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?" + warmupQ.Encode() + warmupURL := proxyURL("https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?" + warmupQ.Encode()) client := &http.Client{Timeout: 10 * time.Second} req, _ := http.NewRequest("GET", warmupURL, nil) req.Header.Set("User-Agent", UA) req.Header.Set("Referer", getReferer(productID)) req.Header.Set("Accept", "application/json") + setWorkerSecret(req) client.Do(req) cacheMutex.Lock() @@ -324,13 +359,14 @@ func handleProxy(w http.ResponseWriter, r *http.Request) { proxyQ.Set("friendlyFileName", "undefined") proxyQ.Set("Locale", LOCALE) proxyQ.Set("sessionID", sessionID) - reqURL := "https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?" + proxyQ.Encode() + reqURL := proxyURL("https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?" + proxyQ.Encode()) client := &http.Client{Timeout: 15 * time.Second} req, _ := http.NewRequest("GET", reqURL, nil) req.Header.Set("User-Agent", UA) req.Header.Set("Referer", getReferer(productID)) req.Header.Set("Accept", "application/json") + setWorkerSecret(req) resp, err := client.Do(req) if err != nil { diff --git a/cloudflare-worker/worker.js b/cloudflare-worker/worker.js new file mode 100644 index 0000000..9081acf --- /dev/null +++ b/cloudflare-worker/worker.js @@ -0,0 +1,54 @@ +const ALLOWED_HOSTS = [ + "www.microsoft.com", + "vlscppe.microsoft.com", + "ov-df.microsoft.com", +]; + +export default { + async fetch(request, env) { + const url = new URL(request.url); + + if (env.CF_WORKER_SECRET) { + const secret = request.headers.get("X-Worker-Secret"); + if (secret !== env.CF_WORKER_SECRET) { + return new Response(JSON.stringify({ error: "Forbidden" }), { + status: 403, + headers: { "Content-Type": "application/json" }, + }); + } + } + + // Target host comes from the ?host= param; path+search are forwarded as-is + const targetHost = url.searchParams.get("host"); + if (!targetHost || !ALLOWED_HOSTS.includes(targetHost)) { + return new Response(JSON.stringify({ error: "Invalid or missing host parameter" }), { + status: 400, + headers: { "Content-Type": "application/json" }, + }); + } + + url.searchParams.delete("host"); + url.hostname = targetHost; + url.protocol = "https:"; + url.port = ""; + + const upstreamRequest = new Request(url.toString(), { + method: request.method, + headers: { + "User-Agent": request.headers.get("User-Agent") || "Mozilla/5.0", + "Referer": request.headers.get("Referer") || "", + "Accept": request.headers.get("Accept") || "application/json", + }, + }); + + const upstream = await fetch(upstreamRequest); + + return new Response(upstream.body, { + status: upstream.status, + headers: { + "Content-Type": upstream.headers.get("Content-Type") || "application/json", + "Access-Control-Allow-Origin": "*", + }, + }); + }, +}; diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..e478721 Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/src/pages/ProductDetailPage.tsx b/frontend/src/pages/ProductDetailPage.tsx index dc7fe82..28f5b38 100644 --- a/frontend/src/pages/ProductDetailPage.tsx +++ b/frontend/src/pages/ProductDetailPage.tsx @@ -213,7 +213,7 @@ export default function ProductDetailPage() {