whalibmob 5.11.0 → 5.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/lib/Registration.js +130 -34
  2. package/package.json +1 -1
@@ -136,51 +136,144 @@ async function httpPostViaSocks(path, body, waVersion, proxyUrl, authHeader) {
136
136
  `Content-Length: ${Buffer.byteLength(body)}`
137
137
  ];
138
138
  if (authHeader) reqLines.push(`Authorization: ${authHeader}`);
139
- // close, not keep-alive: the response is read by waiting for the server to
140
- // end the stream. Asking it to keep the connection open means that never
141
- // happens and the request hangs until something else times out.
139
+ // Asked for, but not relied on: the server answers and then keeps the
140
+ // connection open anyway. The response is finished by its own length, not by
141
+ // the socket closing.
142
142
  reqLines.push(`Connection: close`, '', body);
143
143
  const req = reqLines.join('\r\n');
144
144
 
145
145
  tlsSocket.write(req);
146
146
 
147
- const chunks = [];
148
- await new Promise((res, rej) => {
147
+ const { status, body: bodyBuf } = await readHttpResponse(tlsSocket, path);
148
+ const bodyStr = bodyBuf.toString('utf8');
149
+
150
+ if (status !== 200) throw new Error(`HTTP ${status} ${path}: ${bodyStr}`);
151
+ try { return JSON.parse(bodyStr); } catch (_) { return { raw: bodyStr }; }
152
+ }
153
+
154
+ // A response is over when its own length says so, not when the socket shuts.
155
+ //
156
+ // This is the part `https.request` does for you and a hand-rolled socket does
157
+ // not. WhatsApp answers a registration request in well under a second and then
158
+ // leaves the connection open — `Connection: close` in the request does not
159
+ // change that. Waiting for end or close therefore waited for something that
160
+ // never came, and every proxied registration died on the read timeout with the
161
+ // reply already sitting in the buffer.
162
+ //
163
+ // So: parse as the bytes arrive, and stop at the end of the body — the byte
164
+ // count in Content-Length, or the terminating zero-length chunk. end and close
165
+ // stay as a fallback, for a reply that declares neither and can only be
166
+ // delimited by the connection itself.
167
+ const MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
168
+
169
+ function readHttpResponse(socket, label, timeoutMs) {
170
+ return new Promise((resolve, reject) => {
171
+ let raw = Buffer.alloc(0);
172
+ let done = false;
173
+
149
174
  const timer = setTimeout(() => {
150
- tlsSocket.destroy();
151
- rej(new Error('SOCKS proxy timed out reading ' + path));
152
- }, 30000);
153
- const finish = (fn, arg) => { clearTimeout(timer); fn(arg); };
154
- tlsSocket.on('data', d => chunks.push(d));
155
- tlsSocket.on('end', () => finish(res));
156
- tlsSocket.on('close', () => finish(res));
157
- tlsSocket.on('error', e => finish(rej, e));
175
+ settle(reject, new Error('SOCKS proxy timed out reading ' + label));
176
+ }, timeoutMs || 30000);
177
+
178
+ function settle(fn, arg) {
179
+ if (done) return;
180
+ done = true;
181
+ clearTimeout(timer);
182
+ socket.removeListener('data', onData);
183
+ socket.removeListener('end', onEnd);
184
+ socket.removeListener('close', onEnd);
185
+ socket.removeListener('error', onError);
186
+ socket.destroy();
187
+ fn(arg);
188
+ }
189
+
190
+ function onData(chunk) {
191
+ raw = raw.length ? Buffer.concat([raw, chunk]) : chunk;
192
+ if (raw.length > MAX_RESPONSE_BYTES) {
193
+ return settle(reject, new Error('response too large reading ' + label));
194
+ }
195
+ const parsed = parseHttpResponse(raw, false);
196
+ if (parsed) settle(resolve, parsed);
197
+ }
198
+
199
+ // The connection did close after all — then whatever arrived is the whole
200
+ // reply, even without a declared length.
201
+ function onEnd() {
202
+ const parsed = parseHttpResponse(raw, true);
203
+ if (parsed) return settle(resolve, parsed);
204
+ settle(reject, new Error('connection closed mid-response reading ' + label));
205
+ }
206
+
207
+ function onError(err) { settle(reject, err); }
208
+
209
+ socket.on('data', onData);
210
+ socket.on('end', onEnd);
211
+ socket.on('close', onEnd);
212
+ socket.on('error', onError);
158
213
  });
