index.js 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. "use strict";
  2. // This module exports a function that connects to SSB and returns an interface
  3. // to call methods over MuxRPC. It's a thin wrapper around SSB-Client, which is
  4. // a thin wrapper around the MuxRPC module.
  5. const { promisify } = require("util");
  6. const ssbClient = require("ssb-client");
  7. const ssbConfig = require("ssb-config");
  8. const ssbKeys = require("ssb-keys");
  9. const debug = require("debug")("oasis");
  10. const path = require("path");
  11. const lodash = require("lodash");
  12. const fs = require("fs");
  13. const os = require("os");
  14. const flotilla = require("./flotilla");
  15. // Use temporary path if we're running a test.
  16. if (process.env.OASIS_TEST) {
  17. ssbConfig.path = fs.mkdtempSync(path.join(os.tmpdir(), "oasis-"));
  18. ssbConfig.keys = ssbKeys.generate();
  19. }
  20. const socketPath = path.join(ssbConfig.path, "socket");
  21. const publicInteger = ssbConfig.keys.public.replace(".ed25519", "");
  22. const remote = `unix:${socketPath}~noauth:${publicInteger}`;
  23. /**
  24. * @param formatter {string} input
  25. * @param args {any[]} input
  26. */
  27. const log = (formatter, ...args) => {
  28. const isDebugEnabled = debug.enabled;
  29. debug.enabled = true;
  30. debug(formatter, ...args);
  31. debug.enabled = isDebugEnabled;
  32. };
  33. /**
  34. * @param [options] {object} - options to pass to SSB-Client
  35. * @returns Promise
  36. */
  37. const connect = (options) =>
  38. new Promise((resolve, reject) => {
  39. const onSuccess = (ssb) => {
  40. resolve(ssb);
  41. };
  42. ssbClient(process.env.OASIS_TEST ? ssbConfig.keys : null, options)
  43. .then(onSuccess)
  44. .catch(reject);
  45. });
  46. let closing = false;
  47. let serverHandle;
  48. let clientHandle;
  49. /**
  50. * Attempts connection over Unix socket, falling back to TCP socket if that
  51. * fails. If the TCP socket fails, the promise is rejected.
  52. * @returns Promise
  53. */
  54. const attemptConnection = () =>
  55. new Promise((resolve, reject) => {
  56. const originalConnect = process.env.OASIS_TEST
  57. ? new Promise((resolve, reject) =>
  58. reject({
  59. message: "could not connect to sbot",
  60. })
  61. )
  62. : connect({ remote });
  63. originalConnect
  64. .then((ssb) => {
  65. debug("Connected to existing Scuttlebutt service over Unix socket");
  66. resolve(ssb);
  67. })
  68. .catch((e) => {
  69. if (closing) return;
  70. debug("Unix socket failed");
  71. if (e.message !== "could not connect to sbot") {
  72. throw e;
  73. }
  74. connect()
  75. .then((ssb) => {
  76. log("Connected to existing Scuttlebutt service over TCP socket");
  77. resolve(ssb);
  78. })
  79. .catch((e) => {
  80. if (closing) return;
  81. debug("TCP socket failed");
  82. if (e.message !== "could not connect to sbot") {
  83. throw e;
  84. }
  85. reject(new Error("Both connection options failed"));
  86. });
  87. });
  88. });
  89. let pendingConnection = null;
  90. const ensureConnection = (customConfig) => {
  91. if (pendingConnection === null) {
  92. pendingConnection = new Promise((resolve) => {
  93. setTimeout(() => {
  94. attemptConnection()
  95. .then((ssb) => {
  96. resolve(ssb);
  97. })
  98. .catch(() => {
  99. serverHandle = flotilla(customConfig);
  100. attemptConnection()
  101. .then(resolve)
  102. .catch((e) => {
  103. throw new Error(e);
  104. });
  105. }, 100);
  106. });
  107. });
  108. const cancel = () => (pendingConnection = null);
  109. pendingConnection.then(cancel, cancel);
  110. }
  111. return pendingConnection;
  112. };
  113. module.exports = ({ offline }) => {
  114. if (offline) {
  115. log("Offline mode activated - not connecting to scuttlebutt peers or pubs");
  116. }
  117. // Make a copy of `ssbConfig` to avoid mutating.
  118. const customConfig = JSON.parse(JSON.stringify(ssbConfig));
  119. // Only change the config if `--offline` is true.
  120. if (offline === true) {
  121. lodash.set(customConfig, "conn.autostart", false);
  122. }
  123. // Use `conn.hops`, or default to `friends.hops`, or default to `0`.
  124. lodash.set(
  125. customConfig,
  126. "conn.hops",
  127. lodash.get(ssbConfig, "conn.hops", lodash.get(ssbConfig.friends.hops, 0))
  128. );
  129. /**
  130. * This is "cooler", a tiny interface for opening or reusing an instance of
  131. * SSB-Client.
  132. */
  133. const cooler = {
  134. open() {
  135. // This has interesting behavior that may be unexpected.
  136. //
  137. // If `clientHandle` is already an active [non-closed] connection, return that.
  138. //
  139. // If the connection is closed, we need to restart it. It's important to
  140. // note that if we're depending on an external service (like Patchwork) and
  141. // that app is closed, then Oasis will seamlessly start its own SSB service.
  142. return new Promise((resolve, reject) => {
  143. if (clientHandle && clientHandle.closed === false) {
  144. resolve(clientHandle);
  145. } else {
  146. ensureConnection(customConfig).then((ssb) => {
  147. clientHandle = ssb;
  148. if (closing) {
  149. cooler.close();
  150. reject(new Error("Closing Oasis"));
  151. } else {
  152. resolve(ssb);
  153. }
  154. });
  155. }
  156. });
  157. },
  158. close() {
  159. closing = true;
  160. if (clientHandle && clientHandle.closed === false) {
  161. clientHandle.close();
  162. }
  163. if (serverHandle) {
  164. serverHandle.close();
  165. }
  166. },
  167. };
  168. cooler.open();
  169. return cooler;
  170. };