initial commit
This commit is contained in:
parent
732a35ddf2
commit
3ee39e63c5
39 changed files with 15767 additions and 2 deletions
10
.babelrc
Normal file
10
.babelrc
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"env": {
|
||||
"test": {
|
||||
"plugins": [
|
||||
"transform-es2015-modules-commonjs",
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
70
.eslintrc.json
Normal file
70
.eslintrc.json
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
{
|
||||
"extends": ["eslint:all", "prettier", "plugin:jest/all"],
|
||||
"plugins": ["prettier", "jest", "babel"],
|
||||
"rules": {
|
||||
"prettier/prettier": "error",
|
||||
"strict": "off",
|
||||
"id-length": ["error", { "exceptions": ["_"] }],
|
||||
|
||||
"no-console": "off",
|
||||
|
||||
"max-statements": "off",
|
||||
|
||||
"global-require": "off",
|
||||
|
||||
"new-cap": "off",
|
||||
|
||||
"one-var": "off",
|
||||
|
||||
"max-lines-per-function": "off",
|
||||
|
||||
"no-underscore-dangle": "off",
|
||||
|
||||
"no-implicit-coercion": "off",
|
||||
|
||||
"no-magic-numbers": "off",
|
||||
|
||||
"no-negated-condition": "off",
|
||||
|
||||
"capitalized-comments": "off",
|
||||
|
||||
"max-params": "off",
|
||||
|
||||
"multiline-comment-style": "off",
|
||||
|
||||
"spaced-comment": "off",
|
||||
|
||||
"no-inline-comments": "off",
|
||||
|
||||
"sort-keys": "off",
|
||||
|
||||
"max-lines": "off",
|
||||
|
||||
"prefer-template": "off",
|
||||
|
||||
"callback-return": "off",
|
||||
|
||||
"no-ternary": "off",
|
||||
|
||||
"no-invalid-this": "off",
|
||||
|
||||
"babel/no-invalid-this": "error",
|
||||
|
||||
"complexity": "off",
|
||||
|
||||
"yoda": "off",
|
||||
|
||||
"prefer-promise-reject-errors": "off",
|
||||
|
||||
"camelcase": "off",
|
||||
|
||||
"consistent-return": "off",
|
||||
|
||||
"no-shadow": "off"
|
||||
},
|
||||
"parser": "babel-eslint",
|
||||
"env": {
|
||||
"node": true,
|
||||
"es6": true
|
||||
}
|
||||
}
|
||||
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
node_modules
|
||||
services/auth/secrets.json
|
||||
.env
|
||||
*.log
|
||||
.directory
|
||||
|
||||
radata/
|
||||
*.cert
|
||||
*.key
|
||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"requirePragma": true,
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"endOfLine": "lf"
|
||||
}
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"eslint.enable": true
|
||||
}
|
||||
31
README.md
31
README.md
|
|
@ -1,2 +1,29 @@
|
|||
# api
|
||||
backend for wallet that communicates with gun+lnd
|
||||
This is an early test release of the [ShockWallet](shockwallet.app) API.
|
||||
|
||||
For easy setup on your Laptop/Desktop, [a wizard is available here.](https://github.com/shocknet/wizard)
|
||||
|
||||
|
||||
### Manual Installation
|
||||
#### Notes:
|
||||
* The service defaults to port `9835`
|
||||
* Looks for local LND in its default path
|
||||
* Default gun peer is `gun.shock.network`
|
||||
* Change defaults in `defaults.js`
|
||||
* Requires [Node.js](https://nodejs.org) LTS
|
||||
|
||||
#### Steps:
|
||||
1) Run [LND](https://github.com/lightningnetwork/lnd/releases) - *Example testnet startup*:
|
||||
|
||||
```./lnd --bitcoin.active --bitcoin.testnet --bitcoin.node=neutrino --neutrino.connect=faucet.lightning.community```
|
||||
|
||||
|
||||
2) Download and Install API
|
||||
|
||||
```
|
||||
git pull https://github.com/shocknet/api
|
||||
cd api
|
||||
yarn install
|
||||
```
|
||||
|
||||
3) Run with `node main -h 0.0.0.0`
|
||||
4) Connect with ShockWallet
|
||||
|
|
|
|||
52
config/defaults.js
Normal file
52
config/defaults.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
const os = require("os");
|
||||
const path = require("path");
|
||||
const platform = os.platform();
|
||||
const homeDir = os.homedir();
|
||||
|
||||
const getLndDirectory = () => {
|
||||
if (platform === "darwin") {
|
||||
return homeDir + "/Library/Application Support/Lnd";
|
||||
} else if (platform === "win32") {
|
||||
// eslint-disable-next-line no-process-env
|
||||
const { APPDATA = "" } = process.env;
|
||||
return path.resolve(APPDATA, "../Local/Lnd");
|
||||
}
|
||||
|
||||
return homeDir + "/.lnd";
|
||||
};
|
||||
|
||||
const parsePath = (filePath = "") => {
|
||||
if (platform === "win32") {
|
||||
return filePath.replace("/", "\\");
|
||||
}
|
||||
|
||||
return filePath;
|
||||
};
|
||||
|
||||
const lndDirectory = getLndDirectory();
|
||||
|
||||
module.exports = (mainnet = false) => {
|
||||
const network = mainnet ? "mainnet" : "testnet";
|
||||
|
||||
return {
|
||||
serverPort: 9835,
|
||||
serverHost: "localhost",
|
||||
sessionSecret: "my session secret",
|
||||
sessionMaxAge: 300000,
|
||||
lndAddress: "127.0.0.1:9735",
|
||||
maxNumRoutesToQuery: 20,
|
||||
lndProto: parsePath(`${__dirname}/rpc.proto`),
|
||||
lndHost: "localhost:10009",
|
||||
lndCertPath: parsePath(`${lndDirectory}/tls.cert`),
|
||||
macaroonPath: parsePath(
|
||||
`${lndDirectory}/data/chain/bitcoin/${network}/admin.macaroon`
|
||||
),
|
||||
dataPath: parsePath(`${lndDirectory}/data`),
|
||||
loglevel: "info",
|
||||
logfile: "shockapi.log",
|
||||
lndLogFile: parsePath(`${lndDirectory}/logs/bitcoin/${network}/lnd.log`),
|
||||
lndDirPath: lndDirectory,
|
||||
peers: ["http://gun.shock.network:8765/gun"],
|
||||
tokenExpirationMS: 4500000
|
||||
};
|
||||
};
|
||||
22
config/log.js
Normal file
22
config/log.js
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// config/log.js
|
||||
|
||||
const winston = require("winston");
|
||||
require("winston-daily-rotate-file");
|
||||
|
||||
module.exports = function(logFileName, logLevel) {
|
||||
winston.cli();
|
||||
|
||||
winston.level = logLevel;
|
||||
|
||||
winston.add(winston.transports.DailyRotateFile, {
|
||||
filename: logFileName,
|
||||
datePattern: "yyyy-MM-dd.",
|
||||
prepend: true,
|
||||
json: false,
|
||||
maxSize: 1000000,
|
||||
maxFiles: 7,
|
||||
level: logLevel
|
||||
});
|
||||
|
||||
return winston;
|
||||
};
|
||||
2799
config/rpc.proto
Normal file
2799
config/rpc.proto
Normal file
File diff suppressed because it is too large
Load diff
18
constants/errors.js
Normal file
18
constants/errors.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
module.exports = {
|
||||
MACAROON_PATH: path => `
|
||||
The specified macaroon path "${path}" was not found.
|
||||
This issue can be caused by:
|
||||
|
||||
1. Setting an invalid path for your Macaroon file.
|
||||
2. Not initializing your wallet before using the ShockAPI
|
||||
`,
|
||||
CERT_PATH: path => `
|
||||
The specified LND certificate file "${path}" was not found.
|
||||
This issue can be caused by:
|
||||
|
||||
1. Setting an invalid path for your Certificates.
|
||||
2. Not initializing your wallet before using the ShockAPI
|
||||
`,
|
||||
CERT_MISSING: () =>
|
||||
"Required LND certificate path missing from application configuration."
|
||||
};
|
||||
29
main.js
Normal file
29
main.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
const program = require("commander");
|
||||
|
||||
// parse command line parameters
|
||||
program
|
||||
.version("1.0.0")
|
||||
.option("-s, --serverport [port]", "web server http listening port (defaults to 8280)")
|
||||
.option("-x, --httpsport [port]", "web server https listening port (defaults to 8283)")
|
||||
.option("-h, --serverhost [host]", "web server listening host (defaults to localhost)")
|
||||
.option("-l, --lndhost [host:port]", "RPC lnd host (defaults to localhost:10009)")
|
||||
.option("-t, --usetls [path]", "path to a directory containing key.pem and cert.pem files")
|
||||
.option("-u, --user [login]", "basic authentication login")
|
||||
.option("-p, --pwd [password]", "basic authentication password")
|
||||
.option("-r, --limituser [login]", "basic authentication login for readonly account")
|
||||
.option("-w, --limitpwd [password]", "basic authentication password for readonly account")
|
||||
.option("-m, --macaroon-path [file path]", "path to admin.macaroon file")
|
||||
.option("-d, --lnd-cert-path [file path]", "path to LND cert file")
|
||||
.option("-f, --logfile [file path]", "path to file where to store the application logs")
|
||||
.option("-e, --loglevel [level]", "level of logs to display (debug, info, warn, error)")
|
||||
.option("-n, --lndlogfile <file path>", "path to lnd log file to send to browser")
|
||||
.option("-k, --le-email [email]", "lets encrypt required contact email")
|
||||
.option("-c, --mainnet", "run server on mainnet mode")
|
||||
.parse(process.argv);
|
||||
|
||||
// load server
|
||||
if (program.serverhost && program.leEmail) {
|
||||
require("./app/server-le")(program); // Let"s Encrypt server version
|
||||
} else {
|
||||
require("./src/server")(program); // Standard server version
|
||||
}
|
||||
82
package.json
Normal file
82
package.json
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
{
|
||||
"name": "sw-server",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "src/server.js",
|
||||
"scripts": {
|
||||
"start": "node --experimental-modules src/server.js",
|
||||
"dev": "node main.js -h 0.0.0.0",
|
||||
"dev:watch": "nodemon main.js -- -h 0.0.0.0",
|
||||
"test": "jest --no-cache",
|
||||
"test:watch": "jest --no-cache --watch",
|
||||
"typecheck": "tsc",
|
||||
"eslint": "eslint services/**/*.js src/**/*.js utils/**/*.js config/**/*.js constants/**/*.js",
|
||||
"lint": "eslint \"services/gunDB/**/*.js\"",
|
||||
"format": "prettier --write \"./**/*.js\""
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"precommit": "eslint"
|
||||
}
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.5.1",
|
||||
"axios": "^0.19.0",
|
||||
"basic-auth": "^2.0.0",
|
||||
"bitcore-lib": "^0.15.0",
|
||||
"body-parser": "^1.16.0",
|
||||
"colors": "^1.3.0",
|
||||
"command-exists": "^1.2.6",
|
||||
"commander": "^2.9.0",
|
||||
"cors": "^2.8.4",
|
||||
"debug": "^3.1.0",
|
||||
"dotenv": "^8.1.0",
|
||||
"express": "^4.14.1",
|
||||
"express-session": "^1.15.1",
|
||||
"google-proto-files": "^1.0.3",
|
||||
"graphviz": "0.0.8",
|
||||
"grpc": "^1.21.1",
|
||||
"gun": "^0.2019.1120",
|
||||
"husky": "^3.0.9",
|
||||
"jsonfile": "^4.0.0",
|
||||
"jsonwebtoken": "^8.3.0",
|
||||
"localtunnel": "^1.9.0",
|
||||
"lodash": "^4.17.15",
|
||||
"method-override": "^2.3.7",
|
||||
"promise": "^8.0.1",
|
||||
"request": "^2.87.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"response-time": "^2.3.2",
|
||||
"shelljs": "^0.8.2",
|
||||
"socket.io": "^2.1.1",
|
||||
"text-encoding": "^0.7.0",
|
||||
"tingodb": "^0.6.1",
|
||||
"winston": "^2.3.1",
|
||||
"winston-daily-rotate-file": "^1.4.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-class-properties": "^7.5.5",
|
||||
"@types/dotenv": "^6.1.1",
|
||||
"@types/express": "^4.17.1",
|
||||
"@types/gun": "^0.9.1",
|
||||
"@types/jest": "^24.0.18",
|
||||
"@types/lodash": "^4.14.141",
|
||||
"@types/socket.io": "^2.1.3",
|
||||
"@types/socket.io-client": "^1.4.32",
|
||||
"@types/uuid": "^3.4.5",
|
||||
"babel-eslint": "^10.0.3",
|
||||
"babel-plugin-transform-es2015-modules-commonjs": "^6.26.2",
|
||||
"eslint": "^6.6.0",
|
||||
"eslint-config-prettier": "^6.5.0",
|
||||
"eslint-plugin-babel": "^5.3.0",
|
||||
"eslint-plugin-jest": "^22.20.1",
|
||||
"eslint-plugin-prettier": "^3.1.1",
|
||||
"jest": "^24.9.0",
|
||||
"nodemon": "^1.19.3",
|
||||
"prettier": "^1.18.2",
|
||||
"socket.io-client": "^2.2.0",
|
||||
"typescript": "^3.6.3"
|
||||
}
|
||||
}
|
||||
129
services/auth/auth.js
Normal file
129
services/auth/auth.js
Normal file
|
|
@ -0,0 +1,129 @@
|
|||
/*
|
||||
* @prettier
|
||||
*/
|
||||
// @ts-nocheck
|
||||
const jwt = require('jsonwebtoken')
|
||||
const uuidv1 = require('uuid/v1')
|
||||
const jsonfile = require('jsonfile')
|
||||
const path = require('path')
|
||||
const logger = require('winston')
|
||||
const FS = require('../../utils/fs')
|
||||
const secretsFilePath = path.resolve(__dirname, 'secrets.json')
|
||||
|
||||
class Auth {
|
||||
verifySecretsFile = async () => {
|
||||
try {
|
||||
const fileExists = await FS.access(secretsFilePath)
|
||||
|
||||
if (!fileExists) {
|
||||
return { exists: false }
|
||||
}
|
||||
|
||||
const secretsFile = await FS.readFile(secretsFilePath, {
|
||||
encoding: 'utf8'
|
||||
})
|
||||
|
||||
// Check if secrets file has valid JSON
|
||||
JSON.parse(secretsFile)
|
||||
|
||||
return { exists: true, parsable: true }
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
return { exists: true, parsable: false }
|
||||
}
|
||||
}
|
||||
|
||||
initSecretsFile = async () => {
|
||||
const { exists, parsable } = await this.verifySecretsFile()
|
||||
|
||||
if (exists && parsable) {
|
||||
return true
|
||||
}
|
||||
|
||||
if (exists && !parsable) {
|
||||
await FS.unlink(secretsFilePath)
|
||||
}
|
||||
|
||||
await FS.writeFile(secretsFilePath, '{}')
|
||||
|
||||
logger.info('New secrets file generated!')
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
readSecrets = () =>
|
||||
new Promise((resolve, reject) => {
|
||||
this.initSecretsFile().then(() => {
|
||||
jsonfile.readFile(secretsFilePath, (err, allSecrets) => {
|
||||
if (err) {
|
||||
logger.info('readSecrets err', err)
|
||||
reject('Problem reading secrets file')
|
||||
} else {
|
||||
resolve(allSecrets)
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
writeSecrets(key, value) {
|
||||
return this.initSecretsFile()
|
||||
.then(() => this.readSecrets())
|
||||
.then(allSecrets => {
|
||||
return new Promise((resolve, reject) => {
|
||||
allSecrets[key] = value
|
||||
jsonfile.writeFile(
|
||||
secretsFilePath,
|
||||
allSecrets,
|
||||
{ spaces: 2, EOL: '\r\n' },
|
||||
err => {
|
||||
if (err) {
|
||||
logger.info('writeSecrets err', err)
|
||||
reject(err)
|
||||
} else {
|
||||
resolve(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async generateToken() {
|
||||
const timestamp = Date.now()
|
||||
const secret = uuidv1()
|
||||
const token = jwt.sign(
|
||||
{
|
||||
data: { timestamp }
|
||||
},
|
||||
secret,
|
||||
{ expiresIn: '500h' }
|
||||
)
|
||||
await this.writeSecrets(timestamp, secret)
|
||||
return token
|
||||
}
|
||||
|
||||
async validateToken(token) {
|
||||
try {
|
||||
await this.initSecretsFile()
|
||||
const key = jwt.decode(token).data.timestamp
|
||||
const secrets = await this.readSecrets()
|
||||
const secret = secrets[key]
|
||||
return new Promise((resolve, reject) => {
|
||||
jwt.verify(token, secret, (err, decoded) => {
|
||||
if (err) {
|
||||
logger.info('validateToken err', err)
|
||||
reject(err)
|
||||
} else {
|
||||
logger.info('decoded', decoded)
|
||||
resolve({ valid: true })
|
||||
}
|
||||
})
|
||||
})
|
||||
} catch (err) {
|
||||
console.error(err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new Auth()
|
||||
917
services/gunDB/Mediator/index.js
Normal file
917
services/gunDB/Mediator/index.js
Normal file
|
|
@ -0,0 +1,917 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
const Gun = require('gun')
|
||||
const debounce = require('lodash/debounce')
|
||||
const once = require('lodash/once')
|
||||
|
||||
/** @type {import('../contact-api/SimpleGUN').ISEA} */
|
||||
// @ts-ignore
|
||||
const SEAx = require('gun/sea')
|
||||
|
||||
/** @type {import('../contact-api/SimpleGUN').ISEA} */
|
||||
const mySEA = {}
|
||||
|
||||
const $$__SHOCKWALLET__MSG__ = '$$__SHOCKWALLET__MSG__'
|
||||
const $$__SHOCKWALLET__ENCRYPTED__ = '$$_SHOCKWALLET__ENCRYPTED__'
|
||||
|
||||
mySEA.encrypt = (msg, secret) => {
|
||||
if (typeof msg !== 'string') {
|
||||
throw new TypeError('mySEA.encrypt() -> expected msg to be an string')
|
||||
}
|
||||
|
||||
if (msg.length === 0) {
|
||||
throw new TypeError(
|
||||
'mySEA.encrypt() -> expected msg to be a populated string'
|
||||
)
|
||||
}
|
||||
|
||||
// Avoid this: https://github.com/amark/gun/issues/804 and any other issues
|
||||
const sanitizedMsg = $$__SHOCKWALLET__MSG__ + msg
|
||||
|
||||
return SEAx.encrypt(sanitizedMsg, secret).then(encMsg => {
|
||||
return $$__SHOCKWALLET__ENCRYPTED__ + encMsg
|
||||
})
|
||||
}
|
||||
|
||||
mySEA.decrypt = (encMsg, secret) => {
|
||||
if (typeof encMsg !== 'string') {
|
||||
throw new TypeError('mySEA.encrypt() -> expected encMsg to be an string')
|
||||
}
|
||||
|
||||
if (encMsg.length === 0) {
|
||||
throw new TypeError(
|
||||
'mySEA.encrypt() -> expected encMsg to be a populated string'
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof secret !== 'string') {
|
||||
throw new TypeError('mySea.decrypt() -> expected secret to be an string')
|
||||
}
|
||||
|
||||
if (secret.length === 0) {
|
||||
throw new TypeError(
|
||||
'mySea.decrypt() -> expected secret to be a populated string'
|
||||
)
|
||||
}
|
||||
|
||||
if (encMsg.indexOf($$__SHOCKWALLET__ENCRYPTED__) !== 0) {
|
||||
throw new TypeError(
|
||||
'Trying to pass a non prefixed encrypted string to mySea.decrypt(): ' +
|
||||
encMsg
|
||||
)
|
||||
}
|
||||
|
||||
return SEAx.decrypt(
|
||||
encMsg.slice($$__SHOCKWALLET__ENCRYPTED__.length),
|
||||
secret
|
||||
).then(decodedMsg => {
|
||||
if (typeof decodedMsg !== 'string') {
|
||||
throw new TypeError('Could not decrypt')
|
||||
}
|
||||
|
||||
return decodedMsg.slice($$__SHOCKWALLET__MSG__.length)
|
||||
})
|
||||
}
|
||||
|
||||
mySEA.secret = (recipientOrSenderEpub, recipientOrSenderSEA) => {
|
||||
if (recipientOrSenderEpub === recipientOrSenderSEA.pub) {
|
||||
throw new Error('Do not use pub for mysecret')
|
||||
}
|
||||
return SEAx.secret(recipientOrSenderEpub, recipientOrSenderSEA).then(sec => {
|
||||
if (typeof sec !== 'string') {
|
||||
throw new TypeError('Could not generate secret')
|
||||
}
|
||||
|
||||
return sec
|
||||
})
|
||||
}
|
||||
|
||||
const auth = require('../../auth/auth')
|
||||
|
||||
const Action = require('../action-constants.js')
|
||||
const API = require('../contact-api/index')
|
||||
const Config = require('../config')
|
||||
const Event = require('../event-constants')
|
||||
|
||||
/**
|
||||
* @typedef {import('../contact-api/SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {import('../contact-api/SimpleGUN').UserGUNNode} UserGUNNode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Emission
|
||||
* @prop {boolean} ok
|
||||
* @prop {string|null|Record<string, any>} msg
|
||||
* @prop {Record<string, any>} origBody
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} SimpleSocket
|
||||
* @prop {(eventName: string, data: Emission) => void} emit
|
||||
* @prop {(eventName: string, handler: (data: any) => void) => void} on
|
||||
*/
|
||||
|
||||
/* eslint-disable init-declarations */
|
||||
|
||||
/** @type {GUNNode} */
|
||||
// @ts-ignore
|
||||
let gun
|
||||
|
||||
/** @type {UserGUNNode} */
|
||||
let user
|
||||
|
||||
/* eslint-enable init-declarations */
|
||||
|
||||
let _currentAlias = ''
|
||||
let _currentPass = ''
|
||||
|
||||
let _isAuthenticating = false
|
||||
let _isRegistering = false
|
||||
|
||||
const isAuthenticated = () => typeof user.is === 'object' && user.is !== null
|
||||
const isAuthenticating = () => _isAuthenticating
|
||||
const isRegistering = () => _isRegistering
|
||||
|
||||
/**
|
||||
* Returns a promise containing the public key of the newly created user.
|
||||
* @param {string} alias
|
||||
* @param {string} pass
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const authenticate = async (alias, pass) => {
|
||||
if (isAuthenticated()) {
|
||||
// move this to a subscription; implement off() ? todo
|
||||
API.Jobs.onAcceptedRequests(user, mySEA)
|
||||
return user._.sea.pub
|
||||
}
|
||||
|
||||
if (isAuthenticating()) {
|
||||
throw new Error(
|
||||
'Cannot authenticate while another authentication attempt is going on'
|
||||
)
|
||||
}
|
||||
|
||||
_isAuthenticating = true
|
||||
|
||||
const ack = await new Promise(res => {
|
||||
user.auth(alias, pass, _ack => {
|
||||
res(_ack)
|
||||
})
|
||||
})
|
||||
|
||||
_isAuthenticating = false
|
||||
|
||||
if (typeof ack.err === 'string') {
|
||||
throw new Error(ack.err)
|
||||
} else if (typeof ack.sea === 'object') {
|
||||
API.Jobs.onAcceptedRequests(user, mySEA)
|
||||
|
||||
const mySec = await mySEA.secret(user._.sea.epub, user._.sea)
|
||||
if (typeof mySec !== 'string') {
|
||||
throw new TypeError('mySec not an string')
|
||||
}
|
||||
|
||||
_currentAlias = user.is ? user.is.alias : ''
|
||||
_currentPass = await mySEA.encrypt(pass, mySec)
|
||||
|
||||
await new Promise(res => setTimeout(res, 5000))
|
||||
|
||||
return ack.sea.pub
|
||||
} else {
|
||||
throw new Error('Unknown error.')
|
||||
}
|
||||
}
|
||||
|
||||
const instantiateGun = async () => {
|
||||
let mySecret = ''
|
||||
|
||||
if (user && user.is) {
|
||||
mySecret = /** @type {string} */ (await mySEA.secret(
|
||||
user._.sea.epub,
|
||||
user._.sea
|
||||
))
|
||||
}
|
||||
if (typeof mySecret !== 'string') {
|
||||
throw new TypeError("typeof mySec !== 'string'")
|
||||
}
|
||||
|
||||
const _gun = new Gun({
|
||||
axe: false,
|
||||
peers: Config.PEERS
|
||||
})
|
||||
|
||||
// please typescript
|
||||
const __gun = /** @type {unknown} */ (_gun)
|
||||
|
||||
gun = /** @type {GUNNode} */ (__gun)
|
||||
|
||||
user = gun.user()
|
||||
|
||||
if (_currentAlias && _currentPass) {
|
||||
const pass = await mySEA.decrypt(_currentPass, mySecret)
|
||||
|
||||
if (typeof pass !== 'string') {
|
||||
throw new Error('could not decrypt stored in memory current pass')
|
||||
}
|
||||
|
||||
user.leave()
|
||||
|
||||
await authenticate(_currentAlias, pass)
|
||||
}
|
||||
}
|
||||
|
||||
instantiateGun()
|
||||
|
||||
/**
|
||||
* @param {string} token
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const isValidToken = async token => {
|
||||
const validation = await auth.validateToken(token)
|
||||
|
||||
if (typeof validation !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (validation === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof validation.valid !== 'boolean') {
|
||||
return false
|
||||
}
|
||||
|
||||
return validation.valid
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} token
|
||||
* @throws {Error} If the token is invalid
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const throwOnInvalidToken = async token => {
|
||||
const isValid = await isValidToken(token)
|
||||
|
||||
if (!isValid) {
|
||||
throw new Error('Token expired.')
|
||||
}
|
||||
}
|
||||
|
||||
class Mediator {
|
||||
/**
|
||||
* @param {Readonly<SimpleSocket>} socket
|
||||
*/
|
||||
constructor(socket) {
|
||||
this.socket = socket
|
||||
|
||||
this.connected = true
|
||||
|
||||
socket.on('disconnect', this.onDisconnect)
|
||||
|
||||
socket.on(Action.ACCEPT_REQUEST, this.acceptRequest)
|
||||
socket.on(Action.BLACKLIST, this.blacklist)
|
||||
socket.on(Action.GENERATE_NEW_HANDSHAKE_NODE, this.generateHandshakeNode)
|
||||
socket.on(Action.SEND_HANDSHAKE_REQUEST, this.sendHandshakeRequest)
|
||||
socket.on(
|
||||
Action.SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG,
|
||||
this.sendHRWithInitialMsg
|
||||
)
|
||||
socket.on(Action.SEND_MESSAGE, this.sendMessage)
|
||||
socket.on(Action.SET_AVATAR, this.setAvatar)
|
||||
socket.on(Action.SET_DISPLAY_NAME, this.setDisplayName)
|
||||
|
||||
socket.on(Event.ON_AVATAR, this.onAvatar)
|
||||
socket.on(Event.ON_BLACKLIST, this.onBlacklist)
|
||||
socket.on(Event.ON_CHATS, this.onChats)
|
||||
socket.on(Event.ON_DISPLAY_NAME, this.onDisplayName)
|
||||
socket.on(Event.ON_HANDSHAKE_ADDRESS, this.onHandshakeAddress)
|
||||
socket.on(Event.ON_RECEIVED_REQUESTS, this.onReceivedRequests)
|
||||
socket.on(Event.ON_SENT_REQUESTS, this.onSentRequests)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ requestID: string , token: string }>} body
|
||||
*/
|
||||
acceptRequest = async body => {
|
||||
try {
|
||||
const { requestID, token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Actions.acceptRequest(requestID, gun, user, mySEA)
|
||||
|
||||
this.socket.emit(Action.ACCEPT_REQUEST, {
|
||||
ok: true,
|
||||
msg: null,
|
||||
origBody: body
|
||||
})
|
||||
|
||||
// refresh received requests
|
||||
API.Events.onSimplerReceivedRequests(
|
||||
debounce(
|
||||
once(receivedRequests => {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('---received requests---')
|
||||
console.log(receivedRequests)
|
||||
console.log('-----------------------')
|
||||
}
|
||||
|
||||
this.socket.emit(Event.ON_RECEIVED_REQUESTS, {
|
||||
msg: receivedRequests,
|
||||
ok: true,
|
||||
origBody: body
|
||||
})
|
||||
}),
|
||||
300
|
||||
),
|
||||
gun,
|
||||
user,
|
||||
mySEA
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Action.ACCEPT_REQUEST, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ publicKey: string , token: string }>} body
|
||||
*/
|
||||
blacklist = async body => {
|
||||
try {
|
||||
const { publicKey, token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Actions.blacklist(publicKey, user)
|
||||
|
||||
this.socket.emit(Action.BLACKLIST, {
|
||||
ok: true,
|
||||
msg: null,
|
||||
origBody: body
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Action.BLACKLIST, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
onDisconnect = () => {
|
||||
this.connected = false
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ token: string }>} body
|
||||
*/
|
||||
generateHandshakeNode = async body => {
|
||||
try {
|
||||
const { token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Actions.generateHandshakeAddress(user)
|
||||
|
||||
this.socket.emit(Action.GENERATE_NEW_HANDSHAKE_NODE, {
|
||||
ok: true,
|
||||
msg: null,
|
||||
origBody: body
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Action.GENERATE_NEW_HANDSHAKE_NODE, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ recipientPublicKey: string , token: string }>} body
|
||||
*/
|
||||
sendHandshakeRequest = async body => {
|
||||
try {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('\n')
|
||||
console.log('------------------------------')
|
||||
console.log('will now try to send a handshake request')
|
||||
console.log('------------------------------')
|
||||
console.log('\n')
|
||||
}
|
||||
|
||||
const { recipientPublicKey, token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Actions.sendHandshakeRequest(
|
||||
recipientPublicKey,
|
||||
gun,
|
||||
user,
|
||||
mySEA
|
||||
)
|
||||
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('\n')
|
||||
console.log('------------------------------')
|
||||
console.log('handshake request successfuly sent')
|
||||
console.log('------------------------------')
|
||||
console.log('\n')
|
||||
}
|
||||
|
||||
this.socket.emit(Action.SEND_HANDSHAKE_REQUEST, {
|
||||
ok: true,
|
||||
msg: null,
|
||||
origBody: body
|
||||
})
|
||||
|
||||
API.Events.onSimplerSentRequests(
|
||||
debounce(
|
||||
once(srs => {
|
||||
this.socket.emit(Event.ON_SENT_REQUESTS, {
|
||||
ok: true,
|
||||
msg: srs,
|
||||
origBody: body
|
||||
})
|
||||
}),
|
||||
350
|
||||
),
|
||||
gun,
|
||||
user,
|
||||
mySEA
|
||||
)
|
||||
} catch (err) {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('\n')
|
||||
console.log('------------------------------')
|
||||
console.log('handshake request send fail: ' + err.message)
|
||||
console.log('------------------------------')
|
||||
console.log('\n')
|
||||
}
|
||||
|
||||
this.socket.emit(Action.SEND_HANDSHAKE_REQUEST, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ initialMsg: string , recipientPublicKey: string , token: string }>} body
|
||||
*/
|
||||
sendHRWithInitialMsg = async body => {
|
||||
try {
|
||||
const { initialMsg, recipientPublicKey, token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Actions.sendHRWithInitialMsg(
|
||||
initialMsg,
|
||||
recipientPublicKey,
|
||||
gun,
|
||||
user,
|
||||
mySEA
|
||||
)
|
||||
|
||||
this.socket.emit(Action.SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG, {
|
||||
ok: true,
|
||||
msg: null,
|
||||
origBody: body
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Action.SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ body: string , recipientPublicKey: string , token: string }>} reqBody
|
||||
*/
|
||||
sendMessage = async reqBody => {
|
||||
try {
|
||||
const { body, recipientPublicKey, token } = reqBody
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Actions.sendMessage(recipientPublicKey, body, user, mySEA)
|
||||
|
||||
this.socket.emit(Action.SEND_MESSAGE, {
|
||||
ok: true,
|
||||
msg: null,
|
||||
origBody: reqBody
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Action.SEND_MESSAGE, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: reqBody
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ avatar: string|null , token: string }>} body
|
||||
*/
|
||||
setAvatar = async body => {
|
||||
try {
|
||||
const { avatar, token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Actions.setAvatar(avatar, user)
|
||||
|
||||
this.socket.emit(Action.SET_AVATAR, {
|
||||
ok: true,
|
||||
msg: null,
|
||||
origBody: body
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Action.SET_AVATAR, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ displayName: string , token: string }>} body
|
||||
*/
|
||||
setDisplayName = async body => {
|
||||
try {
|
||||
const { displayName, token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Actions.setDisplayName(displayName, user)
|
||||
|
||||
this.socket.emit(Action.SET_DISPLAY_NAME, {
|
||||
ok: true,
|
||||
msg: null,
|
||||
origBody: body
|
||||
})
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Action.SET_DISPLAY_NAME, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
//////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ token: string }>} body
|
||||
*/
|
||||
onAvatar = async body => {
|
||||
try {
|
||||
const { token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
API.Events.onAvatar(avatar => {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('---avatar---')
|
||||
console.log(avatar)
|
||||
console.log('-----------------------')
|
||||
}
|
||||
|
||||
this.socket.emit(Event.ON_AVATAR, {
|
||||
msg: avatar,
|
||||
ok: true,
|
||||
origBody: body
|
||||
})
|
||||
}, user)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Event.ON_AVATAR, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ token: string }>} body
|
||||
*/
|
||||
onBlacklist = async body => {
|
||||
try {
|
||||
const { token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
API.Events.onBlacklist(blacklist => {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('---blacklist---')
|
||||
console.log(blacklist)
|
||||
console.log('-----------------------')
|
||||
}
|
||||
|
||||
this.socket.emit(Event.ON_BLACKLIST, {
|
||||
msg: blacklist,
|
||||
ok: true,
|
||||
origBody: body
|
||||
})
|
||||
}, user)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Event.ON_BLACKLIST, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ token: string }>} body
|
||||
*/
|
||||
onChats = async body => {
|
||||
try {
|
||||
const { token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
API.Events.onChats(
|
||||
chats => {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('---chats---')
|
||||
console.log(chats)
|
||||
console.log('-----------------------')
|
||||
}
|
||||
|
||||
this.socket.emit(Event.ON_CHATS, {
|
||||
msg: chats,
|
||||
ok: true,
|
||||
origBody: body
|
||||
})
|
||||
},
|
||||
gun,
|
||||
user,
|
||||
mySEA
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Event.ON_CHATS, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ token: string }>} body
|
||||
*/
|
||||
onDisplayName = async body => {
|
||||
try {
|
||||
const { token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
API.Events.onDisplayName(displayName => {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('---displayName---')
|
||||
console.log(displayName)
|
||||
console.log('-----------------------')
|
||||
}
|
||||
|
||||
this.socket.emit(Event.ON_DISPLAY_NAME, {
|
||||
msg: displayName,
|
||||
ok: true,
|
||||
origBody: body
|
||||
})
|
||||
}, user)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Event.ON_DISPLAY_NAME, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ token: string }>} body
|
||||
*/
|
||||
onHandshakeAddress = async body => {
|
||||
try {
|
||||
const { token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
API.Events.onCurrentHandshakeAddress(addr => {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('---addr---')
|
||||
console.log(addr)
|
||||
console.log('-----------------------')
|
||||
}
|
||||
|
||||
this.socket.emit(Event.ON_HANDSHAKE_ADDRESS, {
|
||||
ok: true,
|
||||
msg: addr,
|
||||
origBody: body
|
||||
})
|
||||
}, user)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Event.ON_HANDSHAKE_ADDRESS, {
|
||||
ok: false,
|
||||
msg: err.message,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ token: string }>} body
|
||||
*/
|
||||
onReceivedRequests = async body => {
|
||||
try {
|
||||
const { token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
API.Events.onSimplerReceivedRequests(
|
||||
receivedRequests => {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('---receivedRequests---')
|
||||
console.log(receivedRequests)
|
||||
console.log('-----------------------')
|
||||
}
|
||||
|
||||
this.socket.emit(Event.ON_RECEIVED_REQUESTS, {
|
||||
msg: receivedRequests,
|
||||
ok: true,
|
||||
origBody: body
|
||||
})
|
||||
},
|
||||
gun,
|
||||
user,
|
||||
mySEA
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Event.ON_RECEIVED_REQUESTS, {
|
||||
msg: err.message,
|
||||
ok: false,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Readonly<{ token: string }>} body
|
||||
*/
|
||||
onSentRequests = async body => {
|
||||
try {
|
||||
const { token } = body
|
||||
|
||||
await throwOnInvalidToken(token)
|
||||
|
||||
await API.Events.onSimplerSentRequests(
|
||||
sentRequests => {
|
||||
if (Config.SHOW_LOG) {
|
||||
console.log('---sentRequests---')
|
||||
console.log(sentRequests)
|
||||
console.log('-----------------------')
|
||||
}
|
||||
|
||||
this.socket.emit(Event.ON_SENT_REQUESTS, {
|
||||
msg: sentRequests,
|
||||
ok: true,
|
||||
origBody: body
|
||||
})
|
||||
},
|
||||
gun,
|
||||
user,
|
||||
mySEA
|
||||
)
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
this.socket.emit(Event.ON_SENT_REQUESTS, {
|
||||
msg: err.message,
|
||||
ok: false,
|
||||
origBody: body
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an user for gun. Returns a promise containing the public key of the
|
||||
* newly created user.
|
||||
* @param {string} alias
|
||||
* @param {string} pass
|
||||
* @throws {Error} If gun is authenticated or is in the process of
|
||||
* authenticating. Use `isAuthenticating()` and `isAuthenticated()` to check for
|
||||
* this first. It can also throw if the alias is already registered on gun.
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const register = async (alias, pass) => {
|
||||
if (isRegistering()) {
|
||||
throw new Error('Already registering.')
|
||||
}
|
||||
|
||||
if (isAuthenticating()) {
|
||||
throw new Error(
|
||||
'Cannot register while gun is being authenticated (reminder: there should only be one user created for each node).'
|
||||
)
|
||||
}
|
||||
|
||||
if (isAuthenticated()) {
|
||||
throw new Error(
|
||||
'Cannot register if gun is already authenticated (reminder: there should only be one user created for each node).'
|
||||
)
|
||||
}
|
||||
|
||||
_isRegistering = true
|
||||
|
||||
/** @type {import('../contact-api/SimpleGUN').CreateAck} */
|
||||
const ack = await new Promise(res =>
|
||||
user.create(alias, pass, ack => res(ack))
|
||||
)
|
||||
|
||||
_isRegistering = false
|
||||
|
||||
const mySecret = await mySEA.secret(user._.sea.epub, user._.sea)
|
||||
if (typeof mySecret !== 'string') {
|
||||
throw new Error('Could not generate secret for user.')
|
||||
}
|
||||
|
||||
if (typeof ack.err === 'string') {
|
||||
throw new Error(ack.err)
|
||||
} else if (typeof ack.pub === 'string') {
|
||||
_currentAlias = alias
|
||||
_currentPass = await mySEA.encrypt(pass, mySecret)
|
||||
} else {
|
||||
throw new Error('unknown error')
|
||||
}
|
||||
|
||||
// restart instances so write to user graph work, there's an issue with gun
|
||||
// (at least on node) where after initial user creation, writes to user graph
|
||||
// don't work
|
||||
await instantiateGun()
|
||||
|
||||
user.leave()
|
||||
|
||||
return authenticate(alias, pass).then(async pub => {
|
||||
await API.Actions.setDisplayName('anon' + pub.slice(0, 8), user)
|
||||
await API.Actions.generateHandshakeAddress(user)
|
||||
return pub
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SimpleSocket} socket
|
||||
* @throws {Error} If gun is not authenticated or is in the process of
|
||||
* authenticating. Use `isAuthenticating()` and `isAuthenticated()` to check for
|
||||
* this first.
|
||||
* @returns {Mediator}
|
||||
*/
|
||||
const createMediator = socket => {
|
||||
// if (isAuthenticating() || !isAuthenticated()) {
|
||||
// throw new Error("Gun must be authenticated to create a Mediator");
|
||||
// }
|
||||
|
||||
return new Mediator(socket)
|
||||
}
|
||||
|
||||
const getGun = () => {
|
||||
return gun
|
||||
}
|
||||
|
||||
const getUser = () => {
|
||||
return user
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
authenticate,
|
||||
createMediator,
|
||||
isAuthenticated,
|
||||
isAuthenticating,
|
||||
isRegistering,
|
||||
register,
|
||||
instantiateGun,
|
||||
getGun,
|
||||
getUser
|
||||
}
|
||||
12
services/gunDB/action-constants.js
Normal file
12
services/gunDB/action-constants.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
const Actions = {
|
||||
ACCEPT_REQUEST: "ACCEPT_REQUEST",
|
||||
BLACKLIST: "BLACKLIST",
|
||||
GENERATE_NEW_HANDSHAKE_NODE: "GENERATE_NEW_HANDSHAKE_NODE",
|
||||
SEND_HANDSHAKE_REQUEST: "SEND_HANDSHAKE_REQUEST",
|
||||
SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG: "SEND_HANDSHAKE_REQUEST_WITH_INITIAL_MSG",
|
||||
SEND_MESSAGE: "SEND_MESSAGE",
|
||||
SET_AVATAR: "SET_AVATAR",
|
||||
SET_DISPLAY_NAME: "SET_DISPLAY_NAME"
|
||||
};
|
||||
|
||||
module.exports = Actions;
|
||||
23
services/gunDB/config.js
Normal file
23
services/gunDB/config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
/* eslint-disable no-process-env */
|
||||
|
||||
const dotenv = require('dotenv')
|
||||
const defaults = require('../../config/defaults')(false)
|
||||
|
||||
dotenv.config()
|
||||
|
||||
// @ts-ignore Let it crash if undefined
|
||||
exports.DATA_FILE_NAME = process.env.DATA_FILE_NAME || defaults.dataFileName
|
||||
|
||||
// @ts-ignore Let it crash if undefined
|
||||
exports.PEERS = process.env.PEERS
|
||||
? JSON.parse(process.env.PEERS)
|
||||
: defaults.peers
|
||||
|
||||
exports.MS_TO_TOKEN_EXPIRATION = Number(
|
||||
process.env.MS_TO_TOKEN_EXPIRATION || defaults.tokenExpirationMS
|
||||
)
|
||||
|
||||
exports.SHOW_LOG = process.env.SHOW_GUN_DB_LOG === 'true'
|
||||
106
services/gunDB/contact-api/SimpleGUN.ts
Normal file
106
services/gunDB/contact-api/SimpleGUN.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
type Primitive = boolean | string | number
|
||||
|
||||
export interface Data {
|
||||
[K: string]: ValidDataValue
|
||||
}
|
||||
|
||||
export type ValidDataValue = Primitive | null | Data
|
||||
|
||||
export interface Ack {
|
||||
err: string | undefined
|
||||
}
|
||||
|
||||
type ListenerObjSoul = {
|
||||
'#': string
|
||||
}
|
||||
|
||||
export type ListenerObj = Record<string, ListenerObjSoul | Primitive | null> & {
|
||||
_: ListenerObjSoul
|
||||
}
|
||||
|
||||
export type ListenerData = Primitive | null | ListenerObj | undefined
|
||||
|
||||
export type Listener = (data: ListenerData, key: string) => void
|
||||
export type Callback = (ack: Ack) => void
|
||||
|
||||
export interface Soul {
|
||||
get: string
|
||||
put: Primitive | null | object | undefined
|
||||
}
|
||||
|
||||
export interface GUNNode {
|
||||
_: Soul
|
||||
get(key: string): GUNNode
|
||||
map(): GUNNode
|
||||
put(data: ValidDataValue | GUNNode, cb?: Callback): GUNNode
|
||||
on(this: GUNNode, cb: Listener): void
|
||||
once(this: GUNNode, cb?: Listener): GUNNode
|
||||
set(data: ValidDataValue | GUNNode, cb?: Callback): GUNNode
|
||||
off(): void
|
||||
user(): UserGUNNode
|
||||
user(epub: string): GUNNode
|
||||
|
||||
then(): Promise<ListenerData>
|
||||
then<T>(cb: (v: ListenerData) => T): Promise<ListenerData>
|
||||
}
|
||||
|
||||
export interface CreateAck {
|
||||
pub: string | undefined
|
||||
err: string | undefined
|
||||
}
|
||||
|
||||
export type CreateCB = (ack: CreateAck) => void
|
||||
|
||||
export interface AuthAck {
|
||||
err: string | undefined
|
||||
sea:
|
||||
| {
|
||||
pub: string
|
||||
}
|
||||
| undefined
|
||||
}
|
||||
|
||||
export type AuthCB = (ack: AuthAck) => void
|
||||
|
||||
export interface UserPair {
|
||||
epriv: string
|
||||
epub: string
|
||||
priv: string
|
||||
pub: string
|
||||
}
|
||||
|
||||
export interface UserSoul extends Soul {
|
||||
sea: UserPair
|
||||
}
|
||||
|
||||
export interface UserGUNNode extends GUNNode {
|
||||
_: UserSoul
|
||||
auth(user: string, pass: string, cb: AuthCB): void
|
||||
is?: {
|
||||
alias: string
|
||||
pub: string
|
||||
}
|
||||
create(user: string, pass: string, cb: CreateCB): void
|
||||
leave(): void
|
||||
}
|
||||
|
||||
export interface ISEA {
|
||||
encrypt(message: string, senderSecret: string): Promise<string>
|
||||
decrypt(encryptedMessage: string, recipientSecret: string): Promise<string>
|
||||
secret(
|
||||
recipientOrSenderEpub: string,
|
||||
recipientOrSenderUserPair: UserPair
|
||||
): Promise<string>
|
||||
}
|
||||
|
||||
export interface MySEA {
|
||||
encrypt(message: string, senderSecret: string): Promise<string>
|
||||
decrypt(encryptedMessage: string, recipientSecret: string): Promise<string>
|
||||
secret(
|
||||
recipientOrSenderEpub: string,
|
||||
recipientOrSenderUserPair: UserPair
|
||||
): Promise<string>
|
||||
}
|
||||
798
services/gunDB/contact-api/actions.js
Normal file
798
services/gunDB/contact-api/actions.js
Normal file
|
|
@ -0,0 +1,798 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
const uuidv1 = require('uuid/v1')
|
||||
const ErrorCode = require('./errorCode')
|
||||
const Key = require('./key')
|
||||
const Utils = require('./utils')
|
||||
const { isHandshakeRequest } = require('./schema')
|
||||
/**
|
||||
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {import('./SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode
|
||||
* @typedef {import('./schema').HandshakeRequest} HandshakeRequest
|
||||
* @typedef {import('./schema').StoredRequest} StoredReq
|
||||
* @typedef {import('./schema').Message} Message
|
||||
* @typedef {import('./schema').Outgoing} Outgoing
|
||||
* @typedef {import('./schema').PartialOutgoing} PartialOutgoing
|
||||
*/
|
||||
|
||||
/**
|
||||
* An special message signaling the acceptance.
|
||||
*/
|
||||
const INITIAL_MSG = '$$__SHOCKWALLET__INITIAL__MESSAGE'
|
||||
|
||||
/**
|
||||
* @returns {Message}
|
||||
*/
|
||||
const __createInitialMessage = () => ({
|
||||
body: INITIAL_MSG,
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
/**
|
||||
* Create a an outgoing feed. The feed will have an initial special acceptance
|
||||
* message. Returns a promise that resolves to the id of the newly-created
|
||||
* outgoing feed.
|
||||
*
|
||||
* If an outgoing feed is already created for the recipient, then returns the id
|
||||
* of that one.
|
||||
* @param {string} withPublicKey Public key of the intended recipient of the
|
||||
* outgoing feed that will be created.
|
||||
* @throws {Error} If the outgoing feed cannot be created or if the initial
|
||||
* message for it also cannot be created. These errors aren't coded as they are
|
||||
* not meant to be caught outside of this module.
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const __createOutgoingFeed = async (withPublicKey, user, SEA) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
|
||||
if (typeof mySecret !== 'string') {
|
||||
throw new TypeError(
|
||||
"__createOutgoingFeed() -> typeof mySecret !== 'string'"
|
||||
)
|
||||
}
|
||||
const encryptedForMeRecipientPub = await SEA.encrypt(withPublicKey, mySecret)
|
||||
|
||||
const maybeEncryptedForMeOutgoingFeedID = await Utils.tryAndWait(
|
||||
(_, user) =>
|
||||
new Promise(res => {
|
||||
user
|
||||
.get(Key.RECIPIENT_TO_OUTGOING)
|
||||
.get(withPublicKey)
|
||||
.once(data => {
|
||||
res(data)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
let outgoingFeedID = ''
|
||||
|
||||
// if there was no stored outgoing, create an outgoing feed
|
||||
if (typeof maybeEncryptedForMeOutgoingFeedID !== 'string') {
|
||||
/** @type {PartialOutgoing} */
|
||||
const newPartialOutgoingFeed = {
|
||||
with: encryptedForMeRecipientPub
|
||||
}
|
||||
|
||||
/** @type {string} */
|
||||
const newOutgoingFeedID = await new Promise((res, rej) => {
|
||||
const _outFeedNode = user
|
||||
.get(Key.OUTGOINGS)
|
||||
.set(newPartialOutgoingFeed, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res(_outFeedNode._.get)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (typeof newOutgoingFeedID !== 'string') {
|
||||
throw new TypeError('typeof newOutgoingFeedID !== "string"')
|
||||
}
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.OUTGOINGS)
|
||||
.get(newOutgoingFeedID)
|
||||
.get(Key.MESSAGES)
|
||||
.set(__createInitialMessage(), ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const encryptedForMeNewOutgoingFeedID = await SEA.encrypt(
|
||||
newOutgoingFeedID,
|
||||
mySecret
|
||||
)
|
||||
|
||||
if (typeof encryptedForMeNewOutgoingFeedID === 'undefined') {
|
||||
throw new TypeError(
|
||||
"typeof encryptedForMeNewOutgoingFeedID === 'undefined'"
|
||||
)
|
||||
}
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.RECIPIENT_TO_OUTGOING)
|
||||
.get(withPublicKey)
|
||||
.put(encryptedForMeNewOutgoingFeedID, ack => {
|
||||
if (ack.err) {
|
||||
rej(Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
outgoingFeedID = newOutgoingFeedID
|
||||
}
|
||||
|
||||
// otherwise decrypt stored outgoing
|
||||
else {
|
||||
const decryptedOID = await SEA.decrypt(
|
||||
maybeEncryptedForMeOutgoingFeedID,
|
||||
mySecret
|
||||
)
|
||||
|
||||
if (typeof decryptedOID !== 'string') {
|
||||
throw new TypeError(
|
||||
"__createOutgoingFeed() -> typeof decryptedOID !== 'string'"
|
||||
)
|
||||
}
|
||||
|
||||
outgoingFeedID = decryptedOID
|
||||
}
|
||||
|
||||
if (typeof outgoingFeedID === 'undefined') {
|
||||
throw new TypeError(
|
||||
'__createOutgoingFeed() -> typeof outgoingFeedID === "undefined"'
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof outgoingFeedID !== 'string') {
|
||||
throw new TypeError(
|
||||
'__createOutgoingFeed() -> expected outgoingFeedID to be an string'
|
||||
)
|
||||
}
|
||||
|
||||
if (outgoingFeedID.length === 0) {
|
||||
throw new TypeError(
|
||||
'__createOutgoingFeed() -> expected outgoingFeedID to be a populated string.'
|
||||
)
|
||||
}
|
||||
|
||||
return outgoingFeedID
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a request's ID, that should be found on the user's current handshake
|
||||
* node, accept the request by creating an outgoing feed intended for the
|
||||
* requestor, then encrypting and putting the id of this newly created outgoing
|
||||
* feed on the response prop of the request.
|
||||
* @param {string} requestID The id for the request to accept.
|
||||
* @param {GUNNode} gun
|
||||
* @param {UserGUNNode} user Pass only for testing purposes.
|
||||
* @param {ISEA} SEA
|
||||
* @param {typeof __createOutgoingFeed} outgoingFeedCreator Pass only
|
||||
* for testing. purposes.
|
||||
* @throws {Error} Throws if trying to accept an invalid request, or an error on
|
||||
* gun's part.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const acceptRequest = async (
|
||||
requestID,
|
||||
gun,
|
||||
user,
|
||||
SEA,
|
||||
outgoingFeedCreator = __createOutgoingFeed
|
||||
) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
const handshakeAddress = await Utils.tryAndWait(async (_, user) => {
|
||||
const addr = await user.get(Key.CURRENT_HANDSHAKE_ADDRESS).then()
|
||||
|
||||
if (typeof addr !== 'string') {
|
||||
throw new TypeError("typeof addr !== 'string'")
|
||||
}
|
||||
|
||||
return addr
|
||||
})
|
||||
|
||||
const {
|
||||
response: encryptedForUsIncomingID,
|
||||
from: senderPublicKey
|
||||
} = await Utils.tryAndWait(async gun => {
|
||||
const hr = await gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
.get(handshakeAddress)
|
||||
.get(requestID)
|
||||
.then()
|
||||
|
||||
if (!isHandshakeRequest(hr)) {
|
||||
throw new Error(ErrorCode.TRIED_TO_ACCEPT_AN_INVALID_REQUEST)
|
||||
}
|
||||
|
||||
return hr
|
||||
})
|
||||
|
||||
/** @type {string} */
|
||||
const requestorEpub = await Utils.pubToEpub(senderPublicKey)
|
||||
|
||||
const ourSecret = await SEA.secret(requestorEpub, user._.sea)
|
||||
if (typeof ourSecret !== 'string') {
|
||||
throw new TypeError("typeof ourSecret !== 'string'")
|
||||
}
|
||||
|
||||
const incomingID = await SEA.decrypt(encryptedForUsIncomingID, ourSecret)
|
||||
if (typeof incomingID !== 'string') {
|
||||
throw new TypeError("typeof incomingID !== 'string'")
|
||||
}
|
||||
|
||||
const newlyCreatedOutgoingFeedID = await outgoingFeedCreator(
|
||||
senderPublicKey,
|
||||
user,
|
||||
SEA
|
||||
)
|
||||
|
||||
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
|
||||
if (typeof mySecret !== 'string') {
|
||||
throw new TypeError("acceptRequest() -> typeof mySecret !== 'string'")
|
||||
}
|
||||
const encryptedForMeIncomingID = await SEA.encrypt(incomingID, mySecret)
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.USER_TO_INCOMING)
|
||||
.get(senderPublicKey)
|
||||
.put(encryptedForMeIncomingID, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
// NOTE: perform non-reversable actions before destructive actions
|
||||
// In case any of the non-reversable actions reject.
|
||||
// In this case, writing to the response is the non-revesarble op.
|
||||
////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
const encryptedForUsOutgoingID = await SEA.encrypt(
|
||||
newlyCreatedOutgoingFeedID,
|
||||
ourSecret
|
||||
)
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
.get(handshakeAddress)
|
||||
.get(requestID)
|
||||
.put(
|
||||
{
|
||||
response: encryptedForUsOutgoingID
|
||||
},
|
||||
ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} user
|
||||
* @param {string} pass
|
||||
* @param {UserGUNNode} userNode
|
||||
*/
|
||||
const authenticate = (user, pass, userNode) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (typeof user !== 'string') {
|
||||
throw new TypeError('expected user to be of type string')
|
||||
}
|
||||
|
||||
if (typeof pass !== 'string') {
|
||||
throw new TypeError('expected pass to be of type string')
|
||||
}
|
||||
|
||||
if (user.length === 0) {
|
||||
throw new TypeError('expected user to have length greater than zero')
|
||||
}
|
||||
|
||||
if (pass.length === 0) {
|
||||
throw new TypeError('expected pass to have length greater than zero')
|
||||
}
|
||||
|
||||
if (typeof userNode.is === 'undefined') {
|
||||
throw new Error(ErrorCode.ALREADY_AUTH)
|
||||
}
|
||||
|
||||
userNode.auth(user, pass, ack => {
|
||||
if (ack.err) {
|
||||
reject(new Error(ack.err))
|
||||
} else if (!userNode.is) {
|
||||
reject(new Error('authentication failed'))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} publicKey
|
||||
* @param {UserGUNNode} user Pass only for testing.
|
||||
* @throws {Error} If there's an error saving to the blacklist.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const blacklist = (publicKey, user) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
user.get(Key.BLACKLIST).set(publicKey, ack => {
|
||||
if (ack.err) {
|
||||
reject(new Error(ack.err))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {UserGUNNode} user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const generateHandshakeAddress = user =>
|
||||
new Promise((res, rej) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
const address = uuidv1()
|
||||
|
||||
user.get(Key.CURRENT_HANDSHAKE_ADDRESS).put(address, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} recipientPublicKey
|
||||
* @param {GUNNode} gun
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @throws {Error|TypeError}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const sendHandshakeRequest = async (recipientPublicKey, gun, user, SEA) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
if (typeof recipientPublicKey !== 'string') {
|
||||
throw new TypeError(
|
||||
`recipientPublicKey is not string, got: ${typeof recipientPublicKey}`
|
||||
)
|
||||
}
|
||||
|
||||
if (recipientPublicKey.length === 0) {
|
||||
throw new TypeError('recipientPublicKey is an string of length 0')
|
||||
}
|
||||
|
||||
console.log('sendHR() -> before recipientEpub')
|
||||
|
||||
/** @type {string} */
|
||||
const recipientEpub = await Utils.pubToEpub(recipientPublicKey)
|
||||
|
||||
console.log('sendHR() -> before mySecret')
|
||||
|
||||
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
|
||||
if (typeof mySecret !== 'string') {
|
||||
throw new TypeError(
|
||||
"sendHandshakeRequest() -> typeof mySecret !== 'string'"
|
||||
)
|
||||
}
|
||||
console.log('sendHR() -> before ourSecret')
|
||||
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
|
||||
if (typeof ourSecret !== 'string') {
|
||||
throw new TypeError(
|
||||
"sendHandshakeRequest() -> typeof ourSecret !== 'string'"
|
||||
)
|
||||
}
|
||||
|
||||
// check if successful handshake is present
|
||||
|
||||
console.log('sendHR() -> before alreadyHandshaked')
|
||||
|
||||
/** @type {boolean} */
|
||||
const alreadyHandshaked = await Utils.successfulHandshakeAlreadyExists(
|
||||
recipientPublicKey
|
||||
)
|
||||
|
||||
if (alreadyHandshaked) {
|
||||
throw new Error(ErrorCode.ALREADY_HANDSHAKED)
|
||||
}
|
||||
|
||||
console.log('sendHR() -> before maybeLastRequestIDSentToUser')
|
||||
|
||||
// check that we have already sent a request to this user, on his current
|
||||
// handshake node
|
||||
const maybeLastRequestIDSentToUser = await Utils.tryAndWait((_, user) =>
|
||||
user
|
||||
.get(Key.USER_TO_LAST_REQUEST_SENT)
|
||||
.get(recipientPublicKey)
|
||||
.then()
|
||||
)
|
||||
|
||||
console.log('sendHR() -> before currentHandshakeAddress')
|
||||
|
||||
const currentHandshakeAddress = await Utils.tryAndWait(gun =>
|
||||
gun
|
||||
.user(recipientPublicKey)
|
||||
.get(Key.CURRENT_HANDSHAKE_ADDRESS)
|
||||
.then()
|
||||
)
|
||||
|
||||
if (typeof currentHandshakeAddress !== 'string') {
|
||||
throw new TypeError(
|
||||
'expected current handshake address found on recipients user node to be an string'
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof maybeLastRequestIDSentToUser === 'string') {
|
||||
if (maybeLastRequestIDSentToUser.length < 5) {
|
||||
throw new TypeError(
|
||||
'sendHandshakeRequest() -> maybeLastRequestIDSentToUser.length < 5'
|
||||
)
|
||||
}
|
||||
|
||||
const lastRequestIDSentToUser = maybeLastRequestIDSentToUser
|
||||
|
||||
console.log('sendHR() -> before alreadyContactedOnCurrHandshakeNode')
|
||||
/** @type {boolean} */
|
||||
const alreadyContactedOnCurrHandshakeNode = await Utils.tryAndWait(
|
||||
gun =>
|
||||
new Promise(res => {
|
||||
gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
.get(currentHandshakeAddress)
|
||||
.get(lastRequestIDSentToUser)
|
||||
.once(data => {
|
||||
res(typeof data !== 'undefined')
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
if (alreadyContactedOnCurrHandshakeNode) {
|
||||
throw new Error(ErrorCode.ALREADY_REQUESTED_HANDSHAKE)
|
||||
}
|
||||
}
|
||||
|
||||
console.log('sendHR() -> before __createOutgoingFeed')
|
||||
|
||||
const outgoingFeedID = await __createOutgoingFeed(
|
||||
recipientPublicKey,
|
||||
user,
|
||||
SEA
|
||||
)
|
||||
|
||||
console.log('sendHR() -> before encryptedForUsOutgoingFeedID')
|
||||
|
||||
const encryptedForUsOutgoingFeedID = await SEA.encrypt(
|
||||
outgoingFeedID,
|
||||
ourSecret
|
||||
)
|
||||
|
||||
const timestamp = Date.now()
|
||||
|
||||
/** @type {HandshakeRequest} */
|
||||
const handshakeRequestData = {
|
||||
from: user.is.pub,
|
||||
response: encryptedForUsOutgoingFeedID,
|
||||
timestamp
|
||||
}
|
||||
|
||||
console.log('sendHR() -> before newHandshakeRequestID')
|
||||
/** @type {string} */
|
||||
const newHandshakeRequestID = await new Promise((res, rej) => {
|
||||
const hr = gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
.get(currentHandshakeAddress)
|
||||
.set(handshakeRequestData, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(`Error trying to create request: ${ack.err}`))
|
||||
} else {
|
||||
res(hr._.get)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.USER_TO_LAST_REQUEST_SENT)
|
||||
.get(recipientPublicKey)
|
||||
.put(newHandshakeRequestID, ack => {
|
||||
if (ack.err) {
|
||||
rej(new Error(ack.err))
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// save request id to REQUEST_TO_USER
|
||||
|
||||
const encryptedForMeRecipientPublicKey = await SEA.encrypt(
|
||||
recipientPublicKey,
|
||||
mySecret
|
||||
)
|
||||
|
||||
// This needs to come before the write to sent requests. Because that write
|
||||
// triggers Jobs.onAcceptedRequests and it in turn reads from request-to-user
|
||||
// This also triggers Events.onSimplerSentRequests
|
||||
await new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.REQUEST_TO_USER)
|
||||
.get(newHandshakeRequestID)
|
||||
.put(encryptedForMeRecipientPublicKey, ack => {
|
||||
if (ack.err) {
|
||||
rej(
|
||||
new Error(
|
||||
`Error saving recipient public key to request to user: ${ack.err}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @type {StoredReq}
|
||||
*/
|
||||
const storedReq = {
|
||||
sentReqID: await SEA.encrypt(newHandshakeRequestID, mySecret),
|
||||
recipientPub: encryptedForMeRecipientPublicKey,
|
||||
handshakeAddress: await SEA.encrypt(currentHandshakeAddress, mySecret),
|
||||
timestamp
|
||||
}
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
user.get(Key.STORED_REQS).set(storedReq, ack => {
|
||||
if (ack.err) {
|
||||
rej(
|
||||
new Error(
|
||||
`Error saving newly created request to sent requests: ${ack.err}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} recipientPublicKey
|
||||
* @param {string} body
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const sendMessage = async (recipientPublicKey, body, user, SEA) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
if (typeof recipientPublicKey !== 'string') {
|
||||
throw new TypeError(
|
||||
`expected recipientPublicKey to be an string, but instead got: ${typeof recipientPublicKey}`
|
||||
)
|
||||
}
|
||||
|
||||
if (recipientPublicKey.length === 0) {
|
||||
throw new TypeError(
|
||||
'expected recipientPublicKey to be an string of length greater than zero'
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof body !== 'string') {
|
||||
throw new TypeError(
|
||||
`expected message to be an string, instead got: ${typeof body}`
|
||||
)
|
||||
}
|
||||
|
||||
if (body.length === 0) {
|
||||
throw new TypeError(
|
||||
'expected message to be an string of length greater than zero'
|
||||
)
|
||||
}
|
||||
|
||||
const outgoingID = await Utils.recipientToOutgoingID(
|
||||
recipientPublicKey,
|
||||
user,
|
||||
SEA
|
||||
)
|
||||
|
||||
if (outgoingID === null) {
|
||||
throw new Error(
|
||||
`Could not fetch an outgoing id for user: ${recipientPublicKey}`
|
||||
)
|
||||
}
|
||||
|
||||
const recipientEpub = await Utils.pubToEpub(recipientPublicKey)
|
||||
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
|
||||
if (typeof ourSecret !== 'string') {
|
||||
throw new TypeError("sendMessage() -> typeof ourSecret !== 'string'")
|
||||
}
|
||||
const encryptedBody = await SEA.encrypt(body, ourSecret)
|
||||
|
||||
const newMessage = {
|
||||
body: encryptedBody,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
return new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.OUTGOINGS)
|
||||
.get(outgoingID)
|
||||
.get(Key.MESSAGES)
|
||||
.set(newMessage, ack => {
|
||||
if (ack.err) {
|
||||
rej(ack.err)
|
||||
} else {
|
||||
res()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string|null} avatar
|
||||
* @param {UserGUNNode} user
|
||||
* @throws {TypeError} Rejects if avatar is not an string or an empty string.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const setAvatar = (avatar, user) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
if (typeof avatar === 'string' && avatar.length === 0) {
|
||||
throw new TypeError(
|
||||
"'avatar' must be an string and have length greater than one or be null"
|
||||
)
|
||||
}
|
||||
|
||||
if (typeof avatar !== 'string' && avatar !== null) {
|
||||
throw new TypeError(
|
||||
"'avatar' must be an string and have length greater than one or be null"
|
||||
)
|
||||
}
|
||||
|
||||
user
|
||||
.get(Key.PROFILE)
|
||||
.get(Key.AVATAR)
|
||||
.put(avatar, ack => {
|
||||
if (ack.err) {
|
||||
reject(new Error(ack.err))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} displayName
|
||||
* @param {UserGUNNode} user
|
||||
* @throws {TypeError} Rejects if displayName is not an string or an empty
|
||||
* string.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const setDisplayName = (displayName, user) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
if (typeof displayName !== 'string') {
|
||||
throw new TypeError()
|
||||
}
|
||||
|
||||
if (displayName.length === 0) {
|
||||
throw new TypeError()
|
||||
}
|
||||
|
||||
user
|
||||
.get(Key.PROFILE)
|
||||
.get(Key.DISPLAY_NAME)
|
||||
.put(displayName, ack => {
|
||||
if (ack.err) {
|
||||
reject(new Error(ack.err))
|
||||
} else {
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @param {string} initialMsg
|
||||
* @param {string} recipientPublicKey
|
||||
* @param {GUNNode} gun
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @throws {Error|TypeError}
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const sendHRWithInitialMsg = async (
|
||||
initialMsg,
|
||||
recipientPublicKey,
|
||||
gun,
|
||||
user,
|
||||
SEA
|
||||
) => {
|
||||
/** @type {boolean} */
|
||||
const alreadyHandshaked = await Utils.tryAndWait(
|
||||
(_, user) =>
|
||||
new Promise((res, rej) => {
|
||||
user
|
||||
.get(Key.USER_TO_INCOMING)
|
||||
.get(recipientPublicKey)
|
||||
.once(inc => {
|
||||
if (typeof inc !== 'string') {
|
||||
res(false)
|
||||
} else if (inc.length === 0) {
|
||||
rej(
|
||||
new Error(
|
||||
`sendHRWithInitialMsg()-> obtained encryptedIncomingId from user-to-incoming an string but of length 0`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
res(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
if (!alreadyHandshaked) {
|
||||
await sendHandshakeRequest(recipientPublicKey, gun, user, SEA)
|
||||
}
|
||||
|
||||
await sendMessage(recipientPublicKey, initialMsg, user, SEA)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
INITIAL_MSG,
|
||||
__createOutgoingFeed,
|
||||
acceptRequest,
|
||||
authenticate,
|
||||
blacklist,
|
||||
generateHandshakeAddress,
|
||||
sendHandshakeRequest,
|
||||
sendMessage,
|
||||
sendHRWithInitialMsg,
|
||||
setAvatar,
|
||||
setDisplayName
|
||||
}
|
||||
43
services/gunDB/contact-api/errorCode.js
Normal file
43
services/gunDB/contact-api/errorCode.js
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
exports.ALREADY_AUTH = 'ALREADY_AUTH'
|
||||
|
||||
exports.NOT_AUTH = 'NOT_AUTH'
|
||||
|
||||
exports.COULDNT_ACCEPT_REQUEST = 'COULDNT_ACCEPT_REQUEST'
|
||||
|
||||
exports.COULDNT_SENT_REQUEST = 'COULDNT_SENT_REQUEST'
|
||||
|
||||
exports.COULDNT_PUT_REQUEST_RESPONSE = 'COULDNT_PUT_REQUEST_RESPONSE'
|
||||
|
||||
/**
|
||||
* Error thrown when trying to accept a request, and on retrieval of that
|
||||
* request invalid data (not resembling a request) is received.
|
||||
*/
|
||||
exports.TRIED_TO_ACCEPT_AN_INVALID_REQUEST =
|
||||
'TRIED_TO_ACCEPT_AN_INVALID_REQUEST'
|
||||
|
||||
exports.UNSUCCESSFUL_LOGOUT = 'UNSUCCESSFUL_LOGOUT'
|
||||
|
||||
exports.UNSUCCESSFUL_REQUEST_ACCEPT = 'UNSUCCESSFUL_REQUEST_ACCEPT'
|
||||
|
||||
/**
|
||||
* Error thrown when trying to send a handshake request to an user for which
|
||||
* there's already an successful handshake.
|
||||
*/
|
||||
exports.ALREADY_HANDSHAKED = 'ALREADY_HANDSHAKED'
|
||||
|
||||
/**
|
||||
* Error thrown when trying to send a handshake request to an user for which
|
||||
* there's already a handshake request on his current handshake node.
|
||||
*/
|
||||
exports.ALREADY_REQUESTED_HANDSHAKE = 'ALREADY_REQUESTED_HANDSHAKE'
|
||||
|
||||
/**
|
||||
* Error thrown when trying to send a handshake request to an user on an stale
|
||||
* handshake address.
|
||||
*/
|
||||
exports.STALE_HANDSHAKE_ADDRESS = 'STALE_HANDSHAKE_ADDRESS'
|
||||
|
||||
exports.TIMEOUT_ERR = 'TIMEOUT_ERR'
|
||||
1219
services/gunDB/contact-api/events.js
Normal file
1219
services/gunDB/contact-api/events.js
Normal file
File diff suppressed because it is too large
Load diff
10
services/gunDB/contact-api/index.js
Normal file
10
services/gunDB/contact-api/index.js
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
const Actions = require('./actions')
|
||||
const Events = require('./events')
|
||||
const Jobs = require('./jobs')
|
||||
const Key = require('./key')
|
||||
const Schema = require('./schema')
|
||||
|
||||
module.exports = { Actions, Events, Jobs, Key, Schema }
|
||||
153
services/gunDB/contact-api/jobs.js
Normal file
153
services/gunDB/contact-api/jobs.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/**
|
||||
* @prettier
|
||||
* Taks are subscriptions to events that perform actions (write to GUN) on
|
||||
* response to certain ways events can happen. These tasks need to be fired up
|
||||
* at app launch otherwise certain features won't work as intended. Tasks should
|
||||
* ideally be idempotent, that is, if they were to be fired up after a certain
|
||||
* amount of time after app launch, everything should work as intended. For this
|
||||
* to work, special care has to be put into how these respond to events. These
|
||||
* tasks could be hardcoded inside events but then they wouldn't be easily
|
||||
* auto-testable. These tasks accept factories that are homonymous to the events
|
||||
* on the same
|
||||
*/
|
||||
const ErrorCode = require('./errorCode')
|
||||
const Key = require('./key')
|
||||
const Schema = require('./schema')
|
||||
const Utils = require('./utils')
|
||||
|
||||
/**
|
||||
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {import('./SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @throws {Error} NOT_AUTH
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const onAcceptedRequests = async (user, SEA) => {
|
||||
if (!user.is) {
|
||||
throw new Error(ErrorCode.NOT_AUTH)
|
||||
}
|
||||
|
||||
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
|
||||
|
||||
if (typeof mySecret !== 'string') {
|
||||
console.log("Jobs.onAcceptedRequests() -> typeof mySecret !== 'string'")
|
||||
return
|
||||
}
|
||||
|
||||
user
|
||||
.get(Key.STORED_REQS)
|
||||
.map()
|
||||
.once(async storedReq => {
|
||||
try {
|
||||
if (!Schema.isStoredRequest(storedReq)) {
|
||||
throw new TypeError('Stored request not an StoredRequest')
|
||||
}
|
||||
const recipientPub = await SEA.decrypt(storedReq.recipientPub, mySecret)
|
||||
|
||||
if (typeof recipientPub !== 'string') {
|
||||
throw new TypeError()
|
||||
}
|
||||
if (await Utils.successfulHandshakeAlreadyExists(recipientPub)) {
|
||||
return
|
||||
}
|
||||
const requestAddress = await SEA.decrypt(
|
||||
storedReq.handshakeAddress,
|
||||
mySecret
|
||||
)
|
||||
if (typeof requestAddress !== 'string') {
|
||||
throw new TypeError()
|
||||
}
|
||||
const sentReqID = await SEA.decrypt(storedReq.sentReqID, mySecret)
|
||||
if (typeof sentReqID !== 'string') {
|
||||
throw new TypeError()
|
||||
}
|
||||
|
||||
const latestReqSentID = await Utils.recipientPubToLastReqSentID(
|
||||
recipientPub
|
||||
)
|
||||
|
||||
const isStaleRequest = latestReqSentID !== sentReqID
|
||||
if (isStaleRequest) {
|
||||
return
|
||||
}
|
||||
|
||||
const recipientEpub = await Utils.pubToEpub(recipientPub)
|
||||
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
|
||||
|
||||
if (typeof ourSecret !== 'string') {
|
||||
throw new TypeError("typeof ourSecret !== 'string'")
|
||||
}
|
||||
|
||||
await Utils.tryAndWait(
|
||||
(gun, user) =>
|
||||
new Promise((res, rej) => {
|
||||
gun
|
||||
.get(Key.HANDSHAKE_NODES)
|
||||
.get(requestAddress)
|
||||
.get(sentReqID)
|
||||
.on(async sentReq => {
|
||||
if (!Schema.isHandshakeRequest(sentReq)) {
|
||||
rej(
|
||||
new Error(
|
||||
'sent request found in handshake node not a handshake request'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// The response can be decrypted with the same secret regardless of who
|
||||
// wrote to it last (see HandshakeRequest definition).
|
||||
// This could be our feed ID for the recipient, or the recipient's feed
|
||||
// id if he accepted the request.
|
||||
const feedID = await SEA.decrypt(sentReq.response, ourSecret)
|
||||
|
||||
if (typeof feedID !== 'string') {
|
||||
throw new TypeError("typeof feedID !== 'string'")
|
||||
}
|
||||
|
||||
const feedIDExistsOnRecipientsOutgoings = await Utils.tryAndWait(
|
||||
gun =>
|
||||
new Promise(res => {
|
||||
gun
|
||||
.user(recipientPub)
|
||||
.get(Key.OUTGOINGS)
|
||||
.get(feedID)
|
||||
.once(feed => {
|
||||
res(typeof feed !== 'undefined')
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
if (!feedIDExistsOnRecipientsOutgoings) {
|
||||
return
|
||||
}
|
||||
|
||||
const encryptedForMeIncomingID = await SEA.encrypt(
|
||||
feedID,
|
||||
mySecret
|
||||
)
|
||||
|
||||
user
|
||||
.get(Key.USER_TO_INCOMING)
|
||||
.get(recipientPub)
|
||||
.put(encryptedForMeIncomingID)
|
||||
|
||||
// ensure this listeners gets called at least once
|
||||
res()
|
||||
})
|
||||
})
|
||||
)
|
||||
} catch (err) {
|
||||
console.warn(`Jobs.onAcceptedRequests() -> ${err.message}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
onAcceptedRequests
|
||||
}
|
||||
26
services/gunDB/contact-api/key.js
Normal file
26
services/gunDB/contact-api/key.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
exports.HANDSHAKE_NODES = 'handshakeNodes'
|
||||
|
||||
exports.CURRENT_HANDSHAKE_ADDRESS = 'currentHandshakeAddress'
|
||||
|
||||
exports.MESSAGES = 'messages'
|
||||
exports.OUTGOINGS = 'outgoings'
|
||||
|
||||
exports.RECIPIENT_TO_OUTGOING = 'recipientToOutgoing'
|
||||
exports.USER_TO_INCOMING = 'userToIncoming'
|
||||
|
||||
exports.STORED_REQS = 'storedReqs'
|
||||
exports.REQUEST_TO_USER = 'requestToUser'
|
||||
|
||||
exports.BLACKLIST = 'blacklist'
|
||||
|
||||
exports.PROFILE = 'Profile'
|
||||
exports.AVATAR = 'avatar'
|
||||
exports.DISPLAY_NAME = 'displayName'
|
||||
|
||||
/**
|
||||
* Maps user to the last request sent to them.
|
||||
*/
|
||||
exports.USER_TO_LAST_REQUEST_SENT = 'USER_TO_LAST_REQUEST_SENT'
|
||||
339
services/gunDB/contact-api/schema.js
Normal file
339
services/gunDB/contact-api/schema.js
Normal file
|
|
@ -0,0 +1,339 @@
|
|||
/**
|
||||
* @prettier
|
||||
*/
|
||||
/**
|
||||
* @typedef {object} HandshakeRequest
|
||||
* @prop {string} from Public key of the requestor.
|
||||
* @prop {string} response Encrypted string where, if the recipient accepts the
|
||||
* request, his outgoing feed id will be put. Before that the sender's outgoing
|
||||
* feed ID will be placed here, encrypted so only the recipient can access it.
|
||||
* @prop {number} timestamp Unix time.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} Message
|
||||
* @prop {string} body
|
||||
* @prop {number} timestamp
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} ChatMessage
|
||||
* @prop {string} body
|
||||
* @prop {string} id
|
||||
* @prop {boolean} outgoing True if the message is an outgoing message,
|
||||
* otherwise it is an incoming message.
|
||||
* @prop {number} timestamp
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} item
|
||||
* @returns {item is ChatMessage}
|
||||
*/
|
||||
exports.isChatMessage = item => {
|
||||
if (typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const obj = /** @type {ChatMessage} */ (item)
|
||||
|
||||
if (typeof obj.body !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.id !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.outgoing !== 'boolean') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.timestamp !== 'number') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* A simpler representation of a conversation between two users than the
|
||||
* outgoing/incoming feed paradigm. It combines both the outgoing and incoming
|
||||
* messages into one data structure plus metada about the chat.
|
||||
* @typedef {object} Chat
|
||||
* @prop {string|null} recipientAvatar Base64 encoded image.
|
||||
* @prop {string} recipientPublicKey A way to uniquely identify each chat.
|
||||
* @prop {ChatMessage[]} messages Sorted from most recent to least recent.
|
||||
* @prop {string|null} recipientDisplayName
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {any} item
|
||||
* @returns {item is Chat}
|
||||
*/
|
||||
exports.isChat = item => {
|
||||
if (typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const obj = /** @type {Chat} */ (item)
|
||||
|
||||
if (typeof obj.recipientAvatar !== 'string' && obj.recipientAvatar !== null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!Array.isArray(obj.messages)) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.recipientPublicKey !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (obj.recipientPublicKey.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return obj.messages.every(msg => exports.isChatMessage(msg))
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} Outgoing
|
||||
* @prop {Record<string, Message>} messages
|
||||
* @prop {string} with Public key for whom the outgoing messages are intended.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} PartialOutgoing
|
||||
* @prop {string} with (Encrypted) Public key for whom the outgoing messages are
|
||||
* intended.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {object} StoredRequest
|
||||
* @prop {string} sentReqID
|
||||
* @prop {string} recipientPub
|
||||
* @prop {string} handshakeAddress
|
||||
* @prop {number} timestamp
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {any} item
|
||||
* @returns {item is StoredRequest}
|
||||
*/
|
||||
exports.isStoredRequest = item => {
|
||||
if (typeof item !== 'object') return false
|
||||
if (item === null) return false
|
||||
const obj = /** @type {StoredRequest} */ (item)
|
||||
if (typeof obj.recipientPub !== 'string') return false
|
||||
if (typeof obj.handshakeAddress !== 'string') return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} SimpleSentRequest
|
||||
* @prop {string} id
|
||||
* @prop {string|null} recipientAvatar
|
||||
* @prop {boolean} recipientChangedRequestAddress True if the recipient changed
|
||||
* the request node address and therefore can't no longer accept the request.
|
||||
* @prop {string|null} recipientDisplayName
|
||||
* @prop {string} recipientPublicKey Fallback for when user has no display name.
|
||||
* @prop {number} timestamp
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {any} item
|
||||
* @returns {item is SimpleSentRequest}
|
||||
*/
|
||||
exports.isSimpleSentRequest = item => {
|
||||
if (typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const obj = /** @type {SimpleSentRequest} */ (item)
|
||||
|
||||
if (typeof obj.id !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.recipientAvatar !== 'string' && obj.recipientAvatar !== null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.recipientChangedRequestAddress !== 'boolean') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
typeof obj.recipientDisplayName !== 'string' &&
|
||||
obj.recipientDisplayName !== null
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.recipientPublicKey !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.timestamp !== 'number') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} SimpleReceivedRequest
|
||||
* @prop {string} id
|
||||
* @prop {string|null} requestorAvatar
|
||||
* @prop {string|null} requestorDisplayName
|
||||
* @prop {string} requestorPK
|
||||
* @prop {string} response
|
||||
* @prop {number} timestamp
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {any} item
|
||||
* @returns {item is SimpleReceivedRequest}
|
||||
*/
|
||||
exports.isSimpleReceivedRequest = item => {
|
||||
if (typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const obj = /** @type {SimpleReceivedRequest} */ (item)
|
||||
|
||||
if (typeof obj.id !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.requestorAvatar !== 'string' && obj.requestorAvatar !== null) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
typeof obj.requestorDisplayName !== 'string' &&
|
||||
obj.requestorDisplayName !== null
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.requestorPK !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.response !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.timestamp !== 'number') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} item
|
||||
* @returns {item is HandshakeRequest}
|
||||
*/
|
||||
exports.isHandshakeRequest = item => {
|
||||
if (typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const obj = /** @type {HandshakeRequest} */ (item)
|
||||
|
||||
if (typeof obj.from !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.response !== 'string') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (typeof obj.timestamp !== 'number') {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} item
|
||||
* @returns {item is Message}
|
||||
*/
|
||||
exports.isMessage = item => {
|
||||
if (typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const obj = /** @type {Message} */ (item)
|
||||
|
||||
return typeof obj.body === 'string' && typeof obj.timestamp === 'number'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {any} item
|
||||
* @returns {item is PartialOutgoing}
|
||||
*/
|
||||
exports.isPartialOutgoing = item => {
|
||||
if (typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const obj = /** @type {PartialOutgoing} */ (item)
|
||||
|
||||
return typeof obj.with === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {any} item
|
||||
* @returns {item is Outgoing}
|
||||
*/
|
||||
exports.isOutgoing = item => {
|
||||
if (typeof item !== 'object') {
|
||||
return false
|
||||
}
|
||||
|
||||
if (item === null) {
|
||||
return false
|
||||
}
|
||||
|
||||
const obj = /** @type {Outgoing} */ (item)
|
||||
|
||||
const messagesAreMessages = Object.values(obj.messages).every(msg =>
|
||||
exports.isMessage(msg)
|
||||
)
|
||||
|
||||
return typeof obj.with === 'string' && messagesAreMessages
|
||||
}
|
||||
295
services/gunDB/contact-api/utils.js
Normal file
295
services/gunDB/contact-api/utils.js
Normal file
|
|
@ -0,0 +1,295 @@
|
|||
/**
|
||||
* @format
|
||||
*/
|
||||
const ErrorCode = require('./errorCode')
|
||||
const Key = require('./key')
|
||||
|
||||
/**
|
||||
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
|
||||
* @typedef {import('./SimpleGUN').ISEA} ISEA
|
||||
* @typedef {import('./SimpleGUN').UserGUNNode} UserGUNNode
|
||||
*/
|
||||
|
||||
/**
|
||||
* @param {number} ms
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
const delay = ms => new Promise(res => setTimeout(res, ms))
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Promise<T>} promise
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
const timeout10 = promise => {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise((_, rej) => {
|
||||
setTimeout(() => {
|
||||
rej(new Error(ErrorCode.TIMEOUT_ERR))
|
||||
}, 10000)
|
||||
})
|
||||
])
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {(gun: GUNNode, user: UserGUNNode) => Promise<T>} promGen The function
|
||||
* receives the most recent gun and user instances.
|
||||
* @returns {Promise<T>}
|
||||
*/
|
||||
const tryAndWait = promGen =>
|
||||
timeout10(
|
||||
promGen(
|
||||
require('../Mediator/index').getGun(),
|
||||
require('../Mediator/index').getUser()
|
||||
)
|
||||
)
|
||||
|
||||
/**
|
||||
* @param {string} pub
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const pubToEpub = async pub => {
|
||||
try {
|
||||
const epub = await tryAndWait(async gun => {
|
||||
const _epub = await gun
|
||||
.user(pub)
|
||||
.get('epub')
|
||||
.then()
|
||||
|
||||
if (typeof _epub !== 'string') {
|
||||
throw new TypeError(
|
||||
`Expected gun.user(pub).get(epub) to be an string. Instead got: ${typeof _epub}`
|
||||
)
|
||||
}
|
||||
|
||||
return _epub
|
||||
})
|
||||
|
||||
return epub
|
||||
} catch (err) {
|
||||
console.log(err)
|
||||
throw new Error(`pubToEpub() -> ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} reqID
|
||||
* @param {ISEA} SEA
|
||||
* @param {string} mySecret
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const reqToRecipientPub = async (reqID, SEA, mySecret) => {
|
||||
const maybeEncryptedForMeRecipientPub = await tryAndWait(async (_, user) => {
|
||||
const reqToUser = user.get(Key.REQUEST_TO_USER)
|
||||
const data = await reqToUser.get(reqID).then()
|
||||
|
||||
if (typeof data !== 'string') {
|
||||
throw new TypeError("typeof maybeEncryptedForMeRecipientPub !== 'string'")
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
const encryptedForMeRecipientPub = maybeEncryptedForMeRecipientPub
|
||||
|
||||
const recipientPub = await SEA.decrypt(encryptedForMeRecipientPub, mySecret)
|
||||
|
||||
if (typeof recipientPub !== 'string') {
|
||||
throw new TypeError("typeof recipientPub !== 'string'")
|
||||
}
|
||||
|
||||
return recipientPub
|
||||
}
|
||||
|
||||
/**
|
||||
* Should only be called with a recipient pub that has already been contacted.
|
||||
* @param {string} recipientPub
|
||||
* @returns {Promise<string>}
|
||||
*/
|
||||
const recipientPubToLastReqSentID = async recipientPub => {
|
||||
const lastReqSentID = await tryAndWait(async (_, user) => {
|
||||
const userToLastReqSent = user.get(Key.USER_TO_LAST_REQUEST_SENT)
|
||||
const data = await userToLastReqSent.get(recipientPub).then()
|
||||
|
||||
if (typeof data !== 'string') {
|
||||
throw new TypeError("typeof latestReqSentID !== 'string'")
|
||||
}
|
||||
|
||||
return data
|
||||
})
|
||||
|
||||
return lastReqSentID
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} recipientPub
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const successfulHandshakeAlreadyExists = async recipientPub => {
|
||||
const maybeIncomingID = await tryAndWait((_, user) => {
|
||||
const userToIncoming = user.get(Key.USER_TO_INCOMING)
|
||||
|
||||
return userToIncoming.get(recipientPub).then()
|
||||
})
|
||||
|
||||
return typeof maybeIncomingID === 'string'
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} recipientPub
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
const recipientToOutgoingID = async (recipientPub, user, SEA) => {
|
||||
const mySecret = await SEA.secret(user._.sea.epub, user._.sea)
|
||||
|
||||
if (typeof mySecret !== 'string') {
|
||||
throw new TypeError('could not get mySecret')
|
||||
}
|
||||
|
||||
const maybeEncryptedOutgoingID = await tryAndWait((_, user) =>
|
||||
user
|
||||
.get(Key.RECIPIENT_TO_OUTGOING)
|
||||
.get(recipientPub)
|
||||
.then()
|
||||
)
|
||||
|
||||
if (typeof maybeEncryptedOutgoingID === 'string') {
|
||||
const outgoingID = await SEA.decrypt(maybeEncryptedOutgoingID, mySecret)
|
||||
|
||||
return outgoingID || null
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} reqResponse
|
||||
* @param {string} recipientPub
|
||||
* @param {UserGUNNode} user
|
||||
* @param {ISEA} SEA
|
||||
* @returns {Promise<boolean>}
|
||||
*/
|
||||
const reqWasAccepted = async (reqResponse, recipientPub, user, SEA) => {
|
||||
try {
|
||||
const recipientEpub = await pubToEpub(recipientPub)
|
||||
const ourSecret = await SEA.secret(recipientEpub, user._.sea)
|
||||
if (typeof ourSecret !== 'string') {
|
||||
throw new TypeError('typeof ourSecret !== "string"')
|
||||
}
|
||||
|
||||
const decryptedResponse = await SEA.decrypt(reqResponse, ourSecret)
|
||||
|
||||
if (typeof decryptedResponse !== 'string') {
|
||||
throw new TypeError('typeof decryptedResponse !== "string"')
|
||||
}
|
||||
|
||||
const myFeedID = await recipientToOutgoingID(recipientPub, user, SEA)
|
||||
|
||||
if (typeof myFeedID === 'string' && decryptedResponse === myFeedID) {
|
||||
return false
|
||||
}
|
||||
|
||||
const recipientFeedID = decryptedResponse
|
||||
|
||||
const maybeFeed = await tryAndWait(gun =>
|
||||
gun
|
||||
.user(recipientPub)
|
||||
.get(Key.OUTGOINGS)
|
||||
.get(recipientFeedID)
|
||||
.then()
|
||||
)
|
||||
|
||||
const feedExistsOnRecipient =
|
||||
typeof maybeFeed === 'object' && maybeFeed !== null
|
||||
|
||||
return feedExistsOnRecipient
|
||||
} catch (err) {
|
||||
throw new Error(`reqWasAccepted() -> ${err.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} userPub
|
||||
* @returns {Promise<string|null>}
|
||||
*/
|
||||
const currHandshakeAddress = async userPub => {
|
||||
const maybeAddr = await tryAndWait(gun =>
|
||||
gun
|
||||
.user(userPub)
|
||||
.get(Key.CURRENT_HANDSHAKE_ADDRESS)
|
||||
.then()
|
||||
)
|
||||
|
||||
return typeof maybeAddr === 'string' ? maybeAddr : null
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template U
|
||||
* @param {T[]} arr
|
||||
* @param {(item: T) => Promise<U>} cb
|
||||
* @returns {Promise<U[]>}
|
||||
*/
|
||||
const asyncMap = (arr, cb) => {
|
||||
if (arr.length === 0) {
|
||||
return Promise.resolve([])
|
||||
}
|
||||
|
||||
const promises = arr.map(item => cb(item))
|
||||
|
||||
return Promise.all(promises)
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T[]} arr
|
||||
* @param {(item: T) => Promise<boolean>} cb
|
||||
* @returns {Promise<T[]>}
|
||||
*/
|
||||
const asyncFilter = async (arr, cb) => {
|
||||
if (arr.length === 0) {
|
||||
return []
|
||||
}
|
||||
|
||||
/** @type {Promise<boolean>[]} */
|
||||
const promises = arr.map(item => cb(item))
|
||||
|
||||
/** @type {boolean[]} */
|
||||
const results = await Promise.all(promises)
|
||||
|
||||
return arr.filter((_, idx) => results[idx])
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {import('./SimpleGUN').ListenerData} listenerData
|
||||
* @returns {listenerData is import('./SimpleGUN').ListenerObj}
|
||||
*/
|
||||
const dataHasSoul = listenerData =>
|
||||
typeof listenerData === 'object' && listenerData !== null
|
||||
|
||||
/**
|
||||
* @param {string} pub
|
||||
* @returns {string}
|
||||
*/
|
||||
const defaultName = pub => 'anon' + pub.slice(0, 8)
|
||||
|
||||
module.exports = {
|
||||
asyncMap,
|
||||
asyncFilter,
|
||||
dataHasSoul,
|
||||
defaultName,
|
||||
delay,
|
||||
pubToEpub,
|
||||
reqToRecipientPub,
|
||||
recipientPubToLastReqSentID,
|
||||
successfulHandshakeAlreadyExists,
|
||||
recipientToOutgoingID,
|
||||
reqWasAccepted,
|
||||
currHandshakeAddress,
|
||||
tryAndWait
|
||||
}
|
||||
11
services/gunDB/event-constants.js
Normal file
11
services/gunDB/event-constants.js
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
const Events = {
|
||||
ON_AVATAR: "ON_AVATAR",
|
||||
ON_BLACKLIST: "ON_BLACKLIST",
|
||||
ON_CHATS: "ON_CHATS",
|
||||
ON_DISPLAY_NAME: "ON_DISPLAY_NAME",
|
||||
ON_HANDSHAKE_ADDRESS: "ON_HANDSHAKE_ADDRESS",
|
||||
ON_RECEIVED_REQUESTS: "ON_RECEIVED_REQUESTS",
|
||||
ON_SENT_REQUESTS: "ON_SENT_REQUESTS"
|
||||
};
|
||||
|
||||
module.exports = Events;
|
||||
84
services/lnd/lightning.js
Normal file
84
services/lnd/lightning.js
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
const grpc = require("grpc");
|
||||
const protoLoader = require("@grpc/proto-loader");
|
||||
const fs = require("../../utils/fs");
|
||||
const logger = require("winston");
|
||||
const errorConstants = require("../../constants/errors");
|
||||
|
||||
// expose the routes to our app with module.exports
|
||||
module.exports = async (protoPath, lndHost, lndCertPath, macaroonPath) => {
|
||||
try {
|
||||
process.env.GRPC_SSL_CIPHER_SUITES = "HIGH+ECDSA";
|
||||
|
||||
const packageDefinition = await protoLoader.load(protoPath, {
|
||||
keepCase: true,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true,
|
||||
includeDirs: ["node_modules/google-proto-files", "proto"]
|
||||
});
|
||||
const { lnrpc } = grpc.loadPackageDefinition(packageDefinition);
|
||||
|
||||
const getCredentials = async () => {
|
||||
const lndCert = await fs.readFile(lndCertPath);
|
||||
const sslCreds = grpc.credentials.createSsl(lndCert);
|
||||
|
||||
if (macaroonPath) {
|
||||
const macaroonExists = await fs.access(macaroonPath);
|
||||
|
||||
if (macaroonExists) {
|
||||
const macaroonCreds = grpc.credentials.createFromMetadataGenerator(
|
||||
async (args, callback) => {
|
||||
const adminMacaroon = await fs.readFile(macaroonPath);
|
||||
const metadata = new grpc.Metadata();
|
||||
metadata.add("macaroon", adminMacaroon.toString("hex"));
|
||||
callback(null, metadata);
|
||||
}
|
||||
);
|
||||
return grpc.credentials.combineChannelCredentials(
|
||||
sslCreds,
|
||||
macaroonCreds
|
||||
);
|
||||
}
|
||||
|
||||
const error = errorConstants.MACAROON_PATH(macaroonPath);
|
||||
logger.error(error);
|
||||
throw error;
|
||||
} else {
|
||||
return sslCreds;
|
||||
}
|
||||
};
|
||||
|
||||
if (lndCertPath) {
|
||||
const certExists = await fs.access(lndCertPath);
|
||||
|
||||
if (certExists) {
|
||||
const credentials = await getCredentials();
|
||||
|
||||
const lightning = new lnrpc.Lightning(lndHost, credentials);
|
||||
const walletUnlocker = new lnrpc.WalletUnlocker(lndHost, credentials);
|
||||
|
||||
return {
|
||||
lightning,
|
||||
walletUnlocker
|
||||
};
|
||||
}
|
||||
|
||||
const error = errorConstants.CERT_PATH(lndCertPath);
|
||||
logger.error(error);
|
||||
throw error;
|
||||
} else {
|
||||
const error = errorConstants.MACAROON_PATH(macaroonPath);
|
||||
logger.error(error);
|
||||
throw error;
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.code === 14) {
|
||||
throw {
|
||||
field: "unknown",
|
||||
message:
|
||||
"Failed to connect to LND server, make sure it's up and running."
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
81
services/lnd/lnd.js
Normal file
81
services/lnd/lnd.js
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
// app/lnd.js
|
||||
|
||||
const debug = require("debug")("lncliweb:lnd");
|
||||
const logger = require("winston");
|
||||
|
||||
// TODO
|
||||
module.exports = function(lightning) {
|
||||
const module = {};
|
||||
|
||||
const invoiceListeners = [];
|
||||
|
||||
let lndInvoicesStream = null;
|
||||
|
||||
const openLndInvoicesStream = function() {
|
||||
if (lndInvoicesStream) {
|
||||
logger.debug("Lnd invoices subscription stream already opened.");
|
||||
} else {
|
||||
logger.debug("Opening lnd invoices subscription stream...");
|
||||
lndInvoicesStream = lightning.subscribeInvoices({});
|
||||
logger.debug("Lnd invoices subscription stream opened.");
|
||||
lndInvoicesStream.on("data", function(data) {
|
||||
logger.debug("SubscribeInvoices Data", data);
|
||||
for (let i = 0; i < invoiceListeners.length; i++) {
|
||||
try {
|
||||
invoiceListeners[i].dataReceived(data);
|
||||
} catch (err) {
|
||||
logger.warn(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
lndInvoicesStream.on("end", function() {
|
||||
logger.debug("SubscribeInvoices End");
|
||||
lndInvoicesStream = null;
|
||||
openLndInvoicesStream(); // try opening stream again
|
||||
});
|
||||
lndInvoicesStream.on("error", function(err) {
|
||||
logger.debug("SubscribeInvoices Error", err);
|
||||
});
|
||||
lndInvoicesStream.on("status", function(status) {
|
||||
logger.debug("SubscribeInvoices Status", status);
|
||||
if (status.code == 14) {
|
||||
// Unavailable
|
||||
lndInvoicesStream = null;
|
||||
openLndInvoicesStream(); // try opening stream again
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// register invoice listener
|
||||
module.registerInvoiceListener = function(listener) {
|
||||
invoiceListeners.push(listener);
|
||||
logger.debug(
|
||||
"New lnd invoice listener registered, " +
|
||||
invoiceListeners.length +
|
||||
" listening now"
|
||||
);
|
||||
};
|
||||
|
||||
// unregister invoice listener
|
||||
module.unregisterInvoiceListener = function(listener) {
|
||||
invoiceListeners.splice(invoiceListeners.indexOf(listener), 1);
|
||||
logger.debug(
|
||||
"Lnd invoice listener unregistered, " +
|
||||
invoiceListeners.length +
|
||||
" still listening"
|
||||
);
|
||||
};
|
||||
|
||||
// open lnd invoices stream on start
|
||||
openLndInvoicesStream();
|
||||
|
||||
// check every minute that lnd invoices stream is still opened
|
||||
setInterval(function() {
|
||||
if (!lndInvoicesStream) {
|
||||
openLndInvoicesStream();
|
||||
}
|
||||
}, 60 * 1000);
|
||||
|
||||
return module;
|
||||
};
|
||||
18
src/cors.js
Normal file
18
src/cors.js
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
const setAccessControlHeaders = (req, res) => {
|
||||
res.header("Access-Control-Allow-Origin", "*");
|
||||
res.header(
|
||||
"Access-Control-Allow-Headers",
|
||||
"Origin, X-Requested-With, Content-Type, Accept, Authorization"
|
||||
);
|
||||
};
|
||||
|
||||
module.exports = (req, res, next) => {
|
||||
if (req.method === "OPTIONS") {
|
||||
setAccessControlHeaders(req, res);
|
||||
res.sendStatus(204);
|
||||
return;
|
||||
}
|
||||
|
||||
setAccessControlHeaders(req, res);
|
||||
next();
|
||||
};
|
||||
1355
src/routes.js
Normal file
1355
src/routes.js
Normal file
File diff suppressed because it is too large
Load diff
254
src/server.js
Normal file
254
src/server.js
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
"use strict";
|
||||
|
||||
/**
|
||||
* Module dependencies.
|
||||
*/
|
||||
const server = program => {
|
||||
const Https = require("https");
|
||||
const Http = require("http");
|
||||
const Express = require("express");
|
||||
// const Cluster = require("cluster");
|
||||
// const OS = require("os");
|
||||
const app = Express();
|
||||
|
||||
const FS = require("../utils/fs");
|
||||
const bodyParser = require("body-parser");
|
||||
const session = require("express-session");
|
||||
const methodOverride = require("method-override");
|
||||
// load app default configuration data
|
||||
const defaults = require("../config/defaults")(program.mainnet);
|
||||
// define useful global variables ======================================
|
||||
module.useTLS = program.usetls;
|
||||
module.serverPort = program.serverport || defaults.serverPort;
|
||||
module.httpsPort = module.serverPort;
|
||||
module.serverHost = program.serverhost || defaults.serverHost;
|
||||
|
||||
// setup winston logging ==========
|
||||
const logger = require("../config/log")(
|
||||
program.logfile || defaults.logfile,
|
||||
program.loglevel || defaults.loglevel
|
||||
);
|
||||
|
||||
// utilities functions =================
|
||||
require("../utils/server-utils")(module);
|
||||
|
||||
// setup lightning client =================
|
||||
const lnrpc = require("../services/lnd/lightning");
|
||||
const lndHost = program.lndhost || defaults.lndHost;
|
||||
const lndCertPath = program.lndCertPath || defaults.lndCertPath;
|
||||
const macaroonPath = program.macaroonPath || defaults.macaroonPath;
|
||||
|
||||
logger.info("Mainnet Mode:", !!program.mainnet);
|
||||
|
||||
const wait = seconds =>
|
||||
new Promise(resolve => {
|
||||
const timer = setTimeout(() => resolve(timer), seconds * 1000);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
const startServer = async () => {
|
||||
try {
|
||||
const macaroonExists = await FS.access(macaroonPath);
|
||||
const lnServices = await lnrpc(
|
||||
defaults.lndProto,
|
||||
lndHost,
|
||||
lndCertPath,
|
||||
macaroonExists ? macaroonPath : null
|
||||
);
|
||||
const { lightning } = lnServices;
|
||||
const { walletUnlocker } = lnServices;
|
||||
const lnServicesData = {
|
||||
lndProto: defaults.lndProto,
|
||||
lndHost,
|
||||
lndCertPath,
|
||||
macaroonPath
|
||||
};
|
||||
|
||||
// init lnd module =================
|
||||
const lnd = require("../services/lnd/lnd")(lightning);
|
||||
|
||||
const unprotectedRoutes = {
|
||||
GET: {
|
||||
"/healthz": true,
|
||||
"/ping": true,
|
||||
"/api/lnd/connect": true,
|
||||
"/api/lnd/wallet/status": true,
|
||||
"/api/lnd/auth": true
|
||||
},
|
||||
POST: {
|
||||
"/api/lnd/connect": true,
|
||||
"/api/lnd/wallet": true,
|
||||
"/api/lnd/wallet/existing": true,
|
||||
"/api/lnd/auth": true
|
||||
},
|
||||
PUT: {},
|
||||
DELETE: {}
|
||||
};
|
||||
const auth = require("../services/auth/auth");
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
|
||||
if (unprotectedRoutes[req.method][req.path]) {
|
||||
next();
|
||||
} else {
|
||||
try {
|
||||
const response = await auth.validateToken(
|
||||
req.headers.authorization.replace("Bearer ", "")
|
||||
);
|
||||
if (response.valid) {
|
||||
next();
|
||||
} else {
|
||||
res.status(401).json({ message: "Please log in" });
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(err);
|
||||
res.status(401).json({ message: "Please log in" });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const sensitiveRoutes = {
|
||||
GET: {},
|
||||
POST: {
|
||||
"/api/lnd/connect": true,
|
||||
"/api/lnd/wallet": true
|
||||
},
|
||||
PUT: {},
|
||||
DELETE: {}
|
||||
};
|
||||
app.use((req, res, next) => {
|
||||
if (sensitiveRoutes[req.method][req.path]) {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
time: new Date(),
|
||||
ip: req.ip,
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
sessionId: req.sessionId
|
||||
})
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
JSON.stringify({
|
||||
time: new Date(),
|
||||
ip: req.ip,
|
||||
method: req.method,
|
||||
path: req.path,
|
||||
body: req.body,
|
||||
query: req.query,
|
||||
sessionId: req.sessionId
|
||||
})
|
||||
);
|
||||
}
|
||||
next();
|
||||
});
|
||||
app.use(
|
||||
session({
|
||||
secret: defaults.sessionSecret,
|
||||
cookie: { maxAge: defaults.sessionMaxAge },
|
||||
resave: true,
|
||||
rolling: true,
|
||||
saveUninitialized: true
|
||||
})
|
||||
);
|
||||
app.use(bodyParser.urlencoded({ extended: "true" }));
|
||||
app.use(bodyParser.json());
|
||||
app.use(bodyParser.json({ type: "application/vnd.api+json" }));
|
||||
app.use(methodOverride());
|
||||
// WARNING
|
||||
// error handler middleware, KEEP 4 parameters as express detects the
|
||||
// arity of the function to treat it as a err handling middleware
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
app.use((err, _, res, __) => {
|
||||
// Do logging and user-friendly error message display
|
||||
logger.error(err);
|
||||
res
|
||||
.status(500)
|
||||
.send({ status: 500, message: "internal error", type: "internal" });
|
||||
});
|
||||
|
||||
const createServer = async () => {
|
||||
try {
|
||||
if (program.usetls) {
|
||||
const [key, cert] = await Promise.all([
|
||||
FS.readFile(program.usetls + "/key.pem"),
|
||||
FS.readFile(program.usetls + "/cert.pem")
|
||||
]);
|
||||
const httpsServer = Https.createServer({ key, cert }, app);
|
||||
|
||||
return httpsServer;
|
||||
}
|
||||
|
||||
const httpServer = Http.Server(app);
|
||||
return httpServer;
|
||||
} catch (err) {
|
||||
logger.error(err.message);
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
const serverInstance = await createServer();
|
||||
|
||||
const io = require("socket.io")(serverInstance);
|
||||
|
||||
// setup sockets =================
|
||||
const lndLogfile = program.lndlogfile || defaults.lndLogFile;
|
||||
|
||||
const Sockets = require("./sockets")(
|
||||
io,
|
||||
lightning,
|
||||
lnd,
|
||||
program.user,
|
||||
program.pwd,
|
||||
program.limituser,
|
||||
program.limitpwd,
|
||||
lndLogfile,
|
||||
lnServicesData
|
||||
);
|
||||
|
||||
require("./routes")(
|
||||
app,
|
||||
lightning,
|
||||
defaults,
|
||||
walletUnlocker,
|
||||
lnServicesData,
|
||||
Sockets,
|
||||
{
|
||||
serverHost: module.serverHost,
|
||||
serverPort: module.serverPort
|
||||
}
|
||||
);
|
||||
|
||||
// enable CORS headers
|
||||
app.use(require("./cors"));
|
||||
// app.use(bodyParser.json({limit: '100000mb'}));
|
||||
app.use(bodyParser.json({ limit: "50mb" }));
|
||||
app.use(bodyParser.urlencoded({ limit: "50mb", extended: true }));
|
||||
|
||||
serverInstance.listen(module.serverPort, module.serverhost);
|
||||
|
||||
logger.info(
|
||||
"App listening on " + module.serverHost + " port " + module.serverPort
|
||||
);
|
||||
|
||||
module.server = serverInstance;
|
||||
|
||||
// const localtunnel = require('localtunnel');
|
||||
//
|
||||
// const tunnel = localtunnel(port, (err, t) => {
|
||||
// console.log('err', err);
|
||||
// console.log('t', t.url);
|
||||
// });
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
logger.info("Restarting server in 30 seconds...");
|
||||
await wait(30);
|
||||
startServer();
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
startServer();
|
||||
};
|
||||
|
||||
module.exports = server;
|
||||
147
src/sockets.js
Normal file
147
src/sockets.js
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// app/sockets.js
|
||||
|
||||
const logger = require("winston");
|
||||
const FS = require("../utils/fs");
|
||||
|
||||
module.exports = (
|
||||
io,
|
||||
lightning,
|
||||
lnd,
|
||||
login,
|
||||
pass,
|
||||
limitlogin,
|
||||
limitpass,
|
||||
lndLogfile,
|
||||
lnServicesData
|
||||
) => {
|
||||
const Mediator = require("../services/gunDB/Mediator/index.js");
|
||||
const EventEmitter = require("events");
|
||||
|
||||
class MySocketsEvents extends EventEmitter {}
|
||||
|
||||
const mySocketsEvents = new MySocketsEvents();
|
||||
|
||||
const clients = [];
|
||||
|
||||
const authEnabled = (login && pass) || (limitlogin && limitpass);
|
||||
|
||||
let userToken = null;
|
||||
let limitUserToken = null;
|
||||
if (login && pass) {
|
||||
userToken = Buffer.from(login + ":" + pass).toString("base64");
|
||||
}
|
||||
if (limitlogin && limitpass) {
|
||||
limitUserToken = Buffer.from(limitlogin + ":" + limitpass).toString(
|
||||
"base64"
|
||||
);
|
||||
}
|
||||
|
||||
// register the lnd invoices listener
|
||||
const registerLndInvoiceListener = socket => {
|
||||
socket._invoiceListener = {
|
||||
dataReceived(data) {
|
||||
socket.emit("invoice", data);
|
||||
}
|
||||
};
|
||||
lnd.registerInvoiceListener(socket._invoiceListener);
|
||||
};
|
||||
|
||||
// unregister the lnd invoices listener
|
||||
const unregisterLndInvoiceListener = socket => {
|
||||
lnd.unregisterInvoiceListener(socket._invoiceListener);
|
||||
};
|
||||
|
||||
// register the socket listeners
|
||||
const registerSocketListeners = socket => {
|
||||
registerLndInvoiceListener(socket);
|
||||
};
|
||||
|
||||
// unregister the socket listeners
|
||||
const unregisterSocketListeners = socket => {
|
||||
unregisterLndInvoiceListener(socket);
|
||||
};
|
||||
|
||||
const getSocketAuthToken = socket => {
|
||||
if (socket.handshake.query.auth) {
|
||||
return socket.handshake.query.auth;
|
||||
} else if (socket.handshake.headers.authorization) {
|
||||
return socket.handshake.headers.authorization.substr(6);
|
||||
}
|
||||
|
||||
socket.disconnect("unauthorized");
|
||||
return null;
|
||||
};
|
||||
|
||||
io.on("connection", async socket => {
|
||||
// this is where we create the websocket connection
|
||||
// with the GunDB service.
|
||||
Mediator.createMediator(socket);
|
||||
|
||||
const macaroonExists = await FS.access(lnServicesData.macaroonPath);
|
||||
const lnServices = await require("../services/lnd/lightning")(
|
||||
lnServicesData.lndProto,
|
||||
lnServicesData.lndHost,
|
||||
lnServicesData.lndCertPath,
|
||||
macaroonExists ? lnServicesData.macaroonPath : null
|
||||
);
|
||||
// eslint-disable-next-line prefer-destructuring, no-param-reassign
|
||||
lightning = lnServices.lightning;
|
||||
|
||||
mySocketsEvents.addListener("updateLightning", async () => {
|
||||
const newMacaroonExists = await FS.access(lnServicesData.macaroonPath);
|
||||
const newLNServices = await require("../services/lnd/lightning")(
|
||||
lnServicesData.lndProto,
|
||||
lnServicesData.lndHost,
|
||||
lnServicesData.lndCertPath,
|
||||
newMacaroonExists ? lnServicesData.macaroonPath : null
|
||||
);
|
||||
// eslint-disable-next-line prefer-destructuring, no-param-reassign
|
||||
lightning = newLNServices.lightning;
|
||||
});
|
||||
|
||||
logger.debug("socket.handshake", socket.handshake);
|
||||
|
||||
if (authEnabled) {
|
||||
try {
|
||||
const authorizationHeaderToken = getSocketAuthToken(socket);
|
||||
|
||||
if (authorizationHeaderToken === userToken) {
|
||||
socket._limituser = false;
|
||||
} else if (authorizationHeaderToken === limitUserToken) {
|
||||
socket._limituser = true;
|
||||
} else {
|
||||
socket.disconnect("unauthorized");
|
||||
return;
|
||||
}
|
||||
} catch (err) {
|
||||
// probably because of missing authorization header
|
||||
logger.debug(err);
|
||||
socket.disconnect("unauthorized");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
socket._limituser = false;
|
||||
}
|
||||
|
||||
/** printing out the client who joined */
|
||||
logger.debug("New socket client connected (id=" + socket.id + ").");
|
||||
|
||||
socket.emit("hello", { limitUser: socket._limituser });
|
||||
|
||||
socket.broadcast.emit("hello", { remoteAddress: socket.handshake.address });
|
||||
|
||||
/** pushing new client to client array*/
|
||||
clients.push(socket);
|
||||
|
||||
registerSocketListeners(socket);
|
||||
|
||||
/** listening if client has disconnected */
|
||||
socket.on("disconnect", () => {
|
||||
clients.splice(clients.indexOf(socket), 1);
|
||||
unregisterSocketListeners(socket);
|
||||
logger.debug("client disconnected (id=" + socket.id + ").");
|
||||
});
|
||||
});
|
||||
|
||||
return mySocketsEvents;
|
||||
};
|
||||
64
tsconfig.json
Normal file
64
tsconfig.json
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
{
|
||||
"include": ["./services/gunDB/**/*.*"],
|
||||
"compilerOptions": {
|
||||
/* Basic Options */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
"target": "es5" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */,
|
||||
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */,
|
||||
// "lib": [], /* Specify library files to be included in the compilation. */
|
||||
"allowJs": true /* Allow javascript files to be compiled. */,
|
||||
"checkJs": true /* Report errors in .js files. */,
|
||||
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
|
||||
// "declaration": true, /* Generates corresponding '.d.ts' file. */
|
||||
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
|
||||
// "sourceMap": true, /* Generates corresponding '.map' file. */
|
||||
// "outFile": "./", /* Concatenate and emit output to single file. */
|
||||
// "outDir": "./", /* Redirect output structure to the directory. */
|
||||
// "rootDir": "./" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */,
|
||||
// "composite": true, /* Enable project compilation */
|
||||
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
|
||||
// "removeComments": true, /* Do not emit comments to output. */
|
||||
"noEmit": true /* Do not emit outputs. */,
|
||||
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
|
||||
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
|
||||
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
|
||||
|
||||
/* Strict Type-Checking Options */
|
||||
"strict": true /* Enable all strict type-checking options. */,
|
||||
"noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */,
|
||||
"strictNullChecks": true /* Enable strict null checks. */,
|
||||
"strictFunctionTypes": true /* Enable strict checking of function types. */,
|
||||
"strictBindCallApply": true /* Enable strict 'bind', 'call', and 'apply' methods on functions. */,
|
||||
"strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */,
|
||||
"noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */,
|
||||
"alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */,
|
||||
|
||||
/* Additional Checks */
|
||||
"noUnusedLocals": true /* Report errors on unused locals. */,
|
||||
"noUnusedParameters": true /* Report errors on unused parameters. */,
|
||||
"noImplicitReturns": true /* Report error when not all code paths in function return a value. */,
|
||||
"noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */,
|
||||
|
||||
/* Module Resolution Options */
|
||||
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
|
||||
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
|
||||
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
|
||||
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
|
||||
// "typeRoots": [], /* List of folders to include type definitions from. */
|
||||
// "types": [], /* Type declaration files to be included in compilation. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
|
||||
"esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
|
||||
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
|
||||
/* Source Map Options */
|
||||
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
|
||||
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
|
||||
|
||||
/* Experimental Options */
|
||||
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
|
||||
}
|
||||
}
|
||||
16
utils/colors.js
Normal file
16
utils/colors.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const colors = require("colors/safe");
|
||||
|
||||
colors.setTheme({
|
||||
silly: "rainbow",
|
||||
input: "grey",
|
||||
verbose: "cyan",
|
||||
prompt: "grey",
|
||||
info: "green",
|
||||
data: "grey",
|
||||
help: "cyan",
|
||||
warn: "yellow",
|
||||
debug: "blue",
|
||||
error: "red"
|
||||
});
|
||||
|
||||
module.exports = colors;
|
||||
16
utils/fs.js
Normal file
16
utils/fs.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
const { promisify } = require("util");
|
||||
const FS = require("fs");
|
||||
|
||||
module.exports = {
|
||||
access: path =>
|
||||
new Promise(resolve => {
|
||||
FS.access(path, FS.constants.F_OK, err => {
|
||||
resolve(!err);
|
||||
});
|
||||
}),
|
||||
exists: promisify(FS.exists),
|
||||
readFile: promisify(FS.readFile),
|
||||
writeFile: promisify(FS.writeFile),
|
||||
readdir: promisify(FS.readdir),
|
||||
unlink: promisify(FS.unlink)
|
||||
};
|
||||
14
utils/paginate.js
Normal file
14
utils/paginate.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
const getListPage = ({ entries = [], itemsPerPage = 20, page = 1 }) => {
|
||||
const totalPages = Math.ceil(entries.length / itemsPerPage);
|
||||
const offset = (page - 1) * itemsPerPage;
|
||||
const limit = page * itemsPerPage;
|
||||
const paginatedContent = entries.slice(offset, limit);
|
||||
return {
|
||||
content: paginatedContent,
|
||||
page,
|
||||
totalPages,
|
||||
totalItems: entries.length
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = getListPage;
|
||||
12
utils/server-utils.js
Normal file
12
utils/server-utils.js
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
module.exports = server => {
|
||||
const module = {};
|
||||
|
||||
server.getURL = function getURL() {
|
||||
const HTTPSPort = this.httpsPort === "443" ? "" : ":" + this.httpsPort;
|
||||
const HTTPPort = this.serverPort === "80" ? "" : ":" + this.serverPort;
|
||||
const URLPort = this.useTLS ? HTTPSPort : HTTPPort;
|
||||
return `http${this.useTLS ? "s" : ""}://${this.serverHost}${URLPort}`;
|
||||
};
|
||||
|
||||
return module;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue