From 8b036e94ba0236ea306407f8e95b1536fc0b9781 Mon Sep 17 00:00:00 2001 From: Shekhar Vaidya Date: Mon, 18 May 2026 11:39:47 +0530 Subject: [PATCH] feat: route Microsoft API calls through Cloudflare Worker to avoid IP blocks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add cloudflare-worker/worker.js: edge proxy forwarding requests to allowed Microsoft hosts from Cloudflare distributed IPs. Set CF_WORKER_URL env var to activate; omit to keep direct-to-Microsoft behaviour (self-hosters unaffected). - Wrap all outbound Microsoft URLs in proxyURL() in main.go so the Worker sits transparently in front of every session and download-link request. - Fix Back button navigating to external referrer when user lands directly on a product URL (#5) — use window.history.state?.idx to detect whether there is prior in-app history before calling navigate(-1). Closes #5, Closes #6 Co-Authored-By: Claude Sonnet 4.6 --- backend/main.go | 48 +++++++++++++++++--- cloudflare-worker/worker.js | 54 +++++++++++++++++++++++ frontend/public/favicon.ico | Bin 0 -> 5430 bytes frontend/src/pages/ProductDetailPage.tsx | 2 +- 4 files changed, 97 insertions(+), 7 deletions(-) create mode 100644 cloudflare-worker/worker.js create mode 100644 frontend/public/favicon.ico 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 0000000000000000000000000000000000000000..e478721449be128638018282a6d120cc40e2eede GIT binary patch literal 5430 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 E1N7@g!vFvP literal 0 HcmV?d00001 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() {
{/* Back */}