package main import ( "crypto/md5" "crypto/subtle" "encoding/json" "fmt" "html/template" "io" "log" "math/rand" "net/http" "os" "path/filepath" "sort" "strconv" "strings" "time" ) const ( downloadDir = "./chrome_versions" checkInterval = 24 * time.Hour maxRetries = 3 versionAPI = "https://versionhistory.googleapis.com/v1/chrome/platforms/win64/channels/stable/versions" ) var ( authToken string authEnabled bool port string keepVersions int ) type Version struct { Filename string Size int64 Time time.Time } type ChromeVersion struct { Name string `json:"name"` Version string `json:"version"` } type ChromeVersionResponse struct { Versions []ChromeVersion `json:"versions"` } func init() { authToken = getEnv("AUTH_TOKEN", "") authEnabled = getEnv("AUTH_ENABLED", "false") == "true" || authToken != "" port = getEnv("PORT", "8080") keepVersions, _ = strconv.Atoi(getEnv("KEEP_VERSIONS", "3")) } func getEnv(key, defaultValue string) string { if value := os.Getenv(key); value != "" { return value } return defaultValue } func main() { os.MkdirAll(downloadDir, 0755) log.Printf("Auth enabled: %v, Port: %s, Keep versions: %d", authEnabled, port, keepVersions) log.Println("Performing initial check...") checkAndDownload() go monitor() http.HandleFunc("/", authMiddleware(serveIndex)) http.Handle("/download/", authMiddleware(http.StripPrefix("/download/", http.FileServer(http.Dir(downloadDir))).ServeHTTP)) log.Printf("Server started on :%s", port) log.Fatal(http.ListenAndServe(":"+port, nil)) } func monitor() { for { delay := time.Duration(rand.Intn(3600)) * time.Second time.Sleep(delay) checkAndDownload() time.Sleep(checkInterval) } } func getLatestVersion() (string, error) { resp, err := http.Get(versionAPI) if err != nil { return "", err } defer resp.Body.Close() var versionResp ChromeVersionResponse if err := json.NewDecoder(resp.Body).Decode(&versionResp); err != nil { return "", err } if len(versionResp.Versions) == 0 { return "", fmt.Errorf("no versions found") } return versionResp.Versions[0].Version, nil } func checkAndDownload() { version, err := getLatestVersion() if err != nil { log.Printf("Failed to get latest version: %v", err) return } log.Printf("Latest Chrome version: %s", version) // 尝试多个可能的下载 URL 模式 urls := []string{ fmt.Sprintf("https://dl.google.com/release2/chrome/%%s_%s/%s_chrome_installer.exe", version, version), fmt.Sprintf("https://dl.google.com/tag/s/appguid%%3D%%7B8A69D345-D564-463C-AFF1-A69D9E530F96%%7D%%26iid%%3D%%7B00000000-0000-0000-0000-000000000000%%7D%%26lang%%3Dzh-CN%%26browser%%3D4%%26usagestats%%3D0%%26appname%%3DGoogle%%2520Chrome%%26needsadmin%%3Dprefers%%26ap%%3Dx64-stable-statsdef_1%%26installdataindex%%3Dempty/chrome/install/ChromeStandaloneSetup64.exe"), } var data []byte client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { log.Printf("Redirecting to: %s", req.URL.String()) return nil }, } for _, urlPattern := range urls { for i := 0; i < maxRetries; i++ { url := urlPattern if strings.Contains(url, "%s") { // 跳过需要 hash 的 URL continue } log.Printf("Trying URL: %s", url) resp, err := client.Get(url) if err != nil { log.Printf("Attempt %d failed: %v", i+1, err) time.Sleep(time.Duration(i+1) * 10 * time.Second) continue } contentType := resp.Header.Get("Content-Type") contentLength := resp.Header.Get("Content-Length") log.Printf("Content-Type: %s, Content-Length: %s", contentType, contentLength) data, err = io.ReadAll(resp.Body) resp.Body.Close() if err == nil && len(data) > 10000000 && !strings.Contains(contentType, "text/html") { goto success } log.Printf("Invalid response (size: %d bytes)", len(data)) time.Sleep(time.Duration(i+1) * 10 * time.Second) } } log.Println("All download attempts failed") return success: hash := fmt.Sprintf("%x", md5.Sum(data)) filename := fmt.Sprintf("chrome_%s_%s.exe", version, hash[:8]) filepath := filepath.Join(downloadDir, filename) if _, err := os.Stat(filepath); err == nil { log.Println("File already exists, skipping") return } if err := os.WriteFile(filepath, data, 0644); err != nil { log.Printf("Error saving file: %v", err) return } log.Printf("Downloaded: %s (%.2f MB)", filename, float64(len(data))/1024/1024) cleanupOldVersions() } func cleanupOldVersions() { files, _ := os.ReadDir(downloadDir) if len(files) <= keepVersions { return } var versions []Version for _, f := range files { if strings.HasSuffix(f.Name(), ".exe") { info, _ := f.Info() versions = append(versions, Version{ Filename: f.Name(), Time: info.ModTime(), Size: info.Size(), }) } } sort.Slice(versions, func(i, j int) bool { return versions[i].Time.After(versions[j].Time) }) for i := keepVersions; i < len(versions); i++ { os.Remove(filepath.Join(downloadDir, versions[i].Filename)) log.Printf("Removed old version: %s", versions[i].Filename) } } func authMiddleware(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if authEnabled { token := r.URL.Query().Get("token") if subtle.ConstantTimeCompare([]byte(token), []byte(authToken)) != 1 { http.Error(w, "Unauthorized", http.StatusUnauthorized) return } } next(w, r) } } func serveIndex(w http.ResponseWriter, r *http.Request) { files, _ := os.ReadDir(downloadDir) var versions []Version for _, f := range files { if strings.HasSuffix(f.Name(), ".exe") { info, _ := f.Info() versions = append(versions, Version{ Filename: f.Name(), Time: info.ModTime(), Size: info.Size(), }) } } sort.Slice(versions, func(i, j int) bool { return versions[i].Time.After(versions[j].Time) }) token := r.URL.Query().Get("token") tmpl := template.Must(template.New("index").Parse(` Chrome Offline Installer

Chrome Offline Versions

`)) type VersionDisplay struct { Filename string SizeMB float64 Time time.Time } var displayVersions []VersionDisplay for _, v := range versions { displayVersions = append(displayVersions, VersionDisplay{ Filename: v.Filename, SizeMB: float64(v.Size) / 1024 / 1024, Time: v.Time, }) } tmpl.Execute(w, struct { Versions []VersionDisplay Token string }{displayVersions, token}) }