更新广州平台登录状态
请先登录广州平台,账号:{{.Account}}。
提交后会验证账号和登录状态。验证通过后,下一次定时采集自动继续。
如何取得 token?
- 在 Chrome 登录广州平台,完成必要的短信和图形验证码。
- 打开开发者工具 → Application → Cookies → gzev.d.lnoneos.com。
- 复制 token 的值;如有 bigs,也一并复制。
- 回到此页面粘贴并提交。
package gzev import ( "context" "crypto/rand" "crypto/subtle" "encoding/hex" "encoding/json" "errors" "fmt" "html/template" "io" "net/http" "os" "path/filepath" "strings" "time" ) // UploadHandler accepts local SSH access or the configured HTTPS origin with a one-use recovery code. type UploadHandler struct { PublicOrigin string RecoveryPath string SessionPath string Account string Verify func(context.Context, Session, string) error csrf string } func NewUploadHandler(path, account string) (*UploadHandler, error) { nonce := make([]byte, 32) if _, err := rand.Read(nonce); err != nil { return nil, err } return &UploadHandler{SessionPath: path, Account: account, csrf: hex.EncodeToString(nonce), Verify: func(ctx context.Context, s Session, a string) error { return NewClient(s).VerifyAccount(ctx, a) }}, nil } func (h *UploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Header().Set("Cache-Control", "no-store") // Keep the Origin header on same-origin form POSTs; no-referrer makes // browsers send Origin: null, which the strict check below must reject. w.Header().Set("Referrer-Policy", "same-origin") w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Content-Security-Policy", "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; frame-ancestors 'none'; base-uri 'none'") local := r.Host == "127.0.0.1:20312" || r.Host == "localhost:20312" public := h.PublicOrigin != "" && "https://"+r.Host == h.PublicOrigin if !local && !public { http.Error(w, "仅允许通过 SSH 安全连接访问", 403) return } if r.URL.Path != "/" && r.URL.Path != "/gzev/session/" { http.NotFound(w, r) return } if r.Method == http.MethodGet { w.Header().Set("Content-Type", "text/html; charset=utf-8") uploadPage.Execute(w, map[string]string{"Account": h.Account, "CSRF": h.csrf, "Public": fmt.Sprint(h.PublicOrigin != "")}) return } if r.Method != http.MethodPost { w.Header().Set("Allow", "GET, POST") http.Error(w, "Method not allowed", 405) return } expectedOrigin := "http://" + r.Host if public { expectedOrigin = h.PublicOrigin } if r.Header.Get("Origin") != expectedOrigin { http.Error(w, "请求来源不正确,请刷新上传页面", 403) return } r.Body = http.MaxBytesReader(w, r.Body, 32<<10) if err := r.ParseMultipartForm(32 << 10); err != nil { http.Error(w, "文件或表单无效,大小不能超过 32 KB", 400) return } if r.MultipartForm != nil { defer r.MultipartForm.RemoveAll() } if subtle.ConstantTimeCompare([]byte(r.FormValue("csrf")), []byte(h.csrf)) != 1 { http.Error(w, "页面已过期,请刷新后重新提交", 403) return } var s Session file, _, fileErr := r.FormFile("session") if fileErr == nil { defer file.Close() dec := json.NewDecoder(file) dec.DisallowUnknownFields() if err := dec.Decode(&s); err != nil { http.Error(w, "会话文件格式错误", 400) return } if dec.Decode(new(any)) != io.EOF { http.Error(w, "会话文件含多余内容", 400) return } } else if fileErr == http.ErrMissingFile { token := strings.TrimSpace(r.FormValue("token")) token = strings.TrimPrefix(token, "Bearer ") s = Session{Authorization: "Bearer " + token, Cookies: []SessionCookie{{Name: "token", Value: token}}} if routing := strings.TrimSpace(r.FormValue("routing")); routing != "" { s.Cookies = append(s.Cookies, SessionCookie{Name: "bigs", Value: routing}) } } else { http.Error(w, "无法读取上传文件", 400) return } if ValidateSession(s) != nil { http.Error(w, "会话格式错误,只接受 token 和可选的 bigs Cookie", 400) return } action := func() error { ctx, cancel := context.WithTimeout(r.Context(), 30*time.Second) defer cancel() if err := h.Verify(ctx, s, h.Account); err != nil { return errors.New("verification failed") } return WriteSession(h.SessionPath, s) } var err error if h.PublicOrigin != "" { err = ConsumeRecovery(h.RecoveryPath, r.FormValue("recovery_code"), action) } else { err = action() } if err != nil { http.Error(w, "验证或保存未通过。请检查恢复码、登录状态及账号;现有会话未覆盖。", 422) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") io.WriteString(w, "
验证通过,下一次定时采集将自动继续补齐里程数据。
返回") } func WriteSession(path string, s Session) error { if err := ValidateSession(s); err != nil { return err } if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { return err } f, err := os.CreateTemp(filepath.Dir(path), ".session-*") if err != nil { return err } name := f.Name() defer os.Remove(name) if err := json.NewEncoder(f).Encode(s); err != nil { f.Close() return err } if err := f.Sync(); err != nil { f.Close() return err } if err := f.Close(); err != nil { return err } return os.Rename(name, path) } var uploadPage = template.Must(template.New("upload").Parse(`请先登录广州平台,账号:{{.Account}}。
提交后会验证账号和登录状态。验证通过后,下一次定时采集自动继续。