diff --git a/README.md b/README.md index e88c0fc..69874f0 100644 --- a/README.md +++ b/README.md @@ -285,8 +285,8 @@ Returns real-time cache statistics for the running instance. Auth via `?secret=` |---|---|---| | Windows 11 25H2 | 3262 | x64 | | Windows 11 25H2 | 3265 | ARM64 | -| Windows 11 25H2 (updated) | 3321 | x64 | -| Windows 11 25H2 (updated) | 3324 | ARM64 | +| Windows 11 25H2 (V2) | 3321 | x64 | +| Windows 11 25H2 (V2) | 3324 | ARM64 | | Windows 11 24H2 | 3113 | x64 | | Windows 11 24H2 | 3131 | ARM64 | | Windows 10 22H2 | 2618 | x64 / x86 | diff --git a/backend/main.go b/backend/main.go index d2ea1b4..c544f57 100644 --- a/backend/main.go +++ b/backend/main.go @@ -479,7 +479,8 @@ var validContributeProducts = map[string]bool{ "3131": true, "3132": true, "3133": true, "3262": true, "3263": true, "3264": true, "3265": true, "3266": true, "3267": true, - "3321": true, "3324": true, + "3321": true, "3322": true, "3323": true, + "3324": true, "3325": true, "3326": true, } // allowedCDNSuffixes is the CDN host allow-list for contributed download URLs. diff --git a/cli/catalog.go b/cli/catalog.go index ffa8188..5263ff1 100644 --- a/cli/catalog.go +++ b/cli/catalog.go @@ -29,8 +29,12 @@ var consumerProducts = []Product{ {"3265", "Windows 11 Arm64 25H2 (26200.6584)"}, {"3266", "Windows 11 Arm64 25H2 Home China (26200.6584)"}, {"3267", "Windows 11 Arm64 25H2 Pro China (26200.6584)"}, - {"3321", "Windows 11 25H2 (Updated Oct)"}, - {"3324", "Windows 11 Arm64 25H2 (Updated Oct)"}, + {"3321", "Windows 11 25H2 (V2)"}, + {"3322", "Windows 11 25H2 Home China (V2)"}, + {"3323", "Windows 11 25H2 Pro China (V2)"}, + {"3324", "Windows 11 Arm64 25H2 (V2)"}, + {"3325", "Windows 11 Arm64 25H2 Home China (V2)"}, + {"3326", "Windows 11 Arm64 25H2 Pro China (V2)"}, } var evalProducts = []EvalProduct{ diff --git a/frontend/public/data/products.json b/frontend/public/data/products.json index ee9f298..e4b9c13 100644 --- a/frontend/public/data/products.json +++ b/frontend/public/data/products.json @@ -169,7 +169,7 @@ ] }, "3321": { - "name": "Windows 11 25H2 (Updated Oct)", + "name": "Windows 11 25H2 (V2)", "badge": "LATEST", "archs": [ "x64" @@ -180,8 +180,30 @@ "3113" ] }, + "3322": { + "name": "Windows 11 25H2 Home China (V2)", + "badge": "LATEST", + "archs": [ + "x64" + ], + "related": [ + "3321", + "3323" + ] + }, + "3323": { + "name": "Windows 11 25H2 Pro China (V2)", + "badge": "LATEST", + "archs": [ + "x64" + ], + "related": [ + "3321", + "3322" + ] + }, "3324": { - "name": "Windows 11 Arm64 25H2 (Updated Oct)", + "name": "Windows 11 Arm64 25H2 (V2)", "badge": "LATEST", "archs": [ "ARM64" @@ -191,5 +213,27 @@ "3262", "3265" ] + }, + "3325": { + "name": "Windows 11 Arm64 25H2 Home China (V2)", + "badge": "LATEST", + "archs": [ + "ARM64" + ], + "related": [ + "3324", + "3326" + ] + }, + "3326": { + "name": "Windows 11 Arm64 25H2 Pro China (V2)", + "badge": "LATEST", + "archs": [ + "ARM64" + ], + "related": [ + "3324", + "3325" + ] } } \ No newline at end of file diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index c7a6ecb..b4d34be 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -63,5 +63,9 @@ https://msdl.tech-latest.com/product/32662026-05-120.8monthly https://msdl.tech-latest.com/product/32672026-05-120.8monthly https://msdl.tech-latest.com/product/33212026-05-120.8monthly + https://msdl.tech-latest.com/product/33222026-07-140.8monthly + https://msdl.tech-latest.com/product/33232026-07-140.8monthly https://msdl.tech-latest.com/product/33242026-05-120.8monthly + https://msdl.tech-latest.com/product/33252026-07-140.8monthly + https://msdl.tech-latest.com/product/33262026-07-140.8monthly diff --git a/scripts/check-new-releases/README.md b/scripts/check-new-releases/README.md new file mode 100644 index 0000000..de4fdd2 --- /dev/null +++ b/scripts/check-new-releases/README.md @@ -0,0 +1,91 @@ +# check-new-releases + +Two complementary tools for finding Windows product edition IDs that exist on +Microsoft's side but aren't yet in `cli/catalog.go` / `products.json` / +`validContributeProducts`. Neither touches those files automatically -- +naming a new entry correctly (build number, refresh qualifiers like "V2", +etc.) still needs a human checking Microsoft's own release-health pages. +Both only *discover* candidates. + +Only Windows 11 is worth checking at all: Windows 8.1 is fully frozen (no +updates since Jan 2023) and Windows 10 is past end-of-life (ESU security +patches only, delivered via Windows Update, not new consumer ISOs). Neither +will ever produce a new release again. + +## Method 1 -- `msdls_v3.py` (brute-force range scan) + +Checks every ID in a range you specify, one by one, against Microsoft's +SKU-info API. Use this when you already have a rough idea of where to look +(e.g. "the last release was around 3260, check up to 3350") or want to +directly merge results into a JSON catalog file. + +```bash +pip install requests +python msdls_v3.py --first 3320 --last 3330 +``` + +Optionally write results to a JSON file (merges with an existing one if +present, preserving fields like `badge`/`archs`/`related`, marking IDs that +stopped responding as `"active": false`): + +```bash +python msdls_v3.py --first 3320 --last 3330 --write scan-results.json +``` + +Don't point `--write` directly at `frontend/public/data/products.json` -- +write to a scratch file first and review before merging into the real +catalog. + +**How it identifies a release:** reads `ProductDisplayName` from the first +SKU in the API response (e.g. `"Windows 11 25H2__V2"`). This field name was +wrong in an earlier version of this script (checked `EditionName`/ +`ReleaseName`/`FriendlyName`, none of which exist in the real response) -- +confirmed live and fixed 2026-07-14. + +**Session handling:** registers the session via `vlscppe`'s permit endpoint +before scanning. A single SKU lookup was confirmed to work without this, but +this script's use case -- many rapid consecutive requests across a range -- +is a much higher-risk pattern for triggering a Sentinel block than a +one-shot lookup, so the permit call is worth keeping. The CLI/backend's full +session flow additionally replays an `ov-df.microsoft.com` fingerprint; add +that too if the permit call alone isn't enough under real scanning load. + +## Method 2 -- `main.go` (auto-discovery) + +Scrapes Microsoft's public `/windows11` download page for the *current* +flagship product edition ID (no range-guessing needed), compares it against +`cli/catalog.go`, and if it's new, probes a bounded range of adjacent IDs to +discover the accompanying variant family (Home China, Pro China, ARM64). +Use this when you just want to know "is there anything new" without knowing +where to look. + +```bash +cd scripts/check-new-releases +go run . +``` + +If a new release is found, it makes real requests to Microsoft to probe the +variant family -- let it finish rather than re-running immediately. + +Flags: +- `-catalog ` — path to `cli/catalog.go` (default: `../../cli/catalog.go`) +- `-probe-range ` — how many adjacent IDs to check after a new flagship is found (default: 20) + +**Known limitation:** the ARM64 family's offset from the flagship isn't +consistent release to release (observed: +3 for the 25H2 family, +18 for +24H2's). The default probe range of 20 covers both known cases, but if no +ARM64 variant shows up, try a wider `-probe-range`. + +## Which one to use + +| | Method 1 (Python) | Method 2 (Go) | +|---|---|---| +| Need to guess a starting range? | Yes | No -- finds the current flagship automatically | +| Gets the real Microsoft name? | Yes (`ProductDisplayName`) | Yes (`ProductDisplayName`) | +| Can write results to a catalog-shaped JSON? | Yes (`--write`) | No -- prints a report only | +| Best for | "I want to scan a specific range and get a mergeable file" | "Just tell me if anything's new" | + +Both were verified end-to-end against live Microsoft endpoints on +2026-07-14, which surfaced a real gap: product IDs `3322`/`3323`/`3325`/`3326` +(Home/Pro China variants of the 25H2 "V2" refresh) existed on Microsoft's +side but weren't yet in the catalog. Since fixed. diff --git a/scripts/check-new-releases/go.mod b/scripts/check-new-releases/go.mod new file mode 100644 index 0000000..da35d70 --- /dev/null +++ b/scripts/check-new-releases/go.mod @@ -0,0 +1,3 @@ +module check-new-releases + +go 1.25.6 diff --git a/scripts/check-new-releases/main.go b/scripts/check-new-releases/main.go new file mode 100644 index 0000000..977a789 --- /dev/null +++ b/scripts/check-new-releases/main.go @@ -0,0 +1,321 @@ +// check-new-releases scrapes Microsoft's public Windows 11 download page for +// the current "flagship" product edition ID and compares it against the +// catalog already tracked in cli/catalog.go. Windows 8.1 and Windows 10 are +// intentionally not checked here -- 8.1 is fully frozen and Windows 10 is +// past end-of-life, so neither will ever produce a new consumer ISO release. +// +// If a new flagship ID is found, it probes a bounded range of adjacent IDs to +// discover the accompanying variant family (Home China, Pro China, ARM64, +// ...), since Microsoft allocates those in a cluster near the flagship ID +// but the exact offset isn't consistent release to release (observed gaps: +// +3 for the 25H2 family, +18 for 24H2's ARM64 family). +// +// This tool only *discovers* candidate IDs -- it does not touch cli/catalog.go, +// products.json, or validContributeProducts automatically. Naming a new entry +// correctly (build number, "Updated Oct"-style qualifiers, etc.) still needs a +// human checking Microsoft's own release-health pages. +// +// Usage: go run . [-catalog path/to/cli/catalog.go] [-probe-range N] +package main + +import ( + "crypto/rand" + "encoding/hex" + "encoding/json" + "flag" + "fmt" + "io" + "net/http" + "net/url" + "os" + "regexp" + "strconv" + "sync" + "time" +) + +// --- Copied from cli/microsoft.go (not imported -- cli/ is package main and +// can't be imported from elsewhere). This is a standalone maintenance tool, +// not part of the shipped CLI/backend; keep in sync manually if the real +// session flow ever changes. --- + +const ( + msUA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36" + msProfile = "606624d44113" + msLocale = "en-US" + msOrgID = "y6jn8c31" + msCustomer = "560dc9f3-1aa5-4a2f-b63c-9e18f8d0e175" +) + +var ( + reW = regexp.MustCompile(`[&?]w=([^&"'\s]+)`) + reRt = regexp.MustCompile(`rticks[="]+\+?\s*(\d{10,})`) +) + +type skuLang struct { + ID string `json:"Id"` + Language string `json:"Language"` + ProductDisplayName string `json:"ProductDisplayName"` +} + +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 +} + +func newSessionID() string { + b := make([]byte, 16) + rand.Read(b) + b[6] = (b[6] & 0x0f) | 0x40 + b[8] = (b[8] & 0x3f) | 0x80 + return fmt.Sprintf("%s-%s-%s-%s-%s", + hex.EncodeToString(b[0:4]), hex.EncodeToString(b[4:6]), + hex.EncodeToString(b[6:8]), hex.EncodeToString(b[8:10]), + hex.EncodeToString(b[10:16])) +} + +func newSession() (*http.Client, string) { + sessionID := newSessionID() + jar := &simpleCookieJar{} + client := &http.Client{Timeout: 15 * time.Second, Jar: jar} + + q1 := url.Values{} + q1.Set("org_id", msOrgID) + q1.Set("session_id", sessionID) + req1, _ := http.NewRequest("GET", "https://vlscppe.microsoft.com/tags?"+q1.Encode(), nil) + req1.Header.Set("User-Agent", msUA) + client.Do(req1) + + q2 := url.Values{} + q2.Set("instanceId", msCustomer) + q2.Set("PageId", "si") + q2.Set("session_id", sessionID) + req2, _ := http.NewRequest("GET", "https://ov-df.microsoft.com/mdt.js?"+q2.Encode(), nil) + req2.Header.Set("User-Agent", msUA) + resp2, err := client.Do(req2) + if err != nil { + return client, sessionID + } + body, _ := io.ReadAll(resp2.Body) + resp2.Body.Close() + + wMatch := reW.FindStringSubmatch(string(body)) + rtMatch := reRt.FindStringSubmatch(string(body)) + if len(wMatch) > 1 && len(rtMatch) > 1 { + q3 := url.Values{} + q3.Set("session_id", sessionID) + q3.Set("CustomerId", msCustomer) + q3.Set("PageId", "si") + q3.Set("w", wMatch[1]) + q3.Set("mdt", fmt.Sprintf("%d", time.Now().UnixMilli())) + q3.Set("rticks", rtMatch[1]) + req3, _ := http.NewRequest("GET", "https://ov-df.microsoft.com/?"+q3.Encode(), nil) + req3.Header.Set("User-Agent", msUA) + client.Do(req3) + } + return client, sessionID +} + +type msErrorEntry struct { + Type float64 `json:"Type"` + Value string `json:"Value"` +} + +func firstError(errs []msErrorEntry) string { + if len(errs) == 0 { + return "" + } + if int(errs[0].Type) == 9 { + if errs[0].Value != "" { + return errs[0].Value + } + return "Your IP has been temporarily blocked by Microsoft (Code 715-123130)" + } + if errs[0].Value != "" { + return errs[0].Value + } + return "Microsoft API error" +} + +func fetchLanguages(client *http.Client, sessionID, productID string) ([]skuLang, error) { + q := url.Values{} + q.Set("profile", msProfile) + q.Set("productEditionId", productID) + q.Set("SKU", "undefined") + q.Set("friendlyFileName", "undefined") + q.Set("Locale", msLocale) + q.Set("sessionID", sessionID) + + req, _ := http.NewRequest("GET", "https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?"+q.Encode(), nil) + req.Header.Set("User-Agent", msUA) + req.Header.Set("Referer", "https://www.microsoft.com/en-us/software-download/windows11") + req.Header.Set("Accept", "application/json") + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer resp.Body.Close() + body, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("Microsoft returned HTTP %d", resp.StatusCode) + } + if len(body) > 0 && body[0] == '"' { + var unquoted string + json.Unmarshal(body, &unquoted) + body = []byte(unquoted) + } + + var data struct { + Skus []skuLang `json:"Skus"` + Errors []msErrorEntry `json:"Errors"` + } + if err := json.Unmarshal(body, &data); err != nil { + return nil, fmt.Errorf("invalid SKU response: %w", err) + } + if msg := firstError(data.Errors); msg != "" { + return nil, fmt.Errorf("%s", msg) + } + if len(data.Skus) == 0 { + return nil, fmt.Errorf("no languages found for this product") + } + return data.Skus, nil +} + +// --- New logic specific to this tool --- + +var catalogEntryRe = regexp.MustCompile(`\{"(\d+)",\s*"([^"]*)"\}`) + +func knownCatalogIDs(catalogPath string) (map[string]string, error) { + data, err := os.ReadFile(catalogPath) + if err != nil { + return nil, err + } + matches := catalogEntryRe.FindAllStringSubmatch(string(data), -1) + out := make(map[string]string, len(matches)) + for _, m := range matches { + out[m[1]] = m[2] + } + return out, nil +} + +var flagshipRe = regexp.MustCompile(`