feat: route Microsoft API calls through Cloudflare Worker to avoid IP blocks
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
a8f9ba1da1
commit
8b036e94ba
4 changed files with 97 additions and 7 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
54
cloudflare-worker/worker.js
Normal file
54
cloudflare-worker/worker.js
Normal file
|
|
@ -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": "*",
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
BIN
frontend/public/favicon.ico
Normal file
BIN
frontend/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
|
|
@ -213,7 +213,7 @@ export default function ProductDetailPage() {
|
|||
<div className="max-w-xl mx-auto px-5 pt-12 pb-10">
|
||||
{/* Back */}
|
||||
<button
|
||||
onClick={() => navigate(-1)}
|
||||
onClick={() => navigate(window.history.state?.idx ? -1 : '/')}
|
||||
className="flex items-center gap-1.5 text-sm text-zinc-500 hover:text-zinc-300 mb-8 transition-colors"
|
||||
>
|
||||
<ArrowLeft size={15} />
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue