Commit Graph

20 Commits

Author SHA1 Message Date
Vincent Young
a43aba64c1
fix(translate): seed cookie jar from www.deepl.com on first call
A real chrome-extension fetch() to oneshot-free.www.deepl.com inherits
whatever cookies the browser has on .deepl.com — at minimum
`userCountry=<iso2>` and `verifiedBot=false`, both of which the
deepl.com server sets on any page load. Our outbound bytes were
otherwise extension-identical but went out cookieless, which is a
distinguishable signal.

Wire a process-wide net/http/cookiejar onto the req.Client and trigger
a single warmup GET to https://www.deepl.com/translator on the first
translate call (sync.Once). The Set-Cookie response (userCountry,
verifiedBot) lands on .deepl.com, which the jar then automatically
echoes back on every subsequent POST to oneshot-free.www.deepl.com
(cookies set on .deepl.com match any *.deepl.com subdomain).

Verified outbound:
  Cookie: userCountry=JP; verifiedBot=false

Latency cost: first call after process start pays one extra HTTP GET
(~1s warmup); subsequent calls are unaffected (sync.Once + connection
keep-alive).

Note: we cannot replicate the _ga / _ga_<id> cookies a real user
would also carry — those are set client-side by GA's JS, which a
non-browser HTTP client can't execute. The userCountry+verifiedBot
pair already matches the "first-time visitor with JS disabled" profile,
which is the closest plausible non-browser approximation.
2026-05-22 12:03:55 +08:00
Vincent Young
10f33401e7
fix(translate): align oneshot request bytes with the real extension
After capturing the exact bytes the Chrome extension's service-worker
fetch() emits (via an offline echo server pointed at deeplx in place of
oneshot-free.www.deepl.com) and diffing them against what we were
sending, several distinguishable signals remained. Close them all.

Headers
-------
- Origin: chrome-extension://cofdbpoegempjloogbagkncekinflcnj
  (was https://www.deepl.com — a request from www.deepl.com itself
   never lands on the oneshot endpoint, so that origin is unusual.
   The extension ID is the canonical sender.)
- Sec-Fetch-Site: cross-site
  (was same-site — wrong; chrome-extension -> www.deepl.com IS cross-site)
- Drop Referer entirely (extension SW fetch sends none)
- Drop Pragma / Cache-Control / Upgrade-Insecure-Requests / Sec-Fetch-User
  (req.ImpersonateChrome() sets these for top-level navigation; a
   fetch() never sends them — leaving them in is a strong nav-vs-XHR tell)
- Accept-Encoding: gzip, deflate, br
  (was just gzip, Go stdlib default — Chrome 120's fetch() sends all
   three; zstd only landed as a default in Chrome 123+ so leave it off)

Body
----
- Add usage_type: "Translate" and the full app_information object
  (os/os_version/app_version/app_build/instance_id) so the JSON the
  server sees is structurally identical to what background.js IN()
  assembles. Field order in oneshotRequest matches the extension's
  object-literal order so encoding/json produces byte-identical output.
- instance_id is a v4 UUID generated once at process start and reused,
  mirroring the extension's chrome.storage-pinned ID rather than
  rotating per-request (rotation would be a far stronger signal).
- All version strings (TLS handshake, User-Agent, sec-ch-ua,
  app_information.os_version) are pinned to Chrome 120 so they tell
  one consistent story.

Transport
---------
- SetBodyBytes instead of bytes.NewReader so Content-Length is set
  (an io.Reader body forces Transfer-Encoding: chunked, which a
   fetch() with JSON.stringify body never emits)
- Once we set Accept-Encoding manually, the Go stdlib disables its
  transparent decompression and req hands us raw compressed bytes.
  Handle gzip / deflate / br by hand from Content-Encoding.
- DisableAutoReadResponse so we own the body stream end-to-end.

The Chrome 120 TLS ClientHello, HTTP/2 SETTINGS frame, pseudo-header
order and sec-ch-ua claim continue to come from ImpersonateChrome()
unchanged.

Verified end-to-end:
- Outbound bytes (against a local echo server) diff-match the
  extension's observed profile on every header and on body JSON order.
- Live oneshot-free.www.deepl.com calls: 4 language pairs OK,
  /v2/translate official-API compat OK, 10x burst 10/10 200.
