package main import ( "crypto/subtle" "encoding/xml" "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 updateURL = "https://tools.google.com/service/update2" ) var ( authToken string authEnabled bool port string keepVersions int ) type Version struct { Filename string Size int64 Time time.Time SHA256 string } type UpdateResponse struct { XMLName xml.Name `xml:"response"` Protocol string `xml:"protocol,attr"` App struct { Status string `xml:"status,attr"` UpdateCheck struct { Status string `xml:"status,attr"` URLs struct { URL []struct { Codebase string `xml:"codebase,attr"` } `xml:"url"` } `xml:"urls"` Manifest struct { Version string `xml:"version,attr"` Packages struct { Package struct { Name string `xml:"name,attr"` Required bool `xml:"required,attr"` Size int64 `xml:"size,attr"` Hash string `xml:"hash,attr"` HashSHA256 string `xml:"hash_sha256,attr"` } `xml:"package"` } `xml:"packages"` } `xml:"manifest"` } `xml:"updatecheck"` } `xml:"app"` } 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 getLatestVersionInfo() (version, downloadURL, sha256 string, err error) { reqBody := ` ` resp, err := http.Post(updateURL, "application/xml", strings.NewReader(reqBody)) if err != nil { return "", "", "", err } defer resp.Body.Close() body, _ := io.ReadAll(resp.Body) var updateResp UpdateResponse if err := xml.Unmarshal(body, &updateResp); err != nil { return "", "", "", err } version = updateResp.App.UpdateCheck.Manifest.Version sha256 = updateResp.App.UpdateCheck.Manifest.Packages.Package.HashSHA256 packageName := updateResp.App.UpdateCheck.Manifest.Packages.Package.Name if len(updateResp.App.UpdateCheck.URLs.URL) > 0 { for _, url := range updateResp.App.UpdateCheck.URLs.URL { if strings.HasPrefix(url.Codebase, "https://dl.google.com") { downloadURL = url.Codebase + packageName break } } if downloadURL == "" { downloadURL = updateResp.App.UpdateCheck.URLs.URL[0].Codebase + packageName } } return version, downloadURL, sha256, nil } func checkAndDownload() { version, downloadURL, sha256, err := getLatestVersionInfo() if err != nil { log.Printf("Failed to get version info: %v", err) return } log.Printf("Latest Chrome version: %s", version) log.Printf("Download URL: %s", downloadURL) log.Printf("SHA256: %s", sha256) if downloadURL == "" { log.Println("No download URL found") return } parts := strings.Split(downloadURL, "/") filename := parts[len(parts)-1] filePath := filepath.Join(downloadDir, filename) sha256File := filePath + ".sha256" if _, err := os.Stat(filePath); err == nil { log.Printf("File %s already exists, skipping download", filename) return } client := &http.Client{} var downloaded bool for i := 0; i < maxRetries; i++ { resp, err := client.Get(downloadURL) if err != nil { log.Printf("Attempt %d failed: %v", i+1, err) time.Sleep(time.Duration(i+1) * 10 * time.Second) continue } tmpFile := filePath + ".tmp" f, err := os.Create(tmpFile) if err != nil { resp.Body.Close() log.Printf("Attempt %d: failed to create file: %v", i+1, err) time.Sleep(time.Duration(i+1) * 10 * time.Second) continue } written, err := io.Copy(f, resp.Body) f.Close() resp.Body.Close() if err == nil && written > 10000000 { os.Rename(tmpFile, filePath) os.WriteFile(sha256File, []byte(sha256), 0644) log.Printf("Downloaded: %s (%.2f MB)", filename, float64(written)/1024/1024) downloaded = true break } os.Remove(tmpFile) log.Printf("Attempt %d: invalid response (size: %d bytes)", i+1, written) time.Sleep(time.Duration(i+1) * 10 * time.Second) } if !downloaded { log.Println("All download attempts failed") return } cleanupOldVersions() } func cleanupOldVersions() { files, _ := os.ReadDir(downloadDir) if len(files) <= keepVersions*2 { 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)) os.Remove(filepath.Join(downloadDir, versions[i].Filename+".sha256")) 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() sha256, _ := os.ReadFile(filepath.Join(downloadDir, f.Name()+".sha256")) versions = append(versions, Version{ Filename: f.Name(), Time: info.ModTime(), Size: info.Size(), SHA256: string(sha256), }) } } 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 (x64)

{{range .Versions}} {{end}}
Filename Size Date SHA256
{{.Filename}} {{printf "%.2f" .SizeMB}} MB {{.Time.Format "2006-01-02 15:04"}} {{.SHA256}}
`)) type VersionDisplay struct { Filename string SizeMB float64 Time time.Time SHA256 string } var displayVersions []VersionDisplay for _, v := range versions { displayVersions = append(displayVersions, VersionDisplay{ Filename: v.Filename, SizeMB: float64(v.Size) / 1024 / 1024, Time: v.Time, SHA256: v.SHA256, }) } tmpl.Execute(w, struct { Versions []VersionDisplay Token string }{displayVersions, token}) }