commit 7d847f8dd10367924ded96d34c91a6059b6144aa Author: Shekhar Vaidya Date: Fri Apr 10 20:15:46 2026 +0530 feat: initial release — Windows ISO Downloader v3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1425e1d --- /dev/null +++ b/.gitignore @@ -0,0 +1,54 @@ +# ── Node / npm ──────────────────────────────────────── +node_modules/ +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +.pnpm-store/ +package-lock.json +yarn.lock +pnpm-lock.yaml + +# ── Frontend build outputs ───────────────────────────── +frontend/dist/ +frontend/.vite/ +frontend/build/ + +# ── Environment files ────────────────────────────────── +.env +.env.local +.env.*.local +*.env + +# ── Go build outputs ─────────────────────────────────── +backend/main +backend/*.exe +backend/tmp/ +go.sum + +# ── OS files ─────────────────────────────────────────── +.DS_Store +.DS_Store? +._* +Thumbs.db +Desktop.ini +ehthumbs.db + +# ── Editor / IDE ─────────────────────────────────────── +.vscode/ +.idea/ +*.swp +*.swo +*.suo +*.user +*.sublime-workspace +*.sublime-project + +# ── Logs ────────────────────────────────────────────── +logs/ +*.log + +# ── Misc ────────────────────────────────────────────── +.cache/ +.temp/ +tmp/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29ab9f0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 TechLatest (https://tech-latest.com) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2ae01b7 --- /dev/null +++ b/README.md @@ -0,0 +1,196 @@ +# windows-iso-downloader + +> A clean, open-source tool for obtaining official Windows ISO files directly from Microsoft's CDN — without a Windows machine, the Media Creation Tool, or browser restrictions. + +**Live:** [msdl.tech-latest.com](https://msdl.tech-latest.com) · **By:** [TechLatest](https://tech-latest.com) + +![License](https://img.shields.io/github/license/starkSV/windows-iso-downloader) +![Stars](https://img.shields.io/github/stars/starkSV/windows-iso-downloader) + +--- + +## What it does + +MSDL replicates the session flow that Microsoft uses to serve ISO download links — identical to the approach used by [Rufus/Fido](https://github.com/pbatard/Fido). You get the exact same signed CDN URL Microsoft would give you, just without the browser requirement. + +- ✅ Direct Microsoft CDN links — no proxying of actual file data +- ✅ 38 languages per release +- ✅ ARM64 + x64 + x86 support +- ✅ No account, no browser lock, no ads, no tracking +- ✅ Links expire in 24 hours (Microsoft's standard behaviour, not a limitation) + +--- + +## Screenshot + +> _Coming soon_ + +--- + +## Project Structure + +``` +windows-iso-downloader/ +├── frontend/ # React 19 + TypeScript + Vite + Tailwind v4 +├── backend/ # Go proxy server (recommended for production) +└── README.md +``` + +--- + +## How It Works + +``` +Browser → Backend Proxy → Microsoft Download API → Signed CDN URL + ↓ + 1. Register session (Microsoft tracking endpoint) + 2. Parse MDT fingerprint script + 3. Fetch SKU list (available languages) + 4. Fetch signed CDN download URL +``` + +The flow mirrors [Fido.ps1](https://github.com/pbatard/Fido) by Pete Batard — the same script bundled with Rufus. + +--- + +## Running Locally + +### Backend (Go) + +```bash +cd backend +go run main.go +# Runs on http://localhost:3002 +``` + +### Frontend + +```bash +cd frontend +npm install +npm run dev +# Runs on http://localhost:5173 +``` + +Create `frontend/.env.local`: + +```env +VITE_API_URL=http://localhost:3002 +``` + +--- + +## API Reference + +### `GET /skuinfo?product_id=` + +Returns available languages for a product. + +```json +{ + "Skus": [ + { + "Id": "0x0409", + "Language": "en-US", + "LocalizedLanguage": "English (United States)" + } + ] +} +``` + +### `GET /proxy?product_id=&sku_id=` + +Returns signed download links from Microsoft's CDN. + +```json +{ + "ProductDownloadOptions": [ + { + "Uri": "https://software.download.prss.microsoft.com/...", + "Architecture": "x64" + } + ] +} +``` + +--- + +## Supported Products + +| Product | ID | Architecture | +|---|---|---| +| Windows 11 25H2 | 3262 | x64 | +| Windows 11 25H2 | 3265 | ARM64 | +| Windows 11 25H2 (updated) | 3321 | x64 | +| Windows 11 25H2 (updated) | 3324 | ARM64 | +| Windows 11 24H2 | 3113 | x64 | +| Windows 11 24H2 | 3131 | ARM64 | +| Windows 10 22H2 | 2618 | x64 / x86 | +| Windows 10 22H2 Home China | 2378 | x64 | +| Windows 8.1 | 52 | x64 / x86 | +| Windows 8.1 Single Language | 48 | x64 / x86 | + +--- + +## Tech Stack + +### Frontend (`frontend/`) + +| | | +|---|---| +| Framework | React 19 + TypeScript | +| Build tool | Vite 8 | +| Styling | Tailwind CSS v4 | +| Animation | Motion (Framer Motion v12) | +| UI primitives | Radix UI | +| Toast | Sonner | +| Font | Geist | +| Router | React Router v7 | + +### Backend (`backend/`) + +| | | +|---|---| +| Language | Go 1.22+ | +| HTTP | `net/http` (stdlib, no framework) | +| Session cache | `sync.RWMutex` in-memory · 15 min TTL | +| UUID | `github.com/google/uuid` | +| MDT parsing | `regexp` (stdlib) | + +--- + +## Deployment + +> ⚠️ **Deploy the backend to a standard VPS** (Hetzner, DigitalOcean, Linode, etc.) — **not** serverless platforms like Vercel, Cloudflare Workers, or AWS Lambda. Microsoft rate-limits known datacenter IP ranges. + +Recommended setup: + +| Component | Platform | +|---|---| +| Frontend | Cloudflare Pages / Vercel (static) | +| Backend | VPS with a non-datacenter IP | + +--- + +## Contributing + +Pull requests welcome. To add a new Windows release: + +1. Find the product ID on `www.microsoft.com/software-download-connector/api/` +2. Add it to `frontend/public/data/products.json` +3. Add related metadata in `ProductDetailPage.tsx` (`PRODUCT_META`, `RELATED_GROUPS`) +4. Update the product table in this README + +--- + +## Disclaimer + +This project is **not affiliated with, endorsed by, or sponsored by Microsoft Corporation**. +Windows is a registered trademark of Microsoft Corporation. +All ISO files are served directly from Microsoft's official CDN — this project does not host any files. + +--- + +## License + +[MIT](./LICENSE) © [TechLatest](https://tech-latest.com) diff --git a/backend/go.mod b/backend/go.mod new file mode 100644 index 0000000..5ca7157 --- /dev/null +++ b/backend/go.mod @@ -0,0 +1,5 @@ +module msdl-backend-go + +go 1.25.6 + +require github.com/google/uuid v1.6.0 // indirect diff --git a/backend/main.go b/backend/main.go new file mode 100644 index 0000000..8ec25ce --- /dev/null +++ b/backend/main.go @@ -0,0 +1,380 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + "regexp" + "strconv" + "sync" + "time" + + "github.com/google/uuid" +) + +const ( + UA = "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" + PROFILE = "606624d44113" + LOCALE = "en-US" + ORG_ID = "y6jn8c31" + CUSTOMER_ID = "560dc9f3-1aa5-4a2f-b63c-9e18f8d0e175" + PORT = ":3002" // Different port to run alongside Node +) + +type SessionEntry struct { + SessionID string + CreatedAt time.Time +} + +var ( + sessionCache = make(map[string]SessionEntry) + cacheMutex sync.RWMutex + SESSION_TTL = 15 * time.Minute +) + +// Map product ID to referer URL +func getReferer(productId string) string { + id, err := strconv.Atoi(productId) + if err != nil { + return "https://www.microsoft.com/en-us/software-download/windows8ISO" + } + if id >= 2935 { + return "https://www.microsoft.com/en-us/software-download/windows11" + } + if id >= 2618 { + return "https://www.microsoft.com/en-us/software-download/windows10ISO" + } + return "https://www.microsoft.com/en-us/software-download/windows8ISO" +} + +// Replicate Fido session tracking +func setupSession() (string, error) { + sessionID := uuid.New().String() + client := &http.Client{Timeout: 10 * time.Second} + + // Step 1: Register session + req1, _ := http.NewRequest("GET", fmt.Sprintf("https://vlscppe.microsoft.com/tags?org_id=%s&session_id=%s", ORG_ID, sessionID), nil) + req1.Header.Set("User-Agent", UA) + client.Do(req1) // Ignore errors intentionally + + // Step 2: Fetch tracking JS + req2, _ := http.NewRequest("GET", fmt.Sprintf("https://ov-df.microsoft.com/mdt.js?instanceId=%s&PageId=si&session_id=%s", CUSTOMER_ID, sessionID), nil) + req2.Header.Set("User-Agent", UA) + resp2, err := client.Do(req2) + if err != nil { + return sessionID, nil // Proceed anyway + } + defer resp2.Body.Close() + + bodyBytes, _ := io.ReadAll(resp2.Body) + mdtText := string(bodyBytes) + + // Regex to extract w and rticks + reW := regexp.MustCompile(`[&?]w=([^&"'\s]+)`) + reRt := regexp.MustCompile(`rticks[="]+\+?\s*(\d{10,})`) + + wMatch := reW.FindStringSubmatch(mdtText) + rtMatch := reRt.FindStringSubmatch(mdtText) + + // Step 3: Send fingerprint response + if len(wMatch) > 1 && len(rtMatch) > 1 { + wVal := wMatch[1] + rtVal := rtMatch[1] + mdt := time.Now().UnixMilli() + + fpURL := fmt.Sprintf("https://ov-df.microsoft.com/?session_id=%s&CustomerId=%s&PageId=si&w=%s&mdt=%d&rticks=%s", + sessionID, CUSTOMER_ID, wVal, mdt, rtVal) + + req3, _ := http.NewRequest("GET", fpURL, nil) + req3.Header.Set("User-Agent", UA) + client.Do(req3) + } + + return sessionID, nil +} + +// Map DownloadType code to string +func mapDownloadType(typeNum int) string { + switch typeNum { + case 0: + return "x86" + case 1: + return "x64" + case 2: + return "ARM64" + default: + return fmt.Sprintf("type_%d", typeNum) + } +} + +// Background cleanup for stale sessions +func cleanupSessions() { + for { + time.Sleep(1 * time.Minute) + cacheMutex.Lock() + now := time.Now() + for key, entry := range sessionCache { + if now.Sub(entry.CreatedAt) > SESSION_TTL { + delete(sessionCache, key) + } + } + cacheMutex.Unlock() + } +} + +// Middleware for CORS +func enableCORS(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + if r.Method == "OPTIONS" { + w.WriteHeader(http.StatusOK) + return + } + next.ServeHTTP(w, r) + }) +} + +func respondJSONError(w http.ResponseWriter, status int, message string) { + w.WriteHeader(status) + json.NewEncoder(w).Encode(map[string]string{"error": message}) +} + +// --- /skuinfo endpoint --- +func handleSkuInfo(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + productID := r.URL.Query().Get("product_id") + if productID == "" { + respondJSONError(w, http.StatusBadRequest, "product_id query parameter is required") + return + } + + sessionID, _ := setupSession() + + cacheMutex.Lock() + sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now()} + cacheMutex.Unlock() + + reqURL := fmt.Sprintf("https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?profile=%s&productEditionId=%s&SKU=undefined&friendlyFileName=undefined&Locale=%s&sessionID=%s", + PROFILE, productID, LOCALE, sessionID) + + client := &http.Client{Timeout: 15 * time.Second} + var finalData map[string]interface{} + + // Retry logic + for attempt := 0; attempt < 3; attempt++ { + if attempt > 0 { + time.Sleep(2 * 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") + + resp, err := client.Do(req) + if err != nil { + if attempt == 2 { + respondJSONError(w, http.StatusBadGateway, "Request failed: "+err.Error()) + return + } + continue + } + + bodyBytes, _ := io.ReadAll(resp.Body) + resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + if attempt == 2 { + respondJSONError(w, http.StatusBadGateway, fmt.Sprintf("Microsoft API returned HTTP %d", resp.StatusCode)) + return + } + continue + } + + // Double encoded decode logic + if len(bodyBytes) > 0 && bodyBytes[0] == '"' { + var unquoted string + json.Unmarshal(bodyBytes, &unquoted) + bodyBytes = []byte(unquoted) + } + + var data map[string]interface{} + json.Unmarshal(bodyBytes, &data) + + // Check for specific MS errors + if errs, ok := data["Errors"].([]interface{}); ok && len(errs) > 0 { + if attempt == 2 { + msg := "Microsoft API error" + if errMap, ok := errs[0].(map[string]interface{}); ok { + if val, exists := errMap["Value"]; exists { + msg = val.(string) + } + } + respondJSONError(w, http.StatusBadGateway, msg) + return + } + continue + } + + finalData = data + break + } + + skus, ok := finalData["Skus"].([]interface{}) + if !ok || len(skus) == 0 { + respondJSONError(w, http.StatusNotFound, "No languages found for this product ID.") + return + } + + log.Printf("/skuinfo: product_id=%s -> %d languages (session=%s)\n", productID, len(skus), sessionID[:8]) + json.NewEncoder(w).Encode(finalData) +} + +// --- /proxy endpoint --- +func handleProxy(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + productID := r.URL.Query().Get("product_id") + skuID := r.URL.Query().Get("sku_id") + + if productID == "" || skuID == "" { + respondJSONError(w, http.StatusBadRequest, "product_id and sku_id query parameters are required") + return + } + + var sessionID string + cacheMutex.RLock() + cached, exists := sessionCache[productID] + cacheMutex.RUnlock() + + if exists && time.Since(cached.CreatedAt) < SESSION_TTL { + sessionID = cached.SessionID + log.Printf("/proxy: Reusing session %s for product_id=%s\n", sessionID[:8], productID) + } else { + log.Printf("/proxy: No valid session for product_id=%s, creating new one...\n", productID) + sessionID, _ = setupSession() + + // Warmup with SKU info request + warmupURL := fmt.Sprintf("https://www.microsoft.com/software-download-connector/api/getskuinformationbyproductedition?profile=%s&productEditionId=%s&SKU=undefined&friendlyFileName=undefined&Locale=%s&sessionID=%s", + PROFILE, productID, LOCALE, sessionID) + + 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") + client.Do(req) + + cacheMutex.Lock() + sessionCache[productID] = SessionEntry{SessionID: sessionID, CreatedAt: time.Now()} + cacheMutex.Unlock() + } + + reqURL := fmt.Sprintf("https://www.microsoft.com/software-download-connector/api/GetProductDownloadLinksBySku?profile=%s&productEditionId=undefined&SKU=%s&friendlyFileName=undefined&Locale=%s&sessionID=%s", + PROFILE, skuID, LOCALE, sessionID) + + 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") + + resp, err := client.Do(req) + if err != nil { + respondJSONError(w, http.StatusBadGateway, "Request failed: "+err.Error()) + return + } + defer resp.Body.Close() + + bodyBytes, _ := io.ReadAll(resp.Body) + if resp.StatusCode != http.StatusOK { + respondJSONError(w, http.StatusBadGateway, fmt.Sprintf("Microsoft API returned HTTP %d", resp.StatusCode)) + return + } + + // Double encoded decode + if len(bodyBytes) > 0 && bodyBytes[0] == '"' { + var unquoted string + json.Unmarshal(bodyBytes, &unquoted) + bodyBytes = []byte(unquoted) + } + + var data map[string]interface{} + json.Unmarshal(bodyBytes, &data) + + // Check for Microsoft errors + if errs, ok := data["Errors"].([]interface{}); ok && len(errs) > 0 { + if errMap, ok := errs[0].(map[string]interface{}); ok { + if typeNum, exists := errMap["Type"].(float64); exists && typeNum == 9 { + respondJSONError(w, http.StatusTooManyRequests, "Your IP has been temporarily blocked by Microsoft. Please try again later. (Code 715-123130)") + return + } + if val, exists := errMap["Value"].(string); exists { + respondJSONError(w, http.StatusBadGateway, val) + return + } + } + respondJSONError(w, http.StatusBadGateway, "Microsoft API error") + return + } + + optsRaw, exists := data["ProductDownloadOptions"] + if !exists { + respondJSONError(w, http.StatusNotFound, "No download links found for this SKU.") + return + } + + opts, ok := optsRaw.([]interface{}) + if !ok || len(opts) == 0 { + respondJSONError(w, http.StatusNotFound, "No download links found for this SKU.") + return + } + + for _, optRaw := range opts { + if optMap, ok := optRaw.(map[string]interface{}); ok { + if arch, hasArch := optMap["Architecture"]; !hasArch || arch == nil { + if dType, hasDType := optMap["DownloadType"].(float64); hasDType { + optMap["Architecture"] = mapDownloadType(int(dType)) + } + } + } + } + + log.Printf("/proxy: product_id=%s, sku_id=%s -> %d links\n", productID, skuID, len(opts)) + + // Write pure JSON + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) // Don't encode & to \u0026 in URLs! + enc.Encode(data) + w.Write(buf.Bytes()) +} + +func main() { + go cleanupSessions() + + mux := http.NewServeMux() + + // API Routing + mux.HandleFunc("/skuinfo", handleSkuInfo) + mux.HandleFunc("/proxy", handleProxy) + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"status":"ok"}`)) + }) + + // Static UI Server + fs := http.FileServer(http.Dir("./public")) + mux.Handle("/", fs) + + handler := enableCORS(mux) + + log.Printf("Go Backend running on http://localhost%s\n", PORT) + err := http.ListenAndServe(PORT, handler) + if err != nil { + log.Fatal(err) + } +} diff --git a/backend/public/index.html b/backend/public/index.html new file mode 100644 index 0000000..a0fa1d8 --- /dev/null +++ b/backend/public/index.html @@ -0,0 +1,163 @@ + + + + + + MSDL Backend API Test + + + + +