2026-05-22 11:53:14 +08:00
Vincent Young
1fa6d7a2e3
feat(translate): migrate to oneshot endpoint to bypass www2 anti-bot
The www2.deepl.com/jsonrpc backends behind LMT_handle_texts /
LMT_handle_jobs now sit behind aggressive WAF + per-IP throttling that
returns HTTP 429 (code 1042911 "Too many requests") within a handful of
calls from any single host — making the free path effectively unusable.

The official DeepL browser extension and iOS app skip that backend
entirely for stateless single-shot translation and POST to a separate
"oneshot" endpoint on a different host pool with its own (much looser)
rate limit. It accepts anonymous traffic with a literal
`Authorization: None` header, returns plain JSON, and supports the same
language pairs.

Switch the free path to:

  POST https://oneshot-free.www.deepl.com/v1/translate
  Authorization: None
  {"text": ["..."], "target_lang": "de", "source_lang": "en"}

Pro users continue to hit oneshot-pro.www.deepl.com with their bearer
token (the `-s` flag now carries an OAuth access token rather than the
legacy dl_session cookie).

This removes:
  - the JSON-RPC envelope (jsonrpc/method/id/params/timestamp wrapper)
  - the `i`-count timestamp trick (getICount + getTimeStamp)
  - the random-id body-spacing trick (handlerBodyMethod)
  - the whatlanggo client-side detection (oneshot detects server-side)

The DeepLXTranslationResult contract is unchanged for service handlers;
Alternatives is now always nil because the oneshot endpoint does not
return alternative translations.

Verified against /translate, /v1/translate and /v2/translate routes
end-to-end (EN/DE/ZH/JA/FR pairs, multi-sentence input, autodetect, 10x
burst) — all 200 OK on an IP that was concurrently being 429'd by www2.
2026-05-22 11:34:55 +08:00
NuanR_Mxi
1f1c0884c0
fix: 503 too many requests (#205) 2025-10-04 01:51:40 +08:00
Vincent Yang
2b0e1e3cbb
fix: 503 unable to request. close #196 close #194 close #193 close #191 close #179 close #178 2025-07-13 23:14:24 +08:00
Vincent Yang
8a605887ff
refactor: update translation request handling to use new LMT_handle_texts method and improve response processing
- Renamed makeRequest to makeRequestWithBody for clarity.
- Introduced new TextItem and TextResponse types for better structure in translation requests and responses.
- Updated translation logic to handle multiple texts and alternatives.
- Enhanced error handling for blocked requests and translation failures.
- Adjusted timestamp and random number generation for improved request uniqueness.
2025-07-13 23:06:47 +08:00
Vincent Yang
84792ead81
chore: bump Go version to 1.24.2 in Dockerfile, go.mod, and CI workflows 2025-04-08 14:27:01 -04:00
Vincent Yang
2400139a8d
fix: unable to translate (switch to DeepL iOS) 2025-03-01 04:24:53 -05:00
Vincent Yang
bbfb808793
fix: handle line breaks #151 2025-01-20 17:11:53 -05:00
Vincent Yang
44b6c2915f
fix: while sourceLang is auto 2024-12-03 11:23:56 -05:00
木末君
547c735b1f
fix: 429 error in /translate endpoint (changed TLS fingerprint) (#161) 2024-11-29 04:13:20 -05:00
Vincent Yang
ede6229b0e
fix: targetlang bug 2024-11-01 23:19:50 -04:00
Vincent Yang
d98f71d1c5
fix: add pro support (may have no effect) 2024-11-01 13:13:28 -04:00
Vincent Yang
9edb997f06
fix: support CommonJobParams 2024-11-01 12:48:26 -04:00
Vincent Yang
62a993bb13
fix: unable to translate 2024-11-01 00:43:57 -04:00
Vincent Young
ac8ec23790
Update translate.go 2024-10-03 16:07:38 -04:00
Vincent Young
9e8c68fc94
Revert "Fix typo. (#143)" (#146)
This reverts commit 1bef182a15.
2024-10-03 16:05:53 -04:00
ryan961
1bef182a15
Fix typo. (#143) 2024-09-18 21:33:11 -04:00
Vincent Young
dce5ccd029
fix: remove unless parameter 2024-09-16 12:10:01 -04:00
Vincent Young
a9a5a914b6
refactor 2024-09-16 12:08:13 -04:00