Mayank
4 years ago
commit
9ad14c3409
37 changed files with 4691 additions and 0 deletions
@ -0,0 +1,5 @@ |
|||||
|
{ |
||||
|
"presets": [ |
||||
|
"@babel/preset-env" |
||||
|
] |
||||
|
} |
@ -0,0 +1,18 @@ |
|||||
|
.dockerignore |
||||
|
node_modules |
||||
|
npm-debug.log |
||||
|
logs |
||||
|
README.md |
||||
|
.git |
||||
|
.gitignore |
||||
|
.env.default |
||||
|
.idea |
||||
|
Dockerfile |
||||
|
pre-commit |
||||
|
.eslintrc |
||||
|
test/ |
||||
|
.eslintignore |
||||
|
coverage |
||||
|
.nyc_output |
||||
|
*.env |
||||
|
db/ |
@ -0,0 +1 @@ |
|||||
|
coverage |
@ -0,0 +1,276 @@ |
|||||
|
{ |
||||
|
"root": true, |
||||
|
"parser": "babel-eslint", |
||||
|
"extends": "eslint:recommended", |
||||
|
"env": { |
||||
|
"es6": true, |
||||
|
"node": true |
||||
|
}, |
||||
|
"globals": {}, |
||||
|
"plugins": [], |
||||
|
"overrides": [ |
||||
|
{ |
||||
|
"files": ["*.integration.js","*.spec.js"], |
||||
|
"rules": { |
||||
|
"no-magic-numbers": "off" |
||||
|
} |
||||
|
}, |
||||
|
{ |
||||
|
"files": ["*.js"], |
||||
|
"rules": { |
||||
|
"prefer-arrow-callback": 0, |
||||
|
"no-process-env": 0, |
||||
|
"no-warning-comments": 0, |
||||
|
"prefer-template": 0, |
||||
|
"no-sync": 0 |
||||
|
} |
||||
|
} |
||||
|
], |
||||
|
"rules": { |
||||
|
//Possible Errors |
||||
|
"comma-dangle": 0, //disallow or enforce trailing commas |
||||
|
"no-cond-assign": 2, //disallow assignment in conditional expressions |
||||
|
"no-console": 2, //disallow use of console in the node environment |
||||
|
"no-constant-condition": 1, //disallow use of constant expressions in conditions |
||||
|
"no-control-regex": 2, //disallow control characters in regular expressions |
||||
|
"no-debugger": 2, //disallow use of debugger |
||||
|
"no-dupe-args": 2, //disallow duplicate arguments in functions |
||||
|
"no-dupe-keys": 2, //disallow duplicate keys when creating object literals |
||||
|
"no-duplicate-case": 2, //disallow a duplicate case label. |
||||
|
"no-empty-character-class": 2, //disallow the use of empty character classes in regular expressions |
||||
|
"no-empty": 2, //disallow empty statements |
||||
|
"no-ex-assign": 2, //disallow assigning to the exception in a catch block |
||||
|
"no-extra-boolean-cast": 2, //disallow double-negation boolean casts in a boolean context |
||||
|
"no-extra-parens": 2, //disallow unnecessary parentheses |
||||
|
"no-extra-semi": 2, //disallow unnecessary semicolons |
||||
|
"no-func-assign": 2, //disallow overwriting functions written as function declarations |
||||
|
"no-inner-declarations": 1, //disallow function or variable declarations in nested blocks |
||||
|
"no-invalid-regexp": 2, //disallow invalid regular expression strings in the RegExp constructor |
||||
|
"no-irregular-whitespace": 2, //disallow irregular whitespace outside of strings and comments |
||||
|
"no-negated-in-lhs": 2, //disallow negation of the left operand of an in expression |
||||
|
"no-obj-calls": 2, //disallow the use of object properties of the global object (Math and JSON) as functions |
||||
|
"no-prototype-builtins": 2, //Disallow use of Object.prototypes builtins directly |
||||
|
"no-regex-spaces": 2, //disallow multiple spaces in a regular expression literal |
||||
|
"no-sparse-arrays": 1, //disallow sparse arrays |
||||
|
"no-unexpected-multiline": 2, //Avoid code that looks like two expressions but is actually one |
||||
|
"no-unreachable": 2, //disallow unreachable statements after a return, throw, continue, or break statement |
||||
|
"no-unsafe-finally": 2, //disallow control flow statements in finally blocks |
||||
|
"use-isnan": 2, //disallow comparisons with the value NaN |
||||
|
"valid-jsdoc": 2, //Ensure JSDoc comments are valid |
||||
|
"valid-typeof": 2, //Ensure that the results of typeof are compared against a valid string |
||||
|
|
||||
|
//Best Practices |
||||
|
"accessor-pairs": 0, //Enforces getter/setter pairs in objects |
||||
|
"array-callback-return": 2, //Enforces return statements in callbacks of array"s methods |
||||
|
"block-scoped-var": 2, //treat var statements as if they were block scoped |
||||
|
"complexity": 1, //specify the maximum cyclomatic complexity allowed in a program |
||||
|
"consistent-return": 0, //require return statements to either always or never specify values |
||||
|
"curly": 2, //specify curly brace conventions for all control statements |
||||
|
"default-case": 2, //require default case in switch statements |
||||
|
"dot-location": [2, "property"], //enforces consistent newlines before or after dots |
||||
|
"dot-notation": 2, //encourages use of dot notation whenever possible |
||||
|
"eqeqeq": 2, //require the use of === and !== |
||||
|
"guard-for-in": 2, //make sure for-in loops have an if statement |
||||
|
"no-alert": 2, //disallow the use of alert, confirm, and prompt |
||||
|
"no-caller": 2, //disallow use of arguments.caller or arguments.callee |
||||
|
"no-case-declarations": 0, //disallow lexical declarations in case clauses |
||||
|
"no-div-regex": 2, //disallow division operators explicitly at beginning of regular expression |
||||
|
"no-else-return": 0, //disallow else after a return in an if |
||||
|
"no-empty-function": 2, //disallow use of empty functions |
||||
|
"no-empty-pattern": 2, //disallow use of empty destructuring patterns |
||||
|
"no-eq-null": 2, //disallow comparisons to null without a type-checking operator |
||||
|
"no-eval": 2, //disallow use of eval() |
||||
|
"no-extend-native": 0, //disallow adding to native types |
||||
|
"no-extra-bind": 1, //disallow unnecessary function binding |
||||
|
"no-extra-label": 2, //disallow unnecessary labels |
||||
|
"no-fallthrough": 2, //disallow fallthrough of case statements |
||||
|
"no-floating-decimal": 2, //disallow the use of leading or trailing decimal points in numeric literals |
||||
|
"no-implicit-coercion": 0, //disallow the type conversions with shorter notations |
||||
|
"no-implicit-globals": 0, //disallow var and named functions in global scope |
||||
|
"no-implied-eval": 2, //disallow use of eval()-like methods |
||||
|
"no-invalid-this": 2, //disallow this keywords outside of classes or class-like objects |
||||
|
"no-iterator": 2, //disallow usage of __iterator__ property |
||||
|
"no-labels": 2, //disallow use of labeled statements |
||||
|
"no-lone-blocks": 2, //disallow unnecessary nested blocks |
||||
|
"no-loop-func": 2, //disallow creation of functions within loops |
||||
|
"no-magic-numbers": [2, { "ignore": [0,1,-1] }], //disallow the use of magic numbers other than 0 or 1 |
||||
|
"no-multi-spaces": 2, //disallow use of multiple spaces |
||||
|
"no-multi-str": 0, //disallow use of multiline strings |
||||
|
"no-native-reassign": 2, //disallow reassignments of native objects |
||||
|
"no-new-func": 1, //disallow use of new operator for Function object |
||||
|
"no-new-wrappers": 2, //disallows creating new instances of String,Number, and Boolean |
||||
|
"no-new": 2, //disallow use of the new operator when not part of an assignment or comparison |
||||
|
"no-octal-escape": 0, //disallow use of octal escape sequences in string literals, such as var foo = "Copyright \251"; |
||||
|
"no-octal": 0, //disallow use of octal literals |
||||
|
"no-param-reassign": 2, //disallow reassignment of function parameters |
||||
|
"no-process-env": 2, //disallow use of process.env |
||||
|
"no-proto": 2, //disallow usage of __proto__ property |
||||
|
"no-redeclare": 2, //disallow declaring the same variable more than once |
||||
|
"no-return-assign": 2, //disallow use of assignment in return statement |
||||
|
"no-script-url": 2, //disallow use of javascript: urls. |
||||
|
"no-self-assign": 2, //disallow assignments where both sides are exactly the same |
||||
|
"no-self-compare": 2, //disallow comparisons where both sides are exactly the same |
||||
|
"no-sequences": 2, //disallow use of the comma operator |
||||
|
"no-throw-literal": 0, //restrict what can be thrown as an exception |
||||
|
"no-unmodified-loop-condition": 2, //disallow unmodified conditions of loops |
||||
|
"no-unused-expressions": 0, //disallow usage of expressions in statement position |
||||
|
"no-unused-labels": 2, //disallow unused labels |
||||
|
"no-useless-call": 2, //disallow unnecessary .call() and .apply() |
||||
|
"no-useless-concat": 2, //disallow unnecessary concatenation of literals or template literals |
||||
|
"no-useless-escape": 2, //disallow unnecessary escape characters |
||||
|
"no-void": 0, //disallow use of the void operator |
||||
|
"no-warning-comments": 1, //disallow usage of configurable warning terms in comments (e.g. TODO or FIXME) |
||||
|
"no-with": 2, //disallow use of the with statement |
||||
|
"radix": 2, //require use of the second argument for parseInt() |
||||
|
"vars-on-top": 0, //require declaration of all vars at the top of their containing scope |
||||
|
"wrap-iife": 2, //require immediate function invocation to be wrapped in parentheses |
||||
|
"yoda": 2, //require or disallow Yoda conditions |
||||
|
|
||||
|
//Strict Mode |
||||
|
"strict": 0, //controls location of Use Strict Directives |
||||
|
|
||||
|
//Variables |
||||
|
"init-declarations": 0, //enforce or disallow variable initializations at definition |
||||
|
"no-catch-shadow": 2, //disallow the catch clause parameter name being the same as a variable in the outer scope |
||||
|
"no-delete-var": 2, //disallow deletion of variables |
||||
|
"no-label-var": 2, //disallow labels that share a name with a variable |
||||
|
"no-restricted-globals": 0, //restrict usage of specified global variables |
||||
|
"no-shadow-restricted-names": 2, //disallow shadowing of names such as arguments |
||||
|
"no-shadow": [2, {"allow": ["err"]}], //disallow declaration of variables already declared in the outer scope |
||||
|
"no-undef-init": 2, //disallow use of undefined when initializing variables |
||||
|
"no-undef": 2, //disallow use of undeclared variables unless mentioned in a /*global */ block |
||||
|
"no-undefined": 0, //disallow use of undefined variable |
||||
|
"no-unused-vars": 2, //disallow declaration of variables that are not used in the code |
||||
|
"no-use-before-define": [2, { "functions": false }], //disallow use of variables before they are defined |
||||
|
|
||||
|
//Node.js and CommonJS |
||||
|
"callback-return": 2, //enforce return after a callback |
||||
|
"global-require": 0, //enforce require() on top-level module scope |
||||
|
"handle-callback-err": 2, //enforce error handling in callbacks |
||||
|
"no-mixed-requires": 2, //disallow mixing regular variable and require declarations |
||||
|
"no-new-require": 2, //disallow use of new operator with the require function |
||||
|
"no-path-concat": 2, //disallow string concatenation with __dirname and __filename |
||||
|
"no-process-exit": 2, //disallow process.exit() |
||||
|
"no-restricted-imports": 0, //restrict usage of specified node imports |
||||
|
"no-restricted-modules": 0, //restrict usage of specified node modules |
||||
|
"no-sync": 1, //disallow use of synchronous methods |
||||
|
|
||||
|
//Stylistic Issues |
||||
|
"array-bracket-spacing": [2, "never"], //enforce spacing inside array brackets |
||||
|
"block-spacing": 0, //disallow or enforce spaces inside of single line blocks |
||||
|
"brace-style": 2, //enforce one true brace style |
||||
|
"camelcase": 1, //require camel case names |
||||
|
"comma-spacing": [2, {"before": false, "after": true}], //enforce spacing before and after comma |
||||
|
"comma-style": 2, //enforce one true comma style |
||||
|
"computed-property-spacing": 2, //require or disallow padding inside computed properties |
||||
|
"consistent-this": 2, //enforce consistent naming when capturing the current execution context |
||||
|
"eol-last": 2, //enforce newline at the end of file, with no multiple empty lines |
||||
|
"func-names": 0, //require function expressions to have a name |
||||
|
"func-style": 0, //enforce use of function declarations or expressions |
||||
|
"id-blacklist": 0, //blacklist certain identifiers to prevent them being used |
||||
|
"id-length": [2, { //this option enforces minimum and maximum identifier lengths (variable names, property names etc.) |
||||
|
"min": 2, |
||||
|
"max": 25, |
||||
|
"exceptions": ["_"] |
||||
|
}], |
||||
|
"id-match": 0, //require identifiers to match the provided regular expression |
||||
|
"indent": ["error", 2], //specify tab or space width for your code |
||||
|
"jsx-quotes": 0, //specify whether double or single quotes should be used in JSX attributes |
||||
|
"key-spacing": 2, //enforce spacing between keys and values in object literal properties |
||||
|
"keyword-spacing": [2, { |
||||
|
"before": true, |
||||
|
"after": true |
||||
|
}], //enforce spacing before and after keywords |
||||
|
"linebreak-style": 2, //disallow mixed "LF" and "CRLF" as linebreaks |
||||
|
"lines-around-comment": ["error", { |
||||
|
"beforeLineComment": true, |
||||
|
"allowBlockStart": true |
||||
|
} |
||||
|
], //enforce empty lines around comments |
||||
|
"max-depth": 1, //specify the maximum depth that blocks can be nested |
||||
|
"max-len": [1, 120], //specify the maximum length of a line in your program |
||||
|
"max-lines": [1, 500], //enforce a maximum file length |
||||
|
"max-nested-callbacks": 2, //specify the maximum depth callbacks can be nested |
||||
|
"max-params": [1, 5], //limits the number of parameters that can be used in the function declaration. |
||||
|
"max-statements": [1, 50], //specify the maximum number of statement allowed in a function |
||||
|
"max-statements-per-line": 1, //enforce a maximum number of statements allowed per line |
||||
|
"new-cap": 0, //require a capital letter for constructors |
||||
|
"new-parens": 2, //disallow the omission of parentheses when invoking a constructor with no arguments |
||||
|
"newline-after-var": 0, //require or disallow an empty newline after variable declarations |
||||
|
"newline-before-return": 2, //require newline before return statement |
||||
|
"newline-per-chained-call": 0, //enforce newline after each call when chaining the calls |
||||
|
"no-array-constructor": 2, //disallow use of the Array constructor |
||||
|
"no-bitwise": 0, //disallow use of bitwise operators |
||||
|
"no-continue": 0, //disallow use of the continue statement |
||||
|
"no-inline-comments": 0, //disallow comments inline after code |
||||
|
"no-lonely-if": 2, //disallow if as the only statement in an else block |
||||
|
"no-mixed-operators": 0, //disallow mixes of different operators |
||||
|
"no-mixed-spaces-and-tabs": 2, //disallow mixed spaces and tabs for indentation |
||||
|
"no-multiple-empty-lines": 2, //disallow multiple empty lines |
||||
|
"no-negated-condition": 0, //disallow negated conditions |
||||
|
"no-nested-ternary": 2, //disallow nested ternary expressions |
||||
|
"no-new-object": 2, //disallow the use of the Object constructor |
||||
|
"no-plusplus": 0, //disallow use of unary operators, ++ and -- |
||||
|
"no-restricted-syntax": 0, //disallow use of certain syntax in code |
||||
|
"no-spaced-func": 2, //disallow space between function identifier and application |
||||
|
"no-ternary": 0, //disallow the use of ternary operators |
||||
|
"no-trailing-spaces": 2, //disallow trailing whitespace at the end of lines |
||||
|
"no-underscore-dangle": 0, //disallow dangling underscores in identifiers |
||||
|
"no-unneeded-ternary": 2, //disallow the use of ternary operators when a simpler alternative exists |
||||
|
"no-whitespace-before-property": 2, //disallow whitespace before properties |
||||
|
"object-curly-newline": 2, //enforce consistent line breaks inside braces |
||||
|
"object-curly-spacing": 2, //require or disallow padding inside curly braces |
||||
|
"object-property-newline": [2, { //enforce placing object properties on either one line or all on separate lines |
||||
|
"allowAllPropertiesOnSameLine": true |
||||
|
} |
||||
|
], |
||||
|
"one-var": [2, "never"], //require or disallow one variable declaration per function |
||||
|
"one-var-declaration-per-line": 2, //require or disallow an newline around variable declarations |
||||
|
"operator-assignment": 0, //require assignment operator shorthand where possible or prohibit it entirely |
||||
|
"operator-linebreak": [1, "before"], //enforce operators to be placed before or after line breaks |
||||
|
"padded-blocks": 0, //enforce padding within blocks |
||||
|
"quote-props": [2, "as-needed"], //require quotes around object literal property names |
||||
|
"quotes": [2, "single"], //specify whether backticks, double or single quotes should be used |
||||
|
"require-jsdoc": 0, //Require JSDoc comment |
||||
|
"semi-spacing": 2, //enforce spacing before and after semicolons |
||||
|
"sort-imports": 0, //sort import declarations within module |
||||
|
"semi": 2, //require or disallow use of semicolons instead of ASI |
||||
|
"sort-vars": 0, //sort variables within the same declaration block |
||||
|
"space-before-blocks": 2, //require or disallow a space before blocks |
||||
|
"space-before-function-paren": [2, "never"], //require or disallow a space before function opening parenthesis |
||||
|
"space-in-parens": 2, //require or disallow spaces inside parentheses |
||||
|
"space-infix-ops": 2, //require spaces around operators |
||||
|
"space-unary-ops": 2, //require or disallow spaces before/after unary operators |
||||
|
"spaced-comment": [2, "always", { "exceptions": ["*"] }], //require or disallow a space immediately following the // or /* in a comment |
||||
|
"unicode-bom": 0, //require or disallow the Unicode BOM |
||||
|
"wrap-regex": 0, //require regex literals to be wrapped in parentheses |
||||
|
|
||||
|
//ECMAScript 6 |
||||
|
"arrow-body-style": [2, "as-needed"], //require braces in arrow function body |
||||
|
"arrow-parens": [2, "as-needed"], //require parens in arrow function arguments |
||||
|
"arrow-spacing": 2, //require space before/after arrow function"s arrow |
||||
|
"constructor-super": 2, //verify calls of super() in constructors |
||||
|
"generator-star-spacing": 0, //enforce spacing around the * in generator functions |
||||
|
"no-class-assign": 2, //disallow modifying variables of class declarations |
||||
|
"no-confusing-arrow": 2, //disallow arrow functions where they could be confused with comparisons |
||||
|
"no-const-assign": 2, //disallow modifying variables that are declared using const |
||||
|
"no-dupe-class-members": 2, //disallow duplicate name in class members |
||||
|
"no-duplicate-imports": 2, //disallow duplicate module imports |
||||
|
"no-new-symbol": 2, //disallow use of the new operator with the Symbol object |
||||
|
"no-this-before-super": 2, //disallow use of this/super before calling super() in constructors. |
||||
|
"no-useless-computed-key": 2, //disallow unnecessary computed property keys in object literals |
||||
|
"no-useless-constructor": 2, //disallow unnecessary constructor |
||||
|
"no-useless-rename": 2, //disallow renaming import, export, and destructured assignments to the same name |
||||
|
"no-var": 0, //require let or const instead of var |
||||
|
"object-shorthand": 1, //require method and property shorthand syntax for object literals |
||||
|
"prefer-arrow-callback": 1, //suggest using arrow functions as callbacks |
||||
|
"prefer-const": 1, //suggest using const declaration for variables that are never modified after declared |
||||
|
"prefer-rest-params": 1, //suggest using the rest parameters instead of arguments |
||||
|
"prefer-spread": 1, //suggest using the spread operator instead of .apply(). |
||||
|
"prefer-template": 1, //suggest using template literals instead of strings concatenation |
||||
|
"require-yield": 0, //disallow generator functions that do not have yield |
||||
|
"rest-spread-spacing": ["error", "never"], //enforce spacing between rest and spread operators and their expressions |
||||
|
"template-curly-spacing": 2, //enforce spacing around embedded expressions of template strings |
||||
|
"yield-star-spacing": [2, "after"] //enforce spacing around the * in yield* expressions |
||||
|
} |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
--- |
||||
|
name: Feature request |
||||
|
about: Suggest an idea for this project |
||||
|
title: '' |
||||
|
labels: '' |
||||
|
assignees: '' |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Issue** |
||||
|
A clear and concise description of what the problem is. |
||||
|
|
||||
|
**Proposed Solution** |
||||
|
A clear and concise description of what you want to happen. |
||||
|
|
||||
|
**Dependencies** |
||||
|
1. Links to PRs |
||||
|
2. Links to Issues |
||||
|
3. Description of other dependencies |
||||
|
|
||||
|
**Additional context** |
||||
|
Add any other context or screenshots about the feature request here. |
@ -0,0 +1,54 @@ |
|||||
|
name: Automatically Build image on tag |
||||
|
env: |
||||
|
DOCKER_CLI_EXPERIMENTAL: enabled |
||||
|
TAG_FMT: '^refs/tags/(((.?[0-9]+){3,4}))$' |
||||
|
|
||||
|
on: |
||||
|
push: |
||||
|
tags: [ '*' ] |
||||
|
|
||||
|
jobs: |
||||
|
build: |
||||
|
runs-on: ubuntu-18.04 |
||||
|
name: Build / Push Umbrel Manager on version tag |
||||
|
steps: |
||||
|
- name: Setup Environment |
||||
|
run: | |
||||
|
if ! echo "$GITHUB_REF" | grep -qE "$TAG_FMT"; then |
||||
|
echo "ERR: TAG must be in format: vX.Y.Z or X.Y.Z or vW.X.Y.Z or W.X.Y.Z" |
||||
|
exit 1 |
||||
|
fi |
||||
|
VERSION="$(echo "$GITHUB_REF" | sed -E "s|$TAG_FMT|\2|")" |
||||
|
|
||||
|
TAG="$(echo "$GITHUB_REF" | sed -E "s|$TAG_FMT|\1|")" |
||||
|
echo ::set-env name=TAG::"$TAG" |
||||
|
|
||||
|
- name: Show set environment variables |
||||
|
run: | |
||||
|
printf " TAG: %s\n" "$TAG" |
||||
|
- name: Login to Docker for building |
||||
|
run: echo "${{ secrets.DOCKER_PASSWORD }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin |
||||
|
- name: Checkout project |
||||
|
uses: actions/checkout@v2 |
||||
|
- name: Setup Docker buildx Action |
||||
|
uses: crazy-max/ghaction-docker-buildx@v1 |
||||
|
id: buildx |
||||
|
with: |
||||
|
buildx-version: latest |
||||
|
qemu-version: latest |
||||
|
- name: Available platforms |
||||
|
run: echo ${{ steps.buildx.outputs.platforms }} |
||||
|
- name: Run Docker build X (against tag) |
||||
|
run: | |
||||
|
docker buildx build \ |
||||
|
--platform linux/amd64,linux/386,linux/arm/v7,linux/arm64 \ |
||||
|
-t getumbrel/manager:$TAG \ |
||||
|
--output "type=registry" \ |
||||
|
. |
||||
|
- name: Run Docker build X (against latest) |
||||
|
run: | |
||||
|
docker buildx build \ |
||||
|
--platform linux/amd64,linux/386,linux/arm/v7,linux/arm64 \ |
||||
|
-t getumbrel/manager:latest \ |
||||
|
--output "type=registry" \ |
||||
|
. |
@ -0,0 +1,14 @@ |
|||||
|
node_modules/ |
||||
|
npm-debug.log |
||||
|
/.idea/ |
||||
|
/tls.cert |
||||
|
*.log |
||||
|
*.env |
||||
|
logs/ |
||||
|
package-lock.json |
||||
|
yarn.lock |
||||
|
*.bak |
||||
|
lb_settings.json |
||||
|
.nyc_output |
||||
|
coverage |
||||
|
db/ |
@ -0,0 +1,28 @@ |
|||||
|
# specify the node base image with your desired version node:<version> |
||||
|
FROM node:8-slim |
||||
|
|
||||
|
# install tools |
||||
|
RUN apt-get update --no-install-recommends \ |
||||
|
&& apt-get install -y --no-install-recommends vim \ |
||||
|
&& apt-get install -y --no-install-recommends rsync \ |
||||
|
&& rm -rf /var/lib/apt/lists/* |
||||
|
|
||||
|
# Create app directory |
||||
|
WORKDIR /usr/src/app |
||||
|
|
||||
|
# Install app dependencies |
||||
|
# A wildcard is used to ensure both package.json AND package-lock.json are copied |
||||
|
# where available (npm@5+) |
||||
|
COPY package*.json ./ |
||||
|
|
||||
|
RUN npm install |
||||
|
# If you are building your code for production |
||||
|
# RUN npm install --only=production |
||||
|
|
||||
|
# Bundle app source |
||||
|
COPY . . |
||||
|
|
||||
|
RUN mkdir -p /root/.lnd |
||||
|
|
||||
|
EXPOSE 3005 |
||||
|
CMD [ "npm", "start" ] |
@ -0,0 +1,21 @@ |
|||||
|
The MIT License |
||||
|
|
||||
|
Copyright (c) 2020 Umbrel. https://getumbrel.com/ |
||||
|
|
||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
|
of this software and associated documentation files (the "Software"), to deal |
||||
|
in the Software without restriction, including without limitation the rights |
||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
|
copies of the Software, and to permit persons to whom the Software is |
||||
|
furnished to do so, subject to the following conditions: |
||||
|
|
||||
|
The above copyright notice and this permission notice shall be included in |
||||
|
all copies or substantial portions of the Software. |
||||
|
|
||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN |
||||
|
THE SOFTWARE. |
@ -0,0 +1,3 @@ |
|||||
|
# Umbrel Manager API |
||||
|
|
||||
|
⚙️ Work in progress. README will be updated with some documentation very soon! |
@ -0,0 +1,51 @@ |
|||||
|
require('module-alias/register'); |
||||
|
require('module-alias').addPath('.'); |
||||
|
require('dotenv').config(); |
||||
|
|
||||
|
const express = require('express'); |
||||
|
const path = require('path'); |
||||
|
const morgan = require('morgan'); |
||||
|
const bodyParser = require('body-parser'); |
||||
|
const passport = require('passport'); |
||||
|
const cors = require('cors'); |
||||
|
|
||||
|
// Keep requestCorrelationId middleware as the first middleware. Otherwise we risk losing logs.
|
||||
|
const requestCorrelationMiddleware = require('middlewares/requestCorrelationId.js'); // eslint-disable-line id-length
|
||||
|
const camelCaseReqMiddleware = require('middlewares/camelCaseRequest.js').camelCaseRequest; |
||||
|
const onionOriginMiddleware = require('middlewares/onionOrigin.js'); |
||||
|
const corsOptions = require('middlewares/cors.js').corsOptions; |
||||
|
const errorHandleMiddleware = require('middlewares/errorHandling.js'); |
||||
|
require('middlewares/auth.js'); |
||||
|
|
||||
|
const logger = require('utils/logger.js'); |
||||
|
|
||||
|
const ping = require('routes/ping.js'); |
||||
|
const account = require('routes/v1/account.js'); |
||||
|
|
||||
|
const app = express(); |
||||
|
|
||||
|
// Handle Cors for Tor Browser 9.0.0 bug and options requests
|
||||
|
app.use(onionOriginMiddleware); |
||||
|
|
||||
|
// Handles Cors for normal requests
|
||||
|
app.use(cors(corsOptions)); |
||||
|
|
||||
|
app.use(bodyParser.json()); |
||||
|
app.use(bodyParser.urlencoded({ extended: true })); |
||||
|
app.use(express.static(path.join(__dirname, 'public'))); |
||||
|
app.use(passport.initialize()); |
||||
|
app.use(passport.session()); |
||||
|
|
||||
|
app.use(requestCorrelationMiddleware); |
||||
|
app.use(camelCaseReqMiddleware); |
||||
|
app.use(morgan(logger.morganConfiguration)); |
||||
|
|
||||
|
app.use('/ping', ping); |
||||
|
app.use('/v1/account', account); |
||||
|
|
||||
|
app.use(errorHandleMiddleware); |
||||
|
app.use((req, res) => { |
||||
|
res.status(404).json(); // eslint-disable-line no-magic-numbers
|
||||
|
}); |
||||
|
|
||||
|
module.exports = app; |
@ -0,0 +1,91 @@ |
|||||
|
#!/usr/bin/env node |
||||
|
|
||||
|
/** |
||||
|
* Module dependencies. |
||||
|
*/ |
||||
|
|
||||
|
var app = require('../app'); |
||||
|
var debug = require('debug')('nodejs-regular-webapp2:server'); |
||||
|
var http = require('http'); |
||||
|
|
||||
|
/** |
||||
|
* Get port from environment and store in Express. |
||||
|
*/ |
||||
|
|
||||
|
var port = normalizePort(process.env.PORT || '3005'); |
||||
|
app.set('port', port); |
||||
|
|
||||
|
/** |
||||
|
* Create HTTP server. |
||||
|
*/ |
||||
|
|
||||
|
var server = http.createServer(app); |
||||
|
|
||||
|
/** |
||||
|
* Listen on provided port, on all network interfaces. |
||||
|
*/ |
||||
|
|
||||
|
server.listen(port); |
||||
|
server.on('error', onError); |
||||
|
server.on('listening', onListening); |
||||
|
|
||||
|
/** |
||||
|
* Normalize a port into a number, string, or false. |
||||
|
*/ |
||||
|
|
||||
|
function normalizePort(val) { |
||||
|
var port = parseInt(val, 10); |
||||
|
|
||||
|
if (isNaN(port)) { |
||||
|
// named pipe |
||||
|
return val; |
||||
|
} |
||||
|
|
||||
|
if (port >= 0) { |
||||
|
// port number |
||||
|
return port; |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Event listener for HTTP server "error" event. |
||||
|
*/ |
||||
|
|
||||
|
function onError(error) { |
||||
|
if (error.syscall !== 'listen') { |
||||
|
throw error; |
||||
|
} |
||||
|
|
||||
|
var bind = typeof port === 'string' |
||||
|
? 'Pipe ' + port |
||||
|
: 'Port ' + port; |
||||
|
|
||||
|
// handle specific listen errors with friendly messages |
||||
|
switch (error.code) { |
||||
|
case 'EACCES': |
||||
|
console.error(bind + ' requires elevated privileges'); |
||||
|
process.exit(1); |
||||
|
break; |
||||
|
case 'EADDRINUSE': |
||||
|
console.error(bind + ' is already in use'); |
||||
|
process.exit(1); |
||||
|
break; |
||||
|
default: |
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Event listener for HTTP server "listening" event. |
||||
|
*/ |
||||
|
|
||||
|
function onListening() { |
||||
|
var addr = server.address(); |
||||
|
var bind = typeof addr === 'string' |
||||
|
? 'pipe ' + addr |
||||
|
: 'port ' + addr.port; |
||||
|
debug('Listening on ' + bind); |
||||
|
console.log('Listening on ' + bind); |
||||
|
} |
@ -0,0 +1,224 @@ |
|||||
|
const bcrypt = require('bcrypt'); |
||||
|
const iocane = require("iocane"); |
||||
|
const diskLogic = require('logic/disk.js'); |
||||
|
// const dockerComposeLogic = require('logic/docker-compose.js');
|
||||
|
// const lnapiService = require('services/lnapi.js');
|
||||
|
const bashService = require('services/bash.js'); |
||||
|
const NodeError = require('models/errors.js').NodeError; |
||||
|
const JWTHelper = require('utils/jwt.js'); |
||||
|
const constants = require('utils/const.js'); |
||||
|
const UUID = require('utils/UUID.js'); |
||||
|
|
||||
|
const saltRounds = 10; |
||||
|
const SYSTEM_USER = UUID.fetchBootUUID() || 'admin'; |
||||
|
|
||||
|
let devicePassword = ''; |
||||
|
let changePasswordStatus; |
||||
|
|
||||
|
resetChangePasswordStatus(); |
||||
|
|
||||
|
function resetChangePasswordStatus() { |
||||
|
changePasswordStatus = { percent: 0 }; |
||||
|
} |
||||
|
|
||||
|
async function sleepSeconds(seconds) { |
||||
|
return new Promise(resolve => { |
||||
|
setTimeout(resolve, seconds * constants.TIME.ONE_SECOND_IN_MILLIS); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Caches the password.
|
||||
|
function cachePassword(password) { |
||||
|
devicePassword = password; |
||||
|
} |
||||
|
|
||||
|
// Gets the cached the password.
|
||||
|
function getCachedPassword() { |
||||
|
return devicePassword; |
||||
|
} |
||||
|
|
||||
|
// Change the device and lnd password.
|
||||
|
async function changePassword(currentPassword, newPassword, jwt) { |
||||
|
|
||||
|
// restart lnd
|
||||
|
resetChangePasswordStatus(); |
||||
|
changePasswordStatus.percent = 1; // eslint-disable-line no-magic-numbers
|
||||
|
// await dockerComposeLogic.dockerComposeStop({ service: constants.SERVICES.LND });
|
||||
|
changePasswordStatus.percent = 40; // eslint-disable-line no-magic-numbers
|
||||
|
// await dockerComposeLogic.dockerComposeUpSingleService({ service: constants.SERVICES.LND });
|
||||
|
|
||||
|
let complete = false; |
||||
|
let attempt = 0; |
||||
|
const MAX_ATTEMPTS = 20; |
||||
|
|
||||
|
do { |
||||
|
try { |
||||
|
attempt++; |
||||
|
|
||||
|
// call lnapi to change password
|
||||
|
changePasswordStatus.percent = 60 + attempt; // eslint-disable-line no-magic-numbers
|
||||
|
// await lnapiService.changePassword(currentPassword, newPassword, jwt);
|
||||
|
|
||||
|
// make new password file
|
||||
|
const credentials = hashCredentials(SYSTEM_USER, newPassword); |
||||
|
|
||||
|
// replace user file
|
||||
|
await diskLogic.deleteUserFile(); |
||||
|
await diskLogic.writeUserFile({ password: credentials.password }); |
||||
|
|
||||
|
// update ssh password
|
||||
|
await hashAccountPassword(newPassword); |
||||
|
|
||||
|
complete = true; |
||||
|
|
||||
|
// cache the password for later use
|
||||
|
cachePassword(newPassword); |
||||
|
|
||||
|
changePasswordStatus.percent = 100; |
||||
|
} catch (error) { |
||||
|
|
||||
|
// wait for lnd to boot up
|
||||
|
if (error.response.status === constants.STATUS_CODES.BAD_GATEWAY) { |
||||
|
await sleepSeconds(1); |
||||
|
|
||||
|
// user supplied incorrect credentials
|
||||
|
} else if (error.response.status === constants.STATUS_CODES.FORBIDDEN) { |
||||
|
changePasswordStatus.forbidden = true; |
||||
|
|
||||
|
// unknown error occurred
|
||||
|
} else { |
||||
|
changePasswordStatus.error = true; |
||||
|
changePasswordStatus.percent = 100; |
||||
|
|
||||
|
throw error; |
||||
|
} |
||||
|
} |
||||
|
} while (!complete && attempt < MAX_ATTEMPTS && !changePasswordStatus.unauthorized && !changePasswordStatus.error); |
||||
|
|
||||
|
if (!complete && attempt === MAX_ATTEMPTS) { |
||||
|
changePasswordStatus.error = true; |
||||
|
changePasswordStatus.percent = 100; |
||||
|
|
||||
|
throw new Error('Unable to change password. Lnd would not restart properly.'); |
||||
|
} |
||||
|
|
||||
|
} |
||||
|
|
||||
|
function getChangePasswordStatus() { |
||||
|
return changePasswordStatus; |
||||
|
} |
||||
|
|
||||
|
// Returns an object with the hashed credentials inside.
|
||||
|
function hashCredentials(username, password) { |
||||
|
const hash = bcrypt.hashSync(password, saltRounds); |
||||
|
|
||||
|
return { password: hash, username, plainTextPassword: password }; |
||||
|
} |
||||
|
|
||||
|
// Returns true if the user is registered otherwise false.
|
||||
|
async function isRegistered() { |
||||
|
try { |
||||
|
await diskLogic.readUserFile(); |
||||
|
|
||||
|
return { registered: true }; |
||||
|
} catch (error) { |
||||
|
return { registered: false }; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Log the user into the device. Caches the password if login is successful. Then returns jwt.
|
||||
|
async function login(user) { |
||||
|
try { |
||||
|
const jwt = await JWTHelper.generateJWT(user.username); |
||||
|
|
||||
|
// Cache plain text password
|
||||
|
// cachePassword(user.plainTextPassword);
|
||||
|
cachePassword(user.password); |
||||
|
|
||||
|
return { jwt: jwt }; |
||||
|
|
||||
|
} catch (error) { |
||||
|
console.log(error); |
||||
|
throw new NodeError('Unable to generate JWT'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function seed(user) { |
||||
|
|
||||
|
//Decrypt mnemonic seed
|
||||
|
try { |
||||
|
const { seed } = await diskLogic.readUserFile(); |
||||
|
|
||||
|
const decryptedSeed = await iocane.createSession().decrypt(seed, user.plainTextPassword); |
||||
|
|
||||
|
return { seed: decryptedSeed.split(",") }; |
||||
|
|
||||
|
} catch (error) { |
||||
|
throw new NodeError('Unable to decrypt mnemonic seed'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Registers the the user to the device. Returns an error if a user already exists.
|
||||
|
async function register(user, seed) { |
||||
|
if ((await isRegistered()).registered) { |
||||
|
throw new NodeError('User already exists', 400); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
|
||||
|
//Encrypt mnemonic seed for storage
|
||||
|
let encryptedSeed; |
||||
|
try { |
||||
|
encryptedSeed = await iocane.createSession().encrypt(seed.join(), user.plainTextPassword); |
||||
|
} catch (error) { |
||||
|
throw new NodeError('Unable to encrypt mnemonic seed'); |
||||
|
} |
||||
|
|
||||
|
//save user
|
||||
|
try { |
||||
|
await diskLogic.writeUserFile({ name: user.name, password: user.password, seed: encryptedSeed }); |
||||
|
} catch (error) { |
||||
|
throw new NodeError('Unable to register user'); |
||||
|
} |
||||
|
|
||||
|
//generate JWt
|
||||
|
let jwt; |
||||
|
try { |
||||
|
jwt = await JWTHelper.generateJWT(user.username); |
||||
|
} catch (error) { |
||||
|
throw new NodeError('Unable to generate JWT'); |
||||
|
} |
||||
|
|
||||
|
//initialize lnd wallet
|
||||
|
try { |
||||
|
// await lnapiService.initializeWallet(user.plainTextPassword, seed, jwt);
|
||||
|
} catch (error) { |
||||
|
await diskLogic.deleteUserFile(); |
||||
|
throw new NodeError(error.response.data); |
||||
|
} |
||||
|
|
||||
|
//return token
|
||||
|
return { jwt: jwt }; |
||||
|
} |
||||
|
|
||||
|
// Generate and return a new jwt token.
|
||||
|
async function refresh(user) { |
||||
|
try { |
||||
|
const jwt = await JWTHelper.generateJWT(user.username); |
||||
|
|
||||
|
return { jwt: jwt }; |
||||
|
} catch (error) { |
||||
|
throw new NodeError('Unable to generate JWT'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
|
||||
|
module.exports = { |
||||
|
changePassword, |
||||
|
getCachedPassword, |
||||
|
getChangePasswordStatus, |
||||
|
hashCredentials, |
||||
|
isRegistered, |
||||
|
seed, |
||||
|
login, |
||||
|
register, |
||||
|
refresh, |
||||
|
}; |
@ -0,0 +1,175 @@ |
|||||
|
const constants = require('utils/const.js'); |
||||
|
const diskService = require('services/disk.js'); |
||||
|
|
||||
|
async function deleteUserFile() { |
||||
|
return await diskService.deleteFile(constants.USER_PASSWORD_FILE); |
||||
|
} |
||||
|
|
||||
|
async function deleteItemsInDir(directory) { |
||||
|
return await diskService.deleteItemsInDir(directory); |
||||
|
} |
||||
|
|
||||
|
async function deleteFoldersInDir(directory) { |
||||
|
await diskService.deleteFoldersInDir(directory); |
||||
|
} |
||||
|
|
||||
|
async function fileExists(path) { |
||||
|
return diskService.readJsonFile(path) |
||||
|
.then(() => Promise.resolve(true)) |
||||
|
.catch(() => Promise.resolve(false)); |
||||
|
} |
||||
|
|
||||
|
async function getBuildDetails(appsToLaunch) { |
||||
|
|
||||
|
const details = []; |
||||
|
|
||||
|
for (const applicationName of Object.keys(appsToLaunch)) { |
||||
|
const application = {}; |
||||
|
application.name = applicationName; |
||||
|
const path = constants.WORKING_DIRECTORY + '/' + application.name + '/' |
||||
|
+ appsToLaunch[application.name].version + '/'; |
||||
|
application.ymlPath = path + application.name + '.yml'; |
||||
|
application.digestsPath = path + 'digests.json'; |
||||
|
details.push(application); |
||||
|
} |
||||
|
|
||||
|
return details; |
||||
|
} |
||||
|
|
||||
|
async function listVersionsForApp(app) { |
||||
|
return await diskService.listDirsInDir(constants.WORKING_DIRECTORY + '/' + app); |
||||
|
} |
||||
|
|
||||
|
async function moveFoldersToDir(fromDir, toDir) { |
||||
|
await diskService.moveFoldersToDir(fromDir, toDir); |
||||
|
} |
||||
|
|
||||
|
async function writeAppVersionFile(application, json) { |
||||
|
return diskService.writeJsonFile(constants.WORKING_DIRECTORY + '/' + application, json); |
||||
|
} |
||||
|
|
||||
|
function readUserFile() { |
||||
|
return diskService.readJsonFile(constants.USER_PASSWORD_FILE); |
||||
|
} |
||||
|
|
||||
|
function readSettingsFile() { |
||||
|
return diskService.readJsonFile(constants.SETTINGS_FILE); |
||||
|
} |
||||
|
|
||||
|
function writeSettingsFile(json) { |
||||
|
return diskService.writeJsonFile(constants.SETTINGS_FILE, json); |
||||
|
} |
||||
|
|
||||
|
async function writeUserFile(json) { |
||||
|
return diskService.writeJsonFile(constants.USER_PASSWORD_FILE, json); |
||||
|
} |
||||
|
|
||||
|
function settingsFileExists() { |
||||
|
return diskService.readJsonFile(constants.SETTINGS_FILE) |
||||
|
.then(() => Promise.resolve(true)) |
||||
|
.catch(() => Promise.resolve(false)); |
||||
|
} |
||||
|
|
||||
|
function hiddenServiceFileExists() { |
||||
|
return readHiddenService() |
||||
|
.then(() => Promise.resolve(true)) |
||||
|
.catch(() => Promise.resolve(false)); |
||||
|
} |
||||
|
|
||||
|
async function readAppVersionFile(application) { |
||||
|
return diskService.readJsonFile(constants.WORKING_DIRECTORY + '/' + application); |
||||
|
} |
||||
|
|
||||
|
function readHiddenService() { |
||||
|
return diskService.readFile(constants.CASA_NODE_HIDDEN_SERVICE_FILE); |
||||
|
} |
||||
|
|
||||
|
function readJWTPrivateKeyFile() { |
||||
|
return diskService.readFile(constants.JWT_PRIVATE_KEY_FILE); |
||||
|
} |
||||
|
|
||||
|
function readJWTPublicKeyFile() { |
||||
|
return diskService.readFile(constants.JWT_PUBLIC_KEY_FILE); |
||||
|
} |
||||
|
|
||||
|
function writeJWTPrivateKeyFile(data) { |
||||
|
return diskService.writeKeyFile(constants.JWT_PRIVATE_KEY_FILE, data); |
||||
|
} |
||||
|
|
||||
|
function writeJWTPublicKeyFile(data) { |
||||
|
return diskService.writeKeyFile(constants.JWT_PUBLIC_KEY_FILE, data); |
||||
|
} |
||||
|
|
||||
|
// Send a signal to shutdown the Casa Node.
|
||||
|
async function shutdown() { |
||||
|
await diskService.writeFile(constants.SHUTDOWN_SIGNAL_FILE, 'true'); |
||||
|
} |
||||
|
|
||||
|
// Send a signal to relaunch the manager.
|
||||
|
async function relaunch() { |
||||
|
await diskService.writeFile(constants.RELAUNCH_SIGNAL_FILE, 'true'); |
||||
|
} |
||||
|
|
||||
|
// Read the contends of a file.
|
||||
|
async function readUtf8File(path) { |
||||
|
return await diskService.readUtf8File(path); |
||||
|
} |
||||
|
|
||||
|
// Read the contents of a file and return a json object.
|
||||
|
async function readJsonFile(path) { |
||||
|
return await diskService.readJsonFile(path); |
||||
|
} |
||||
|
|
||||
|
// Send a signal to perform a migration.
|
||||
|
async function migration() { |
||||
|
await diskService.writeFile(constants.MIGRATION_SIGNAL_FILE, 'true'); |
||||
|
} |
||||
|
|
||||
|
function readMigrationStatusFile() { |
||||
|
return diskService.readJsonFile(constants.MIGRATION_STATUS_FILE); |
||||
|
} |
||||
|
|
||||
|
async function writeMigrationStatusFile(json) { |
||||
|
return diskService.writeJsonFile(constants.MIGRATION_STATUS_FILE, json); |
||||
|
} |
||||
|
|
||||
|
// Send a signal to enable/disable SSH.
|
||||
|
async function enableSsh(state) { |
||||
|
await diskService.writeFile(constants.SSH_SIGNAL_FILE, state); |
||||
|
} |
||||
|
|
||||
|
function readSshSignalFile() { |
||||
|
return diskService.readFile(constants.SSH_SIGNAL_FILE); |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
deleteItemsInDir, |
||||
|
deleteUserFile, |
||||
|
deleteFoldersInDir, |
||||
|
moveFoldersToDir, |
||||
|
fileExists, |
||||
|
getBuildDetails, |
||||
|
listVersionsForApp, |
||||
|
readSettingsFile, |
||||
|
readUserFile, |
||||
|
writeAppVersionFile, |
||||
|
writeSettingsFile, |
||||
|
writeUserFile, |
||||
|
settingsFileExists, |
||||
|
hiddenServiceFileExists, |
||||
|
readAppVersionFile, |
||||
|
readHiddenService, |
||||
|
readJWTPrivateKeyFile, |
||||
|
readJWTPublicKeyFile, |
||||
|
writeJWTPrivateKeyFile, |
||||
|
writeJWTPublicKeyFile, |
||||
|
shutdown, |
||||
|
relaunch, |
||||
|
readUtf8File, |
||||
|
readJsonFile, |
||||
|
writeMigrationStatusFile, |
||||
|
readMigrationStatusFile, |
||||
|
migration, |
||||
|
enableSsh, |
||||
|
readSshSignalFile |
||||
|
}; |
@ -0,0 +1,166 @@ |
|||||
|
const passport = require('passport'); |
||||
|
const passportJWT = require('passport-jwt'); |
||||
|
const passportHTTP = require('passport-http'); |
||||
|
const bcrypt = require('bcrypt'); |
||||
|
const diskLogic = require('logic/disk.js'); |
||||
|
const authLogic = require('logic/auth.js'); |
||||
|
const NodeError = require('models/errors.js').NodeError; |
||||
|
const UUID = require('utils/UUID.js'); |
||||
|
const rsa = require('node-rsa'); |
||||
|
|
||||
|
const JwtStrategy = passportJWT.Strategy; |
||||
|
const BasicStrategy = passportHTTP.BasicStrategy; |
||||
|
const ExtractJwt = passportJWT.ExtractJwt; |
||||
|
|
||||
|
const JWT_AUTH = 'jwt'; |
||||
|
const REGISTRATION_AUTH = 'register'; |
||||
|
const BASIC_AUTH = 'basic'; |
||||
|
|
||||
|
const SYSTEM_USER = UUID.fetchBootUUID() || 'admin'; |
||||
|
|
||||
|
async function generateJWTKeys() { |
||||
|
const key = new rsa({ b: 512 }); // eslint-disable-line id-length
|
||||
|
|
||||
|
const privateKey = key.exportKey('private'); |
||||
|
const publicKey = key.exportKey('public'); |
||||
|
|
||||
|
await diskLogic.writeJWTPrivateKeyFile(privateKey); |
||||
|
await diskLogic.writeJWTPublicKeyFile(publicKey); |
||||
|
} |
||||
|
|
||||
|
async function createJwtOptions() { |
||||
|
await generateJWTKeys(); |
||||
|
const pubKey = await diskLogic.readJWTPublicKeyFile(); |
||||
|
|
||||
|
return { |
||||
|
jwtFromRequest: ExtractJwt.fromAuthHeaderWithScheme('jwt'), |
||||
|
secretOrKey: pubKey, |
||||
|
algorithm: 'RS256' |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
passport.serializeUser(function (user, done) { |
||||
|
return done(null, SYSTEM_USER); |
||||
|
}); |
||||
|
|
||||
|
passport.use(BASIC_AUTH, new BasicStrategy(function (username, password, next) { |
||||
|
const user = { |
||||
|
username: SYSTEM_USER, |
||||
|
password, |
||||
|
plainTextPassword: password |
||||
|
}; |
||||
|
return next(null, user); |
||||
|
})); |
||||
|
|
||||
|
createJwtOptions().then(function (data) { |
||||
|
const jwtOptions = data; |
||||
|
|
||||
|
passport.use(JWT_AUTH, new JwtStrategy(jwtOptions, function (jwtPayload, done) { |
||||
|
return done(null, { username: SYSTEM_USER }); |
||||
|
})); |
||||
|
}); |
||||
|
|
||||
|
passport.use(REGISTRATION_AUTH, new BasicStrategy(function (username, password, next) { |
||||
|
const credentials = authLogic.hashCredentials(SYSTEM_USER, password); |
||||
|
|
||||
|
return next(null, credentials); |
||||
|
})); |
||||
|
|
||||
|
// Override the authorization header with password that is in the body of the request if basic auth was not supplied.
|
||||
|
function convertReqBodyToBasicAuth(req, res, next) { |
||||
|
if (req.body.password && !req.headers.authorization) { |
||||
|
req.headers.authorization = 'Basic ' + Buffer.from(SYSTEM_USER + ':' + req.body.password).toString('base64'); |
||||
|
} |
||||
|
|
||||
|
next(); |
||||
|
} |
||||
|
|
||||
|
function basic(req, res, next) { |
||||
|
passport.authenticate(BASIC_AUTH, { session: false }, function (error, user) { |
||||
|
|
||||
|
function handleCompare(equal) { |
||||
|
if (!equal) { |
||||
|
return next(new NodeError('Incorrect password', 401)); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
req.logIn(user, function (err) { |
||||
|
if (err) { |
||||
|
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
|
||||
|
return next(null, user); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
if (error || user === false) { |
||||
|
return next(new NodeError('Invalid state', 401)); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
|
||||
|
diskLogic.readUserFile() |
||||
|
.then(userData => { |
||||
|
const storedPassword = userData.password; |
||||
|
|
||||
|
bcrypt.compare(user.password, storedPassword) |
||||
|
.then(handleCompare) |
||||
|
.catch(next); |
||||
|
}) |
||||
|
.catch(() => next(new NodeError('No user registered', 401))); // eslint-disable-line no-magic-numbers
|
||||
|
})(req, res, next); |
||||
|
} |
||||
|
|
||||
|
function jwt(req, res, next) { |
||||
|
passport.authenticate(JWT_AUTH, { session: false }, function (error, user) { |
||||
|
if (error || user === false) { |
||||
|
return next(new NodeError('Invalid JWT', 401)); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
req.logIn(user, function (err) { |
||||
|
if (err) { |
||||
|
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
|
||||
|
return next(null, user); |
||||
|
}); |
||||
|
})(req, res, next); |
||||
|
} |
||||
|
|
||||
|
async function accountJWTProtected(req, res, next) { |
||||
|
const isRegistered = await authLogic.isRegistered(); |
||||
|
if (isRegistered.registered) { |
||||
|
passport.authenticate(JWT_AUTH, { session: false }, function (error, user) { |
||||
|
if (error || user === false) { |
||||
|
return next(new NodeError('Invalid JWT', 401)); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
req.logIn(user, function (err) { |
||||
|
if (err) { |
||||
|
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
|
||||
|
return next(null, user); |
||||
|
}); |
||||
|
})(req, res, next); |
||||
|
} else { |
||||
|
return next(null, 'not-registered'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function register(req, res, next) { |
||||
|
passport.authenticate(REGISTRATION_AUTH, { session: false }, function (error, user) { |
||||
|
if (error || user === false) { |
||||
|
return next(new NodeError('Invalid state', 401)); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
req.logIn(user, function (err) { |
||||
|
if (err) { |
||||
|
return next(new NodeError('Unable to authenticate', 401)); // eslint-disable-line no-magic-numbers
|
||||
|
} |
||||
|
|
||||
|
return next(null, user); |
||||
|
}); |
||||
|
})(req, res, next); |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
basic, |
||||
|
convertReqBodyToBasicAuth, |
||||
|
jwt, |
||||
|
register, |
||||
|
accountJWTProtected, |
||||
|
}; |
@ -0,0 +1,12 @@ |
|||||
|
const camelizeKeys = require('camelize-keys'); |
||||
|
|
||||
|
function camelCaseRequest(req, res, next) { |
||||
|
if (req && req.body) { |
||||
|
req.body = camelizeKeys(req.body, '_'); |
||||
|
} |
||||
|
next(); |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
camelCaseRequest, |
||||
|
}; |
@ -0,0 +1,25 @@ |
|||||
|
const corsOptions = { |
||||
|
origin: (origin, callback) => { |
||||
|
const whitelist = [ |
||||
|
'http://localhost:3000', |
||||
|
'http://localhost:8080', |
||||
|
'http://umbrel.local', |
||||
|
process.env.DEVICE_HOST, |
||||
|
]; |
||||
|
|
||||
|
// Whitelist hidden service if exists.
|
||||
|
if (process.env.UMBREL_HIDDEN_SERVICE) { |
||||
|
whitelist.push(process.env.UMBREL_HIDDEN_SERVICE); |
||||
|
} |
||||
|
|
||||
|
if (whitelist.indexOf(origin) !== -1 || !origin) { |
||||
|
return callback(null, true); |
||||
|
} else { |
||||
|
return callback(new Error('Not allowed by CORS')); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
module.exports = { |
||||
|
corsOptions, |
||||
|
}; |
@ -0,0 +1,16 @@ |
|||||
|
/* eslint-disable no-unused-vars, no-magic-numbers */ |
||||
|
const logger = require('utils/logger.js'); |
||||
|
const LndError = require('models/errors.js').LndError; |
||||
|
|
||||
|
function handleError(error, req, res, next) { |
||||
|
|
||||
|
var statusCode = error.statusCode || 500; |
||||
|
var route = req.url || ''; |
||||
|
var message = error.message || ''; |
||||
|
|
||||
|
logger.error(message, route, error.stack); |
||||
|
|
||||
|
res.status(statusCode).json(message); |
||||
|
} |
||||
|
|
||||
|
module.exports = handleError; |
@ -0,0 +1,30 @@ |
|||||
|
/* |
||||
|
Tor 9.0.0 does not automatically include the origin header. This causes cross origin errors. This middleware handles |
||||
|
this bug until Tor releases a bugfix. |
||||
|
|
||||
|
https://trac.torproject.org/projects/tor/ticket/32255
|
||||
|
*/ |
||||
|
function onionOrigin(req, res, next) { |
||||
|
|
||||
|
// Get just the onion address.
|
||||
|
let hiddenService; |
||||
|
if (process.env.UMBREL_HIDDEN_SERVICE |
||||
|
&& process.env.UMBREL_HIDDEN_SERVICE.startsWith('http://')) { |
||||
|
|
||||
|
hiddenService = process.env.UMBREL_HIDDEN_SERVICE.substring(7, // eslint-disable-line no-magic-numbers
|
||||
|
process.env.UMBREL_HIDDEN_SERVICE.length); |
||||
|
} |
||||
|
|
||||
|
// If a hidden service is known and the request has the Tor Browser 9.0.0 bug.
|
||||
|
if (hiddenService |
||||
|
&& req.headers.host.includes(hiddenService) |
||||
|
&& !req.headers.origin) { |
||||
|
|
||||
|
// Manually add hidden service (with http://) to the response header.
|
||||
|
res.setHeader('Access-Control-Allow-Origin', process.env.UMBREL_HIDDEN_SERVICE); |
||||
|
} |
||||
|
|
||||
|
next(); |
||||
|
} |
||||
|
|
||||
|
module.exports = onionOrigin; |
@ -0,0 +1,15 @@ |
|||||
|
const UUID = require('utils/UUID.js'); |
||||
|
const constants = require('utils/const.js'); |
||||
|
const createNamespace = require('continuation-local-storage').createNamespace; |
||||
|
const apiRequest = createNamespace(constants.REQUEST_CORRELATION_NAMESPACE_KEY); |
||||
|
|
||||
|
function addCorrelationId(req, res, next) { |
||||
|
apiRequest.bindEmitter(req); |
||||
|
apiRequest.bindEmitter(res); |
||||
|
apiRequest.run(function () { |
||||
|
apiRequest.set(constants.REQUEST_CORRELATION_ID_KEY, UUID.create()); |
||||
|
next(); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
module.exports = addCorrelationId; |
@ -0,0 +1,22 @@ |
|||||
|
/* eslint-disable no-magic-numbers */ |
||||
|
function NodeError(message, statusCode) { |
||||
|
Error.captureStackTrace(this, this.constructor); |
||||
|
this.name = this.constructor.name; |
||||
|
this.message = message; |
||||
|
this.statusCode = statusCode; |
||||
|
} |
||||
|
require('util').inherits(NodeError, Error); |
||||
|
|
||||
|
function ValidationError(message, statusCode) { |
||||
|
Error.captureStackTrace(this, this.constructor); |
||||
|
this.name = this.constructor.name; |
||||
|
this.message = message; |
||||
|
this.statusCode = statusCode || 400; |
||||
|
} |
||||
|
require('util').inherits(ValidationError, Error); |
||||
|
|
||||
|
module.exports = { |
||||
|
NodeError, |
||||
|
ValidationError |
||||
|
}; |
||||
|
|
@ -0,0 +1,62 @@ |
|||||
|
{ |
||||
|
"name": "umbrel-manager", |
||||
|
"version": "0.0.0", |
||||
|
"description": "Manager for Umbrel Node", |
||||
|
"author": "Umbrel", |
||||
|
"scripts": { |
||||
|
"lint": "eslint", |
||||
|
"start": "nodemon --exec node ./bin/www", |
||||
|
"test": "mocha --file test.setup 'test/**/*.js'", |
||||
|
"coverage": "nyc --all mocha --file test.setup 'test/**/*.js'", |
||||
|
"postcoverage": "codecov" |
||||
|
}, |
||||
|
"dependencies": { |
||||
|
"bcrypt": "^4.0.1", |
||||
|
"big.js": "^5.2.2", |
||||
|
"body-parser": "^1.18.2", |
||||
|
"camelize-keys": "^1.0.0", |
||||
|
"continuation-local-storage": "^3.2.1", |
||||
|
"cors": "^2.8.5", |
||||
|
"debug": "^2.6.1", |
||||
|
"dotenv": "^8.2.0", |
||||
|
"express": "^4.16.3", |
||||
|
"fs-extra": "^9.0.0", |
||||
|
"iocane": "^4.0.0", |
||||
|
"module-alias": "^2.1.0", |
||||
|
"morgan": "^1.9.0", |
||||
|
"node-rsa": "^1.0.8", |
||||
|
"npm": "^6.13.4", |
||||
|
"passport": "^0.4.0", |
||||
|
"passport-http": "^0.3.0", |
||||
|
"passport-jwt": "^4.0.0", |
||||
|
"request-promise": "^4.2.2", |
||||
|
"uuid": "^3.3.2", |
||||
|
"validator": "^9.2.0", |
||||
|
"winston": "^3.0.0-rc5", |
||||
|
"winston-daily-rotate-file": "^3.1.3" |
||||
|
}, |
||||
|
"devDependencies": { |
||||
|
"babel-eslint": "^8.2.6", |
||||
|
"chai": "^4.1.2", |
||||
|
"chai-http": "^4.2.0", |
||||
|
"codecov": "^3.6.5", |
||||
|
"eslint": "^5.3.0", |
||||
|
"mocha": "^5.2.0", |
||||
|
"nodemon": "^2.0.4", |
||||
|
"nyc": "13.0.1", |
||||
|
"proxyquire": "^2.0.1", |
||||
|
"sinon": "^6.1.4" |
||||
|
}, |
||||
|
"nyc": { |
||||
|
"exclude": [ |
||||
|
"test", |
||||
|
"test.setup.js" |
||||
|
], |
||||
|
"sourceMap": false, |
||||
|
"reporter": [ |
||||
|
"lcov", |
||||
|
"text-summary" |
||||
|
], |
||||
|
"cache": "false" |
||||
|
} |
||||
|
} |
File diff suppressed because it is too large
@ -0,0 +1,9 @@ |
|||||
|
const express = require('express'); |
||||
|
const pjson = require('../package.json'); |
||||
|
const router = express.Router(); |
||||
|
|
||||
|
router.get('/', function (req, res) { |
||||
|
res.json({ version: 'umbrel-manager-' + pjson.version }); |
||||
|
}); |
||||
|
|
||||
|
module.exports = router; |
@ -0,0 +1,116 @@ |
|||||
|
const express = require('express'); |
||||
|
const router = express.Router(); |
||||
|
|
||||
|
// const applicationLogic = require('logic/application.js');
|
||||
|
const authLogic = require('logic/auth.js'); |
||||
|
|
||||
|
const auth = require('middlewares/auth.js'); |
||||
|
// const changePasswordAuthHandler = require('middlewares/changePasswordAuthHandler.js');
|
||||
|
|
||||
|
const constants = require('utils/const.js'); |
||||
|
const safeHandler = require('utils/safeHandler'); |
||||
|
const validator = require('utils/validator.js'); |
||||
|
|
||||
|
// const COMPLETE = 100;
|
||||
|
|
||||
|
// // Endpoint to change your lnd password. Wallet must exist and be unlocked. This endpoint is authorized with basic auth
|
||||
|
// // or the property password from the body.
|
||||
|
// router.post('/changePassword', auth.convertReqBodyToBasicAuth, auth.basic, changePasswordAuthHandler,
|
||||
|
// safeHandler(async(req, res, next) => {
|
||||
|
|
||||
|
// // Use password from the body by default. Basic auth has issues handling special characters.
|
||||
|
// const currentPassword = req.body.password;
|
||||
|
// const newPassword = req.body.newPassword;
|
||||
|
|
||||
|
// const jwt = await authLogic.refresh(req.user);
|
||||
|
|
||||
|
// try {
|
||||
|
// validator.isString(currentPassword);
|
||||
|
// validator.isMinPasswordLength(currentPassword);
|
||||
|
// validator.isString(newPassword);
|
||||
|
// validator.isMinPasswordLength(newPassword);
|
||||
|
// } catch (error) {
|
||||
|
// return next(error);
|
||||
|
// }
|
||||
|
|
||||
|
// const status = await authLogic.getChangePasswordStatus();
|
||||
|
|
||||
|
// // return a conflict if a change password process is already running
|
||||
|
// if (status.percent > 0 && status.percent !== COMPLETE) {
|
||||
|
// return res.status(constants.STATUS_CODES.CONFLICT).json();
|
||||
|
// }
|
||||
|
|
||||
|
// // start change password process in the background and immediately return
|
||||
|
// authLogic.changePassword(currentPassword, newPassword, jwt.jwt);
|
||||
|
|
||||
|
// return res.status(constants.STATUS_CODES.ACCEPTED).json();
|
||||
|
// }));
|
||||
|
|
||||
|
// // Returns the current status of the change password process.
|
||||
|
// router.get('/changePassword/status', auth.jwt, safeHandler(async(req, res) => {
|
||||
|
// const status = await authLogic.getChangePasswordStatus();
|
||||
|
|
||||
|
// return res.status(constants.STATUS_CODES.OK).json(status);
|
||||
|
// }));
|
||||
|
|
||||
|
// Registered does not need auth. This is because the user may not be registered at the time and thus won't always have
|
||||
|
// // an auth token.
|
||||
|
// router.get('/registered', safeHandler((req, res) =>
|
||||
|
// authLogic.isRegistered()
|
||||
|
// .then(registered => res.json(registered))
|
||||
|
// ));
|
||||
|
|
||||
|
// Endpoint to register a password with the device. Wallet must not exist. This endpoint is authorized with basic auth
|
||||
|
// or the property password from the body.
|
||||
|
router.post('/register', auth.convertReqBodyToBasicAuth, auth.register, safeHandler(async (req, res, next) => { |
||||
|
|
||||
|
|
||||
|
// return res.json({ seed: req.body.seed });
|
||||
|
|
||||
|
const seed = req.body.seed; |
||||
|
|
||||
|
if (seed.length !== 24) { // eslint-disable-line no-magic-numbers
|
||||
|
throw new Error('Invalid seed length'); |
||||
|
} |
||||
|
|
||||
|
try { |
||||
|
validator.isString(req.body.name); |
||||
|
validator.isString(req.user.plainTextPassword); |
||||
|
validator.isMinPasswordLength(req.user.plainTextPassword); |
||||
|
} catch (error) { |
||||
|
return next(error); |
||||
|
} |
||||
|
|
||||
|
const user = { |
||||
|
name: req.body.name, |
||||
|
password: req.user.password, |
||||
|
plainTextPassword: req.user.plainTextPassword |
||||
|
}; |
||||
|
|
||||
|
const jwt = await authLogic.register(user, seed); |
||||
|
|
||||
|
return res.json(jwt); |
||||
|
})); |
||||
|
|
||||
|
router.post('/login', auth.convertReqBodyToBasicAuth, auth.basic, safeHandler(async (req, res) => { |
||||
|
const jwt = await authLogic.login(req.user); |
||||
|
|
||||
|
return res.json(jwt); |
||||
|
} |
||||
|
|
||||
|
)); |
||||
|
|
||||
|
router.post('/seed', auth.convertReqBodyToBasicAuth, auth.basic, safeHandler(async (req, res) => { |
||||
|
const seed = await authLogic.seed(req.user); |
||||
|
|
||||
|
return res.json(seed); |
||||
|
} |
||||
|
|
||||
|
)); |
||||
|
|
||||
|
// router.post('/refresh', auth.jwt, safeHandler((req, res) =>
|
||||
|
// applicationLogic.refresh(req.user)
|
||||
|
// .then(jwt => res.json(jwt))
|
||||
|
// ));
|
||||
|
|
||||
|
module.exports = router; |
@ -0,0 +1,56 @@ |
|||||
|
const childProcess = require('child_process'); |
||||
|
|
||||
|
// Sets environment variables on container.
|
||||
|
// Env should not contain sensitive data, because environment variables are not secure.
|
||||
|
function extendProcessEnv(env) { |
||||
|
Object.keys(env).map(function (objectKey) { // eslint-disable-line array-callback-return
|
||||
|
process.env[objectKey] = env[objectKey]; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
// Executes docker-compose command with common options
|
||||
|
const exec = (command, args, opts) => new Promise((resolve, reject) => { |
||||
|
const options = opts || {}; |
||||
|
|
||||
|
const cwd = options.cwd || null; |
||||
|
|
||||
|
if (options.env) { |
||||
|
extendProcessEnv(options.env); |
||||
|
} |
||||
|
|
||||
|
const childProc = childProcess.spawn(command, args, { cwd }); |
||||
|
|
||||
|
childProc.on('error', err => { |
||||
|
reject(err); |
||||
|
}); |
||||
|
|
||||
|
const result = { |
||||
|
err: '', |
||||
|
out: '' |
||||
|
}; |
||||
|
|
||||
|
childProc.stdout.on('data', chunk => { |
||||
|
result.out += chunk.toString(); |
||||
|
}); |
||||
|
|
||||
|
childProc.stderr.on('data', chunk => { |
||||
|
result.err += chunk.toString(); |
||||
|
}); |
||||
|
|
||||
|
childProc.on('close', code => { |
||||
|
if (code === 0) { |
||||
|
resolve(result); |
||||
|
} else { |
||||
|
reject(result.err); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
if (options.log) { |
||||
|
childProc.stdout.pipe(process.stdout); |
||||
|
childProc.stderr.pipe(process.stderr); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
module.exports = { |
||||
|
exec, |
||||
|
}; |
@ -0,0 +1,186 @@ |
|||||
|
/** |
||||
|
* Generic disk functions. |
||||
|
*/ |
||||
|
|
||||
|
const logger = require('utils/logger'); |
||||
|
const fs = require('fs'); |
||||
|
const fse = require('fs-extra'); |
||||
|
const crypto = require('crypto'); |
||||
|
const uint32Bytes = 4; |
||||
|
|
||||
|
// Deletes a file from the filesystem
|
||||
|
function deleteFile(filePath) { |
||||
|
return new Promise((resolve, reject) => fs.unlink(filePath, (err, str) => { |
||||
|
if (err) { |
||||
|
reject(err); |
||||
|
} else { |
||||
|
resolve(str); |
||||
|
} |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
async function copyFolder(fromFile, toFile) { |
||||
|
return new Promise((resolve, reject) => fse.copy(fromFile, toFile, err => { |
||||
|
if (err) { |
||||
|
reject(err); |
||||
|
} else { |
||||
|
resolve(); |
||||
|
} |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
// Delete all items in a directory.
|
||||
|
async function deleteItemsInDir(path) { |
||||
|
|
||||
|
const contents = fs.readdirSync(path); |
||||
|
|
||||
|
for (const item of contents) { |
||||
|
|
||||
|
const curPath = path + '/' + item; |
||||
|
if (fs.statSync(curPath).isDirectory()) { |
||||
|
deleteFolderRecursive(curPath); |
||||
|
} else { |
||||
|
fs.unlinkSync(curPath); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function deleteFoldersInDir(path) { |
||||
|
|
||||
|
const contents = fs.readdirSync(path); |
||||
|
|
||||
|
for (const item of contents) { |
||||
|
if (fs.statSync(path + '/' + item).isDirectory()) { |
||||
|
deleteFolderRecursive(path + '/' + item); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function deleteFolderRecursive(path) { |
||||
|
if (fs.existsSync(path)) { |
||||
|
const contents = fs.readdirSync(path); |
||||
|
|
||||
|
for (const file of contents) { |
||||
|
const curPath = path + '/' + file; |
||||
|
if (fs.statSync(curPath).isDirectory()) { |
||||
|
deleteFolderRecursive(curPath); |
||||
|
} else { |
||||
|
fs.unlinkSync(curPath); |
||||
|
} |
||||
|
} |
||||
|
fs.rmdirSync(path); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
async function listDirsInDir(dir) { |
||||
|
const contents = fs.readdirSync(dir); |
||||
|
|
||||
|
const dirs = []; |
||||
|
|
||||
|
for (const item of contents) { |
||||
|
if (fs.statSync(dir + '/' + item).isDirectory()) { |
||||
|
dirs.push(item); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return dirs; |
||||
|
} |
||||
|
|
||||
|
async function moveFoldersToDir(fromDir, toDir) { |
||||
|
|
||||
|
const contents = fs.readdirSync(fromDir); |
||||
|
|
||||
|
for (const item of contents) { |
||||
|
if (item !== '.git' && fs.statSync(fromDir + '/' + item).isDirectory()) { |
||||
|
await copyFolder(fromDir + '/' + item, toDir + '/' + item); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Reads a file. Wraps fs.readFile into a native promise
|
||||
|
function readFile(filePath, encoding) { |
||||
|
return new Promise((resolve, reject) => fs.readFile(filePath, encoding, (err, str) => { |
||||
|
if (err) { |
||||
|
reject(err); |
||||
|
} else { |
||||
|
resolve(str); |
||||
|
} |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
// Reads a file as a utf8 string. Wraps fs.readFile into a native promise
|
||||
|
function readUtf8File(filePath) { |
||||
|
return readFile(filePath, 'utf8'); |
||||
|
} |
||||
|
|
||||
|
async function readJsonFile(filePath) { |
||||
|
return readUtf8File(filePath).then(JSON.parse); |
||||
|
} |
||||
|
|
||||
|
// Writes a string to a file. Wraps fs.writeFile into a native promise
|
||||
|
// This is _not_ concurrency safe, so don't export it without making it like writeJsonFile
|
||||
|
function writeFile(filePath, data, encoding) { |
||||
|
return new Promise((resolve, reject) => fs.writeFile(filePath, data, encoding, err => { |
||||
|
if (err) { |
||||
|
reject(err); |
||||
|
} else { |
||||
|
resolve(); |
||||
|
} |
||||
|
})); |
||||
|
} |
||||
|
|
||||
|
function writeJsonFile(filePath, obj) { |
||||
|
const tempFileName = `${filePath}.${crypto.randomBytes(uint32Bytes).readUInt32LE(0)}`; |
||||
|
|
||||
|
return writeFile(tempFileName, JSON.stringify(obj, null, 2), 'utf8') |
||||
|
.then(() => new Promise((resolve, reject) => fs.rename(tempFileName, filePath, err => { |
||||
|
if (err) { |
||||
|
reject(err); |
||||
|
} else { |
||||
|
resolve(); |
||||
|
} |
||||
|
}))) |
||||
|
.catch(err => { |
||||
|
if (err) { |
||||
|
fs.unlink(tempFileName, err => { |
||||
|
logger.warn('Error removing temporary file after error', 'disk', { err, tempFileName }); |
||||
|
}); |
||||
|
} |
||||
|
throw err; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function writeKeyFile(filePath, obj) { |
||||
|
const tempFileName = `${filePath}.${crypto.randomBytes(uint32Bytes).readUInt32LE(0)}`; |
||||
|
|
||||
|
return writeFile(tempFileName, obj, 'utf8') |
||||
|
.then(() => new Promise((resolve, reject) => fs.rename(tempFileName, filePath, err => { |
||||
|
if (err) { |
||||
|
reject(err); |
||||
|
} else { |
||||
|
resolve(); |
||||
|
} |
||||
|
}))) |
||||
|
.catch(err => { |
||||
|
if (err) { |
||||
|
fs.unlink(tempFileName, err => { |
||||
|
logger.warn('Error removing temporary file after error', 'disk', { err, tempFileName }); |
||||
|
}); |
||||
|
} |
||||
|
throw err; |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
deleteItemsInDir, |
||||
|
deleteFile, |
||||
|
deleteFoldersInDir, |
||||
|
listDirsInDir, |
||||
|
moveFoldersToDir, |
||||
|
readFile, |
||||
|
readUtf8File, |
||||
|
readJsonFile, |
||||
|
writeJsonFile, |
||||
|
writeKeyFile, |
||||
|
writeFile, |
||||
|
}; |
@ -0,0 +1,9 @@ |
|||||
|
{ |
||||
|
"extends": "../.eslintrc", |
||||
|
"env": { |
||||
|
"mocha": true |
||||
|
}, |
||||
|
"rules": { |
||||
|
"no-magic-numbers": "off" |
||||
|
} |
||||
|
} |
@ -0,0 +1,17 @@ |
|||||
|
/* globals requester */ |
||||
|
|
||||
|
describe('ping', () => { |
||||
|
it('should respond on /ping GET', done => { |
||||
|
requester |
||||
|
.get('/ping') |
||||
|
.end((err, res) => { |
||||
|
if (err) { |
||||
|
done(err); |
||||
|
} |
||||
|
res.should.have.status(200); |
||||
|
res.should.be.json; |
||||
|
res.body.should.have.property('version'); |
||||
|
done(); |
||||
|
}); |
||||
|
}); |
||||
|
}); |
@ -0,0 +1,22 @@ |
|||||
|
const chai = require('chai'); |
||||
|
const chaiHttp = require('chai-http'); |
||||
|
const server = require('../app.js'); |
||||
|
|
||||
|
chai.use(chaiHttp); |
||||
|
chai.should(); |
||||
|
|
||||
|
global.expect = chai.expect; |
||||
|
global.assert = chai.assert; |
||||
|
|
||||
|
before(() => { |
||||
|
global.requester = chai.request(server).keepOpen(); |
||||
|
}); |
||||
|
|
||||
|
global.reset = () => { |
||||
|
global.Lightning.reset(); |
||||
|
global.WalletUnlocker.reset(); |
||||
|
}; |
||||
|
|
||||
|
after(() => { |
||||
|
global.requester.close(); |
||||
|
}); |
@ -0,0 +1,24 @@ |
|||||
|
const bashService = require('services/bash.js'); |
||||
|
const uuidv4 = require('uuid/v4'); |
||||
|
|
||||
|
function fetchBootUUID() { |
||||
|
bashService.exec('cat', ['/proc/sys/kernel/random/boot_id'], {}) |
||||
|
.then(uuid => Promise.resolve(uuid)) |
||||
|
.catch(() => Promise.resolve()); |
||||
|
} |
||||
|
|
||||
|
function fetchSerial() { |
||||
|
const commandOptions = ['/proc/cpuinfo', |
||||
|
'|', 'egrep', '"Serial"', |
||||
|
'|', 'awk', '\'{print $3}\'']; |
||||
|
|
||||
|
bashService.exec('cat', commandOptions, {}) |
||||
|
.then(serial => Promise.resolve(serial)) |
||||
|
.catch(() => Promise.resolve()); |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
create: uuidv4, |
||||
|
fetchBootUUID, |
||||
|
fetchSerial, |
||||
|
}; |
@ -0,0 +1,13 @@ |
|||||
|
/* eslint-disable id-length */ |
||||
|
module.exports = { |
||||
|
REQUEST_CORRELATION_NAMESPACE_KEY: 'umbrel-manager-request', |
||||
|
USER_PASSWORD_FILE: process.env.USER_PASSWORD_FILE || '/home/umbrel/db/user.json', |
||||
|
JWT_PUBLIC_KEY_FILE: process.env.JWT_PUBLIC_KEY_FILE || '/home/umbrel/db/jwt/jwt.pem', |
||||
|
JWT_PRIVATE_KEY_FILE: process.env.JWT_PRIVATE_KEY_FILE || '/home/umbrel/db/jwt/jwt.key', |
||||
|
REQUEST_CORRELATION_ID_KEY: 'reqId', |
||||
|
STATUS_CODES: { |
||||
|
BAD_GATEWAY: 502, |
||||
|
FORBIDDEN: 403, |
||||
|
OK: 200, |
||||
|
}, |
||||
|
}; |
@ -0,0 +1,66 @@ |
|||||
|
// Source from https://github.com/richardschneider/bitcoin-convert/
|
||||
|
const Big = require('big.js'); |
||||
|
const BTC = 1; |
||||
|
const SAT = 0.00000001; |
||||
|
|
||||
|
const units = { |
||||
|
btc: new Big(BTC), |
||||
|
sat: new Big(SAT), |
||||
|
}; |
||||
|
|
||||
|
function convert(from, fromUnit, toUnit, representation) { |
||||
|
const fromFactor = units[fromUnit]; |
||||
|
if (fromFactor === undefined) { |
||||
|
throw new Error(`'${fromUnit}' is not a bitcoin unit`); |
||||
|
} |
||||
|
const toFactor = units[toUnit]; |
||||
|
if (toFactor === undefined) { |
||||
|
throw new Error(`'${toUnit}' is not a bitcoin unit`); |
||||
|
} |
||||
|
|
||||
|
if (Number.isNaN(from)) { |
||||
|
if (!representation || representation === 'Number') { |
||||
|
return from; |
||||
|
} else if (representation === 'Big') { |
||||
|
return new Big(from); // throws BigError
|
||||
|
} else if (representation === 'String') { |
||||
|
return from.toString(); |
||||
|
} |
||||
|
throw new Error(`'${representation}' is not a valid representation`); |
||||
|
} |
||||
|
|
||||
|
const result = new Big(from).times(fromFactor).div(toFactor); |
||||
|
|
||||
|
if (!representation || representation === 'Number') { |
||||
|
return Number(result); |
||||
|
} else if (representation === 'Big') { |
||||
|
return result; |
||||
|
} else if (representation === 'String') { |
||||
|
return result.toString(); |
||||
|
} |
||||
|
|
||||
|
throw new Error(`'${representation}' is not a valid representation`); |
||||
|
} |
||||
|
|
||||
|
convert.units = function () { |
||||
|
return Object.keys(units); |
||||
|
}; |
||||
|
|
||||
|
convert.addUnit = function addUnit(unit, factor) { |
||||
|
const bigFactor = new Big(factor); |
||||
|
const existing = units[unit]; |
||||
|
if (existing && !existing.eq(bigFactor)) { |
||||
|
throw new Error(`'${unit}' already exists with a different conversion factor`); |
||||
|
} |
||||
|
units[unit] = bigFactor; |
||||
|
}; |
||||
|
|
||||
|
const predefinedUnits = convert.units(); |
||||
|
convert.removeUnit = function removeUnit(unit) { |
||||
|
if (predefinedUnits.indexOf(unit) >= 0) { |
||||
|
throw new Error(`'${unit}' is predefined and cannot be removed`); |
||||
|
} |
||||
|
delete units[unit]; |
||||
|
}; |
||||
|
|
||||
|
module.exports = convert; |
@ -0,0 +1,28 @@ |
|||||
|
const jwt = require('jsonwebtoken'); |
||||
|
const diskLogic = require('logic/disk.js'); |
||||
|
|
||||
|
// Environmental variables are Strings, the expiry will be interpreted as milliseconds if not converted to int.
|
||||
|
// eslint-disable-next-line no-magic-numbers
|
||||
|
const expiresIn = process.env.JWT_EXPIRATION ? parseInt(process.env.JWT_EXPIRATION, 3600) : 3600; |
||||
|
|
||||
|
async function generateJWT(account) { |
||||
|
|
||||
|
const jwtPrivateKey = await diskLogic.readJWTPrivateKeyFile(); |
||||
|
|
||||
|
const jwtPubKey = await diskLogic.readJWTPublicKeyFile(); |
||||
|
|
||||
|
// eslint-disable-next-line object-shorthand
|
||||
|
const token = await jwt.sign({ id: account }, jwtPrivateKey, { expiresIn: expiresIn, algorithm: 'RS256' }); |
||||
|
|
||||
|
await jwt.verify(token, jwtPubKey, function (error) { |
||||
|
if (error) { |
||||
|
return Promise.reject(new Error('Error generating JWT token.')); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
return token; |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
generateJWT, |
||||
|
}; |
@ -0,0 +1,133 @@ |
|||||
|
require('winston-daily-rotate-file'); |
||||
|
const constants = require('utils/const.js'); |
||||
|
const fs = require('fs'); |
||||
|
const path = require('path'); |
||||
|
const winston = require('winston'); |
||||
|
const { format } = require('winston'); |
||||
|
const { combine, timestamp, printf } = format; |
||||
|
const getNamespace = require('continuation-local-storage').getNamespace; |
||||
|
|
||||
|
const LOCAL = 'local'; |
||||
|
const logDir = './logs'; |
||||
|
const ENV = process.env.NODE_ENV; |
||||
|
|
||||
|
if (!fs.existsSync(logDir)) { |
||||
|
fs.mkdirSync(logDir); |
||||
|
} |
||||
|
|
||||
|
const appendCorrelationId = format((info, opts) => { |
||||
|
var apiRequest = getNamespace(constants.REQUEST_CORRELATION_NAMESPACE_KEY); |
||||
|
if (apiRequest) { |
||||
|
info.internalCorrelationId = apiRequest.get(constants.REQUEST_CORRELATION_ID_KEY); |
||||
|
} |
||||
|
|
||||
|
return info; |
||||
|
}); |
||||
|
|
||||
|
const errorFileTransport = new winston.transports.DailyRotateFile({ |
||||
|
filename: path.join(logDir, 'error-%DATE%.log'), |
||||
|
datePattern: 'YYYY-MM-DD', |
||||
|
level: 'error', |
||||
|
maxSize: '10m', |
||||
|
maxFiles: '7d' |
||||
|
}); |
||||
|
|
||||
|
const apiFileTransport = new winston.transports.DailyRotateFile({ |
||||
|
filename: path.join(logDir, 'api-%DATE%.log'), |
||||
|
datePattern: 'YYYY-MM-DD', |
||||
|
maxSize: '10m', |
||||
|
maxFiles: '7d' |
||||
|
}); |
||||
|
|
||||
|
const localLogFormat = printf(info => { |
||||
|
var data = ''; |
||||
|
if (info.data) { |
||||
|
data = JSON.stringify({ data: info.data }); |
||||
|
} |
||||
|
|
||||
|
return `${info.timestamp} ${info.level.toUpperCase()}: ${info.internalCorrelationId} [${info._module}] ${info.message} ${data}`; |
||||
|
}); |
||||
|
|
||||
|
const localLoggerTransports = [ |
||||
|
errorFileTransport, |
||||
|
apiFileTransport, |
||||
|
]; |
||||
|
|
||||
|
if (ENV === 'development') { |
||||
|
localLoggerTransports.push(new winston.transports.Console()); |
||||
|
} |
||||
|
|
||||
|
winston.loggers.add(LOCAL, { |
||||
|
level: 'info', |
||||
|
format: combine( |
||||
|
timestamp(), |
||||
|
appendCorrelationId(), |
||||
|
localLogFormat |
||||
|
), |
||||
|
transports: localLoggerTransports |
||||
|
}); |
||||
|
|
||||
|
const morganConfiguration = { |
||||
|
stream: { |
||||
|
write: function (message) { |
||||
|
info(message, 'umbrel-manager'); |
||||
|
} |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
const localLogger = winston.loggers.get(LOCAL); |
||||
|
|
||||
|
function printToStandardOut(data) { |
||||
|
if (data) { |
||||
|
console.log(data); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function error(message, _module, data) { |
||||
|
printToStandardOut(message); |
||||
|
printToStandardOut(_module); |
||||
|
printToStandardOut(data); |
||||
|
localLogger.error(message, { |
||||
|
_module: _module, |
||||
|
data: data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function warn(message, _module, data) { |
||||
|
printToStandardOut(message); |
||||
|
printToStandardOut(_module); |
||||
|
printToStandardOut(data); |
||||
|
localLogger.warn(message, { |
||||
|
_module: _module, |
||||
|
data: data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function info(message, _module, data) { |
||||
|
printToStandardOut(message); |
||||
|
printToStandardOut(_module); |
||||
|
printToStandardOut(data); |
||||
|
localLogger.info(message, { |
||||
|
_module: _module, |
||||
|
data: data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
function debug(message, _module, data) { |
||||
|
printToStandardOut(message); |
||||
|
printToStandardOut(_module); |
||||
|
printToStandardOut(data); |
||||
|
localLogger.debug(message, { |
||||
|
_module: _module, |
||||
|
data: data |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
error: error, |
||||
|
warn: warn, |
||||
|
info: info, |
||||
|
debug: debug, |
||||
|
morganConfiguration: morganConfiguration, |
||||
|
}; |
||||
|
|
@ -0,0 +1,15 @@ |
|||||
|
// this safe handler is used to wrap our api methods
|
||||
|
// so that we always fallback and return an exception if there is an error
|
||||
|
// inside of an async function
|
||||
|
// Mostly copied from vault/server/utils/safeHandler.js
|
||||
|
function safeHandler(handler) { |
||||
|
return async (req, res, next) => { |
||||
|
try { |
||||
|
return await handler(req, res, next); |
||||
|
} catch (err) { |
||||
|
return next(err); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
module.exports = safeHandler; |
@ -0,0 +1,86 @@ |
|||||
|
const validator = require('validator'); |
||||
|
|
||||
|
const ValidationError = require('models/errors.js').ValidationError; |
||||
|
|
||||
|
// Max length is listed here,
|
||||
|
// https://github.com/lightningnetwork/lnd/blob/fd1f6a7bc46b1e50ff3879b8bd3876d347dbb73d/channeldb/invoices.go#L84
|
||||
|
const MAX_MEMO_LENGTH = 1024; |
||||
|
const MIN_PASSWORD_LENGTH = 12; |
||||
|
|
||||
|
function isAlphanumeric(string) { |
||||
|
|
||||
|
isDefined(string); |
||||
|
|
||||
|
if (!validator.isAlphanumeric(string)) { |
||||
|
throw new ValidationError('Must include only alpha numeric characters.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isAlphanumericAndSpaces(string) { |
||||
|
|
||||
|
isDefined(string); |
||||
|
|
||||
|
if (!validator.matches(string, '^[a-zA-Z0-9\\s]*$')) { |
||||
|
throw new ValidationError('Must include only alpha numeric characters and spaces.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isBoolean(value) { |
||||
|
if (value !== true && value !== false) { |
||||
|
throw new ValidationError('Must be true or false.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isDecimal(amount) { |
||||
|
if (!validator.isDecimal(amount)) { |
||||
|
throw new ValidationError('Must be decimal.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isDefined(object) { |
||||
|
if (object === undefined) { |
||||
|
throw new ValidationError('Must define variable.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isMinPasswordLength(password) { |
||||
|
if (password.length < MIN_PASSWORD_LENGTH) { |
||||
|
throw new ValidationError('Must be ' + MIN_PASSWORD_LENGTH + ' or more characters.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isPositiveInteger(amount) { |
||||
|
if (!validator.isInt(amount + '', { gt: 0 })) { |
||||
|
throw new ValidationError('Must be positive integer.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isPositiveIntegerOrZero(amount) { |
||||
|
if (!validator.isInt(amount + '', { gt: -1 })) { |
||||
|
throw new ValidationError('Must be positive integer.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isString(object) { |
||||
|
if (typeof object !== 'string') { |
||||
|
throw new ValidationError('Object must be of type string.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
function isValidMemoLength(string) { |
||||
|
if (Buffer.byteLength(string, 'utf8') > MAX_MEMO_LENGTH) { |
||||
|
throw new ValidationError('Must be less than ' + MAX_MEMO_LENGTH + ' bytes.'); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
module.exports = { |
||||
|
isAlphanumeric, |
||||
|
isAlphanumericAndSpaces, |
||||
|
isBoolean, |
||||
|
isDecimal, |
||||
|
isMinPasswordLength, |
||||
|
isPositiveInteger, |
||||
|
isPositiveIntegerOrZero, |
||||
|
isString, |
||||
|
isValidMemoLength, |
||||
|
}; |
Loading…
Reference in new issue