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:
parent
04e745272b
commit
6f2c02d79b
2 changed files with 78 additions and 23 deletions
|
|
@ -38,9 +38,45 @@ const (
|
||||||
|
|
||||||
// --- Session cache (short-lived, used to chain /skuinfo → /proxy) ---
|
// --- 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 {
|
type SessionEntry struct {
|
||||||
SessionID string
|
SessionID string
|
||||||
CreatedAt time.Time
|
CreatedAt time.Time
|
||||||
|
Jar *simpleCookieJar
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|
@ -355,9 +391,10 @@ func getReferer(productId string) string {
|
||||||
return "https://www.microsoft.com/en-us/software-download/windows8ISO"
|
return "https://www.microsoft.com/en-us/software-download/windows8ISO"
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupSession() (string, error) {
|
func setupSession() (string, *simpleCookieJar, error) {
|
||||||
sessionID := uuid.New().String()
|
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 := url.Values{}
|
||||||
q1.Set("org_id", ORG_ID)
|
q1.Set("org_id", ORG_ID)
|
||||||
|
|
@ -376,7 +413,7 @@ func setupSession() (string, error) {
|
||||||
setWorkerSecret(req2)
|
setWorkerSecret(req2)
|
||||||
resp2, err := client.Do(req2)
|
resp2, err := client.Do(req2)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return sessionID, nil
|
return sessionID, jar, nil
|
||||||
}
|
}
|
||||||
defer resp2.Body.Close()
|
defer resp2.Body.Close()
|
||||||
|
|
||||||
|
|
@ -409,7 +446,7 @@ func setupSession() (string, error) {
|
||||||
client.Do(req3)
|
client.Do(req3)
|
||||||
}
|
}
|
||||||
|
|
||||||
return sessionID, nil
|
return sessionID, jar, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func mapDownloadType(typeNum int) string {
|
func mapDownloadType(typeNum int) string {
|
||||||
|
|
@ -431,10 +468,10 @@ func mapDownloadType(typeNum int) string {
|
||||||
// Returns (rawJSON, httpErrorCode, error). httpErrorCode is non-zero only on
|
// Returns (rawJSON, httpErrorCode, error). httpErrorCode is non-zero only on
|
||||||
// rate-limit/API errors that should be stored in the negative cache.
|
// rate-limit/API errors that should be stored in the negative cache.
|
||||||
func fetchSkuInfoFromMS(productID string) ([]byte, int, error) {
|
func fetchSkuInfoFromMS(productID string) ([]byte, int, error) {
|
||||||
sessionID, _ := setupSession()
|
sessionID, jar, _ := setupSession()
|
||||||
|
|
||||||
cacheMutex.Lock()
|
cacheMutex.Lock()
|
||||||
sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now()}
|
sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now(), Jar: jar}
|
||||||
cacheMutex.Unlock()
|
cacheMutex.Unlock()
|
||||||
|
|
||||||
skuQ := url.Values{}
|
skuQ := url.Values{}
|
||||||
|
|
@ -446,7 +483,7 @@ func fetchSkuInfoFromMS(productID string) ([]byte, int, error) {
|
||||||
skuQ.Set("sessionID", sessionID)
|
skuQ.Set("sessionID", sessionID)
|
||||||
reqURL := proxyURL("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}
|
client := &http.Client{Timeout: 15 * time.Second, Jar: jar}
|
||||||
var finalData map[string]interface{}
|
var finalData map[string]interface{}
|
||||||
var finalRaw []byte
|
var finalRaw []byte
|
||||||
|
|
||||||
|
|
@ -528,18 +565,20 @@ func fetchSkuInfoFromMS(productID string) ([]byte, int, error) {
|
||||||
// Returns (rawJSON, error). rateLimitError is returned on 429/715-123130.
|
// Returns (rawJSON, error). rateLimitError is returned on 429/715-123130.
|
||||||
func fetchDownloadLinksFromMS(productID, skuID string) ([]byte, error) {
|
func fetchDownloadLinksFromMS(productID, skuID string) ([]byte, error) {
|
||||||
var sessionID string
|
var sessionID string
|
||||||
|
var jar *simpleCookieJar
|
||||||
cacheMutex.RLock()
|
cacheMutex.RLock()
|
||||||
cached, exists := sessionCache[productID]
|
cached, exists := sessionCache[productID]
|
||||||
cacheMutex.RUnlock()
|
cacheMutex.RUnlock()
|
||||||
|
|
||||||
if exists && time.Since(cached.CreatedAt) < SESSION_TTL {
|
if exists && time.Since(cached.CreatedAt) < SESSION_TTL {
|
||||||
sessionID = cached.SessionID
|
sessionID = cached.SessionID
|
||||||
|
jar = cached.Jar
|
||||||
log.Printf("MS fetch /proxy: reusing session %s for product_id=%s\n", sessionID[:8], productID)
|
log.Printf("MS fetch /proxy: reusing session %s for product_id=%s\n", sessionID[:8], productID)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("MS fetch /proxy: new session for product_id=%s\n", productID)
|
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 := url.Values{}
|
||||||
warmupQ.Set("profile", PROFILE)
|
warmupQ.Set("profile", PROFILE)
|
||||||
warmupQ.Set("productEditionId", productID)
|
warmupQ.Set("productEditionId", productID)
|
||||||
|
|
@ -549,19 +588,24 @@ func fetchDownloadLinksFromMS(productID, skuID string) ([]byte, error) {
|
||||||
warmupQ.Set("sessionID", sessionID)
|
warmupQ.Set("sessionID", sessionID)
|
||||||
warmupURL := proxyURL("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}
|
warmupClient := &http.Client{Timeout: 10 * time.Second, Jar: jar}
|
||||||
req, _ := http.NewRequest("GET", warmupURL, nil)
|
req, _ := http.NewRequest("GET", warmupURL, nil)
|
||||||
req.Header.Set("User-Agent", UA)
|
req.Header.Set("User-Agent", UA)
|
||||||
req.Header.Set("Referer", getReferer(productID))
|
req.Header.Set("Referer", getReferer(productID))
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
setWorkerSecret(req)
|
setWorkerSecret(req)
|
||||||
client.Do(req)
|
warmupClient.Do(req)
|
||||||
|
|
||||||
cacheMutex.Lock()
|
cacheMutex.Lock()
|
||||||
sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now()}
|
sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now(), Jar: jar}
|
||||||
cacheMutex.Unlock()
|
cacheMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Guard against sessions cached before the cookie jar was introduced
|
||||||
|
if jar == nil {
|
||||||
|
jar = &simpleCookieJar{}
|
||||||
|
}
|
||||||
|
|
||||||
proxyQ := url.Values{}
|
proxyQ := url.Values{}
|
||||||
proxyQ.Set("profile", PROFILE)
|
proxyQ.Set("profile", PROFILE)
|
||||||
proxyQ.Set("productEditionId", "undefined")
|
proxyQ.Set("productEditionId", "undefined")
|
||||||
|
|
@ -571,14 +615,14 @@ func fetchDownloadLinksFromMS(productID, skuID string) ([]byte, error) {
|
||||||
proxyQ.Set("sessionID", sessionID)
|
proxyQ.Set("sessionID", sessionID)
|
||||||
reqURL := proxyURL("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}
|
downloadClient := &http.Client{Timeout: 15 * time.Second, Jar: jar}
|
||||||
req, _ := http.NewRequest("GET", reqURL, nil)
|
req, _ := http.NewRequest("GET", reqURL, nil)
|
||||||
req.Header.Set("User-Agent", UA)
|
req.Header.Set("User-Agent", UA)
|
||||||
req.Header.Set("Referer", getReferer(productID))
|
req.Header.Set("Referer", getReferer(productID))
|
||||||
req.Header.Set("Accept", "application/json")
|
req.Header.Set("Accept", "application/json")
|
||||||
setWorkerSecret(req)
|
setWorkerSecret(req)
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := downloadClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("request failed: %w", err)
|
return nil, fmt.Errorf("request failed: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,23 +32,34 @@ export default {
|
||||||
url.protocol = "https:";
|
url.protocol = "https:";
|
||||||
url.port = "";
|
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(), {
|
const upstreamRequest = new Request(url.toString(), {
|
||||||
method: request.method,
|
method: request.method,
|
||||||
headers: {
|
headers: forwardHeaders,
|
||||||
"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);
|
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, {
|
return new Response(upstream.body, {
|
||||||
status: upstream.status,
|
status: upstream.status,
|
||||||
headers: {
|
headers: responseHeaders,
|
||||||
"Content-Type": upstream.headers.get("Content-Type") || "application/json",
|
|
||||||
"Access-Control-Allow-Origin": "*",
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue