更新runway

This commit is contained in:
2026-07-14 22:44:18 +08:00
parent 4a1b91b759
commit 7a781fecfc
3 changed files with 215 additions and 37 deletions
+65 -18
View File
@@ -19,6 +19,7 @@ import (
http "github.com/bogdanfinn/fhttp"
tlsclient "github.com/bogdanfinn/tls-client"
"github.com/bogdanfinn/tls-client/profiles"
"github.com/google/uuid"
)
const (
@@ -29,8 +30,46 @@ const (
// registration + generation in the reference HAR (Edge 150 on Windows).
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36 Edg/150.0.0.0"
secChUA = `"Not;A=Brand";v="8", "Chromium";v="150", "Microsoft Edge";v="150"`
// sourceApp / buildHash are the x-runway-source-application[-version] the real
// web app stamps on EVERY authed API call (319/336 requests in the reference
// HAR). buildHash is the web bundle's git sha; it also appears as the
// `sentry-release` segment of the baggage header. Runway anti-abuse keys on
// these being present + client-id (see clientIDFromToken): a write/upload that
// lacks them is judged a non-web client and the account's free credits are
// zeroed on the FIRST upload ("上传清零积分"). buildHash tracks Runway's web
// releases — refresh it from a current HAR if uploads start getting flagged.
sourceApp = "web"
buildHash = "3e96b2f0f85b8c0cafb7c0dcd7a6878305aaa0f0"
)
// clientIDOverride, when non-empty, forces the x-runway-client-id (used to pin an
// account to the exact persistent device id its real browser session used).
var clientIDOverride string
// clientIDUUIDNamespace is a fixed namespace so clientIDFromToken is stable
// across processes/restarts — the same account always derives the same
// x-runway-client-id, exactly like the browser's localStorage-persisted id.
var clientIDUUIDNamespace = uuid.MustParse("6ba7b811-9dad-11d1-80b4-00c04fd430c8")
// clientIDFromToken derives the persistent per-account x-runway-client-id. The
// real SPA generates this UUID once and stores it in localStorage; every request
// from that browser reuses the SAME value (5b89c786-… appears on all 319 authed
// calls in the HAR). We reproduce that stability by deterministically deriving a
// v5 UUID from the account's JWT "id" claim, so one account == one client-id for
// its whole lifetime without needing a DB column. Falls back to the raw token if
// the id claim is missing.
func clientIDFromToken(token string) string {
if clientIDOverride != "" {
return clientIDOverride
}
seed := TeamIDFromToken(token)
if seed == "" {
seed = strings.TrimPrefix(token, "Bearer ")
}
return uuid.NewSHA1(clientIDUUIDNamespace, []byte("runway-client-id:"+seed)).String()
}
// randHex returns n random bytes hex-encoded (2n chars), for sentry trace ids.
func randHex(n int) string {
b := make([]byte, n)
@@ -49,29 +88,37 @@ func browserHeaders(token, teamID string) http.Header {
traceID := randHex(16)
spanID := randHex(8)
h := http.Header{
"accept": {"application/json"},
"accept-language": {"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"},
"authorization": {"Bearer " + strings.TrimPrefix(token, "Bearer ")},
"baggage": {"sentry-environment=production,sentry-public_key=8ea832c064ed4bbcb4b8952c02ba119a,sentry-trace_id=" + traceID},
"content-type": {"application/json"},
"origin": {origin},
"priority": {"u=1, i"},
"referer": {origin + "/"},
"sec-ch-ua": {secChUA},
"sec-ch-ua-mobile": {"?0"},
"sec-ch-ua-platform": {`"Windows"`},
"sec-fetch-dest": {"empty"},
"sec-fetch-mode": {"cors"},
"sec-fetch-site": {"same-site"},
"sentry-trace": {traceID + "-" + spanID},
"user-agent": {userAgent},
"x-runway-workspace": {teamID},
"accept": {"application/json"},
"accept-language": {"zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6"},
"authorization": {"Bearer " + strings.TrimPrefix(token, "Bearer ")},
"baggage": {"sentry-environment=production,sentry-release=" + buildHash + ",sentry-public_key=8ea832c064ed4bbcb4b8952c02ba119a,sentry-trace_id=" + traceID},
"content-type": {"application/json"},
"origin": {origin},
"priority": {"u=1, i"},
"referer": {origin + "/"},
"sec-ch-ua": {secChUA},
"sec-ch-ua-mobile": {"?0"},
"sec-ch-ua-platform": {`"Windows"`},
"sec-fetch-dest": {"empty"},
"sec-fetch-mode": {"cors"},
"sec-fetch-site": {"same-site"},
"sentry-trace": {traceID + "-" + spanID},
"user-agent": {userAgent},
// x-runway-* identify this as the real web app to Runway's anti-abuse.
// client-id must be persistent per account (see clientIDFromToken);
// omitting these is what zeroes an account's credits on first upload.
"x-runway-client-id": {clientIDFromToken(token)},
"x-runway-source-application": {sourceApp},
"x-runway-source-application-version": {buildHash},
"x-runway-workspace": {teamID},
http.HeaderOrderKey: {
"accept", "accept-language", "authorization", "baggage",
"content-type", "origin", "priority", "referer",
"sec-ch-ua", "sec-ch-ua-mobile", "sec-ch-ua-platform",
"sec-fetch-dest", "sec-fetch-mode", "sec-fetch-site",
"sentry-trace", "user-agent", "x-runway-workspace",
"sentry-trace", "user-agent",
"x-runway-client-id", "x-runway-source-application",
"x-runway-source-application-version", "x-runway-workspace",
},
}
if strings.TrimSpace(teamID) == "" {