MSDL API Test UI

+ +
+

Step 1: Get Languages (SKU Info)

+
+ + + +
+ +
+
+ + + +
+

API Response

+
Ready.
+
+ + + + diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..820919e --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +# Copy to .env.local and set your backend URL +VITE_API_URL=http://localhost:3002 diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..7dbf7eb --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,73 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs) +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) + +## React Compiler + +The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + tseslint.configs.stylisticTypeChecked, + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x' +import reactDom from 'eslint-plugin-react-dom' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended, + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname, + }, + // other options... + }, + }, +]) +``` diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5e6b472 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,23 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import { defineConfig, globalIgnores } from 'eslint/config' + +export default defineConfig([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + js.configs.recommended, + tseslint.configs.recommended, + reactHooks.configs.flat.recommended, + reactRefresh.configs.vite, + ], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + }, +]) diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..8e8497d --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,18 @@ + + + + + + + Windows ISO Downloader | Official Microsoft Images + + + + + + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..178a815 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,41 @@ +{ + "name": "msdl-frontend-v2", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "@fontsource-variable/geist": "^5.2.8", + "@radix-ui/react-accordion": "^1.2.12", + "@radix-ui/react-select": "^2.2.6", + "geist": "^1.7.0", + "lucide-react": "^1.7.0", + "motion": "^12.38.0", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router-dom": "^7.13.2", + "sonner": "^2.0.7", + "tslib": "^2.8.1" + }, + "devDependencies": { + "@eslint/js": "^9.39.4", + "@tailwindcss/vite": "^4.2.2", + "@types/node": "^24.12.0", + "@types/react": "^19.2.14", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^6.0.1", + "eslint": "^9.39.4", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.4.0", + "tailwindcss": "^4.2.2", + "typescript": "~5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^8.0.1" + } +} diff --git a/frontend/public/_redirects b/frontend/public/_redirects new file mode 100644 index 0000000..7797f7c --- /dev/null +++ b/frontend/public/_redirects @@ -0,0 +1 @@ +/* /index.html 200 diff --git a/frontend/public/data/products.json b/frontend/public/data/products.json new file mode 100644 index 0000000..ee8ad0f --- /dev/null +++ b/frontend/public/data/products.json @@ -0,0 +1,18 @@ +{ + "48": "Windows 8.1 Single Language (9600.17415)", + "52": "Windows 8.1 (9600.17415)", + "2378": "Windows 10 22H2 Home China (19045.2006)", + "2618": "Windows 10 22H2 v1 (19045.2965)", + "3113": "Windows 11 24H2 (26100.1742)", + "3114": "Windows 11 24H2 Home China (26100.1742)", + "3115": "Windows 11 24H2 Pro China (26100.1742)", + "3131": "Windows 11 Arm64 24H2 (26100.1742)", + "3132": "Windows 11 Arm64 24H2 Home China (26100.1742)", + "3133": "Windows 11 Arm64 24H2 Pro China (26100.1742)", + "3262": "Windows 11 25H2 (26200.6584)", + "3263": "Windows 11 25H2 Home China (26200.6584)", + "3264": "Windows 11 25H2 Pro China (26200.6584)", + "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)" +} \ No newline at end of file diff --git a/frontend/public/favicon.svg b/frontend/public/favicon.svg new file mode 100644 index 0000000..6893eb1 --- /dev/null +++ b/frontend/public/favicon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/public/icons.svg b/frontend/public/icons.svg new file mode 100644 index 0000000..e952219 --- /dev/null +++ b/frontend/public/icons.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..8dc178e --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,42 @@ +import { Routes, Route } from 'react-router-dom' +import { Toaster } from 'sonner' +import Dock from './components/Dock' +import SiteFooter from './components/SiteFooter' +import HomePage from './pages/HomePage' +import ProductsPage from './pages/ProductsPage' +import ProductDetailPage from './pages/ProductDetailPage' +import AboutPage from './pages/AboutPage' +import PrivacyPolicyPage from './pages/PrivacyPolicyPage' +import DisclaimerPage from './pages/DisclaimerPage' +import ScrollToTop from './components/ScrollToTop' + +export default function App() { + return ( +
+ + + } /> + } /> + } /> + } /> + } /> + } /> + + + + +
+ ) +} diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png new file mode 100644 index 0000000..cc51a3d Binary files /dev/null and b/frontend/src/assets/hero.png differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg new file mode 100644 index 0000000..5101b67 --- /dev/null +++ b/frontend/src/assets/vite.svg @@ -0,0 +1 @@ +Vite diff --git a/frontend/src/components/Aria2Tip.tsx b/frontend/src/components/Aria2Tip.tsx new file mode 100644 index 0000000..cf23a7e --- /dev/null +++ b/frontend/src/components/Aria2Tip.tsx @@ -0,0 +1,78 @@ +import { useState } from 'react' +import { motion, AnimatePresence } from 'motion/react' +import { Terminal, ChevronRight, Copy, Check } from 'lucide-react' +import { toast } from 'sonner' + +interface Aria2TipProps { + downloadUrl?: string +} + +export default function Aria2Tip({ downloadUrl }: Aria2TipProps) { + const [open, setOpen] = useState(false) + const [copied, setCopied] = useState(false) + + const urlPlaceholder = downloadUrl || 'PASTE_YOUR_LINK_HERE' + const command = `aria2c -x 16 -s 16 "${urlPlaceholder}"` + + function handleCopy() { + navigator.clipboard.writeText(command) + setCopied(true) + toast.success('Command copied!') + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+ + + + {open && ( + +
+
+ Terminal + +
+
+ + $ + {command} + +
+ {!downloadUrl && ( +

+ Generate a link above, then paste it into this command for 16× speed. +

+ )} +
+
+ )} +
+
+ ) +} diff --git a/frontend/src/components/Badge.tsx b/frontend/src/components/Badge.tsx new file mode 100644 index 0000000..e05c38b --- /dev/null +++ b/frontend/src/components/Badge.tsx @@ -0,0 +1,34 @@ +interface BadgeProps { + variant: 'latest' | 'stable' | 'eol' | 'legacy' + label?: string +} + +const config = { + latest: { + label: 'LATEST', + className: 'bg-blue-500/15 text-blue-400 border-blue-500/25', + }, + stable: { + label: 'STABLE', + className: 'bg-green-500/15 text-green-400 border-green-500/25', + }, + eol: { + label: 'END OF LIFE', + className: 'bg-amber-500/15 text-amber-400 border-amber-500/25', + }, + legacy: { + label: 'LEGACY', + className: 'bg-zinc-500/15 text-zinc-400 border-zinc-500/25', + }, +} + +export default function Badge({ variant, label }: BadgeProps) { + const { label: defaultLabel, className } = config[variant] + return ( + + {label ?? defaultLabel} + + ) +} diff --git a/frontend/src/components/ComparisonTable.tsx b/frontend/src/components/ComparisonTable.tsx new file mode 100644 index 0000000..883a061 --- /dev/null +++ b/frontend/src/components/ComparisonTable.tsx @@ -0,0 +1,77 @@ +import { motion } from 'motion/react' +import { Check, X, Minus } from 'lucide-react' + +type CellValue = true | false | 'varies' + +interface Row { + feature: string + msdl: CellValue + official: CellValue + others: CellValue +} + +const rows: Row[] = [ + { feature: 'Direct CDN link', msdl: true, official: true, others: false }, + { feature: 'No browser required', msdl: true, official: false, others: 'varies' }, + { feature: 'ARM64 support', msdl: true, official: false, others: false }, + { feature: 'No account needed', msdl: true, official: true, others: 'varies' }, + { feature: 'No ads or tracking', msdl: true, official: true, others: false }, +] + +function Cell({ value, highlight = false }: { value: CellValue; highlight?: boolean }) { + if (value === true) + return + if (value === false) + return + return +} + +export default function ComparisonTable() { + return ( +
+ +

Why MSDL?

+

Compared to your other options.

+ + {/* Scrollable wrapper on mobile to prevent page-level overflow */} +
+
+ {/* Header */} +
+
Feature
+
MSDL
+
Official
+
Others
+
+ + {/* Rows */} + {rows.map((row, i) => ( +
+
{row.feature}
+
+ +
+
+ +
+
+ +
+
+ ))} +
+
+ +

← scroll →

+
+
+ ) +} diff --git a/frontend/src/components/Dock.tsx b/frontend/src/components/Dock.tsx new file mode 100644 index 0000000..3c3c48b --- /dev/null +++ b/frontend/src/components/Dock.tsx @@ -0,0 +1,125 @@ +import { useState, useEffect } from 'react' +import { useLocation, useNavigate } from 'react-router-dom' +import { motion } from 'motion/react' +import { Home, LayoutGrid, Info, ExternalLink, Shield } from 'lucide-react' + +interface DockItem { + label: string + icon: React.ReactNode + path?: string + href?: string +} + +const items: DockItem[] = [ + { label: 'Home', icon: , path: '/' }, + { label: 'Products', icon: , path: '/products' }, + { label: 'About', icon: , path: '/about' }, + { label: 'Privacy', icon: , path: '/privacy-policy' }, + { label: 'TechLatest', icon: , href: 'https://tech-latest.com' }, +] + +export default function Dock() { + const location = useLocation() + const navigate = useNavigate() + const [activeIndex, setActiveIndex] = useState(0) + + useEffect(() => { + const idx = items.findIndex(item => item.path && item.path === location.pathname) + if (idx !== -1) setActiveIndex(idx) + }, [location.pathname]) + + function handleClick(item: DockItem, index: number) { + setActiveIndex(index) + if (item.href) { + window.open(item.href, '_blank', 'noopener noreferrer') + } else if (item.path) { + navigate(item.path) + } + } + + return ( + <> + {/* ── DESKTOP DOCK (sm+) — centered floating pill ── */} +
+ + {items.map((item, i) => { + const isActive = i === activeIndex + return ( + handleClick(item, i)} + whileHover={{ scale: 1.06, y: -2 }} + whileTap={{ scale: 0.93 }} + transition={{ type: 'spring', stiffness: 400, damping: 22 }} + className={` + relative flex items-center gap-2 rounded-xl cursor-pointer px-3 py-2 + transition-all duration-200 + ${isActive + ? 'bg-white/10 border border-white/15 text-white' + : 'border border-transparent text-zinc-500 hover:text-zinc-300' + } + `} + aria-label={item.label} + > + {item.icon} + + {/* Expanding active label */} + + {item.label} + + + {/* Active dot */} + {isActive && ( + + )} + + ) + })} + +
+ + {/* ── MOBILE DOCK ( + {items.map((item, i) => { + const isActive = i === activeIndex + return ( + + ) + })} + + + ) +} diff --git a/frontend/src/components/FAQAccordion.tsx b/frontend/src/components/FAQAccordion.tsx new file mode 100644 index 0000000..8e68069 --- /dev/null +++ b/frontend/src/components/FAQAccordion.tsx @@ -0,0 +1,92 @@ +import { useState } from 'react' +import { motion, AnimatePresence } from 'motion/react' +import { ChevronRight } from 'lucide-react' + +interface FaqItem { + q: string + a: string +} + +const faqs: FaqItem[] = [ + { + q: 'Is this legal?', + a: 'Yes. MSDL does not host, modify, or redistribute any Microsoft files. It automates access to download links that Microsoft already provides for free at microsoft.com/software-download — the same way Rufus and the open-source Fido script do. Microsoft itself recommends Rufus (which uses the identical approach) in its official documentation. You are downloading a file directly from Microsoft\'s own servers. A valid Windows license is still required to activate the OS.', + }, + { + q: 'Are these download links safe?', + a: 'Yes. MSDL never stores or modifies any files. It retrieves signed, time-limited download links directly from Microsoft\'s servers (software.download.prss.microsoft.com). You can verify this by inspecting your browser\'s network requests.', + }, + { + q: 'Why do links expire in 24 hours?', + a: 'Microsoft generates short-lived, IP-tied signed URLs to prevent redistribution and hotlinking of their ISOs. This is the same link you\'d get from the official Microsoft Software Download page — we just make it accessible without a browser.', + }, + { + q: 'Can I use this with IDM or aria2?', + a: 'Absolutely — and we strongly recommend it. Microsoft throttles single-threaded browser downloads. Tools like aria2 (aria2c -x 16 -s 16 "URL") or IDM use 16 parallel connections, which typically give you full line speed on their CDN.', + }, + { + q: 'What\'s the difference between the various product IDs?', + a: 'Each Windows release has a unique product ID on Microsoft\'s servers. For example, 3262 is Windows 11 25H2 (x64) and 3265 is Windows 11 25H2 (ARM64). MSDL exposes all known IDs so you can always find the exact build you need.', + }, + { + q: 'Is MSDL open source?', + a: 'Yes. Both the frontend and the backend proxy are fully open source. The backend implements the same session-based flow as the Fido PowerShell script, ported to Go and Node.js. You can self-host the entire stack.', + }, +] + +export default function FAQAccordion() { + const [open, setOpen] = useState(null) + + return ( +
+ +

Frequently asked questions

+

Everything you need to know before downloading.

+ +
+ {faqs.map((faq, i) => ( +
+ + + + {open === i && ( + +

+ {faq.a} +

+
+ )} +
+
+ ))} +
+
+
+ ) +} diff --git a/frontend/src/components/HowItWorks.tsx b/frontend/src/components/HowItWorks.tsx new file mode 100644 index 0000000..580a93a --- /dev/null +++ b/frontend/src/components/HowItWorks.tsx @@ -0,0 +1,62 @@ +import { motion } from 'motion/react' +import { LayoutGrid, Globe, Link2 } from 'lucide-react' + +const steps = [ + { + num: '1', + icon: LayoutGrid, + title: 'Pick a release', + desc: 'Choose from Windows 11, 10, and 8.1 across all feature updates.', + }, + { + num: '2', + icon: Globe, + title: 'Choose language', + desc: '38 localised languages available for every release.', + }, + { + num: '3', + icon: Link2, + title: 'Get CDN link', + desc: 'Direct from Microsoft\'s servers. No proxy, no middleman.', + }, +] + +export default function HowItWorks() { + return ( +
+ +

How it works

+

Three steps to your official Windows ISO.

+ +
+ {steps.map((step, i) => { + const Icon = step.icon + return ( +
+
+ + {i + 1} + + +
+
+

{step.title}

+

{step.desc}

+
+
+ ) + })} +
+
+
+ ) +} diff --git a/frontend/src/components/ProductCard.tsx b/frontend/src/components/ProductCard.tsx new file mode 100644 index 0000000..5f66901 --- /dev/null +++ b/frontend/src/components/ProductCard.tsx @@ -0,0 +1,79 @@ +import { motion } from 'motion/react' +import { Download, ArrowRight } from 'lucide-react' +import Badge from './Badge' + +const WinLogo = ({ className }: { className?: string }) => ( + + + +) + +type BadgeVariant = 'latest' | 'stable' | 'eol' | 'legacy' + +interface ProductCardProps { + id: string + name: string + version: string + build: string + description: string + badge: BadgeVariant + archs?: string[] + onClick: () => void +} + +export default function ProductCard({ + name, + version, + build, + description, + badge, + archs = ['x64'], + onClick, +}: ProductCardProps) { + return ( + + {/* Header row */} +
+
+ +
+ +
+ + {/* Title */} +
+

{name} {version}

+

Build {build}

+
+ + {/* Description */} +

{description}

+ + {/* Footer */} +
+
+ {archs.map(arch => ( + + {arch} + + ))} +
+
+ + Get links + +
+
+
+ ) +} diff --git a/frontend/src/components/RelatedReleases.tsx b/frontend/src/components/RelatedReleases.tsx new file mode 100644 index 0000000..f9e9ae4 --- /dev/null +++ b/frontend/src/components/RelatedReleases.tsx @@ -0,0 +1,38 @@ +import { useNavigate } from 'react-router-dom' +import { ArrowRight } from 'lucide-react' + +interface RelatedProduct { + id: string + label: string +} + +interface RelatedReleasesProps { + current: string + items: RelatedProduct[] +} + +export default function RelatedReleases({ current, items }: RelatedReleasesProps) { + const navigate = useNavigate() + const filtered = items.filter(i => i.id !== current) + if (filtered.length === 0) return null + + return ( +
+

+ Also available +

+
+ {filtered.map(item => ( + + ))} +
+
+ ) +} diff --git a/frontend/src/components/ScrollToTop.tsx b/frontend/src/components/ScrollToTop.tsx new file mode 100644 index 0000000..6b85a02 --- /dev/null +++ b/frontend/src/components/ScrollToTop.tsx @@ -0,0 +1,12 @@ +import { useEffect } from 'react' +import { useLocation } from 'react-router-dom' + +export default function ScrollToTop() { + const { pathname } = useLocation() + + useEffect(() => { + window.scrollTo({ top: 0, behavior: 'instant' }) + }, [pathname]) + + return null +} diff --git a/frontend/src/components/SiteFooter.tsx b/frontend/src/components/SiteFooter.tsx new file mode 100644 index 0000000..49c57aa --- /dev/null +++ b/frontend/src/components/SiteFooter.tsx @@ -0,0 +1,29 @@ +import { Link } from 'react-router-dom' + +export default function SiteFooter() { + return ( +
+

+ This open-source project is not affiliated with Microsoft Corporation.{' '} + Windows is a registered trademark of Microsoft Corporation.{' '} + All ISO files are hosted on Microsoft's official CDN. +

+
+ Privacy Policy + · + Disclaimer + · + About + · + + GitHub + +
+
+ ) +} diff --git a/frontend/src/components/StatsBar.tsx b/frontend/src/components/StatsBar.tsx new file mode 100644 index 0000000..52c41fb --- /dev/null +++ b/frontend/src/components/StatsBar.tsx @@ -0,0 +1,33 @@ +import { motion } from 'motion/react' + +interface Stat { + value: string + label: string +} + +const stats: Stat[] = [ + { value: '17', label: 'releases available' }, + { value: '38', label: 'languages' }, + { value: '0', label: 'intermediaries' }, +] + +export default function StatsBar() { + return ( + + {stats.map((s, i) => ( + + + {s.value} + {' '}{s.label} + + {i < stats.length - 1 && ·} + + ))} + + ) +} diff --git a/frontend/src/components/SystemRequirements.tsx b/frontend/src/components/SystemRequirements.tsx new file mode 100644 index 0000000..ca81a67 --- /dev/null +++ b/frontend/src/components/SystemRequirements.tsx @@ -0,0 +1,42 @@ +import { Cpu, MemoryStick, HardDrive, Shield } from 'lucide-react' + +interface Requirement { + icon: React.ReactNode + value: string + label: string +} + +interface SystemRequirementsProps { + isWin11?: boolean +} + +export default function SystemRequirements({ isWin11 = false }: SystemRequirementsProps) { + const requirements: Requirement[] = [ + { icon: , value: '1 GHz+', label: 'Processor' }, + { icon: , value: isWin11 ? '4 GB' : '2 GB', label: 'RAM' }, + { icon: , value: '64 GB', label: 'Storage' }, + ] + + return ( +
+

+ System Requirements +

+
+ {requirements.map(req => ( +
+ {req.icon} + {req.value} + {req.label} +
+ ))} +
+ {isWin11 && ( +
+ + TPM 2.0 required for Windows 11 +
+ )} +
+ ) +} diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..9f9d4ff --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,57 @@ +@import "tailwindcss"; +@import "@fontsource-variable/geist"; + +@theme { + /* Background layers */ + --color-bg: #09090b; + --color-surface: #111113; + --color-surface-2: #18181b; + + /* Borders */ + --color-border: rgba(255, 255, 255, 0.07); + --color-border-hover: rgba(255, 255, 255, 0.13); + + /* Accent */ + --color-accent: #3b82f6; + --color-accent-hover: #2563eb; + + /* Text */ + --color-text: #fafafa; + --color-text-2: #a1a1aa; + --color-text-muted: #52525b; + + /* Status */ + --color-success: #22c55e; + --color-warning: #f59e0b; + --color-error: #ef4444; + + /* Font families */ + --font-sans: 'Geist Variable', 'Geist', 'Inter', ui-sans-serif, system-ui, sans-serif; + --font-mono: 'Geist Mono Variable', 'Geist Mono', 'JetBrains Mono', ui-monospace, monospace; +} + +* { + box-sizing: border-box; +} + +html { + background-color: #09090b; + color: #fafafa; + font-family: var(--font-sans); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + scroll-padding-bottom: 120px; + overflow-x: hidden; +} + +body { + margin: 0; + min-height: 100vh; + overflow-x: hidden; +} + +/* Thin, minimal scrollbar */ +::-webkit-scrollbar { width: 4px; height: 4px; } +::-webkit-scrollbar-track { background: transparent; } +::-webkit-scrollbar-thumb { background: #27272a; border-radius: 2px; } +::-webkit-scrollbar-thumb:hover { background: #3f3f46; } diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..ade9d64 --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import { BrowserRouter } from 'react-router-dom' +import './index.css' +import App from './App.tsx' + +createRoot(document.getElementById('root')!).render( + + + + + , +) diff --git a/frontend/src/pages/AboutPage.tsx b/frontend/src/pages/AboutPage.tsx new file mode 100644 index 0000000..6223aa2 --- /dev/null +++ b/frontend/src/pages/AboutPage.tsx @@ -0,0 +1,110 @@ +import { motion } from 'motion/react' + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+
{children}
+
+ ) +} + +export default function AboutPage() { + return ( +
+ +
+

About MSDL

+

A TechLatest open-source project

+
+ +
+

+ MSDL (Microsoft Software Download Links) is a clean, open-source web tool for + obtaining official Windows ISO files directly from Microsoft's content delivery network + — without needing a Windows machine, the Media Creation Tool, or a browser lock check. +

+

+ There are no third-party hosts, no redirects, no registration, and no ads. + Every link you receive is exactly the same signed URL you would get from + Microsoft's own download page. +

+
+ +
+

+ Our backend replicates the session-based authentication flow that Microsoft uses to + serve download links to end users. The same approach is used by{' '} + + Fido + {' '} + (the PowerShell script bundled with Rufus). The flow is: +

+
    +
  1. Register a session with Microsoft's tracking endpoint
  2. +
  3. Fetch and parse the MDT fingerprinting script
  4. +
  5. Call the SKU info API to retrieve available languages
  6. +
  7. Call the download links API using the warmed session to get signed CDN URLs
  8. +
+

+ Links are IP-tied to the server and expire after 24 hours — this is standard + Microsoft behaviour, not a limitation of MSDL. +

+
+ +
+

+ All product data (release names, build numbers, and available architectures) is sourced + from Microsoft's official software download connector API: +

+

+ + www.microsoft.com/software-download-connector/api/ + +

+

+ Product IDs are maintained manually based on Microsoft's release cadence. + Windows 11 25H2, 24H2, Windows 10 22H2, and Windows 8.1 are currently listed. + New releases are added as Microsoft publishes them. +

+
+ +
+

+ The underlying session flow is inspired by{' '} + + Fido + {' '} + by Pete Batard, the same mechanism powering Rufus's ISO download feature. +

+

+ Built and maintained by{' '} + + TechLatest + . +

+
+
+
+ ) +} diff --git a/frontend/src/pages/DisclaimerPage.tsx b/frontend/src/pages/DisclaimerPage.tsx new file mode 100644 index 0000000..2f4b053 --- /dev/null +++ b/frontend/src/pages/DisclaimerPage.tsx @@ -0,0 +1,75 @@ +import { motion } from 'motion/react' + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+
{children}
+
+ ) +} + +export default function DisclaimerPage() { + return ( +
+ +
+

Disclaimer

+

Last updated: April 9, 2026

+
+ +
+ This open-source project is not affiliated with, endorsed by, or sponsored by + Microsoft Corporation in any way. Windows is a registered trademark of Microsoft Corporation. +
+ +
+

+ MSDL is an independent, open-source project. It is not produced, approved, or supported + by Microsoft Corporation. The name "Windows" and the Windows logo are registered + trademarks of Microsoft Corporation. +

+
+ +
+

+ MSDL exists solely to make it easier for users to access official, unmodified Microsoft + Windows ISO files that Microsoft already makes freely available for download. All files + are served directly from Microsoft's own servers — we do not host, mirror, or modify + any content. +

+
+ +
+

+ This tool is provided "as is" without warranty of any kind. We make no guarantees + about the availability of Microsoft's download links, the uptime of our backend, or + the compatibility of any downloaded ISO with your hardware configuration. +

+
+ +
+

+ By using this tool, you agree that you are solely responsible for complying with + Microsoft's End User License Agreement (EULA) and any applicable laws in your jurisdiction. + Downloading Windows does not grant you a license to use it — a valid product key or + digital license is required for activation. +

+
+ +
+

+ The source code is publicly available. You are free to inspect, fork, and self-host + this project under the terms of its open-source license. The project credits the + open-source Fido script by Pete Batard for the underlying session flow. +

+
+
+
+ ) +} diff --git a/frontend/src/pages/HomePage.tsx b/frontend/src/pages/HomePage.tsx new file mode 100644 index 0000000..23c358d --- /dev/null +++ b/frontend/src/pages/HomePage.tsx @@ -0,0 +1,153 @@ +import { useNavigate } from 'react-router-dom' +import { motion } from 'motion/react' +import { ArrowRight } from 'lucide-react' +import ProductCard from '../components/ProductCard' +import StatsBar from '../components/StatsBar' +import HowItWorks from '../components/HowItWorks' +import ComparisonTable from '../components/ComparisonTable' +import FAQAccordion from '../components/FAQAccordion' + +const featured = [ + { + id: '3262', + name: 'Windows 11', + version: '25H2', + build: '26200.6584', + description: 'The latest Windows 11 release with AI features and improved performance.', + badge: 'latest' as const, + archs: ['x64', 'ARM64'], + }, + { + id: '3113', + name: 'Windows 11', + version: '24H2', + build: '26100.1742', + description: 'The widely deployed stable release. Recommended for enterprise environments.', + badge: 'stable' as const, + archs: ['x64', 'ARM64'], + }, + { + id: '2618', + name: 'Windows 10', + version: '22H2', + build: '19045.2965', + description: 'The final Windows 10 feature update. Security support until October 2025.', + badge: 'eol' as const, + archs: ['x64', 'x86'], + }, + { + id: '52', + name: 'Windows 8.1', + version: 'RTM', + build: '9600.17415', + description: 'Legacy release for older hardware compatibility and historical reference.', + badge: 'legacy' as const, + archs: ['x64', 'x86'], + }, +] + +const container = { + hidden: {}, + show: { transition: { staggerChildren: 0.08 } }, +} + +const cardVariant = { + hidden: { opacity: 0, y: 20 }, + show: { opacity: 1, y: 0, transition: { type: 'spring' as const, stiffness: 280, damping: 28 } }, +} + +export default function HomePage() { + const navigate = useNavigate() + + return ( +
+ + {/* Ambient glow — fixed so it never affects document layout/scroll */} +
+ + {/* Hero */} + +

+ Official Microsoft ISOs +

+

+ Windows ISO Downloader +

+

+ Direct links from Microsoft's CDN. No ads. No registration. +

+
+ + + Always official + + · + + + Real-time links + + · + + + Free forever + +
+
+ + {/* Stats bar */} +
+ +
+ + {/* Featured grid */} + + {featured.map(product => ( + + navigate(`/product/${product.id}`)} + /> + + ))} + + + {/* Browse all CTA */} + + + + + {/* How it works */} + + + {/* Comparison table */} + + + {/* FAQ */} + +
+ ) +} diff --git a/frontend/src/pages/PrivacyPolicyPage.tsx b/frontend/src/pages/PrivacyPolicyPage.tsx new file mode 100644 index 0000000..05267b0 --- /dev/null +++ b/frontend/src/pages/PrivacyPolicyPage.tsx @@ -0,0 +1,78 @@ +import { motion } from 'motion/react' + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+
{children}
+
+ ) +} + +export default function PrivacyPolicyPage() { + return ( +
+ +
+

Privacy Policy

+

Last updated: April 9, 2026

+
+ +
+

+ MSDL does not collect, store, log, or share any personally identifiable information. + There are no user accounts, no cookies, no analytics trackers, and no third-party + advertising scripts of any kind. +

+
+ +
+

+ When you request a download link, our backend server contacts Microsoft's software + download API on your behalf using a non-Windows browser user-agent (the same method + used by open-source tools like Rufus/Fido). Microsoft's servers return a time-limited, + IP-tied signed URL. +

+

+ Our backend does not log your IP address, does not store the generated link, and does + not proxy the actual file download — the ISO is downloaded directly from Microsoft's CDN. +

+
+ +
+

+ All Windows ISO files are hosted on Microsoft's official content delivery network + (software.download.prss.microsoft.com). + We are not responsible for Microsoft's privacy practices or link availability. +

+
+ +
+

+ The full source code for both the frontend and backend is publicly available. + You can inspect, audit, or self-host the entire stack. +

+
+ +
+

+ Questions? Reach us via{' '} + + TechLatest + . +

+
+
+
+ ) +} diff --git a/frontend/src/pages/ProductDetailPage.tsx b/frontend/src/pages/ProductDetailPage.tsx new file mode 100644 index 0000000..dacb0bf --- /dev/null +++ b/frontend/src/pages/ProductDetailPage.tsx @@ -0,0 +1,403 @@ +import { useState, useEffect } from 'react' +import { useParams, useNavigate } from 'react-router-dom' +import { motion, AnimatePresence } from 'motion/react' +import * as Select from '@radix-ui/react-select' +import { ArrowLeft, ChevronDown, Download, AlertTriangle, Check, ExternalLink } from 'lucide-react' +import { toast } from 'sonner' +import type { Sku, DownloadOption } from '../types' +import SystemRequirements from '../components/SystemRequirements' +import Aria2Tip from '../components/Aria2Tip' +import RelatedReleases from '../components/RelatedReleases' + +const API_BASE = import.meta.env.VITE_API_URL ?? 'http://localhost:3002' + +// Map product ID → metadata +const PRODUCT_META: Record = { + '3262': { badge: 'LATEST', archs: ['x64'] }, + '3265': { badge: 'LATEST', archs: ['ARM64'] }, + '3321': { badge: 'LATEST', archs: ['x64'] }, + '3324': { badge: 'LATEST', archs: ['ARM64'] }, + '3113': { badge: 'STABLE', archs: ['x64'] }, + '3131': { badge: 'STABLE', archs: ['ARM64'] }, + '2618': { badge: 'EOL SOON', archs: ['x64', 'x86'] }, + '2378': { badge: 'EOL SOON', archs: ['x64'] }, + '52': { badge: 'LEGACY', archs: ['x64', 'x86'] }, + '48': { badge: 'LEGACY', archs: ['x64', 'x86'] }, +} + +// Sibling release groups for "Also available" +const RELATED_GROUPS: Record = { + '3262': [ + { id: '3265', label: 'Win 11 25H2 ARM64' }, + { id: '3321', label: 'Win 11 25H2 (updated)' }, + { id: '3113', label: 'Win 11 24H2' }, + ], + '3265': [ + { id: '3262', label: 'Win 11 25H2 x64' }, + { id: '3321', label: 'Win 11 25H2 (updated)' }, + { id: '3131', label: 'Win 11 24H2 ARM64' }, + ], + '3321': [ + { id: '3262', label: 'Win 11 25H2' }, + { id: '3324', label: 'Win 11 25H2 ARM64' }, + { id: '3113', label: 'Win 11 24H2' }, + ], + '3324': [ + { id: '3321', label: 'Win 11 25H2 x64' }, + { id: '3262', label: 'Win 11 25H2' }, + { id: '3265', label: 'Win 11 25H2 ARM64 (alt)' }, + ], + '3113': [ + { id: '3262', label: 'Win 11 25H2' }, + { id: '3131', label: 'Win 11 24H2 ARM64' }, + { id: '2618', label: 'Win 10 22H2' }, + ], + '3131': [ + { id: '3265', label: 'Win 11 25H2 ARM64' }, + { id: '3113', label: 'Win 11 24H2 x64' }, + ], + '2618': [ + { id: '3113', label: 'Win 11 24H2' }, + { id: '3262', label: 'Win 11 25H2' }, + { id: '52', label: 'Win 8.1' }, + ], + '52': [ + { id: '48', label: 'Win 8.1 Single Language' }, + { id: '2618', label: 'Win 10 22H2' }, + ], +} + +function WinLogo({ className }: { className?: string }) { + return ( + + + + ) +} + +function archFromUri(uri: string): string { + const name = uri.split('/').pop()?.split('?')[0]?.toLowerCase() ?? '' + if (name.includes('arm')) return 'ARM64' + if (name.includes('x64') || name.includes('64')) return 'x64' + if (name.includes('x32') || name.includes('32') || name.includes('x86')) return 'x86' + return 'ISO' +} + +function isWin11(productId: string): boolean { + const id = Number(productId) + return id >= 3113 +} + +export default function ProductDetailPage() { + const { productId } = useParams<{ productId: string }>() + const navigate = useNavigate() + + const [productName, setProductName] = useState('') + const [buildStr, setBuildStr] = useState('') + const [languages, setLanguages] = useState([]) + const [selectedSku, setSelectedSku] = useState(null) + const [downloadLinks, setDownloadLinks] = useState([]) + const [isLoadingLangs, setIsLoadingLangs] = useState(true) + const [isFetching, setIsFetching] = useState(false) + const [error, setError] = useState(null) + const [copiedUri, setCopiedUri] = useState(null) + + const meta = PRODUCT_META[productId!] ?? { badge: '', archs: ['x64'] } + const related = RELATED_GROUPS[productId!] ?? [] + + // Load product name + build string + useEffect(() => { + fetch('/data/products.json') + .then(r => r.json()) + .then((data: Record) => { + const name = data[productId!] ?? `Product ${productId}` + setProductName(name) + // Extract build from parentheses e.g. "(26200.6584)" + const match = name.match(/\(([^)]+)\)/) + if (match) setBuildStr(match[1]) + }) + .catch(() => setProductName(`Product ${productId}`)) + }, [productId]) + + // Fetch languages on mount + useEffect(() => { + if (!productId) return + setIsLoadingLangs(true) + setError(null) + setDownloadLinks([]) + setSelectedSku(null) + + fetch(`${API_BASE}/skuinfo?product_id=${productId}`) + .then(r => r.json()) + .then(data => { + if (data.error) throw new Error(data.error) + if (!data.Skus?.length) throw new Error('No languages found for this product.') + setLanguages(data.Skus) + // Default to English if present + const english = data.Skus.find((s: Sku) => + s.LocalizedLanguage.toLowerCase().includes('english') && + !s.LocalizedLanguage.toLowerCase().includes('international') + ) ?? data.Skus[0] + setSelectedSku(english) + }) + .catch(e => setError(e.message)) + .finally(() => setIsLoadingLangs(false)) + }, [productId]) + + async function handleGetLinks() { + if (!selectedSku || !productId) return + setIsFetching(true) + setError(null) + setDownloadLinks([]) + + fetch(`${API_BASE}/proxy?product_id=${productId}&sku_id=${selectedSku.Id}`) + .then(r => r.json()) + .then(data => { + if (data.error) throw new Error(data.error) + if (!data.ProductDownloadOptions?.length) throw new Error('No download links returned.') + setDownloadLinks(data.ProductDownloadOptions) + }) + .catch(e => { + setError(e.message) + toast.error(e.message) + }) + .finally(() => setIsFetching(false)) + } + + function handleCopy(uri: string) { + navigator.clipboard.writeText(uri) + setCopiedUri(uri) + toast.success('Link copied to clipboard!') + setTimeout(() => setCopiedUri(null), 2000) + } + + // First download link URI for aria2 tip + const firstUri = downloadLinks[0]?.Uri + + return ( +
+ {/* Back */} + + + + {/* Product header */} +
+
+ +
+
+

