dialer.html 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Browser Dialer</title>
  5. </head>
  6. <body>
  7. <script>
  8. "use strict";
  9. // Enable a much more aggressive JIT for performance gains
  10. // Copyright (c) 2021 XRAY. Mozilla Public License 2.0.
  11. let url = "ws://" + window.location.host + "/websocket?token=csrfToken";
  12. let clientIdleCount = 0;
  13. let upstreamGetCount = 0;
  14. let upstreamWsCount = 0;
  15. let upstreamPostCount = 0;
  16. function prepareRequestInit(extra) {
  17. const requestInit = {};
  18. if (extra.referrer) {
  19. // note: we have to strip the protocol and host part.
  20. // Browsers disallow that, and will reset the value to current page if attempted.
  21. const referrer = URL.parse(extra.referrer);
  22. requestInit.referrer = referrer.pathname + referrer.search + referrer.hash;
  23. requestInit.referrerPolicy = "unsafe-url";
  24. }
  25. if (extra.headers) {
  26. requestInit.headers = extra.headers;
  27. }
  28. return requestInit;
  29. }
  30. let check = function () {
  31. if (clientIdleCount > 0) {
  32. return;
  33. }
  34. clientIdleCount += 1;
  35. console.log("Prepare", url);
  36. let ws = new WebSocket(url);
  37. // arraybuffer is significantly faster in chrome than default
  38. // blob, tested with chrome 123
  39. ws.binaryType = "arraybuffer";
  40. // note: this event listener is later overwritten after the
  41. // handshake has completed. do not attempt to modernize it without
  42. // double-checking that this continues to work
  43. ws.onmessage = function (event) {
  44. clientIdleCount -= 1;
  45. let task = JSON.parse(event.data);
  46. switch (task.method) {
  47. case "WS": {
  48. upstreamWsCount += 1;
  49. console.log("Dial WS", task.url, task.extra.protocol);
  50. const wss = new WebSocket(task.url, task.extra.protocol);
  51. wss.binaryType = "arraybuffer";
  52. let opened = false;
  53. ws.onmessage = function (event) {
  54. wss.send(event.data)
  55. };
  56. wss.onopen = function (event) {
  57. opened = true;
  58. ws.send("ok")
  59. };
  60. wss.onmessage = function (event) {
  61. ws.send(event.data)
  62. };
  63. wss.onclose = function (event) {
  64. upstreamWsCount -= 1;
  65. console.log("Dial WS DONE, remaining: ", upstreamWsCount);
  66. ws.close()
  67. };
  68. wss.onerror = function (event) {
  69. !opened && ws.send("fail")
  70. wss.close()
  71. };
  72. ws.onclose = function (event) {
  73. wss.close()
  74. };
  75. break;
  76. }
  77. case "GET": {
  78. (async () => {
  79. const requestInit = prepareRequestInit(task.extra);
  80. console.log("Dial GET", task.url);
  81. ws.send("ok");
  82. const controller = new AbortController();
  83. /*
  84. Aborting a streaming response in JavaScript
  85. requires two levers to be pulled:
  86. First, the streaming read itself has to be cancelled using
  87. reader.cancel(), only then controller.abort() will actually work.
  88. If controller.abort() alone is called while a
  89. reader.read() is ongoing, it will block until the server closes the
  90. response, the page is refreshed or the network connection is lost.
  91. */
  92. let reader = null;
  93. ws.onclose = (event) => {
  94. try {
  95. reader && reader.cancel();
  96. } catch(e) {}
  97. try {
  98. controller.abort();
  99. } catch(e) {}
  100. };
  101. try {
  102. upstreamGetCount += 1;
  103. requestInit.signal = controller.signal;
  104. const response = await fetch(task.url, requestInit);
  105. const body = await response.body;
  106. reader = body.getReader();
  107. while (true) {
  108. const { done, value } = await reader.read();
  109. if (value) ws.send(value); // don't send back "undefined" string when received nothing
  110. if (done) break;
  111. }
  112. } finally {
  113. upstreamGetCount -= 1;
  114. console.log("Dial GET DONE, remaining: ", upstreamGetCount);
  115. ws.close();
  116. }
  117. })();
  118. break;
  119. }
  120. case "POST": {
  121. upstreamPostCount += 1;
  122. const requestInit = prepareRequestInit(task.extra);
  123. requestInit.method = "POST";
  124. console.log("Dial POST", task.url);
  125. ws.send("ok");
  126. ws.onmessage = async (event) => {
  127. try {
  128. requestInit.body = event.data;
  129. const response = await fetch(task.url, requestInit);
  130. if (response.ok) {
  131. ws.send("ok");
  132. } else {
  133. console.error("bad status code");
  134. ws.send("fail");
  135. }
  136. } finally {
  137. upstreamPostCount -= 1;
  138. console.log("Dial POST DONE, remaining: ", upstreamPostCount);
  139. ws.close();
  140. }
  141. };
  142. break;
  143. }
  144. }
  145. check();
  146. };
  147. ws.onerror = function (event) {
  148. ws.close();
  149. };
  150. };
  151. let checkTask = setInterval(check, 1000);
  152. </script>
  153. </body>
  154. </html>