diff --git a/ws/cookies.js b/ws/cookies.js index 814a169..248b7e3 100644 --- a/ws/cookies.js +++ b/ws/cookies.js @@ -1,6 +1,6 @@ "use strict"; -/** @type CookieStore */ +/** @type {import('../').CookieStore} */ let Cookies = module.exports; let Cookie = require("tough-cookie"); @@ -8,7 +8,9 @@ let Cookie = require("tough-cookie"); //let FileCookieStore = require("@root/file-cookie-store"); //let cookies_store = new FileCookieStore("./cookie.txt", { auto_sync: false }); let jar = new Cookie.CookieJar(/*cookies_store*/); +//@ts-ignore jar.setCookieAsync = require("util").promisify(jar.setCookie); +//@ts-ignore jar.getCookiesAsync = require("util").promisify(jar.getCookies); //cookies_store.saveAsync = require("util").promisify(cookies_store.save); @@ -18,23 +20,29 @@ jar.getCookiesAsync = require("util").promisify(jar.getCookies); * @returns {Promise} */ Cookies.set = async function _setCookie(url, resp) { + /** @type {Array} */ let cookies; - if (resp.headers["set-cookie"]) { - if (Array.isArray(resp.headers["set-cookie"])) { - cookies = resp.headers["set-cookie"].map(Cookie.parse); - } else { - cookies = [Cookie.parse(resp.headers["set-cookie"])]; - } + let moreCookies = resp.headers["set-cookie"]; + if (!moreCookies) { + return; } + if (!Array.isArray(moreCookies)) { + moreCookies = [moreCookies]; + } + //@ts-ignore + cookies = moreCookies.map(Cookie.parse); + // let Cookie = //require('set-cookie-parser'); // Cookie.parse(resp, { decodeValues: true }); - await Promise.all( - cookies.map(async function (cookie) { - //console.log('DEBUG cookie:', cookie.toJSON()); - await jar.setCookieAsync(cookie, url, { now: new Date() }); - }), - ); + let ps = cookies.map(async function (cookie) { + //console.log('DEBUG cookie:', cookie.toJSON()); + let jarOpts = { now: new Date() }; + //@ts-ignore + await jar.setCookieAsync(cookie, url, jarOpts); + }); + + await Promise.all(ps); //await cookies_store.saveAsync(); }; @@ -43,5 +51,8 @@ Cookies.set = async function _setCookie(url, resp) { * @returns {Promise} */ Cookies.get = async function _getCookie(url) { - return (await jar.getCookiesAsync(url)).toString(); + //@ts-ignore + let cookieObj = await jar.getCookiesAsync(url); + let cookieStr = cookieObj.toString(); + return cookieStr; }; diff --git a/ws/index.js b/ws/index.js index c5626cc..db69bfe 100644 --- a/ws/index.js +++ b/ws/index.js @@ -9,7 +9,7 @@ let WSClient = require("ws"); /** * @typedef {Object} WsOpts * @prop {String} [baseUrl] - (deprecated by dashsocketBaseUrl) ex: https://insight.dash.org - * @prop {CookieStore} cookieStore - only needed for insight APIs hosted behind an AWS load balancer + * @prop {import('../').CookieStore} cookieStore - only needed for insight APIs hosted behind an AWS load balancer * @prop {Boolean} debug * @prop {Function} onClose * @prop {Function} onError @@ -48,22 +48,19 @@ Ws.create = function ({ let now = Date.now(); let sidUrl = `${dashsocketBaseUrl}/?EIO=3&transport=polling&t=${now}`; - let cookies = await cookieStore.get(sidUrl); - let sidResp = await fetch(sidUrl, { + let cookiesStr = await cookieStore.get(sidUrl); + let sidResp = await Ws.fetch(sidUrl, { //agent: httpAgent, //@ts-ignore - request function is not typed correctly headers: { - Cookie: cookies, + Cookie: cookiesStr, }, }); - if (!sidResp.ok) { - console.error(await sidResp.json()); - throw new Error("bad response"); - } + await cookieStore.set(sidUrl, sidResp); // ex: `97:0{"sid":"xxxx",...}` - let msg = await sidResp.json(); + let msg = sidResp.body || ""; let colonIndex = msg.indexOf(":"); // 0 is CONNECT, which will always follow our first message let start = colonIndex + ":0".length; @@ -95,24 +92,21 @@ Ws.create = function ({ let len = msg.length; let body = `${len}:${msg}`; - let cookies = await cookieStore.get(subUrl); - let subResp = await fetch(subUrl, { + let cookiesStr = await cookieStore.get(subUrl); + let subResp = await Ws.fetch(subUrl, { //agent: httpAgent, method: "POST", headers: { "Content-Type": "text/plain;charset=UTF-8", - Cookie: cookies, + Cookie: cookiesStr, }, body: body, }); - if (!subResp.ok) { - console.error(await subResp.json()); - throw new Error("bad response"); - } + await cookieStore.set(subUrl, subResp); // "ok" - return await subResp.json(); + return subResp.body; }; /* @@ -147,14 +141,15 @@ Ws.create = function ({ Eio3.connectWs = async function (sid) { let dashsocketBaseUrlPart = dashsocketBaseUrl.slice(4); // trim leading 'http' let url = `ws${dashsocketBaseUrlPart}/?EIO=3&transport=websocket&sid=${sid}`; + let sidUrl = `${dashsocketBaseUrl}/`; - let cookies = await cookieStore.get(`${dashsocketBaseUrl}/`); + let cookiesStr = await cookieStore.get(sidUrl); let ws = new WSClient(url, { //agent: httpAgent, //perMessageDeflate: false, //@ts-ignore - see above headers: { - Cookie: cookies, + Cookie: cookiesStr, }, }); @@ -169,10 +164,25 @@ Ws.create = function ({ ws.once("error", function (err) { if (onError) { onError(err); - } else { - console.error("WebSocket Error:"); - console.error(err); + return; } + + console.error("WebSocket Error:"); + console.error(err); + }); + + ws.once("unexpected-response", function (req, res) { + let err = new Error("unexpected-response"); + //@ts-ignore + err.response = res; + + if (onError) { + onError(err); + return; + } + + console.error("WebSocket Unexpected Response:"); + console.error(err); }); ws.once("message", function message(data) { @@ -261,7 +271,7 @@ Ws.create = function ({ return; } - /** @type {InsightPush} */ + /** @type {import('../').InsightPush} */ let [evname, data] = JSON.parse(msg.slice(2)); if (onMessage) { onMessage(evname, data); @@ -303,7 +313,7 @@ Ws.create = function ({ /** * @callback Finder * @param {String} evname - * @param {InsightSocketEventData} data + * @param {import('../').InsightSocketEventData} data */ /** @@ -359,8 +369,8 @@ Ws.listen = async function (dashsocketBaseUrl, find, opts) { * @param {String} addr * @param {Number} [amount] * @param {Number} [maxTxLockWait] - * @param {WsOpts} [opts] - * @returns {Promise} + * @param {Partial} [opts] + * @returns {Promise} */ Ws.waitForVout = async function ( dashsocketBaseUrl, @@ -374,13 +384,13 @@ Ws.waitForVout = async function ( } // Listen for Response - /** @type SocketPayment */ + /** @type {import('../').SocketPayment} */ let mempoolTx; return await Ws.listen(dashsocketBaseUrl, findResponse, opts); /** * @param {String} evname - * @param {InsightSocketEventData} data + * @param {import('../').InsightSocketEventData} data */ function findResponse(evname, data) { if (!["tx", "txlock"].includes(evname)) { @@ -397,39 +407,116 @@ Ws.waitForVout = async function ( let result; // TODO should fetch tx and match hotwallet as vin - data.vout.some(function (vout) { - if (!(addr in vout)) { - return false; - } - - let duffs = vout[addr]; - if (amount && duffs !== amount) { - return false; - } + data.vout.some( + /** @param {Record} vout */ + function (vout) { + if (!(addr in vout)) { + return false; + } - let newTx = { - address: addr, - timestamp: now, - txid: data.txid, - satoshis: duffs, - txlock: data.txlock, - }; + let duffs = vout[addr]; + if (amount && duffs !== amount) { + return false; + } - if ("txlock" !== evname) { - if (!mempoolTx) { - mempoolTx = newTx; + let newTx = { + address: addr, + timestamp: now, + txid: data.txid, + satoshis: duffs, + txlock: data.txlock, + }; + + if ("txlock" !== evname) { + if (!mempoolTx) { + mempoolTx = newTx; + } + return false; } - return false; - } - result = newTx; - return true; - }); + result = newTx; + return true; + }, + ); return result; } }; +/** @type {RequestInit} */ +let defaultRequest = { + mode: "cors", + credentials: "include", +}; + +/** + * @param {String | URL | Request} url + * @param {RequestInit} [_opts] + */ +Ws.fetch = async function dashfetch(url, _opts) { + let opts = Object.assign(defaultRequest, _opts); + + let resp = await fetch(url, opts); + /** @type {Record>} */ + let headers = {}; + + // for the Set-Cookie headers through AWS load balancer + let headerEntries = resp.headers.entries(); + for (let [key, value] of headerEntries) { + if (!headers[key]) { + headers[key] = value; + continue; + } + + let isArray = Array.isArray(headers[key]); + if (!isArray) { + //@ts-ignore + headers[key] = [headers[key]]; + } + //@ts-ignore + headers[key].push(value); + } + + let body = await resp.text(); + + let response = { + ok: resp.ok, + statusCode: resp.status, // backwards compat + statusText: resp.statusText, + headers: headers, + body: body, + toJSON: function () { + return { + ok: response.ok, + statusCode: response.statusCode, + statusText: response.statusText, + headers: response.headers, + body: response.body, + }; + }, + get status() { + console.warn( + "deprecated: please use either 'statusText' or 'statusCode' (node.js and browser both have 'status', but flipped)", + ); + return resp.statusText; + }, + _request: opts, + _response: resp, + }; + + if (resp.ok) { + return response; + } + + let err = new Error( + `http request was ${resp.status}, not ok. See err.response for details.`, + ); + + // @ts-ignore + err.response = response; + throw err; +}; + /* async function sleep(ms) { return await new Promise(function (resolve) {