+ {productName || `Product ${productId}`} +

+ {buildStr && ( +

Build {buildStr}

+ )} + {meta.archs.length > 0 && ( +
+ {meta.archs.map(arch => ( + + {arch} + + ))} + {meta.badge && ( + + {meta.badge} + + )} +
+ )} +
+
+ + {/* Main card */} +
+ {isLoadingLangs ? ( +
+
+
+
+
+ ) : error && !languages.length ? ( +
+ +
+

Failed to load languages

+

{error}

+
+
+ ) : ( + <> + {/* Language selector */} +
+ + { + const sku = languages.find(l => l.Id === val) + if (sku) setSelectedSku(sku) + setDownloadLinks([]) + setError(null) + }} + > + + + + + + + + + + + {languages.map(lang => ( + + {lang.LocalizedLanguage} + + + + + ))} + + + + +
+ + {/* CTA */} + + + )} + + {/* Error after fetch */} + + {error && languages.length > 0 && ( + + + {error} + + )} + + + {/* Download links */} + + {downloadLinks.length > 0 && ( + +
+ {/* Warning */} +
+ + Links expire in 24 hours · IP-tied · Use a download manager for full speed +
+ + {/* Links */} + {downloadLinks.map(link => { + const filename = link.Uri.split('/').pop()?.split('?')[0] ?? 'download.iso' + const arch = link.Architecture || archFromUri(link.Uri) + const isCopied = copiedUri === link.Uri + return ( +
+
+

{filename}

+

{arch}

+
+
+ + + + {arch} + + +
+
+ ) + })} +
+
+ )} +
+
+ + {/* System requirements */} + + + {/* aria2 tip */} + + + {/* Related releases */} + {related.length > 0 && ( + + )} + +
+ ) +} diff --git a/frontend/src/pages/ProductsPage.tsx b/frontend/src/pages/ProductsPage.tsx new file mode 100644 index 0000000..6d701de --- /dev/null +++ b/frontend/src/pages/ProductsPage.tsx @@ -0,0 +1,123 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { motion } from 'motion/react' +import { Search, ArrowRight } from 'lucide-react' +import type { Product } from '../types' + +const quickSearches = [ + { label: 'Windows 11 25H2', query: '25H2' }, + { label: 'Windows 11 24H2', query: '24H2' }, + { label: 'Windows 10', query: 'Windows 10' }, + { label: 'Windows 8.1', query: 'Windows 8.1' }, +] + +export default function ProductsPage() { + const navigate = useNavigate() + const [products, setProducts] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [query, setQuery] = useState('') + + useEffect(() => { + fetch('/data/products.json') + .then(r => r.json()) + .then((data: Record) => { + setProducts(Object.entries(data).map(([id, name]) => ({ id, name }))) + }) + .finally(() => setIsLoading(false)) + }, []) + + const filtered = products.filter(p => + p.name.toLowerCase().includes(query.toLowerCase()) + ) + + return ( +
+ +

All Products

+

+ {products.length} versions available +

+ + {/* Search */} +
+ + setQuery(e.target.value)} + placeholder="Search Windows versions..." + className="w-full bg-white/5 border border-white/8 rounded-xl pl-10 pr-4 py-3 text-sm text-white placeholder:text-white/25 focus:outline-none focus:border-white/20 focus:bg-white/8 transition-all" + /> +
+ + {/* Quick filters */} +
+ {quickSearches.map(s => ( + + ))} + {query && ( + + )} +
+ + {/* List */} + {isLoading ? ( +
+ {[...Array(8)].map((_, i) => ( +
+ ))} +
+ ) : filtered.length === 0 ? ( +

No products found.

+ ) : ( + + {filtered.map(product => ( + + + + ))} + + )} + +
+ ) +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..de5bda3 --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,24 @@ +export interface Sku { + Id: string + Language: string + LocalizedLanguage: string + FriendlyFileNames?: string[] +} + +export interface SkuInfoResponse { + Skus: Sku[] +} + +export interface DownloadOption { + Uri: string + Architecture: string +} + +export interface ProxyResponse { + ProductDownloadOptions: DownloadOption[] +} + +export interface Product { + id: string + name: string +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json new file mode 100644 index 0000000..af516fc --- /dev/null +++ b/frontend/tsconfig.app.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2023", + "useDefineForClassFields": true, + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "types": ["vite/client"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json new file mode 100644 index 0000000..8a67f62 --- /dev/null +++ b/frontend/tsconfig.node.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "types": ["node"], + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..c676acd --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import tailwindcss from '@tailwindcss/vite' + +export default defineConfig({ + plugins: [ + react(), + tailwindcss(), + ], +})