214
+ }
215
+
216
+ /**
217
+ * Parse what has arrived so far.
218
+ *
219
+ * Returns { status, headers, body } once the response is complete, or null
220
+ * while it is still short. With `atEof` the caller is telling us no more bytes
221
+ * are coming, which is itself a valid way for a body to end.
222
+ */
223
+ function parseHttpResponse(raw, atEof) {
224
+ const headerEnd = raw.indexOf('\r\n\r\n');
225
+ if (headerEnd < 0) return null;
226
+
227
+ // Headers are latin1 by definition; only the body may be UTF-8.
228
+ const headers = raw.slice(0, headerEnd).toString('latin1');
229
+ const status = parseInt(headers.split(' ')[1], 10);
230
+ const bodyStart = headerEnd + 4;
231
+
232
+ if (/^transfer-encoding:[ \t]*chunked/im.test(headers)) {
233
+ const decoded = decodeChunkedBody(raw.slice(bodyStart));
234
+ if (decoded) return { status, headers, body: decoded };
235
+ return atEof ? { status, headers, body: Buffer.alloc(0) } : null;
236
+ }
159
237
 
160
- const raw = Buffer.concat(chunks);
161
- const rawStr = raw.toString('utf8');
162
- const headerEnd = rawStr.indexOf('\r\n\r\n');
163
- const headerPart = rawStr.slice(0, headerEnd);
164
- const httpStatus = parseInt(rawStr.split(' ')[1]);
165
- let bodyStr = rawStr.slice(headerEnd + 4);
166
-
167
- // Handle chunked transfer encoding
168
- if (/transfer-encoding:\s*chunked/i.test(headerPart)) {
169
- let decoded = '';
170
- let pos = 0;
171
- while (pos < bodyStr.length) {
172
- const lineEnd = bodyStr.indexOf('\r\n', pos);
173
- if (lineEnd === -1) break;
174
- const chunkSize = parseInt(bodyStr.slice(pos, lineEnd), 16);
175
- if (!chunkSize) break;
176
- decoded += bodyStr.slice(lineEnd + 2, lineEnd + 2 + chunkSize);
177
- pos = lineEnd + 2 + chunkSize + 2;
238
+ const match = /^content-length:[ \t]*(\d+)/im.exec(headers);
239
+ if (match) {
240
+ const length = parseInt(match[1], 10);
241
+ // Counted in bytes, which is why this works on the buffer and not on a
242
+ // decoded string — one accented character in an error message is two bytes
243
+ // and would leave the check one short forever.
244
+ if (raw.length - bodyStart >= length) {
245
+ return { status, headers, body: raw.slice(bodyStart, bodyStart + length) };
178
246
  }
179
- bodyStr = decoded;
247
+ return atEof ? { status, headers, body: raw.slice(bodyStart) } : null;
180
248
  }
181
249
 
182
- if (httpStatus !== 200) throw new Error(`HTTP ${httpStatus} ${path}: ${bodyStr}`);
183
- try { return JSON.parse(bodyStr); } catch (_) { return { raw: bodyStr }; }
250
+ // These two carry no body at all, whatever else the headers say.
251
+ if (status === 204 || status === 304) {
252
+ return { status, headers, body: Buffer.alloc(0) };
253
+ }
254
+
255
+ // No length anywhere: the body runs to the end of the connection.
256
+ return atEof ? { status, headers, body: raw.slice(bodyStart) } : null;
257
+ }
258
+
259
+ /** Decode a chunked body, or null while the terminating chunk is still missing. */
260
+ function decodeChunkedBody(buf) {
261
+ const parts = [];
262
+ let pos = 0;
263
+
264
+ for (;;) {
265
+ const lineEnd = buf.indexOf('\r\n', pos);
266
+ if (lineEnd < 0) return null;
267
+ // A size line may carry extensions after a semicolon; parseInt stops there.
268
+ const size = parseInt(buf.slice(pos, lineEnd).toString('latin1'), 16);
269
+ if (!Number.isFinite(size) || size < 0) return null;
270
+ if (size === 0) return Buffer.concat(parts); // the terminating chunk
271
+ const start = lineEnd + 2;
272
+ const end = start + size;
273
+ if (buf.length < end + 2) return null; // chunk still arriving
274
+ parts.push(buf.slice(start, end));
275
+ pos = end + 2;
276
+ }
184
277
  }
185
278
  const {
186
279
  REGISTRATION_ENDPOINT,
@@ -953,3 +1046,6 @@ async function verifyCode(store, code) {
953
1046
  }
954
1047
 
955
1048
  module.exports = { checkIfRegistered, checkNumberStatus, requestSmsCode, verifyCode, fetchIosVersion, fetchAndroidVersion, fetchWaVersion, parsePhone, getCountryMeta, assertRegistrationKeys, parseSocksProxy, socksProxyUrl };
1049
+
1050
+ // The HTTP reader, exposed for tests. Not part of the public API.
1051
+ module.exports._http = { readHttpResponse, parseHttpResponse, decodeChunkedBody };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whalibmob",
3
- "version": "5.11.0",
3
+ "version": "5.11.1",
4
4
  "description": "WhatsApp library for interaction with WhatsApp Mobile API no web",
5
5
  "author": "Kunboruto20",
6
6
  "main": "index.js",