Files
chrome-monitor/main.go
2025-12-17 01:35:10 +08:00

282 lines
6.6 KiB
Go

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(`
<!DOCTYPE html>
<html>
<head><title>Chrome Offline Installer</title></head>
<body>
<h1>Chrome Offline Versions</h1>
<ul>
{{range .Versions}}
<li><a href="/download/{{.Filename}}{{if $.Token}}?token={{$.Token}}{{end}}">{{.Filename}}</a> ({{printf "%.2f" .SizeMB}} MB, {{.Time.Format "2006-01-02 15:04"}})</li>
{{end}}
</ul>
</body>
</html>
`))
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})
}