fix: forward cookies through CF Worker to satisfy Microsoft Sentinel

Sentinel started rejecting all /proxy requests because cookies set during
the fingerprinting flow (vlscppe/ov-df) were never reaching the
GetProductDownloadLinksBySku call.

Two gaps closed:
- CF Worker now forwards Cookie header to Microsoft and Set-Cookie headers
  back to the backend
- Backend uses a simpleCookieJar (domain-scoping disabled) shared across
  the full session: setup → warmup → download link fetch. Standard
  cookiejar rejects microsoft.com cookies when the request URL is the
  Worker domain.

Tested locally without Worker (direct MS calls): download links returned,
no Sentinel error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Shekhar Vaidya 2026-06-15 21:36:43 +05:30
parent 04e745272b
commit 6f2c02d79b
2 changed files with 78 additions and 23 deletions

View file

@ -38,9 +38,45 @@ const (
// --- Session cache (short-lived, used to chain /skuinfo → /proxy) ---
// simpleCookieJar stores cookies without domain scoping. The standard
// net/http/cookiejar rejects cookies whose Domain (e.g. .microsoft.com) doesn't
// match the request URL (our CF Worker domain). This jar stores all cookies and
// replays them on every request so they flow through the Worker to Microsoft.
type simpleCookieJar struct {
mu sync.Mutex
cookies []*http.Cookie
}
func (j *simpleCookieJar) SetCookies(_ *url.URL, cookies []*http.Cookie) {
j.mu.Lock()
defer j.mu.Unlock()
for _, c := range cookies {
found := false
for i, existing := range j.cookies {
if existing.Name == c.Name {
j.cookies[i] = c
found = true
break
}
}
if !found {
j.cookies = append(j.cookies, c)
}
}
}
func (j *simpleCookieJar) Cookies(_ *url.URL) []*http.Cookie {
j.mu.Lock()
defer j.mu.Unlock()
out := make([]*http.Cookie, len(j.cookies))
copy(out, j.cookies)
return out
}
type SessionEntry struct {
SessionID string
CreatedAt time.Time
Jar *simpleCookieJar
}
var (
@ -355,9 +391,10 @@ func getReferer(productId string) string {
return "https://www.microsoft.com/en-us/software-download/windows8ISO"
}
func setupSession() (string, error) {
func setupSession() (string, *simpleCookieJar, error) {
sessionID := uuid.New().String()
client := &http.Client{Timeout: 10 * time.Second}
jar := &simpleCookieJar{}
client := &http.Client{Timeout: 10 * time.Second, Jar: jar}
q1 := url.Values{}
q1.Set("org_id", ORG_ID)
@ -376,7 +413,7 @@ func setupSession() (string, error) {
setWorkerSecret(req2)
resp2, err := client.Do(req2)
if err != nil {
return sessionID, nil
return sessionID, jar, nil
}
defer resp2.Body.Close()
@ -409,7 +446,7 @@ func setupSession() (string, error) {
client.Do(req3)
}
return sessionID, nil
return sessionID, jar, nil
}
func mapDownloadType(typeNum int) string {
@ -431,10 +468,10 @@ func mapDownloadType(typeNum int) string {
// Returns (rawJSON, httpErrorCode, error). httpErrorCode is non-zero only on
// rate-limit/API errors that should be stored in the negative cache.
func fetchSkuInfoFromMS(productID string) ([]byte, int, error) {
sessionID, _ := setupSession()
sessionID, jar, _ := setupSession()
cacheMutex.Lock()
sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now()}
sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now(), Jar: jar}
cacheMutex.Unlock()
skuQ := url.Values{}
@ -446,7 +483,7 @@ func fetchSkuInfoFromMS(productID string) ([]byte, int, error) {
skuQ.Set("sessionID", sessionID)
reqURL := proxyURL("https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?" + skuQ.Encode())
client := &http.Client{Timeout: 15 * time.Second}
client := &http.Client{Timeout: 15 * time.Second, Jar: jar}
var finalData map[string]interface{}
var finalRaw []byte
@ -528,18 +565,20 @@ func fetchSkuInfoFromMS(productID string) ([]byte, int, error) {
// Returns (rawJSON, error). rateLimitError is returned on 429/715-123130.
func fetchDownloadLinksFromMS(productID, skuID string) ([]byte, error) {
var sessionID string
var jar *simpleCookieJar
cacheMutex.RLock()
cached, exists := sessionCache[productID]
cacheMutex.RUnlock()
if exists && time.Since(cached.CreatedAt) < SESSION_TTL {
sessionID = cached.SessionID
jar = cached.Jar
log.Printf("MS fetch /proxy: reusing session %s for product_id=%s\n", sessionID[:8], productID)
} else {
log.Printf("MS fetch /proxy: new session for product_id=%s\n", productID)
sessionID, _ = setupSession()
sessionID, jar, _ = setupSession()
// Warm the session with a SKU info call
// Warm the session with a SKU info call so cookies from setup carry through
warmupQ := url.Values{}
warmupQ.Set("profile", PROFILE)
warmupQ.Set("productEditionId", productID)
@ -549,19 +588,24 @@ func fetchDownloadLinksFromMS(productID, skuID string) ([]byte, error) {
warmupQ.Set("sessionID", sessionID)
warmupURL := proxyURL("https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?" + warmupQ.Encode())
client := &http.Client{Timeout: 10 * time.Second}
warmupClient := &http.Client{Timeout: 10 * time.Second, Jar: jar}
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)
warmupClient.Do(req)
cacheMutex.Lock()
sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now()}
sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now(), Jar: jar}
cacheMutex.Unlock()
}
// Guard against sessions cached before the cookie jar was introduced
if jar == nil {
jar = &simpleCookieJar{}
}
proxyQ := url.Values{}
proxyQ.Set("profile", PROFILE)
proxyQ.Set("productEditionId", "undefined")
@ -571,14 +615,14 @@ func fetchDownloadLinksFromMS(productID, skuID string) ([]byte, error) {
proxyQ.Set("sessionID", sessionID)
reqURL := proxyURL("https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?" + proxyQ.Encode())
client := &http.Client{Timeout: 15 * time.Second}
downloadClient := &http.Client{Timeout: 15 * time.Second, Jar: jar}
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)
resp, err := downloadClient.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}

View file

@ -32,23 +32,34 @@ export default {
url.protocol = "https:";
url.port = "";
const forwardHeaders = {
"User-Agent": request.headers.get("User-Agent") || "Mozilla/5.0",
"Referer": request.headers.get("Referer") || "",
"Accept": request.headers.get("Accept") || "application/json",
};
const cookie = request.headers.get("Cookie");
if (cookie) forwardHeaders["Cookie"] = cookie;
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",
},
headers: forwardHeaders,
});
const upstream = await fetch(upstreamRequest);
const responseHeaders = new Headers();
responseHeaders.set("Content-Type", upstream.headers.get("Content-Type") || "application/json");
responseHeaders.set("Access-Control-Allow-Origin", "*");
// Forward Set-Cookie headers so the backend's cookie jar can accumulate them
const setCookies = upstream.headers.getAll
? upstream.headers.getAll("set-cookie")
: (upstream.headers.get("set-cookie") ? [upstream.headers.get("set-cookie")] : []);
for (const c of setCookies) responseHeaders.append("Set-Cookie", c);
return new Response(upstream.body, {
status: upstream.status,
headers: {
"Content-Type": upstream.headers.get("Content-Type") || "application/json",
"Access-Control-Allow-Origin": "*",
},
headers: responseHeaders,
});
},
};