index.js 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  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 ssbTangle = require("ssb-tangle");
  9. const ssbKeys = require("ssb-keys");
  10. const debug = require("debug")("oasis");
  11. const path = require("path");
  12. const lodash = require("lodash");
  13. const fs = require("fs");
  14. const os = require("os");
  15. const flotilla = require("./flotilla");
  16. // Use temporary path if we're running a test.
  17. // TODO: Refactor away 'OASIS_TEST' variable.
  18. if (process.env.OASIS_TEST) {
  19. ssbConfig.path = fs.mkdtempSync(path.join(os.tmpdir(), "oasis-"));
  20. ssbConfig.keys = ssbKeys.generate();
  21. }
  22. const socketPath = path.join(ssbConfig.path, "socket");
  23. const publicInteger = ssbConfig.keys.public.replace(".ed25519", "");
  24. const remote = `unix:${socketPath}~noauth:${publicInteger}`;
  25. /**
  26. * @param formatter {string} input
  27. * @param args {any[]} input
  28. */
  29. const log = (formatter, ...args) => {
  30. const isDebugEnabled = debug.enabled;
  31. debug.enabled = true;
  32. debug(formatter, ...args);
  33. debug.enabled = isDebugEnabled;
  34. };
  35. /**
  36. * @param [options] {object} - options to pass to SSB-Client
  37. * @returns Promise
  38. */
  39. const connect = (options) =>
  40. new Promise((resolve, reject) => {
  41. const onSuccess = (api) => {
  42. if (api.tangle === undefined) {
  43. // HACK: SSB-Tangle isn't available in Patchwork, but we want that
  44. // compatibility. This code automatically injects SSB-Tangle into our
  45. // stack so that we don't get weird errors when using Patchwork.
  46. api.tangle = ssbTangle.init(api);
  47. // MuxRPC supports promises but the raw plugin does not.
  48. api.tangle.branch = promisify(api.tangle.branch);
  49. }
  50. resolve(api);
  51. };
  52. ssbClient(process.env.OASIS_TEST ? ssbConfig.keys : null, options)
  53. .then(onSuccess)
  54. .catch(reject);
  55. });
  56. let closing = false;
  57. let serverHandle;
  58. let clientHandle;
  59. /**
  60. * Attempts connection over Unix socket, falling back to TCP socket if that
  61. * fails. If the TCP socket fails, the promise is rejected.
  62. * @returns Promise
  63. */
  64. const attemptConnection = () =>
  65. new Promise((resolve, reject) => {
  66. const originalConnect = process.env.OASIS_TEST
  67. ? new Promise((resolve, reject) =>
  68. reject({
  69. message: "could not connect to sbot",
  70. })
  71. )
  72. : connect({ remote });
  73. originalConnect
  74. .then((ssb) => {
  75. debug("Connected to existing Scuttlebutt service over Unix socket");
  76. resolve(ssb);
  77. })
  78. .catch((e) => {
  79. if (closing) return;
  80. debug("Unix socket failed");
  81. if (e.message !== "could not connect to sbot") {
  82. throw e;
  83. }
  84. connect()
  85. .then((ssb) => {
  86. log("Connected to existing Scuttlebutt service over TCP socket");
  87. resolve(ssb);
  88. })
  89. .catch((e) => {
  90. if (closing) return;
  91. debug("TCP socket failed");
  92. if (e.message !== "could not connect to sbot") {
  93. throw e;
  94. }
  95. reject(new Error("Both connection options failed"));
  96. });
  97. });
  98. });
  99. let pendingConnection = null;
  100. const ensureConnection = (customConfig) => {
  101. if (pendingConnection === null) {
  102. pendingConnection = new Promise((resolve) => {
  103. attemptConnection()
  104. .then((ssb) => {
  105. resolve(ssb);
  106. })
  107. .catch(() => {
  108. debug("Connection attempts to existing Scuttlebutt services failed");
  109. log("Starting Scuttlebutt service");
  110. // Adjust with `customConfig`, which declares further preferences.
  111. serverHandle = flotilla(customConfig);
  112. // Give the server a moment to start. This is a race condition. :/
  113. setTimeout(() => {
  114. attemptConnection()
  115. .then(resolve)
  116. .catch((e) => {
  117. throw new Error(e);
  118. });
  119. }, 100);
  120. });
  121. });
  122. const cancel = () => (pendingConnection = null);
  123. pendingConnection.then(cancel, cancel);
  124. }
  125. return pendingConnection;
  126. };
  127. module.exports = ({ offline }) => {
  128. if (offline) {
  129. log("Offline mode activated - not connecting to scuttlebutt peers or pubs");
  130. log(
  131. "WARNING: Oasis can connect to the internet through your other SSB apps if they're running."
  132. );
  133. }
  134. // Make a copy of `ssbConfig` to avoid mutating.
  135. const customConfig = JSON.parse(JSON.stringify(ssbConfig));
  136. // This is unnecessary when https://github.com/ssbc/ssb-config/pull/72 is merged
  137. customConfig.connections.incoming.unix = [
  138. { scope: "device", transform: "noauth" },
  139. ];
  140. // Only change the config if `--offline` is true.
  141. if (offline === true) {
  142. lodash.set(customConfig, "conn.autostart", false);
  143. }
  144. // Use `conn.hops`, or default to `friends.hops`, or default to `0`.
  145. lodash.set(
  146. customConfig,
  147. "conn.hops",
  148. lodash.get(ssbConfig, "conn.hops", lodash.get(ssbConfig.friends.hops, 0))
  149. );
  150. /**
  151. * This is "cooler", a tiny interface for opening or reusing an instance of
  152. * SSB-Client.
  153. */
  154. const cooler = {
  155. open() {
  156. // This has interesting behavior that may be unexpected.
  157. //
  158. // If `clientHandle` is already an active [non-closed] connection, return that.
  159. //
  160. // If the connection is closed, we need to restart it. It's important to
  161. // note that if we're depending on an external service (like Patchwork) and
  162. // that app is closed, then Oasis will seamlessly start its own SSB service.
  163. return new Promise((resolve, reject) => {
  164. if (clientHandle && clientHandle.closed === false) {
  165. resolve(clientHandle);
  166. } else {
  167. ensureConnection(customConfig).then((ssb) => {
  168. clientHandle = ssb;
  169. if (closing) {
  170. cooler.close();
  171. reject(new Error("Closing Oasis"));
  172. } else {
  173. resolve(ssb);
  174. }
  175. });
  176. }
  177. });
  178. },
  179. close() {
  180. closing = true;
  181. if (clientHandle && clientHandle.closed === false) {
  182. clientHandle.close();
  183. }
  184. if (serverHandle) {
  185. serverHandle.close();
  186. }
  187. },
  188. };
  189. // Important: This ensures that we have an SSB connection as soon as Oasis
  190. // starts. If we don't do this, then we don't even attempt an SSB connection
  191. // until we receive our first HTTP request.
  192. cooler.open();
  193. return cooler;
  194. };