feat(emex): Floxy sticky sessions (5min) to hold one IP across a chained flow
emex's ssd tokens appear IP-bound: a rotating proxy sends each hop of a decode→tree→drill→parts flow from a different exit IP, so emex rejects the ssd and returns empty (the rotating-Floxy verify still seeded 0). Pin the Floxy exit IP with a sticky session (password suffix `_session-<id>_lifetime-300`); a rolling session id is reused across the flow and rotates near expiry or after a transport failure (dead IP → fresh one). lifetime via EMEX_FLOXY_LIFETIME (0 = rotating). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -116,13 +116,20 @@ export class EmexService {
|
||||
portEnd: number;
|
||||
} | null;
|
||||
// Floxy residential fallback — used when the DataImpulse pool throws transport
|
||||
// errors. Single rotating endpoint (one port; IP rotates per request).
|
||||
// errors. `lifetime` > 0 → sticky session (one exit IP held that many seconds,
|
||||
// appended to the password as `_session-<id>_lifetime-<n>`); 0 → rotating.
|
||||
private readonly emexFloxy: {
|
||||
host: string;
|
||||
port: number;
|
||||
user: string;
|
||||
pass: string;
|
||||
lifetime: number;
|
||||
} | null;
|
||||
// Rolling Floxy sticky-session id: held for ~lifetime so a chained emex flow
|
||||
// (decode → tree → drill → parts) keeps ONE exit IP and emex's IP-bound ssd
|
||||
// tokens stay valid. Rotated when stale or after a transport failure.
|
||||
private floxySessionId = Math.random().toString(36).slice(2, 10);
|
||||
private floxySessionBornMs = Date.now();
|
||||
private readonly emexDirectFallback: boolean;
|
||||
|
||||
constructor(
|
||||
@@ -176,13 +183,17 @@ export class EmexService {
|
||||
const floxyEnabled = this.configService.get<string>("EMEX_FLOXY_FALLBACK", "true") === "true";
|
||||
if (floxyEnabled) {
|
||||
const fport = Number(this.configService.get("EMEX_FLOXY_PORT", 12321));
|
||||
const flife = Number(this.configService.get("EMEX_FLOXY_LIFETIME", 300));
|
||||
this.emexFloxy = {
|
||||
host: this.configService.get<string>("EMEX_FLOXY_HOST", "residential.floxy.io"),
|
||||
port: Number.isInteger(fport) && fport >= 1 && fport <= 65535 ? fport : 12321,
|
||||
user: this.configService.get<string>("EMEX_FLOXY_USER", "d739255e819b"),
|
||||
pass: this.configService.get<string>("EMEX_FLOXY_PASS", "9092873ba4e0"),
|
||||
lifetime: Number.isInteger(flife) && flife >= 0 ? flife : 300,
|
||||
};
|
||||
this.logger.log(`EMEX Floxy fallback enabled: ${this.emexFloxy.host}:${this.emexFloxy.port}`);
|
||||
this.logger.log(
|
||||
`EMEX Floxy fallback enabled: ${this.emexFloxy.host}:${this.emexFloxy.port} (${this.emexFloxy.lifetime > 0 ? `sticky ${this.emexFloxy.lifetime}s` : "rotating"})`,
|
||||
);
|
||||
} else {
|
||||
this.emexFloxy = null;
|
||||
}
|
||||
@@ -293,12 +304,32 @@ export class EmexService {
|
||||
* without requiring authentication cookies.
|
||||
*/
|
||||
/** Build a fresh proxy agent on a random port from the pool (null if proxy off). */
|
||||
/** Fresh Floxy sticky-session id (new exit IP). */
|
||||
private rotateFloxySession(): void {
|
||||
this.floxySessionId = Math.random().toString(36).slice(2, 10);
|
||||
this.floxySessionBornMs = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Floxy auth password. With lifetime > 0, appends `_session-<id>_lifetime-<n>`
|
||||
* to pin one exit IP across a chained emex flow; the session id rolls over
|
||||
* once it nears the lifetime so we never reuse an expired sticky slot.
|
||||
*/
|
||||
private floxyPassword(): string {
|
||||
if (!this.emexFloxy) return "";
|
||||
const { pass, lifetime } = this.emexFloxy;
|
||||
if (lifetime <= 0) return pass; // rotating mode
|
||||
// Roll at ~80% of lifetime so a sticky IP is never reused past expiry.
|
||||
if (Date.now() - this.floxySessionBornMs > lifetime * 800) this.rotateFloxySession();
|
||||
return `${pass}_session-${this.floxySessionId}_lifetime-${lifetime}`;
|
||||
}
|
||||
|
||||
private newProxyAgent(provider: "dataimpulse" | "floxy" = "dataimpulse"): ProxyAgent | null {
|
||||
if (provider === "floxy") {
|
||||
if (!this.emexFloxy) return null;
|
||||
const { host, port, user, pass } = this.emexFloxy;
|
||||
const { host, port, user } = this.emexFloxy;
|
||||
return new ProxyAgent({
|
||||
uri: `http://${user}:${pass}@${host}:${port}`,
|
||||
uri: `http://${user}:${this.floxyPassword()}@${host}:${port}`,
|
||||
connect: { timeout: 30000 },
|
||||
requestTls: { timeout: 30000 },
|
||||
});
|
||||
@@ -354,6 +385,9 @@ export class EmexService {
|
||||
`${e.message} ${String(e.cause ?? "")}`,
|
||||
));
|
||||
if (!transient || attempt === maxAttempts) break;
|
||||
// A Floxy transport failure means the current sticky exit IP is dead —
|
||||
// roll to a fresh session so the next Floxy attempt gets a new IP.
|
||||
if (provider === "floxy") this.rotateFloxySession();
|
||||
const next = schedule[attempt];
|
||||
const via =
|
||||
next === "floxy" && provider !== "floxy" ? "via Floxy fallback" : "with fresh proxy";
|
||||
|
||||
Reference in New Issue
Block a user