Moved v3 code from NginxProxyManager/nginx-proxy-manager-3 to NginxProxyManager/nginx-proxy-manager

This commit is contained in:
Jamie Curnow
2022-05-12 08:47:31 +10:00
parent 4db34f5894
commit 2110ecc382
830 changed files with 38168 additions and 36635 deletions
.dockerignore.gitignore.versionDEV-README.mdJenkinsfileREADME.md
backend
.editorconfig.eslintrc.json.gitignore.golangci.yml.nancy-ignore
.vscode
README.mdTaskfile.ymlapp.js
cmd
server
config
db.js
doc
embed
go.modgo.sumindex.js
internal
access-list.js
acme
api
audit-log.js
cache
certificate.js
config
database
dead-host.js
dnsproviders
entity
errors
host.jsip_ranges.js
jwt
logger
model
nginx.js
nginx
proxy-host.jsredirection-host.jsreport.jssetting.js
state
stream.jstoken.js
types
user.js
util
validator
worker
knexfile.js
lib
logger.jsmigrate.js
migrations
models
nodemon.jsonpackage.json
routes
schema
scripts
setup.js
templates
yarn.lock
docker
docs
frontend
.babelrc.env.development.eslintrc.gitignore.prettierrcREADME.mdcheck-locales.js
fonts
globalSetup.js
html
imagesjest.eslint.js
js
package.json
public
scss
src
App.test.tsxApp.tsxRouter.tsx
api
components
context
declarations.d.ts
fonts
hooks
img
index.scssindex.tsx
locale
modals
modules
pages
react-app-env.d.ts
styles
theme
tsconfig.jsonwebpack.config.jsyarn.lock
global
scripts
test

@ -1,534 +0,0 @@
const _ = require('lodash');
const fs = require('fs');
const batchflow = require('batchflow');
const logger = require('../logger').access;
const error = require('../lib/error');
const accessListModel = require('../models/access_list');
const accessListAuthModel = require('../models/access_list_auth');
const accessListClientModel = require('../models/access_list_client');
const proxyHostModel = require('../models/proxy_host');
const internalAuditLog = require('./audit-log');
const internalNginx = require('./nginx');
const utils = require('../lib/utils');
function omissions () {
return ['is_deleted'];
}
const internalAccessList = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
return access.can('access_lists:create', data)
.then((/*access_data*/) => {
return accessListModel
.query()
.omit(omissions())
.insertAndFetch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
owner_user_id: access.token.getUserId(1)
});
})
.then((row) => {
data.id = row.id;
let promises = [];
// Now add the items
data.items.map((item) => {
promises.push(accessListAuthModel
.query()
.insert({
access_list_id: row.id,
username: item.username,
password: item.password
})
);
});
// Now add the clients
if (typeof data.clients !== 'undefined' && data.clients) {
data.clients.map((client) => {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: row.id,
address: client.address,
directive: client.directive
})
);
});
}
return Promise.all(promises);
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
}, true /* <- skip masking */);
})
.then((row) => {
// Audit log
data.meta = _.assign({}, data.meta || {}, row.meta);
return internalAccessList.build(row)
.then(() => {
if (row.proxy_host_count) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'access-list',
object_id: row.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
return internalAccessList.maskItems(row);
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} [data.name]
* @param {String} [data.items]
* @return {Promise}
*/
update: (access, data) => {
return access.can('access_lists:update', data.id)
.then((/*access_data*/) => {
return internalAccessList.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('Access List could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
})
.then(() => {
// patch name if specified
if (typeof data.name !== 'undefined' && data.name) {
return accessListModel
.query()
.where({id: data.id})
.patch({
name: data.name,
satisfy_any: data.satisfy_any,
pass_auth: data.pass_auth,
});
}
})
.then(() => {
// Check for items and add/update/remove them
if (typeof data.items !== 'undefined' && data.items) {
let promises = [];
let items_to_keep = [];
data.items.map(function (item) {
if (item.password) {
promises.push(accessListAuthModel
.query()
.insert({
access_list_id: data.id,
username: item.username,
password: item.password
})
);
} else {
// This was supplied with an empty password, which means keep it but don't change the password
items_to_keep.push(item.username);
}
});
let query = accessListAuthModel
.query()
.delete()
.where('access_list_id', data.id);
if (items_to_keep.length) {
query.andWhere('username', 'NOT IN', items_to_keep);
}
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(() => {
// Check for clients and add/update/remove them
if (typeof data.clients !== 'undefined' && data.clients) {
let promises = [];
data.clients.map(function (client) {
if (client.address) {
promises.push(accessListClientModel
.query()
.insert({
access_list_id: data.id,
address: client.address,
directive: client.directive
})
);
}
});
let query = accessListClientModel
.query()
.delete()
.where('access_list_id', data.id);
return query
.then(() => {
// Add new items
if (promises.length) {
return Promise.all(promises);
}
});
}
})
.then(internalNginx.reload)
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'access-list',
object_id: data.id,
meta: internalAccessList.maskItems(data)
});
})
.then(() => {
// re-fetch with expansions
return internalAccessList.get(access, {
id: data.id,
expand: ['owner', 'items', 'clients', 'proxy_hosts.access_list.[clients,items]']
}, true /* <- skip masking */);
})
.then((row) => {
return internalAccessList.build(row)
.then(() => {
if (row.proxy_host_count) {
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
}
})
.then(() => {
return internalAccessList.maskItems(row);
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @param {Boolean} [skip_masking]
* @return {Promise}
*/
get: (access, data, skip_masking) => {
if (typeof data === 'undefined') {
data = {};
}
return access.can('access_lists:get', data.id)
.then((access_data) => {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.where('access_list.is_deleted', 0)
.andWhere('access_list.id', data.id)
.allowEager('[owner,items,clients,proxy_hosts.[*, access_list.[clients,items]]]')
.omit(['access_list.is_deleted'])
.first();
if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
}
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
if (!skip_masking && typeof row.items !== 'undefined' && row.items) {
row = internalAccessList.maskItems(row);
}
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
}
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Integer} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: (access, data) => {
return access.can('access_lists:delete', data.id)
.then(() => {
return internalAccessList.get(access, {id: data.id, expand: ['proxy_hosts', 'items', 'clients']});
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
// 1. update row to be deleted
// 2. update any proxy hosts that were using it (ignoring permissions)
// 3. reconfigure those hosts
// 4. audit log
// 1. update row to be deleted
return accessListModel
.query()
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// 2. update any proxy hosts that were using it (ignoring permissions)
if (row.proxy_hosts) {
return proxyHostModel
.query()
.where('access_list_id', '=', row.id)
.patch({access_list_id: 0})
.then(() => {
// 3. reconfigure those hosts, then reload nginx
// set the access_list_id to zero for these items
row.proxy_hosts.map(function (val, idx) {
row.proxy_hosts[idx].access_list_id = 0;
});
return internalNginx.bulkGenerateConfigs('proxy_host', row.proxy_hosts);
})
.then(() => {
return internalNginx.reload();
});
}
})
.then(() => {
// delete the htpasswd file
let htpasswd_file = internalAccessList.getFilename(row);
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
})
.then(() => {
// 4. audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'access-list',
object_id: row.id,
meta: _.omit(internalAccessList.maskItems(row), ['is_deleted', 'proxy_hosts'])
});
});
})
.then(() => {
return true;
});
},
/**
* All Lists
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('access_lists:list')
.then((access_data) => {
let query = accessListModel
.query()
.select('access_list.*', accessListModel.raw('COUNT(proxy_host.id) as proxy_host_count'))
.joinRaw('LEFT JOIN `proxy_host` ON `proxy_host`.`access_list_id` = `access_list`.`id` AND `proxy_host`.`is_deleted` = 0')
.where('access_list.is_deleted', 0)
.groupBy('access_list.id')
.omit(['access_list.is_deleted'])
.allowEager('[owner,items,clients]')
.orderBy('access_list.name', 'ASC');
if (access_data.permission_visibility !== 'all') {
query.andWhere('access_list.owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('name', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.eager('[' + expand.join(', ') + ']');
}
return query;
})
.then((rows) => {
if (rows) {
rows.map(function (row, idx) {
if (typeof row.items !== 'undefined' && row.items) {
rows[idx] = internalAccessList.maskItems(row);
}
});
}
return rows;
});
},
/**
* Report use
*
* @param {Integer} user_id
* @param {String} visibility
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
let query = accessListModel
.query()
.count('id as count')
.where('is_deleted', 0);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
}
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
},
/**
* @param {Object} list
* @returns {Object}
*/
maskItems: (list) => {
if (list && typeof list.items !== 'undefined') {
list.items.map(function (val, idx) {
let repeat_for = 8;
let first_char = '*';
if (typeof val.password !== 'undefined' && val.password) {
repeat_for = val.password.length - 1;
first_char = val.password.charAt(0);
}
list.items[idx].hint = first_char + ('*').repeat(repeat_for);
list.items[idx].password = '';
});
}
return list;
},
/**
* @param {Object} list
* @param {Integer} list.id
* @returns {String}
*/
getFilename: (list) => {
return '/data/access/' + list.id;
},
/**
* @param {Object} list
* @param {Integer} list.id
* @param {String} list.name
* @param {Array} list.items
* @returns {Promise}
*/
build: (list) => {
logger.info('Building Access file #' + list.id + ' for: ' + list.name);
return new Promise((resolve, reject) => {
let htpasswd_file = internalAccessList.getFilename(list);
// 1. remove any existing access file
try {
fs.unlinkSync(htpasswd_file);
} catch (err) {
// do nothing
}
// 2. create empty access file
try {
fs.writeFileSync(htpasswd_file, '', {encoding: 'utf8'});
resolve(htpasswd_file);
} catch (err) {
reject(err);
}
})
.then((htpasswd_file) => {
// 3. generate password for each user
if (list.items.length) {
return new Promise((resolve, reject) => {
batchflow(list.items).sequential()
.each((i, item, next) => {
if (typeof item.password !== 'undefined' && item.password.length) {
logger.info('Adding: ' + item.username);
utils.exec('/usr/bin/htpasswd -b "' + htpasswd_file + '" "' + item.username + '" "' + item.password + '"')
.then((/*result*/) => {
next();
})
.catch((err) => {
logger.error(err);
next(err);
});
}
})
.error((err) => {
logger.error(err);
reject(err);
})
.end((results) => {
logger.success('Built Access file #' + list.id + ' for: ' + list.name);
resolve(results);
});
});
}
});
}
};
module.exports = internalAccessList;

@ -0,0 +1,200 @@
package acme
// Some light reading:
// https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert
import (
"fmt"
"os"
"os/exec"
"strings"
"npm/internal/config"
"npm/internal/entity/certificateauthority"
"npm/internal/entity/dnsprovider"
"npm/internal/logger"
)
func getAcmeShFilePath() (string, error) {
path, err := exec.LookPath("acme.sh")
if err != nil {
return path, fmt.Errorf("Cannot find acme.sh execuatable script in PATH")
}
return path, nil
}
func getCommonEnvVars() []string {
return []string{
fmt.Sprintf("ACMESH_CONFIG_HOME=%s", os.Getenv("ACMESH_CONFIG_HOME")),
fmt.Sprintf("ACMESH_HOME=%s", os.Getenv("ACMESH_HOME")),
fmt.Sprintf("CERT_HOME=%s", os.Getenv("CERT_HOME")),
fmt.Sprintf("LE_CONFIG_HOME=%s", os.Getenv("LE_CONFIG_HOME")),
fmt.Sprintf("LE_WORKING_DIR=%s", os.Getenv("LE_WORKING_DIR")),
}
}
// GetAcmeShVersion will return the acme.sh script version
func GetAcmeShVersion() string {
if r, err := shExec([]string{"--version"}, nil); err == nil {
// modify the output
r = strings.Trim(r, "\n")
v := strings.Split(r, "\n")
return v[len(v)-1]
}
return ""
}
// CreateAccountKey is required for each server initially
func CreateAccountKey(ca *certificateauthority.Model) error {
args := []string{"--create-account-key", "--accountkeylength", "2048"}
if ca != nil {
logger.Info("Acme.sh CreateAccountKey for %s", ca.AcmeshServer)
args = append(args, "--server", ca.AcmeshServer)
if ca.CABundle != "" {
args = append(args, "--ca-bundle", ca.CABundle)
}
} else {
logger.Info("Acme.sh CreateAccountKey")
}
args = append(args, getCommonArgs()...)
ret, err := shExec(args, nil)
if err != nil {
return err
}
logger.Debug("CreateAccountKey returned:\n%+v", ret)
return nil
}
// RequestCert does all the heavy lifting
func RequestCert(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) (string, error) {
args, err := buildCertRequestArgs(domains, method, outputFullchainFile, outputKeyFile, dnsProvider, ca, force)
if err != nil {
return err.Error(), err
}
envs := make([]string, 0)
if dnsProvider != nil {
envs, err = dnsProvider.GetAcmeShEnvVars()
if err != nil {
return err.Error(), err
}
}
ret, err := shExec(args, envs)
if err != nil {
return ret, err
}
return "", nil
}
// shExec executes the acme.sh with arguments
func shExec(args []string, envs []string) (string, error) {
acmeSh, err := getAcmeShFilePath()
if err != nil {
logger.Error("AcmeShError", err)
return "", err
}
logger.Debug("CMD: %s %v", acmeSh, args)
// nolint: gosec
c := exec.Command(acmeSh, args...)
c.Env = append(getCommonEnvVars(), envs...)
b, e := c.Output()
if e != nil {
logger.Error("AcmeShError", fmt.Errorf("Command error: %s -- %v\n%+v", acmeSh, args, e))
logger.Warn(string(b))
}
return string(b), e
}
func getCommonArgs() []string {
args := make([]string, 0)
if config.Configuration.Acmesh.Home != "" {
args = append(args, "--home", config.Configuration.Acmesh.Home)
}
if config.Configuration.Acmesh.ConfigHome != "" {
args = append(args, "--config-home", config.Configuration.Acmesh.ConfigHome)
}
if config.Configuration.Acmesh.CertHome != "" {
args = append(args, "--cert-home", config.Configuration.Acmesh.CertHome)
}
args = append(args, "--log", "/data/logs/acme.sh.log")
args = append(args, "--debug", "2")
return args
}
// This is split out into it's own function so it's testable
func buildCertRequestArgs(domains []string, method, outputFullchainFile, outputKeyFile string, dnsProvider *dnsprovider.Model, ca *certificateauthority.Model, force bool) ([]string, error) {
// The argument order matters.
// see https://github.com/acmesh-official/acme.sh/wiki/How-to-issue-a-cert#3-multiple-domains-san-mode--hybrid-mode
// for multiple domains and note that the method of validation is required just after the domain arg, each time.
// TODO log file location configurable
args := []string{"--issue"}
if ca != nil {
args = append(args, "--server", ca.AcmeshServer)
if ca.CABundle != "" {
args = append(args, "--ca-bundle", ca.CABundle)
}
}
if outputFullchainFile != "" {
args = append(args, "--fullchain-file", outputFullchainFile)
}
if outputKeyFile != "" {
args = append(args, "--key-file", outputKeyFile)
}
methodArgs := make([]string, 0)
switch method {
case "dns":
if dnsProvider == nil {
return nil, ErrDNSNeedsDNSProvider
}
methodArgs = append(methodArgs, "--dns", dnsProvider.AcmeshName)
if dnsProvider.DNSSleep > 0 {
// See: https://github.com/acmesh-official/acme.sh/wiki/dnscheck
methodArgs = append(methodArgs, "--dnssleep", fmt.Sprintf("%d", dnsProvider.DNSSleep))
}
case "http":
if dnsProvider != nil {
return nil, ErrHTTPHasDNSProvider
}
methodArgs = append(methodArgs, "-w", config.Configuration.Acmesh.GetWellknown())
default:
return nil, ErrMethodNotSupported
}
hasMethod := false
// Add domains to args
for _, domain := range domains {
args = append(args, "-d", domain)
// Method has to appear after each domain
if !hasMethod {
args = append(args, methodArgs...)
hasMethod = true
}
}
if force {
args = append(args, "--force")
}
args = append(args, getCommonArgs()...)
return args, nil
}

@ -0,0 +1,204 @@
package acme
import (
"fmt"
"testing"
"npm/internal/config"
"npm/internal/entity/certificateauthority"
"npm/internal/entity/dnsprovider"
"github.com/stretchr/testify/assert"
)
// Tear up/down
/*
func TestMain(m *testing.M) {
config.Init(&version, &commit, &sentryDSN)
code := m.Run()
os.Exit(code)
}
*/
// TODO configurable
const acmeLogFile = "/data/logs/acme.sh.log"
func TestBuildCertRequestArgs(t *testing.T) {
type want struct {
args []string
err error
}
wellknown := config.Configuration.Acmesh.GetWellknown()
exampleKey := fmt.Sprintf("%s/example.com.key", config.Configuration.Acmesh.CertHome)
exampleChain := fmt.Sprintf("%s/a.crt", config.Configuration.Acmesh.CertHome)
tests := []struct {
name string
domains []string
method string
outputFullchainFile string
outputKeyFile string
dnsProvider *dnsprovider.Model
ca *certificateauthority.Model
want want
}{
{
name: "http single domain",
domains: []string{"example.com"},
method: "http",
outputFullchainFile: exampleChain,
outputKeyFile: exampleKey,
dnsProvider: nil,
ca: nil,
want: want{
args: []string{
"--issue",
"--fullchain-file",
exampleChain,
"--key-file",
exampleKey,
"-d",
"example.com",
"-w",
wellknown,
"--log",
acmeLogFile,
"--debug",
"2",
},
err: nil,
},
},
{
name: "http multiple domains",
domains: []string{"example.com", "example-two.com", "example-three.com"},
method: "http",
outputFullchainFile: exampleChain,
outputKeyFile: exampleKey,
dnsProvider: nil,
ca: nil,
want: want{
args: []string{
"--issue",
"--fullchain-file",
exampleChain,
"--key-file",
exampleKey,
"-d",
"example.com",
"-w",
wellknown,
"-d",
"example-two.com",
"-d",
"example-three.com",
"--log",
acmeLogFile,
"--debug",
"2",
},
err: nil,
},
},
{
name: "http single domain with dns provider",
domains: []string{"example.com"},
method: "http",
outputFullchainFile: exampleChain,
outputKeyFile: exampleKey,
dnsProvider: &dnsprovider.Model{
AcmeshName: "dns_cf",
},
ca: nil,
want: want{
args: nil,
err: ErrHTTPHasDNSProvider,
},
},
{
name: "dns single domain",
domains: []string{"example.com"},
method: "dns",
outputFullchainFile: exampleChain,
outputKeyFile: exampleKey,
dnsProvider: &dnsprovider.Model{
AcmeshName: "dns_cf",
},
ca: nil,
want: want{
args: []string{
"--issue",
"--fullchain-file",
exampleChain,
"--key-file",
exampleKey,
"-d",
"example.com",
"--dns",
"dns_cf",
"--log",
acmeLogFile,
"--debug",
"2",
},
err: nil,
},
},
{
name: "dns multiple domains",
domains: []string{"example.com", "example-two.com", "example-three.com"},
method: "dns",
outputFullchainFile: exampleChain,
outputKeyFile: exampleKey,
dnsProvider: &dnsprovider.Model{
AcmeshName: "dns_cf",
},
ca: nil,
want: want{
args: []string{
"--issue",
"--fullchain-file",
exampleChain,
"--key-file",
exampleKey,
"-d",
"example.com",
"--dns",
"dns_cf",
"-d",
"example-two.com",
"-d",
"example-three.com",
"--log",
acmeLogFile,
"--debug",
"2",
},
err: nil,
},
},
{
name: "dns single domain no provider",
domains: []string{"example.com"},
method: "dns",
outputFullchainFile: exampleChain,
outputKeyFile: exampleKey,
dnsProvider: nil,
ca: nil,
want: want{
args: nil,
err: ErrDNSNeedsDNSProvider,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
args, err := buildCertRequestArgs(tt.domains, tt.method, tt.outputFullchainFile, tt.outputKeyFile, tt.dnsProvider, tt.ca, false)
assert.Equal(t, tt.want.args, args)
assert.Equal(t, tt.want.err, err)
})
}
}

@ -0,0 +1,10 @@
package acme
import "errors"
// All errors relating to Acme.sh use
var (
ErrDNSNeedsDNSProvider = errors.New("RequestCert dns method requires a dns provider")
ErrHTTPHasDNSProvider = errors.New("RequestCert http method does not need a dns provider")
ErrMethodNotSupported = errors.New("RequestCert method not supported")
)

@ -0,0 +1,25 @@
package context
var (
// BodyCtxKey is the name of the Body value on the context
BodyCtxKey = &contextKey{"Body"}
// UserIDCtxKey is the name of the UserID value on the context
UserIDCtxKey = &contextKey{"UserID"}
// FiltersCtxKey is the name of the Filters value on the context
FiltersCtxKey = &contextKey{"Filters"}
// PrettyPrintCtxKey is the name of the pretty print context
PrettyPrintCtxKey = &contextKey{"Pretty"}
// ExpansionCtxKey is the name of the expansion context
ExpansionCtxKey = &contextKey{"Expansion"}
)
// contextKey is a value for use with context.WithValue. It's used as
// a pointer so it fits in an interface{} without allocation. This technique
// for defining context keys was copied from Go 1.7's new use of context in net/http.
type contextKey struct {
name string
}
func (k *contextKey) String() string {
return "context value: " + k.name
}

@ -0,0 +1,208 @@
package filters
import (
"fmt"
"strings"
)
// NewFilterSchema is the main method to specify a new Filter Schema for use in Middleware
func NewFilterSchema(fieldSchemas []string) string {
return fmt.Sprintf(baseFilterSchema, strings.Join(fieldSchemas, ", "))
}
// BoolFieldSchema returns the Field Schema for a Boolean accepted value field
func BoolFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
%s,
{
"type": "array",
"items": %s
}
]
}
}
}`, fieldName, boolModifiers, filterBool, filterBool)
}
// IntFieldSchema returns the Field Schema for a Integer accepted value field
func IntFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^[0-9]+$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^[0-9]+$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
// StringFieldSchema returns the Field Schema for a String accepted value field
func StringFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
%s,
{
"type": "array",
"items": %s
}
]
}
}
}`, fieldName, stringModifiers, filterString, filterString)
}
// RegexFieldSchema returns the Field Schema for a String accepted value field matching a Regex
func RegexFieldSchema(fieldName string, regex string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "%s"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "%s"
}
}
]
}
}
}`, fieldName, stringModifiers, regex, regex)
}
// DateFieldSchema returns the Field Schema for a String accepted value field matching a Date format
func DateFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
// DateTimeFieldSchema returns the Field Schema for a String accepted value field matching a Date format
// 2020-03-01T10:30:00+10:00
func DateTimeFieldSchema(fieldName string) string {
return fmt.Sprintf(`{
"type": "object",
"properties": {
"field": {
"type": "string",
"pattern": "^%s$"
},
"modifier": %s,
"value": {
"oneOf": [
{
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
},
{
"type": "array",
"items": {
"type": "string",
"pattern": "^([12]\\d{3}-(0[1-9]|1[0-2])-(0[1-9]|[12]\\d|3[01]))$"
}
}
]
}
}
}`, fieldName, allModifiers)
}
const allModifiers = `{
"type": "string",
"pattern": "^(equals|not|contains|starts|ends|in|notin|min|max|greater|less)$"
}`
const boolModifiers = `{
"type": "string",
"pattern": "^(equals|not)$"
}`
const stringModifiers = `{
"type": "string",
"pattern": "^(equals|not|contains|starts|ends|in|notin)$"
}`
const filterBool = `{
"type": "string",
"pattern": "^(TRUE|true|t|yes|y|on|1|FALSE|f|false|n|no|off|0)$"
}`
const filterString = `{
"type": "string",
"minLength": 1
}`
const baseFilterSchema = `{
"type": "array",
"items": {
"oneOf": [
%s
]
}
}`

@ -0,0 +1,93 @@
package handler
import (
"encoding/json"
"net/http"
"time"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/entity/auth"
"npm/internal/entity/user"
"npm/internal/errors"
"npm/internal/logger"
)
type setAuthModel struct {
Type string `json:"type" db:"type"`
Secret string `json:"secret,omitempty" db:"secret"`
CurrentSecret string `json:"current_secret,omitempty"`
}
// SetAuth sets a auth method. This can be used for "me" and `2` for example
// Route: POST /users/:userID/auth
func SetAuth() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newAuth setAuthModel
err := json.Unmarshal(bodyBytes, &newAuth)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
userID, isSelf, userIDErr := getUserIDFromRequest(r)
if userIDErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
return
}
// Load user
thisUser, thisUserErr := user.GetByID(userID)
if thisUserErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, thisUserErr.Error(), nil)
return
}
if thisUser.IsSystem {
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Cannot set password for system user", nil)
return
}
// Load existing auth for user
userAuth, userAuthErr := auth.GetByUserIDType(userID, newAuth.Type)
if userAuthErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userAuthErr.Error(), nil)
return
}
if isSelf {
// confirm that the current_secret given is valid for the one stored in the database
validateErr := userAuth.ValidateSecret(newAuth.CurrentSecret)
if validateErr != nil {
logger.Debug("%s: %s", "Password change: current password was incorrect", validateErr.Error())
// Sleep for 1 second to prevent brute force password guessing
time.Sleep(time.Second)
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrCurrentPasswordInvalid.Error(), nil)
return
}
}
if newAuth.Type == auth.TypePassword {
err := userAuth.SetPassword(newAuth.Secret)
if err != nil {
logger.Error("SetPasswordError", err)
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
}
}
if err = userAuth.Save(); err != nil {
logger.Error("AuthSaveError", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
return
}
userAuth.Secret = ""
// todo: add to audit-log
h.ResultResponseJSON(w, r, http.StatusOK, userAuth)
}
}

@ -0,0 +1,141 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"npm/internal/acme"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/certificateauthority"
"npm/internal/logger"
)
// GetCertificateAuthorities will return a list of Certificate Authorities
// Route: GET /certificate-authorities
func GetCertificateAuthorities() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
certificates, err := certificateauthority.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, certificates)
}
}
}
// GetCertificateAuthority will return a single Certificate Authority
// Route: GET /certificate-authorities/{caID}
func GetCertificateAuthority() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var caID int
if caID, err = getURLParamInt(r, "caID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
cert, err := certificateauthority.GetByID(caID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, cert)
}
}
}
// CreateCertificateAuthority will create a Certificate Authority
// Route: POST /certificate-authorities
func CreateCertificateAuthority() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newCA certificateauthority.Model
err := json.Unmarshal(bodyBytes, &newCA)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = newCA.Check(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
if err = newCA.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate Authority: %s", err.Error()), nil)
return
}
if err = acme.CreateAccountKey(&newCA); err != nil {
logger.Error("CreateAccountKeyError", err)
}
h.ResultResponseJSON(w, r, http.StatusOK, newCA)
}
}
// UpdateCertificateAuthority updates a ca
// Route: PUT /certificate-authorities/{caID}
func UpdateCertificateAuthority() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var caID int
if caID, err = getURLParamInt(r, "caID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
ca, err := certificateauthority.GetByID(caID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &ca)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = ca.Check(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
if err = ca.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, ca)
}
}
}
// DeleteCertificateAuthority deletes a ca
// Route: DELETE /certificate-authorities/{caID}
func DeleteCertificateAuthority() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var caID int
if caID, err = getURLParamInt(r, "caID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
cert, err := certificateauthority.GetByID(caID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete())
}
}
}

@ -0,0 +1,145 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/api/schema"
"npm/internal/entity/certificate"
)
// GetCertificates will return a list of Certificates
// Route: GET /certificates
func GetCertificates() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
certificates, err := certificate.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, certificates)
}
}
}
// GetCertificate will return a single Certificate
// Route: GET /certificates/{certificateID}
func GetCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
cert, err := certificate.GetByID(certificateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, cert)
}
}
}
// CreateCertificate will create a Certificate
// Route: POST /certificates
func CreateCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newCertificate certificate.Model
err := json.Unmarshal(bodyBytes, &newCertificate)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newCertificate.UserID = userID
if err = newCertificate.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Certificate: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newCertificate)
}
}
// UpdateCertificate updates a cert
// Route: PUT /certificates/{certificateID}
func UpdateCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
certificateObject, err := certificate.GetByID(certificateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
// This is a special endpoint, as it needs to verify the schema payload
// based on the certificate type, without being given a type in the payload.
// The middleware would normally handle this.
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
schemaErrors, jsonErr := middleware.CheckRequestSchema(r.Context(), schema.UpdateCertificate(certificateObject.Type), bodyBytes)
if jsonErr != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", jsonErr), nil)
return
}
if len(schemaErrors) > 0 {
h.ResultSchemaErrorJSON(w, r, schemaErrors)
return
}
err := json.Unmarshal(bodyBytes, &certificateObject)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = certificateObject.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, certificateObject)
}
}
}
// DeleteCertificate deletes a cert
// Route: DELETE /certificates/{certificateID}
func DeleteCertificate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var certificateID int
if certificateID, err = getURLParamInt(r, "certificateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
cert, err := certificate.GetByID(certificateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, cert.Delete())
}
}
}

@ -0,0 +1,15 @@
package handler
import (
"net/http"
h "npm/internal/api/http"
"npm/internal/config"
)
// Config returns the entire configuration, for debug purposes
// Route: GET /config
func Config() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
h.ResultResponseJSON(w, r, http.StatusOK, config.Configuration)
}
}

@ -0,0 +1,159 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/dnsproviders"
"npm/internal/entity/dnsprovider"
)
// GetDNSProviders will return a list of DNS Providers
// Route: GET /dns-providers
func GetDNSProviders() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
items, err := dnsprovider.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, items)
}
}
}
// GetDNSProvider will return a single DNS Provider
// Route: GET /dns-providers/{providerID}
func GetDNSProvider() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var providerID int
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := dnsprovider.GetByID(providerID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, item)
}
}
}
// CreateDNSProvider will create a DNS Provider
// Route: POST /dns-providers
func CreateDNSProvider() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newItem dnsprovider.Model
err := json.Unmarshal(bodyBytes, &newItem)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newItem.UserID = userID
if err = newItem.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save DNS Provider: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newItem)
}
}
// UpdateDNSProvider updates a provider
// Route: PUT /dns-providers/{providerID}
func UpdateDNSProvider() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var providerID int
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := dnsprovider.GetByID(providerID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &item)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = item.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, item)
}
}
}
// DeleteDNSProvider removes a provider
// Route: DELETE /dns-providers/{providerID}
func DeleteDNSProvider() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var providerID int
if providerID, err = getURLParamInt(r, "providerID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, err := dnsprovider.GetByID(providerID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, item.Delete())
}
}
}
// GetAcmeshProviders will return a list of acme.sh providers
// Route: GET /dns-providers/acmesh
func GetAcmeshProviders() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
items := dnsproviders.List()
h.ResultResponseJSON(w, r, http.StatusOK, items)
}
}
// GetAcmeshProvider will return a single acme.sh provider
// Route: GET /dns-providers/acmesh/{acmeshID}
func GetAcmeshProvider() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var acmeshID string
var err error
if acmeshID, err = getURLParamString(r, "acmeshID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
item, getErr := dnsproviders.Get(acmeshID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, getErr.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, item)
}
}
}

@ -0,0 +1,34 @@
package handler
import (
"net/http"
"npm/internal/acme"
h "npm/internal/api/http"
"npm/internal/config"
)
type healthCheckResponse struct {
Version string `json:"version"`
Commit string `json:"commit"`
AcmeShVersion string `json:"acme.sh"`
Healthy bool `json:"healthy"`
IsSetup bool `json:"setup"`
ErrorReporting bool `json:"error_reporting"`
}
// Health returns the health of the api
// Route: GET /health
func Health() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
health := healthCheckResponse{
Version: config.Version,
Commit: config.Commit,
Healthy: true,
IsSetup: config.IsSetup,
AcmeShVersion: acme.GetAcmeShVersion(),
ErrorReporting: config.ErrorReporting,
}
h.ResultResponseJSON(w, r, http.StatusOK, health)
}
}

@ -0,0 +1,175 @@
package handler
import (
"fmt"
"net/http"
"strconv"
"strings"
"time"
"npm/internal/api/context"
"npm/internal/model"
"github.com/go-chi/chi"
)
const defaultLimit = 10
func getPageInfoFromRequest(r *http.Request) (model.PageInfo, error) {
var pageInfo model.PageInfo
var err error
pageInfo.FromDate, pageInfo.ToDate, err = getDateRanges(r)
if err != nil {
return pageInfo, err
}
pageInfo.Offset, pageInfo.Limit, err = getPagination(r)
if err != nil {
return pageInfo, err
}
pageInfo.Sort = getSortParameter(r)
return pageInfo, nil
}
func getDateRanges(r *http.Request) (time.Time, time.Time, error) {
queryValues := r.URL.Query()
from := queryValues.Get("from")
fromDate := time.Now().AddDate(0, -1, 0) // 1 month ago by default
to := queryValues.Get("to")
toDate := time.Now()
if from != "" {
var fromErr error
fromDate, fromErr = time.Parse(time.RFC3339, from)
if fromErr != nil {
return fromDate, toDate, fmt.Errorf("From date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+"))
}
}
if to != "" {
var toErr error
toDate, toErr = time.Parse(time.RFC3339, to)
if toErr != nil {
return fromDate, toDate, fmt.Errorf("To date is not in correct format: %v", strings.ReplaceAll(time.RFC3339, "Z", "+"))
}
}
return fromDate, toDate, nil
}
func getSortParameter(r *http.Request) []model.Sort {
var sortFields []model.Sort
queryValues := r.URL.Query()
sortString := queryValues.Get("sort")
if sortString == "" {
return sortFields
}
// Split sort fields up in to slice
sorts := strings.Split(sortString, ",")
for _, sortItem := range sorts {
if strings.Contains(sortItem, ".") {
theseItems := strings.Split(sortItem, ".")
switch strings.ToLower(theseItems[1]) {
case "desc":
fallthrough
case "descending":
theseItems[1] = "DESC"
default:
theseItems[1] = "ASC"
}
sortFields = append(sortFields, model.Sort{
Field: theseItems[0],
Direction: theseItems[1],
})
} else {
sortFields = append(sortFields, model.Sort{
Field: sortItem,
Direction: "ASC",
})
}
}
return sortFields
}
func getQueryVarInt(r *http.Request, varName string, required bool, defaultValue int) (int, error) {
queryValues := r.URL.Query()
varValue := queryValues.Get(varName)
if varValue == "" && required {
return 0, fmt.Errorf("%v was not supplied in the request", varName)
} else if varValue == "" {
return defaultValue, nil
}
varInt, intErr := strconv.Atoi(varValue)
if intErr != nil {
return 0, fmt.Errorf("%v is not a valid number", varName)
}
return varInt, nil
}
func getURLParamInt(r *http.Request, varName string) (int, error) {
required := true
defaultValue := 0
paramStr := chi.URLParam(r, varName)
var err error
var paramInt int
if paramStr == "" && required {
return 0, fmt.Errorf("%v was not supplied in the request", varName)
} else if paramStr == "" {
return defaultValue, nil
}
if paramInt, err = strconv.Atoi(paramStr); err != nil {
return 0, fmt.Errorf("%v is not a valid number", varName)
}
return paramInt, nil
}
func getURLParamString(r *http.Request, varName string) (string, error) {
required := true
defaultValue := ""
paramStr := chi.URLParam(r, varName)
if paramStr == "" && required {
return "", fmt.Errorf("%v was not supplied in the request", varName)
} else if paramStr == "" {
return defaultValue, nil
}
return paramStr, nil
}
func getPagination(r *http.Request) (int, int, error) {
var err error
offset, err := getQueryVarInt(r, "offset", false, 0)
if err != nil {
return 0, 0, err
}
limit, err := getQueryVarInt(r, "limit", false, defaultLimit)
if err != nil {
return 0, 0, err
}
return offset, limit, nil
}
// getExpandFromContext returns the Expansion setting
func getExpandFromContext(r *http.Request) []string {
expand, ok := r.Context().Value(context.ExpansionCtxKey).([]string)
if !ok {
return nil
}
return expand
}

@ -0,0 +1,130 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/host"
"npm/internal/entity/hosttemplate"
)
// GetHostTemplates will return a list of Host Templates
// Route: GET /host-templates
func GetHostTemplates() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
hosts, err := hosttemplate.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
}
}
}
// GetHostTemplate will return a single Host Template
// Route: GET /host-templates/{templateID}
func GetHostTemplate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var templateID int
if templateID, err = getURLParamInt(r, "templateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := hosttemplate.GetByID(templateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host)
}
}
}
// CreateHostTemplate will create a Host Template
// Route: POST /host-templates
func CreateHostTemplate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newHostTemplate hosttemplate.Model
err := json.Unmarshal(bodyBytes, &newHostTemplate)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newHostTemplate.UserID = userID
if err = newHostTemplate.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host Template: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newHostTemplate)
}
}
// UpdateHostTemplate updates a host template
// Route: PUT /host-templates/{templateID}
func UpdateHostTemplate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var templateID int
if templateID, err = getURLParamInt(r, "templateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
hostTemplate, err := hosttemplate.GetByID(templateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &hostTemplate)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = hostTemplate.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate)
}
}
}
// DeleteHostTemplate removes a host template
// Route: DELETE /host-templates/{templateID}
func DeleteHostTemplate() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var templateID int
if templateID, err = getURLParamInt(r, "templateID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
hostTemplate, err := host.GetByID(templateID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, hostTemplate.Delete())
}
}
}

@ -0,0 +1,140 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/host"
"npm/internal/validator"
)
// GetHosts will return a list of Hosts
// Route: GET /hosts
func GetHosts() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
hosts, err := host.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
}
}
}
// GetHost will return a single Host
// Route: GET /hosts/{hostID}
func GetHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
hostObject, err := host.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
// nolint: errcheck,gosec
hostObject.Expand(getExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, hostObject)
}
}
}
// CreateHost will create a Host
// Route: POST /hosts
func CreateHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newHost host.Model
err := json.Unmarshal(bodyBytes, &newHost)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newHost.UserID = userID
if err = validator.ValidateHost(newHost); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
if err = newHost.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Host: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
}
}
// UpdateHost updates a host
// Route: PUT /hosts/{hostID}
func UpdateHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
hostObject, err := host.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &hostObject)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = hostObject.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
// nolint: errcheck,gosec
hostObject.Expand(getExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, hostObject)
}
}
}
// DeleteHost removes a host
// Route: DELETE /hosts/{hostID}
func DeleteHost() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := host.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host.Delete())
}
}
}

@ -0,0 +1,14 @@
package handler
import (
"net/http"
h "npm/internal/api/http"
)
// NotAllowed is a json error handler for when method is not allowed
func NotAllowed() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not allowed", nil)
}
}

@ -0,0 +1,64 @@
package handler
import (
"errors"
"io"
"io/fs"
"mime"
"net/http"
"path/filepath"
"strings"
"npm/embed"
h "npm/internal/api/http"
)
var (
assetsSub fs.FS
errIsDir = errors.New("path is dir")
)
// NotFound is a json error handler for 404's and method not allowed.
// It also serves the react frontend as embedded files in the golang binary.
func NotFound() func(http.ResponseWriter, *http.Request) {
assetsSub, _ = fs.Sub(embed.Assets, "assets")
return func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimLeft(r.URL.Path, "/")
if path == "" {
path = "index.html"
}
err := tryRead(assetsSub, path, w)
if err == errIsDir {
err = tryRead(assetsSub, "index.html", w)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
}
} else if err == nil {
return
}
h.ResultErrorJSON(w, r, http.StatusNotFound, "Not found", nil)
}
}
func tryRead(folder fs.FS, requestedPath string, w http.ResponseWriter) error {
f, err := folder.Open(requestedPath)
if err != nil {
return err
}
// nolint: errcheck
defer f.Close()
stat, _ := f.Stat()
if stat.IsDir() {
return errIsDir
}
contentType := mime.TypeByExtension(filepath.Ext(requestedPath))
w.Header().Set("Content-Type", contentType)
_, err = io.Copy(w, f)
return err
}

@ -0,0 +1,108 @@
package handler
import (
"encoding/json"
"fmt"
"io/fs"
"net/http"
"strings"
"npm/embed"
"npm/internal/api/schema"
"npm/internal/config"
"npm/internal/logger"
jsref "github.com/jc21/jsref"
"github.com/jc21/jsref/provider"
)
var (
swaggerSchema []byte
apiDocsSub fs.FS
)
// Schema simply reads the swagger schema from disk and returns is raw
// Route: GET /schema
func Schema() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, string(getSchema()))
}
}
func getSchema() []byte {
if swaggerSchema == nil {
apiDocsSub, _ = fs.Sub(embed.APIDocFiles, "api_docs")
// nolint:gosec
swaggerSchema, _ = fs.ReadFile(apiDocsSub, "api.swagger.json")
// Replace {{VERSION}} with Config Version
swaggerSchema = []byte(strings.ReplaceAll(string(swaggerSchema), "{{VERSION}}", config.Version))
// Dereference the JSON Schema:
var schema interface{}
if err := json.Unmarshal(swaggerSchema, &schema); err != nil {
logger.Error("SwaggerUnmarshalError", err)
return nil
}
provider := provider.NewIoFS(apiDocsSub, "")
resolver := jsref.New()
err := resolver.AddProvider(provider)
if err != nil {
logger.Error("SchemaProviderError", err)
}
result, err := resolver.Resolve(schema, "", []jsref.Option{jsref.WithRecursiveResolution(true)}...)
if err != nil {
logger.Error("SwaggerResolveError", err)
} else {
var marshalErr error
swaggerSchema, marshalErr = json.MarshalIndent(result, "", " ")
if marshalErr != nil {
logger.Error("SwaggerMarshalError", err)
}
}
// End dereference
// Replace incoming schemas with those we actually use in code
swaggerSchema = replaceIncomingSchemas(swaggerSchema)
}
return swaggerSchema
}
func replaceIncomingSchemas(swaggerSchema []byte) []byte {
str := string(swaggerSchema)
// Remember to include the double quotes in the replacement!
str = strings.ReplaceAll(str, `"{{schema.SetAuth}}"`, schema.SetAuth())
str = strings.ReplaceAll(str, `"{{schema.GetToken}}"`, schema.GetToken())
str = strings.ReplaceAll(str, `"{{schema.CreateCertificateAuthority}}"`, schema.CreateCertificateAuthority())
str = strings.ReplaceAll(str, `"{{schema.UpdateCertificateAuthority}}"`, schema.UpdateCertificateAuthority())
str = strings.ReplaceAll(str, `"{{schema.CreateCertificate}}"`, schema.CreateCertificate())
str = strings.ReplaceAll(str, `"{{schema.UpdateCertificate}}"`, schema.UpdateCertificate(""))
str = strings.ReplaceAll(str, `"{{schema.CreateSetting}}"`, schema.CreateSetting())
str = strings.ReplaceAll(str, `"{{schema.UpdateSetting}}"`, schema.UpdateSetting())
str = strings.ReplaceAll(str, `"{{schema.CreateUser}}"`, schema.CreateUser())
str = strings.ReplaceAll(str, `"{{schema.UpdateUser}}"`, schema.UpdateUser())
str = strings.ReplaceAll(str, `"{{schema.CreateHost}}"`, schema.CreateHost())
str = strings.ReplaceAll(str, `"{{schema.UpdateHost}}"`, schema.UpdateHost())
str = strings.ReplaceAll(str, `"{{schema.CreateHostTemplate}}"`, schema.CreateHostTemplate())
str = strings.ReplaceAll(str, `"{{schema.UpdateHostTemplate}}"`, schema.UpdateHostTemplate())
str = strings.ReplaceAll(str, `"{{schema.CreateStream}}"`, schema.CreateStream())
str = strings.ReplaceAll(str, `"{{schema.UpdateStream}}"`, schema.UpdateStream())
str = strings.ReplaceAll(str, `"{{schema.CreateDNSProvider}}"`, schema.CreateDNSProvider())
str = strings.ReplaceAll(str, `"{{schema.UpdateDNSProvider}}"`, schema.UpdateDNSProvider())
return []byte(str)
}

@ -0,0 +1,98 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/setting"
"github.com/go-chi/chi"
)
// GetSettings will return a list of Settings
// Route: GET /settings
func GetSettings() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
settings, err := setting.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, settings)
}
}
}
// GetSetting will return a single Setting
// Route: GET /settings/{name}
func GetSetting() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
name := chi.URLParam(r, "name")
sett, err := setting.GetByName(name)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, sett)
}
}
}
// CreateSetting will create a Setting
// Route: POST /settings
func CreateSetting() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newSetting setting.Model
err := json.Unmarshal(bodyBytes, &newSetting)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = newSetting.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Setting: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newSetting)
}
}
// UpdateSetting updates a setting
// Route: PUT /settings/{name}
func UpdateSetting() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
settingName := chi.URLParam(r, "name")
setting, err := setting.GetByName(settingName)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &setting)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = setting.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, setting)
}
}
}

@ -0,0 +1,129 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/entity/stream"
)
// GetStreams will return a list of Streams
// Route: GET /hosts/streams
func GetStreams() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
hosts, err := stream.List(pageInfo, middleware.GetFiltersFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, hosts)
}
}
}
// GetStream will return a single Streams
// Route: GET /hosts/streams/{hostID}
func GetStream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := stream.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host)
}
}
}
// CreateStream will create a Stream
// Route: POST /hosts/steams
func CreateStream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newHost stream.Model
err := json.Unmarshal(bodyBytes, &newHost)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Get userID from token
userID, _ := r.Context().Value(c.UserIDCtxKey).(int)
newHost.UserID = userID
if err = newHost.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, fmt.Sprintf("Unable to save Stream: %s", err.Error()), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, newHost)
}
}
// UpdateStream updates a stream
// Route: PUT /hosts/streams/{hostID}
func UpdateStream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := stream.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &host)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = host.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
h.ResultResponseJSON(w, r, http.StatusOK, host)
}
}
}
// DeleteStream removes a stream
// Route: DELETE /hosts/streams/{hostID}
func DeleteStream() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var err error
var hostID int
if hostID, err = getURLParamInt(r, "hostID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
host, err := stream.GetByID(hostID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, host.Delete())
}
}
}

@ -0,0 +1,89 @@
package handler
import (
"encoding/json"
"net/http"
h "npm/internal/api/http"
"npm/internal/errors"
"npm/internal/logger"
"time"
c "npm/internal/api/context"
"npm/internal/entity/auth"
"npm/internal/entity/user"
njwt "npm/internal/jwt"
)
// tokenPayload is the structure we expect from a incoming login request
type tokenPayload struct {
Type string `json:"type"`
Identity string `json:"identity"`
Secret string `json:"secret"`
}
// NewToken Also known as a Login, requesting a new token with credentials
// Route: POST /tokens
func NewToken() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// Read the bytes from the body
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var payload tokenPayload
err := json.Unmarshal(bodyBytes, &payload)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
// Find user
userObj, userErr := user.GetByEmail(payload.Identity)
if userErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
return
}
if userObj.IsDisabled {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, errors.ErrUserDisabled.Error(), nil)
return
}
// Get Auth
authObj, authErr := auth.GetByUserIDType(userObj.ID, payload.Type)
if authErr != nil {
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), authErr.Error())
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
return
}
// Verify Auth
validateErr := authObj.ValidateSecret(payload.Secret)
if validateErr != nil {
logger.Debug("%s: %s", errors.ErrInvalidLogin.Error(), validateErr.Error())
// Sleep for 1 second to prevent brute force password guessing
time.Sleep(time.Second)
h.ResultErrorJSON(w, r, http.StatusBadRequest, errors.ErrInvalidLogin.Error(), nil)
return
}
if response, err := njwt.Generate(&userObj); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, response)
}
}
}
// RefreshToken an existing token by given them a new one with the same claims
// Route: GET /tokens
func RefreshToken() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// TODO: Use your own methods to verify an existing user is
// able to refresh their token and then give them a new one
userObj, _ := user.GetByEmail("jc@jc21.com")
if response, err := njwt.Generate(&userObj); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, response)
}
}
}

@ -0,0 +1,235 @@
package handler
import (
"encoding/json"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/api/middleware"
"npm/internal/config"
"npm/internal/entity/auth"
"npm/internal/entity/user"
"npm/internal/errors"
"npm/internal/logger"
"github.com/go-chi/chi"
)
// GetUsers returns all users
// Route: GET /users
func GetUsers() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
pageInfo, err := getPageInfoFromRequest(r)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
users, err := user.List(pageInfo, middleware.GetFiltersFromContext(r), getExpandFromContext(r))
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, users)
}
}
}
// GetUser returns a specific user
// Route: GET /users/{userID}
func GetUser() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
userID, _, userIDErr := getUserIDFromRequest(r)
if userIDErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
return
}
userObject, err := user.GetByID(userID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
// nolint: errcheck,gosec
userObject.Expand(getExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, userObject)
}
}
}
// UpdateUser updates a user
// Route: PUT /users/{userID}
func UpdateUser() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
userID, self, userIDErr := getUserIDFromRequest(r)
if userIDErr != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, userIDErr.Error(), nil)
return
}
userObject, err := user.GetByID(userID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
// nolint: errcheck,gosec
userObject.Expand([]string{"capabilities"})
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
err := json.Unmarshal(bodyBytes, &userObject)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if userObject.IsDisabled && self {
h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot disable yourself!", nil)
return
}
if err = userObject.Save(); err != nil {
if err == errors.ErrDuplicateEmailUser || err == errors.ErrSystemUserReadonly {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
logger.Error("UpdateUserError", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil)
}
return
}
if !self {
err = userObject.SaveCapabilities()
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
}
// nolint: errcheck,gosec
userObject.Expand(getExpandFromContext(r))
h.ResultResponseJSON(w, r, http.StatusOK, userObject)
}
}
}
// DeleteUser removes a user
// Route: DELETE /users/{userID}
func DeleteUser() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
var userID int
var err error
if userID, err = getURLParamInt(r, "userID"); err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
return
}
myUserID, _ := r.Context().Value(c.UserIDCtxKey).(int)
if myUserID == userID {
h.ResultErrorJSON(w, r, http.StatusBadRequest, "You cannot delete yourself!", nil)
return
}
user, err := user.GetByID(userID)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
h.ResultResponseJSON(w, r, http.StatusOK, user.Delete())
}
}
}
// CreateUser creates a user
// Route: POST /users
func CreateUser() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
var newUser user.Model
err := json.Unmarshal(bodyBytes, &newUser)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, h.ErrInvalidPayload.Error(), nil)
return
}
if err = newUser.Save(); err != nil {
if err == errors.ErrDuplicateEmailUser || err == errors.ErrSystemUserReadonly {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
logger.Error("UpdateUserError", err)
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save User", nil)
}
return
}
// Set the permissions to full-admin for this user
if !config.IsSetup {
newUser.Capabilities = []string{user.CapabilityFullAdmin}
}
// nolint: errcheck,gosec
err = newUser.SaveCapabilities()
if err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
return
}
// newUser has been saved, now save their auth
if newUser.Auth.Secret != "" && newUser.Auth.ID == 0 {
newUser.Auth.UserID = newUser.ID
if newUser.Auth.Type == auth.TypePassword {
err = newUser.Auth.SetPassword(newUser.Auth.Secret)
if err != nil {
logger.Error("SetPasswordError", err)
}
}
if err = newUser.Auth.Save(); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, "Unable to save Authentication for User", nil)
return
}
newUser.Auth.Secret = ""
}
if !config.IsSetup {
config.IsSetup = true
logger.Info("A new user was created, leaving Setup Mode")
}
h.ResultResponseJSON(w, r, http.StatusOK, newUser)
}
}
// DeleteUsers is only available in debug mode for cypress tests
// Route: DELETE /users
func DeleteUsers() func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
err := user.DeleteAll()
if err != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, err.Error(), nil)
} else {
// also change setup to true
config.IsSetup = false
logger.Info("Users have been wiped, entering Setup Mode")
h.ResultResponseJSON(w, r, http.StatusOK, true)
}
}
}
func getUserIDFromRequest(r *http.Request) (int, bool, error) {
userIDstr := chi.URLParam(r, "userID")
selfUserID, _ := r.Context().Value(c.UserIDCtxKey).(int)
var userID int
self := false
if userIDstr == "me" {
// Get user id from Token
userID = selfUserID
self = true
} else {
var userIDerr error
if userID, userIDerr = getURLParamInt(r, "userID"); userIDerr != nil {
return 0, false, userIDerr
}
self = selfUserID == userID
}
return userID, self, nil
}

@ -0,0 +1,46 @@
package http
import (
"context"
"encoding/json"
"errors"
"github.com/qri-io/jsonschema"
)
var (
// ErrInvalidJSON is an error for invalid json
ErrInvalidJSON = errors.New("JSON is invalid")
// ErrInvalidPayload is an error for invalid incoming data
ErrInvalidPayload = errors.New("Payload is invalid")
)
// ValidateRequestSchema takes a Schema and the Content to validate against it
func ValidateRequestSchema(schema string, requestBody []byte) ([]jsonschema.KeyError, error) {
var jsonErrors []jsonschema.KeyError
var schemaBytes = []byte(schema)
// Make sure the body is valid JSON
if !isJSON(requestBody) {
return jsonErrors, ErrInvalidJSON
}
rs := &jsonschema.Schema{}
if err := json.Unmarshal(schemaBytes, rs); err != nil {
return jsonErrors, err
}
var validationErr error
ctx := context.TODO()
if jsonErrors, validationErr = rs.ValidateBytes(ctx, requestBody); len(jsonErrors) > 0 {
return jsonErrors, validationErr
}
// Valid
return nil, nil
}
func isJSON(bytes []byte) bool {
var js map[string]interface{}
return json.Unmarshal(bytes, &js) == nil
}

@ -0,0 +1,91 @@
package http
import (
"encoding/json"
"fmt"
"net/http"
"reflect"
c "npm/internal/api/context"
"npm/internal/errors"
"npm/internal/logger"
"github.com/qri-io/jsonschema"
)
// Response interface for standard API results
type Response struct {
Result interface{} `json:"result"`
Error interface{} `json:"error,omitempty"`
}
// ErrorResponse interface for errors returned via the API
type ErrorResponse struct {
Code interface{} `json:"code"`
Message interface{} `json:"message"`
Invalid interface{} `json:"invalid,omitempty"`
}
// ResultResponseJSON will write the result as json to the http output
func ResultResponseJSON(w http.ResponseWriter, r *http.Request, status int, result interface{}) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(status)
var response Response
resultClass := fmt.Sprintf("%v", reflect.TypeOf(result))
if resultClass == "http.ErrorResponse" {
response = Response{
Error: result,
}
} else {
response = Response{
Result: result,
}
}
var payload []byte
var err error
if getPrettyPrintFromContext(r) {
payload, err = json.MarshalIndent(response, "", " ")
} else {
payload, err = json.Marshal(response)
}
if err != nil {
logger.Error("ResponseMarshalError", err)
}
fmt.Fprint(w, string(payload))
}
// ResultSchemaErrorJSON will format the result as a standard error object and send it for output
func ResultSchemaErrorJSON(w http.ResponseWriter, r *http.Request, errs []jsonschema.KeyError) {
errorResponse := ErrorResponse{
Code: http.StatusBadRequest,
Message: errors.ErrValidationFailed,
Invalid: errs,
}
ResultResponseJSON(w, r, http.StatusBadRequest, errorResponse)
}
// ResultErrorJSON will format the result as a standard error object and send it for output
func ResultErrorJSON(w http.ResponseWriter, r *http.Request, status int, message string, extended interface{}) {
errorResponse := ErrorResponse{
Code: status,
Message: message,
Invalid: extended,
}
ResultResponseJSON(w, r, status, errorResponse)
}
// getPrettyPrintFromContext returns the PrettyPrint setting
func getPrettyPrintFromContext(r *http.Request) bool {
pretty, ok := r.Context().Value(c.PrettyPrintCtxKey).(bool)
if !ok {
return false
}
return pretty
}

@ -0,0 +1,13 @@
package middleware
import (
"net/http"
)
// AccessControl sets http headers for responses
func AccessControl(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
next.ServeHTTP(w, r)
})
}

@ -0,0 +1,94 @@
package middleware
import (
"context"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/config"
"npm/internal/entity/user"
njwt "npm/internal/jwt"
"npm/internal/logger"
"npm/internal/util"
"github.com/go-chi/jwtauth"
)
// DecodeAuth decodes an auth header
func DecodeAuth() func(http.Handler) http.Handler {
privateKey, privateKeyParseErr := njwt.GetPrivateKey()
if privateKeyParseErr != nil && privateKey == nil {
logger.Error("PrivateKeyParseError", privateKeyParseErr)
}
publicKey, publicKeyParseErr := njwt.GetPublicKey()
if publicKeyParseErr != nil && publicKey == nil {
logger.Error("PublicKeyParseError", publicKeyParseErr)
}
tokenAuth := jwtauth.New("RS256", privateKey, publicKey)
return jwtauth.Verifier(tokenAuth)
}
// Enforce is a authentication middleware to enforce access from the
// jwtauth.Verifier middleware request context values. The Authenticator sends a 401 Unauthorised
// response for any unverified tokens and passes the good ones through.
func Enforce(permission string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
if config.IsSetup {
token, claims, err := jwtauth.FromContext(ctx)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, err.Error(), nil)
return
}
userID := int(claims["uid"].(float64))
_, enabled := user.IsEnabled(userID)
if token == nil || !token.Valid || !enabled {
h.ResultErrorJSON(w, r, http.StatusUnauthorized, "Unauthorised", nil)
return
}
// Check if permissions exist for this user
if permission != "" {
// Since the permission that we require is not on the token, we have to get it from the DB
// So we don't go crazy with hits, we will use a memory cache
cacheKey := fmt.Sprintf("userCapabilties.%v", userID)
cacheItem, found := AuthCache.Get(cacheKey)
var userCapabilities []string
if found {
userCapabilities = cacheItem.([]string)
} else {
// Get from db and store it
userCapabilities, err = user.GetCapabilities(userID)
if err != nil {
AuthCacheSet(cacheKey, userCapabilities)
}
}
// Now check that they have the permission in their admin capabilities
// full-admin can do anything
if !util.SliceContainsItem(userCapabilities, user.CapabilityFullAdmin) && !util.SliceContainsItem(userCapabilities, permission) {
// Access denied
logger.Debug("User has: %+v but needs %s", userCapabilities, permission)
h.ResultErrorJSON(w, r, http.StatusForbidden, "Forbidden", nil)
return
}
}
// Add claims to context
ctx = context.WithValue(ctx, c.UserIDCtxKey, userID)
}
// Token is authenticated, continue as normal
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

@ -0,0 +1,23 @@
package middleware
import (
"time"
"npm/internal/logger"
cache "github.com/patrickmn/go-cache"
)
// AuthCache is a cache item that stores the Admin API data for each admin that has been requesting endpoints
var AuthCache *cache.Cache
// AuthCacheInit will create a new Memory Cache
func AuthCacheInit() {
logger.Debug("Creating a new AuthCache")
AuthCache = cache.New(1*time.Minute, 5*time.Minute)
}
// AuthCacheSet will store the item in memory for the expiration time
func AuthCacheSet(k string, x interface{}) {
AuthCache.Set(k, x, cache.DefaultExpiration)
}

@ -0,0 +1,26 @@
package middleware
import (
"context"
"io/ioutil"
"net/http"
c "npm/internal/api/context"
)
// BodyContext simply adds the body data to a context item
func BodyContext() func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Grab the Body Data
var body []byte
if r.Body != nil {
body, _ = ioutil.ReadAll(r.Body)
}
// Add it to the context
ctx := r.Context()
ctx = context.WithValue(ctx, c.BodyCtxKey, body)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}

@ -0,0 +1,88 @@
package middleware
import (
"fmt"
"net/http"
"strings"
"github.com/go-chi/chi"
)
var methodMap = []string{
http.MethodGet,
http.MethodHead,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodConnect,
http.MethodTrace,
}
func getRouteMethods(routes chi.Router, path string) []string {
var methods []string
tctx := chi.NewRouteContext()
for _, method := range methodMap {
if routes.Match(tctx, method, path) {
methods = append(methods, method)
}
}
return methods
}
var headersAllowedByCORS = []string{
"Authorization",
"Host",
"Content-Type",
"Connection",
"User-Agent",
"Cache-Control",
"Accept-Encoding",
"X-Jumbo-AppKey",
"X-Jumbo-SKey",
"X-Jumbo-SV",
"X-Jumbo-Timestamp",
"X-Jumbo-Version",
"X-Jumbo-Customer-Id",
}
// Cors handles cors headers
func Cors(routes chi.Router) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
methods := getRouteMethods(routes, r.URL.Path)
if len(methods) == 0 {
// no route no cors
next.ServeHTTP(w, r)
return
}
methods = append(methods, http.MethodOptions)
w.Header().Set("Access-Control-Allow-Methods", strings.Join(methods, ","))
w.Header().Set("Access-Control-Allow-Headers",
strings.Join(headersAllowedByCORS, ","),
)
next.ServeHTTP(w, r)
})
}
}
// Options handles options requests
func Options(routes chi.Router) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
methods := getRouteMethods(routes, r.URL.Path)
if len(methods) == 0 {
// no route shouldn't have options
next.ServeHTTP(w, r)
return
}
if r.Method == http.MethodOptions {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, "{}")
return
}
next.ServeHTTP(w, r)
})
}
}

@ -0,0 +1,28 @@
package middleware
import (
"fmt"
"net/http"
h "npm/internal/api/http"
"npm/internal/config"
)
// EnforceSetup will error if the config setup doesn't match what is required
func EnforceSetup(shouldBeSetup bool) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config.IsSetup != shouldBeSetup {
state := "during"
if config.IsSetup {
state = "after"
}
h.ResultErrorJSON(w, r, http.StatusForbidden, fmt.Sprintf("Not available %s setup phase", state), nil)
return
}
// All good
next.ServeHTTP(w, r)
})
}
}

@ -0,0 +1,24 @@
package middleware
import (
"context"
"net/http"
"strings"
c "npm/internal/api/context"
)
// Expansion will determine whether the request should have objects expanded
// with ?expand=1 or ?expand=true
func Expansion(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
expandStr := r.URL.Query().Get("expand")
if expandStr != "" {
ctx := r.Context()
ctx = context.WithValue(ctx, c.ExpansionCtxKey, strings.Split(expandStr, ","))
next.ServeHTTP(w, r.WithContext(ctx))
} else {
next.ServeHTTP(w, r)
}
})
}

@ -0,0 +1,115 @@
package middleware
import (
"context"
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"npm/internal/model"
"npm/internal/util"
"strings"
"github.com/qri-io/jsonschema"
)
// Filters will accept a pre-defined schemaData to validate against the GET query params
// passed in to this endpoint. This will ensure that the filters are not injecting SQL.
// After we have determined what the Filters are to be, they are saved on the Context
// to be used later in other endpoints.
func Filters(schemaData string) func(http.Handler) http.Handler {
reservedFilterKeys := []string{
"limit",
"offset",
"sort",
"order",
"expand",
"t", // This is used as a timestamp paramater in some clients and can be ignored
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var filters []model.Filter
for key, val := range r.URL.Query() {
key = strings.ToLower(key)
// Split out the modifier from the field name and set a default modifier
var keyParts []string
keyParts = strings.Split(key, ":")
if len(keyParts) == 1 {
// Default modifier
keyParts = append(keyParts, "equals")
}
// Only use this filter if it's not a reserved get param
if !util.SliceContainsItem(reservedFilterKeys, keyParts[0]) {
for _, valItem := range val {
// Check that the val isn't empty
if len(strings.TrimSpace(valItem)) > 0 {
valSlice := []string{valItem}
if keyParts[1] == "in" || keyParts[1] == "notin" {
valSlice = strings.Split(valItem, ",")
}
filters = append(filters, model.Filter{
Field: keyParts[0],
Modifier: keyParts[1],
Value: valSlice,
})
}
}
}
}
// Only validate schema if there are filters to validate
if len(filters) > 0 {
ctx := r.Context()
// Marshal the Filters in to a JSON string so that the Schema Validation works against it
filterData, marshalErr := json.MarshalIndent(filters, "", " ")
if marshalErr != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", marshalErr), nil)
return
}
// Create root schema
rs := &jsonschema.Schema{}
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, fmt.Sprintf("Schema Fatal: %v", err), nil)
return
}
// Validate it
errors, jsonError := rs.ValidateBytes(ctx, filterData)
if jsonError != nil {
h.ResultErrorJSON(w, r, http.StatusBadRequest, jsonError.Error(), nil)
return
}
if len(errors) > 0 {
h.ResultErrorJSON(w, r, http.StatusBadRequest, "Invalid Filters", errors)
return
}
ctx = context.WithValue(ctx, c.FiltersCtxKey, filters)
next.ServeHTTP(w, r.WithContext(ctx))
} else {
next.ServeHTTP(w, r)
}
})
}
}
// GetFiltersFromContext returns the Filters
func GetFiltersFromContext(r *http.Request) []model.Filter {
filters, ok := r.Context().Value(c.FiltersCtxKey).([]model.Filter)
if !ok {
// the assertion failed
var emptyFilters []model.Filter
return emptyFilters
}
return filters
}

@ -0,0 +1,23 @@
package middleware
import (
"context"
"net/http"
c "npm/internal/api/context"
)
// PrettyPrint will determine whether the request should be pretty printed in output
// with ?pretty=1 or ?pretty=true
func PrettyPrint(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
prettyStr := r.URL.Query().Get("pretty")
if prettyStr == "1" || prettyStr == "true" {
ctx := r.Context()
ctx = context.WithValue(ctx, c.PrettyPrintCtxKey, true)
next.ServeHTTP(w, r.WithContext(ctx))
} else {
next.ServeHTTP(w, r)
}
})
}

@ -0,0 +1,55 @@
package middleware
import (
"context"
"encoding/json"
"fmt"
"net/http"
c "npm/internal/api/context"
h "npm/internal/api/http"
"github.com/qri-io/jsonschema"
)
// CheckRequestSchema checks the payload against schema
func CheckRequestSchema(ctx context.Context, schemaData string, payload []byte) ([]jsonschema.KeyError, error) {
// Create root schema
rs := &jsonschema.Schema{}
if err := json.Unmarshal([]byte(schemaData), rs); err != nil {
return nil, fmt.Errorf("Schema Fatal: %v", err)
}
// Validate it
schemaErrors, jsonError := rs.ValidateBytes(ctx, payload)
if jsonError != nil {
return nil, jsonError
}
return schemaErrors, nil
}
// EnforceRequestSchema accepts a schema and validates the request body against it
func EnforceRequestSchema(schemaData string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get content from context
bodyBytes, _ := r.Context().Value(c.BodyCtxKey).([]byte)
schemaErrors, err := CheckRequestSchema(r.Context(), schemaData, bodyBytes)
if err != nil {
h.ResultErrorJSON(w, r, http.StatusInternalServerError, err.Error(), nil)
return
}
if len(schemaErrors) > 0 {
h.ResultSchemaErrorJSON(w, r, schemaErrors)
return
}
// All good
next.ServeHTTP(w, r)
})
}
}

@ -0,0 +1,198 @@
package api
import (
"net/http"
"time"
"npm/internal/api/handler"
"npm/internal/api/middleware"
"npm/internal/api/schema"
"npm/internal/config"
"npm/internal/entity/certificate"
"npm/internal/entity/certificateauthority"
"npm/internal/entity/dnsprovider"
"npm/internal/entity/host"
"npm/internal/entity/hosttemplate"
"npm/internal/entity/setting"
"npm/internal/entity/stream"
"npm/internal/entity/user"
"npm/internal/logger"
"github.com/go-chi/chi"
chiMiddleware "github.com/go-chi/chi/middleware"
"github.com/go-chi/cors"
)
// NewRouter returns a new router object
func NewRouter() http.Handler {
// Cors
cors := cors.New(cors.Options{
AllowedOrigins: []string{"*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-Requested-With"},
AllowCredentials: true,
MaxAge: 300,
})
r := chi.NewRouter()
r.Use(
middleware.AccessControl,
middleware.Cors(r),
middleware.Options(r),
cors.Handler,
chiMiddleware.RealIP,
chiMiddleware.Recoverer,
chiMiddleware.Throttle(5),
chiMiddleware.Timeout(30*time.Second),
middleware.PrettyPrint,
middleware.Expansion,
middleware.DecodeAuth(),
middleware.BodyContext(),
)
return applyRoutes(r)
}
// applyRoutes is where the magic happens
func applyRoutes(r chi.Router) chi.Router {
middleware.AuthCacheInit()
r.NotFound(handler.NotFound())
r.MethodNotAllowed(handler.NotAllowed())
// API
r.Route("/api", func(r chi.Router) {
r.Get("/", handler.Health())
r.Get("/schema", handler.Schema())
r.With(middleware.EnforceSetup(true), middleware.Enforce("")).
Get("/config", handler.Config())
// Tokens
r.With(middleware.EnforceSetup(true)).Route("/tokens", func(r chi.Router) {
r.With(middleware.EnforceRequestSchema(schema.GetToken())).
Post("/", handler.NewToken())
r.With(middleware.Enforce("")).
Get("/", handler.RefreshToken())
})
// Users
r.Route("/users", func(r chi.Router) {
r.With(middleware.EnforceSetup(true), middleware.Enforce("")).Get("/{userID:(?:me)}", handler.GetUser())
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityUsersManage)).Get("/{userID:(?:[0-9]+)}", handler.GetUser())
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityUsersManage)).Delete("/{userID:(?:[0-9]+|me)}", handler.DeleteUser())
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityUsersManage)).With(middleware.Filters(user.GetFilterSchema())).
Get("/", handler.GetUsers())
r.With(middleware.EnforceRequestSchema(schema.CreateUser()), middleware.Enforce(user.CapabilityUsersManage)).
Post("/", handler.CreateUser())
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.UpdateUser()), middleware.Enforce("")).
Put("/{userID:(?:me)}", handler.UpdateUser())
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.UpdateUser()), middleware.Enforce(user.CapabilityUsersManage)).
Put("/{userID:(?:[0-9]+)}", handler.UpdateUser())
// Auth
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.SetAuth()), middleware.Enforce("")).
Post("/{userID:(?:me)}/auth", handler.SetAuth())
r.With(middleware.EnforceSetup(true)).With(middleware.EnforceRequestSchema(schema.SetAuth()), middleware.Enforce(user.CapabilityUsersManage)).
Post("/{userID:(?:[0-9]+)}/auth", handler.SetAuth())
})
// Only available in debug mode: delete users without auth
if config.GetLogLevel() == logger.DebugLevel {
r.Delete("/users", handler.DeleteUsers())
}
// Settings
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilitySettingsManage)).Route("/settings", func(r chi.Router) {
r.With(middleware.Filters(setting.GetFilterSchema())).
Get("/", handler.GetSettings())
r.Get("/{name}", handler.GetSetting())
r.With(middleware.EnforceRequestSchema(schema.CreateSetting())).
Post("/", handler.CreateSetting())
r.With(middleware.EnforceRequestSchema(schema.UpdateSetting())).
Put("/{name}", handler.UpdateSetting())
})
// DNS Providers
r.With(middleware.EnforceSetup(true)).Route("/dns-providers", func(r chi.Router) {
r.With(middleware.Filters(dnsprovider.GetFilterSchema()), middleware.Enforce(user.CapabilityDNSProvidersView)).
Get("/", handler.GetDNSProviders())
r.With(middleware.Enforce(user.CapabilityDNSProvidersView)).Get("/{providerID:[0-9]+}", handler.GetDNSProvider())
r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).Delete("/{providerID:[0-9]+}", handler.DeleteDNSProvider())
r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).With(middleware.EnforceRequestSchema(schema.CreateDNSProvider())).
Post("/", handler.CreateDNSProvider())
r.With(middleware.Enforce(user.CapabilityDNSProvidersManage)).With(middleware.EnforceRequestSchema(schema.UpdateDNSProvider())).
Put("/{providerID:[0-9]+}", handler.UpdateDNSProvider())
r.With(middleware.EnforceSetup(true), middleware.Enforce(user.CapabilityDNSProvidersView)).Route("/acmesh", func(r chi.Router) {
r.Get("/{acmeshID:[a-z0-9_]+}", handler.GetAcmeshProvider())
r.Get("/", handler.GetAcmeshProviders())
})
})
// Certificate Authorities
r.With(middleware.EnforceSetup(true)).Route("/certificate-authorities", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesView), middleware.Filters(certificateauthority.GetFilterSchema())).
Get("/", handler.GetCertificateAuthorities())
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesView)).Get("/{caID:[0-9]+}", handler.GetCertificateAuthority())
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).Delete("/{caID:[0-9]+}", handler.DeleteCertificateAuthority())
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).With(middleware.EnforceRequestSchema(schema.CreateCertificateAuthority())).
Post("/", handler.CreateCertificateAuthority())
r.With(middleware.Enforce(user.CapabilityCertificateAuthoritiesManage)).With(middleware.EnforceRequestSchema(schema.UpdateCertificateAuthority())).
Put("/{caID:[0-9]+}", handler.UpdateCertificateAuthority())
})
// Certificates
r.With(middleware.EnforceSetup(true)).Route("/certificates", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityCertificatesView), middleware.Filters(certificate.GetFilterSchema())).
Get("/", handler.GetCertificates())
r.With(middleware.Enforce(user.CapabilityCertificatesView)).Get("/{certificateID:[0-9]+}", handler.GetCertificate())
r.With(middleware.Enforce(user.CapabilityCertificatesManage)).Delete("/{certificateID:[0-9]+}", handler.DeleteCertificate())
r.With(middleware.Enforce(user.CapabilityCertificatesManage)).With(middleware.EnforceRequestSchema(schema.CreateCertificate())).
Post("/", handler.CreateCertificate())
/*
r.With(middleware.EnforceRequestSchema(schema.UpdateCertificate())).
Put("/{certificateID:[0-9]+}", handler.UpdateCertificate())
*/
r.With(middleware.Enforce(user.CapabilityCertificatesManage)).Put("/{certificateID:[0-9]+}", handler.UpdateCertificate())
})
// Hosts
r.With(middleware.EnforceSetup(true)).Route("/hosts", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityHostsView), middleware.Filters(host.GetFilterSchema())).
Get("/", handler.GetHosts())
r.With(middleware.Enforce(user.CapabilityHostsView)).Get("/{hostID:[0-9]+}", handler.GetHost())
r.With(middleware.Enforce(user.CapabilityHostsManage)).Delete("/{hostID:[0-9]+}", handler.DeleteHost())
r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.CreateHost())).
Post("/", handler.CreateHost())
r.With(middleware.Enforce(user.CapabilityHostsManage)).With(middleware.EnforceRequestSchema(schema.UpdateHost())).
Put("/{hostID:[0-9]+}", handler.UpdateHost())
})
// Host Templates
r.With(middleware.EnforceSetup(true)).Route("/host-templates", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityHostTemplatesView), middleware.Filters(hosttemplate.GetFilterSchema())).
Get("/", handler.GetHostTemplates())
r.With(middleware.Enforce(user.CapabilityHostTemplatesView)).Get("/{templateID:[0-9]+}", handler.GetHostTemplates())
r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).Delete("/{templateID:[0-9]+}", handler.DeleteHostTemplate())
r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.CreateHostTemplate())).
Post("/", handler.CreateHostTemplate())
r.With(middleware.Enforce(user.CapabilityHostTemplatesManage)).With(middleware.EnforceRequestSchema(schema.UpdateHostTemplate())).
Put("/{templateID:[0-9]+}", handler.UpdateHostTemplate())
})
// Streams
r.With(middleware.EnforceSetup(true)).Route("/streams", func(r chi.Router) {
r.With(middleware.Enforce(user.CapabilityStreamsView), middleware.Filters(stream.GetFilterSchema())).
Get("/", handler.GetStreams())
r.With(middleware.Enforce(user.CapabilityStreamsView)).Get("/{hostID:[0-9]+}", handler.GetStream())
r.With(middleware.Enforce(user.CapabilityStreamsManage)).Delete("/{hostID:[0-9]+}", handler.DeleteStream())
r.With(middleware.Enforce(user.CapabilityStreamsManage)).With(middleware.EnforceRequestSchema(schema.CreateStream())).
Post("/", handler.CreateStream())
r.With(middleware.Enforce(user.CapabilityStreamsManage)).With(middleware.EnforceRequestSchema(schema.UpdateStream())).
Put("/{hostID:[0-9]+}", handler.UpdateStream())
})
})
return r
}

@ -0,0 +1,44 @@
package api
import (
"net/http"
"net/http/httptest"
"os"
"testing"
"npm/internal/config"
"github.com/stretchr/testify/assert"
)
var (
r = NewRouter()
version = "3.0.0"
commit = "abcdefgh"
sentryDSN = ""
)
// Tear up/down
func TestMain(m *testing.M) {
config.Init(&version, &commit, &sentryDSN)
code := m.Run()
os.Exit(code)
}
func TestGetHealthz(t *testing.T) {
respRec := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/", nil)
r.ServeHTTP(respRec, req)
assert.Equal(t, http.StatusOK, respRec.Code)
assert.Contains(t, respRec.Body.String(), "healthy")
}
func TestNonExistent(t *testing.T) {
respRec := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/non-existent-endpoint", nil)
r.ServeHTTP(respRec, req)
assert.Equal(t, http.StatusNotFound, respRec.Code)
assert.Equal(t, respRec.Body.String(), `{"result":null,"error":{"code":404,"message":"Not found"}}`, "404 Message should match")
}

@ -0,0 +1,209 @@
package schema
import (
"fmt"
"npm/internal/entity/certificate"
)
// This validation is strictly for Custom certificates
// and the combination of values that must be defined
func createCertificateCustom() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"type",
"name",
"domain_names"
],
"properties": {
"type": %s,
"name": %s,
"domain_names": %s,
"meta": {
"type": "object"
}
}
}`, strictString("custom"), stringMinMax(1, 100), domainNames())
}
// This validation is strictly for HTTP certificates
// and the combination of values that must be defined
func createCertificateHTTP() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"type",
"certificate_authority_id",
"name",
"domain_names"
],
"properties": {
"type": %s,
"certificate_authority_id": %s,
"name": %s,
"domain_names": %s,
"meta": {
"type": "object"
},
"is_ecc": {
"type": "integer",
"minimum": 0,
"maximum": 1
}
}
}`, strictString("http"), intMinOne, stringMinMax(1, 100), domainNames())
}
// This validation is strictly for DNS certificates
// and the combination of values that must be defined
func createCertificateDNS() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"type",
"certificate_authority_id",
"dns_provider_id",
"name",
"domain_names"
],
"properties": {
"type": %s,
"certificate_authority_id": %s,
"dns_provider_id": %s,
"name": %s,
"domain_names": %s,
"meta": {
"type": "object"
},
"is_ecc": {
"type": "integer",
"minimum": 0,
"maximum": 1
}
}
}`, strictString("dns"), intMinOne, intMinOne, stringMinMax(1, 100), domainNames())
}
// This validation is strictly for MKCERT certificates
// and the combination of values that must be defined
func createCertificateMkcert() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"type",
"name",
"domain_names"
],
"properties": {
"type": %s,
"name": %s,
"domain_names": %s,
"meta": {
"type": "object"
}
}
}`, strictString("mkcert"), stringMinMax(1, 100), domainNames())
}
func updateCertificateHTTP() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"certificate_authority_id": %s,
"name": %s,
"domain_names": %s,
"meta": {
"type": "object"
}
}
}`, intMinOne, stringMinMax(1, 100), domainNames())
}
func updateCertificateDNS() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"certificate_authority_id": %s,
"dns_provider_id": %s,
"name": %s,
"domain_names": %s,
"meta": {
"type": "object"
}
}
}`, intMinOne, intMinOne, stringMinMax(1, 100), domainNames())
}
func updateCertificateCustom() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"name": %s,
"domain_names": %s,
"meta": {
"type": "object"
}
}
}`, stringMinMax(1, 100), domainNames())
}
func updateCertificateMkcert() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"name": %s,
"domain_names": %s,
"meta": {
"type": "object"
}
}
}`, stringMinMax(1, 100), domainNames())
}
// CreateCertificate is the schema for incoming data validation
func CreateCertificate() string {
return fmt.Sprintf(`
{
"oneOf": [%s, %s, %s, %s]
}`, createCertificateHTTP(), createCertificateDNS(), createCertificateCustom(), createCertificateMkcert())
}
// UpdateCertificate is the schema for incoming data validation
func UpdateCertificate(certificateType string) string {
switch certificateType {
case certificate.TypeHTTP:
return updateCertificateHTTP()
case certificate.TypeDNS:
return updateCertificateDNS()
case certificate.TypeCustom:
return updateCertificateCustom()
case certificate.TypeMkcert:
return updateCertificateMkcert()
default:
return fmt.Sprintf(`
{
"oneOf": [%s, %s, %s, %s]
}`, updateCertificateHTTP(), updateCertificateDNS(), updateCertificateCustom(), updateCertificateMkcert())
}
}

@ -0,0 +1,70 @@
package schema
import "fmt"
func strictString(value string) string {
return fmt.Sprintf(`{
"type": "string",
"pattern": "^%s$"
}`, value)
}
const intMinOne = `
{
"type": "integer",
"minimum": 1
}
`
const boolean = `
{
"type": "boolean"
}
`
func stringMinMax(minLength, maxLength int) string {
return fmt.Sprintf(`{
"type": "string",
"minLength": %d,
"maxLength": %d
}`, minLength, maxLength)
}
func capabilties() string {
return `{
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"minLength": 1
}
}`
}
func domainNames() string {
return fmt.Sprintf(`
{
"type": "array",
"minItems": 1,
"items": %s
}`, stringMinMax(4, 255))
}
const anyType = `
{
"anyOf": [
{
"type": "array"
},
{
"type": "boolean"
},
{
"type": "object"
},
{
"type": "integer"
}
]
}
`

@ -0,0 +1,25 @@
package schema
import "fmt"
// CreateCertificateAuthority is the schema for incoming data validation
func CreateCertificateAuthority() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"name",
"acmesh_server",
"max_domains"
],
"properties": {
"name": %s,
"acmesh_server": %s,
"max_domains": %s,
"ca_bundle": %s,
"is_wildcard_supported": %s
}
}
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne, stringMinMax(2, 255), boolean)
}

@ -0,0 +1,51 @@
package schema
import (
"fmt"
"strings"
"npm/internal/dnsproviders"
"npm/internal/util"
)
// CreateDNSProvider is the schema for incoming data validation
func CreateDNSProvider() string {
allProviders := dnsproviders.GetAll()
fmtStr := fmt.Sprintf(`{"oneOf": [%s]}`, strings.TrimRight(strings.Repeat("\n%s,", len(allProviders)), ","))
allSchemasWrapped := make([]string, 0)
for providerName, provider := range allProviders {
allSchemasWrapped = append(allSchemasWrapped, createDNSProviderType(providerName, provider.Schema))
}
return fmt.Sprintf(fmtStr, util.ConvertStringSliceToInterface(allSchemasWrapped)...)
}
func createDNSProviderType(name, metaSchema string) string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"acmesh_name",
"name",
"meta"
],
"properties": {
"acmesh_name": {
"type": "string",
"pattern": "^%s$"
},
"name": {
"type": "string",
"minLength": 1,
"maxLength": 100
},
"dns_sleep": {
"type": "integer"
},
"meta": %s
}
}
`, name, metaSchema)
}

@ -0,0 +1,80 @@
package schema
import "fmt"
// CreateHost is the schema for incoming data validation
// This schema supports 3 possible types with different data combinations:
// - proxy
// - redirection
// - dead
func CreateHost() string {
return fmt.Sprintf(`
{
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": [
"type",
"domain_names",
"host_template_id"
],
"properties": {
"type": {
"type": "string",
"pattern": "^proxy$"
},
"host_template_id": {
"type": "integer",
"minimum": 1
},
"listen_interface": %s,
"domain_names": %s,
"upstream_id": {
"type": "integer"
},
"certificate_id": {
"type": "integer"
},
"access_list_id": {
"type": "integer"
},
"ssl_forced": {
"type": "boolean"
},
"caching_enabled": {
"type": "boolean"
},
"block_exploits": {
"type": "boolean"
},
"allow_websocket_upgrade": {
"type": "boolean"
},
"http2_support": {
"type": "boolean"
},
"hsts_enabled": {
"type": "boolean"
},
"hsts_subdomains": {
"type": "boolean"
},
"paths": {
"type": "string"
},
"upstream_options": {
"type": "string"
},
"advanced_config": {
"type": "string"
},
"is_disabled": {
"type": "boolean"
}
}
}
]
}
`, stringMinMax(0, 255), domainNames())
}

@ -0,0 +1,30 @@
package schema
// CreateHostTemplate is the schema for incoming data validation
func CreateHostTemplate() string {
return `
{
"type": "object",
"additionalProperties": false,
"required": [
"name",
"host_type",
"template"
],
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"host_type": {
"type": "string",
"pattern": "^proxy|redirect|dead|stream$"
},
"template": {
"type": "string",
"minLength": 20
}
}
}
`
}

@ -0,0 +1,21 @@
package schema
import "fmt"
// CreateSetting is the schema for incoming data validation
func CreateSetting() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"name",
"value"
],
"properties": {
"name": %s,
"value": %s
}
}
`, stringMinMax(2, 100), anyType)
}

@ -0,0 +1,27 @@
package schema
import "fmt"
// CreateStream is the schema for incoming data validation
func CreateStream() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"provider",
"name",
"domain_names"
],
"properties": {
"provider": %s,
"name": %s,
"domain_names": %s,
"expires_on": %s,
"meta": {
"type": "object"
}
}
}
`, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne)
}

@ -0,0 +1,42 @@
package schema
import "fmt"
// CreateUser is the schema for incoming data validation
func CreateUser() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"name",
"email",
"is_disabled",
"capabilities"
],
"properties": {
"name": %s,
"nickname": %s,
"email": %s,
"is_disabled": {
"type": "boolean"
},
"auth": {
"type": "object",
"required": [
"type",
"secret"
],
"properties": {
"type": {
"type": "string",
"pattern": "^password$"
},
"secret": %s
}
},
"capabilities": %s
}
}
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), stringMinMax(8, 255), capabilties())
}

@ -0,0 +1,28 @@
package schema
import "fmt"
// GetToken is the schema for incoming data validation
// nolint: gosec
func GetToken() string {
stdField := stringMinMax(1, 255)
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"type",
"identity",
"secret"
],
"properties": {
"type": {
"type": "string",
"pattern": "^password$"
},
"identity": %s,
"secret": %s
}
}
`, stdField, stdField)
}

@ -0,0 +1,25 @@
package schema
import "fmt"
// SetAuth is the schema for incoming data validation
func SetAuth() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"required": [
"type",
"secret"
],
"properties": {
"type": {
"type": "string",
"pattern": "^password$"
},
"secret": %s,
"current_secret": %s
}
}
`, stringMinMax(8, 225), stringMinMax(8, 225))
}

@ -0,0 +1,21 @@
package schema
import "fmt"
// UpdateCertificateAuthority is the schema for incoming data validation
func UpdateCertificateAuthority() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"name": %s,
"acmesh_server": %s,
"max_domains": %s,
"ca_bundle": %s,
"is_wildcard_supported": %s
}
}
`, stringMinMax(1, 100), stringMinMax(2, 255), intMinOne, stringMinMax(2, 255), boolean)
}

@ -0,0 +1,20 @@
package schema
import "fmt"
// UpdateDNSProvider is the schema for incoming data validation
func UpdateDNSProvider() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"name": %s,
"meta": {
"type": "object"
}
}
}
`, stringMinMax(1, 100))
}

@ -0,0 +1,27 @@
package schema
import "fmt"
// UpdateHost is the schema for incoming data validation
func UpdateHost() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"host_template_id": {
"type": "integer",
"minimum": 1
},
"provider": %s,
"name": %s,
"domain_names": %s,
"expires_on": %s,
"meta": {
"type": "object"
}
}
}
`, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne)
}

@ -0,0 +1,22 @@
package schema
// UpdateHostTemplate is the schema for incoming data validation
func UpdateHostTemplate() string {
return `
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"template": {
"type": "string",
"minLength": 20
}
}
}
`
}

@ -0,0 +1,17 @@
package schema
import "fmt"
// UpdateSetting is the schema for incoming data validation
func UpdateSetting() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"value": %s
}
}
`, anyType)
}

@ -0,0 +1,23 @@
package schema
import "fmt"
// UpdateStream is the schema for incoming data validation
func UpdateStream() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"provider": %s,
"name": %s,
"domain_names": %s,
"expires_on": %s,
"meta": {
"type": "object"
}
}
}
`, stringMinMax(2, 100), stringMinMax(1, 100), domainNames(), intMinOne)
}

@ -0,0 +1,23 @@
package schema
import "fmt"
// UpdateUser is the schema for incoming data validation
func UpdateUser() string {
return fmt.Sprintf(`
{
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"name": %s,
"nickname": %s,
"email": %s,
"is_disabled": {
"type": "boolean"
},
"capabilities": %s
}
}
`, stringMinMax(2, 100), stringMinMax(2, 100), stringMinMax(5, 150), capabilties())
}

@ -0,0 +1,19 @@
package api
import (
"fmt"
"net/http"
"npm/internal/logger"
)
const httpPort = 3000
// StartServer creates a http server
func StartServer() {
logger.Info("Server starting on port %v", httpPort)
err := http.ListenAndServe(fmt.Sprintf(":%v", httpPort), NewRouter())
if err != nil {
logger.Error("HttpListenError", err)
}
}

@ -1,78 +0,0 @@
const error = require('../lib/error');
const auditLogModel = require('../models/audit-log');
const internalAuditLog = {
/**
* All logs
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('auditlog:list')
.then(() => {
let query = auditLogModel
.query()
.orderBy('created_on', 'DESC')
.orderBy('id', 'DESC')
.limit(100)
.allowEager('[user]');
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('meta', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.eager('[' + expand.join(', ') + ']');
}
return query;
});
},
/**
* This method should not be publicly used, it doesn't check certain things. It will be assumed
* that permission to add to audit log is already considered, however the access token is used for
* default user id determination.
*
* @param {Access} access
* @param {Object} data
* @param {String} data.action
* @param {Number} [data.user_id]
* @param {Number} [data.object_id]
* @param {Number} [data.object_type]
* @param {Object} [data.meta]
* @returns {Promise}
*/
add: (access, data) => {
return new Promise((resolve, reject) => {
// Default the user id
if (typeof data.user_id === 'undefined' || !data.user_id) {
data.user_id = access.token.getUserId(1);
}
if (typeof data.action === 'undefined' || !data.action) {
reject(new error.InternalValidationError('Audit log entry must contain an Action'));
} else {
// Make sure at least 1 of the IDs are set and action
resolve(auditLogModel
.query()
.insert({
user_id: data.user_id,
action: data.action,
object_type: data.object_type || '',
object_id: data.object_id || 0,
meta: data.meta || {}
}));
}
});
}
};
module.exports = internalAuditLog;

51
backend/internal/cache/cache.go vendored Normal file

@ -0,0 +1,51 @@
package cache
import (
"time"
"npm/internal/entity/setting"
"npm/internal/logger"
)
// Cache is a memory cache
type Cache struct {
Settings *map[string]setting.Model
}
// Status is the status of last update
type Status struct {
LastUpdate time.Time
Valid bool
}
// NewCache will create and return a new Cache object
func NewCache() *Cache {
return &Cache{
Settings: nil,
}
}
// Refresh will refresh all cache items
func (c *Cache) Refresh() {
c.RefreshSettings()
}
// Clear will clear the cache
func (c *Cache) Clear() {
c.Settings = nil
}
// RefreshSettings will refresh the settings from db
func (c *Cache) RefreshSettings() {
logger.Info("Cache refreshing Settings")
/*
c.ProductOffers = client.GetProductOffers()
if c.ProductOffers != nil {
c.Status["product_offers"] = Status{
LastUpdate: time.Now(),
Valid: true,
}
}
*/
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,28 @@
package config
import (
"fmt"
"os"
"github.com/alexflint/go-arg"
)
// ArgConfig is the settings for passing arguments to the command
type ArgConfig struct {
Version bool `arg:"-v" help:"print version and exit"`
}
var (
appArguments ArgConfig
)
// InitArgs will parse arg vars
func InitArgs(version, commit *string) {
// nolint: errcheck, gosec
arg.MustParse(&appArguments)
if appArguments.Version {
fmt.Printf("v%s (%s)\n", *version, *commit)
os.Exit(0)
}
}

@ -0,0 +1,79 @@
package config
import (
"fmt"
golog "log"
"runtime"
"npm/internal/logger"
"github.com/getsentry/sentry-go"
"github.com/vrischmann/envconfig"
)
// Init will parse environment variables into the Env struct
func Init(version, commit, sentryDSN *string) {
// ErrorReporting is enabled until we load the status of it from the DB later
ErrorReporting = true
Version = *version
Commit = *commit
if err := envconfig.InitWithPrefix(&Configuration, "NPM"); err != nil {
fmt.Printf("%+v\n", err)
}
initLogger(*sentryDSN)
logger.Info("Build Version: %s (%s)", Version, Commit)
createDataFolders()
loadKeys()
}
// Init initialises the Log object and return it
func initLogger(sentryDSN string) {
// this removes timestamp prefixes from logs
golog.SetFlags(0)
switch Configuration.Log.Level {
case "debug":
logLevel = logger.DebugLevel
case "warn":
logLevel = logger.WarnLevel
case "error":
logLevel = logger.ErrorLevel
default:
logLevel = logger.InfoLevel
}
err := logger.Configure(&logger.Config{
LogThreshold: logLevel,
Formatter: Configuration.Log.Format,
SentryConfig: sentry.ClientOptions{
// This is the jc21 NginxProxyManager Sentry project,
// errors will be reported here (if error reporting is enable)
// and this project is private. No personal information should
// be sent in any error messages, only stacktraces.
Dsn: sentryDSN,
Release: Commit,
Dist: Version,
Environment: fmt.Sprintf("%s-%s", runtime.GOOS, runtime.GOARCH),
},
})
if err != nil {
logger.Error("LoggerConfigurationError", err)
}
}
// GetLogLevel returns the logger const level
func GetLogLevel() logger.Level {
return logLevel
}
func isError(errorClass string, err error) bool {
if err != nil {
logger.Error(errorClass, err)
return true
}
return false
}

@ -0,0 +1,34 @@
package config
import (
"fmt"
"npm/internal/logger"
"os"
)
// createDataFolders will recursively create these folders within the
// data folder defined in configuration.
func createDataFolders() {
folders := []string{
"access",
"certificates",
"logs",
// Acme.sh:
Configuration.Acmesh.GetWellknown(),
// Nginx:
"nginx/hosts",
"nginx/streams",
"nginx/temp",
}
for _, folder := range folders {
path := folder
if path[0:1] != "/" {
path = fmt.Sprintf("%s/%s", Configuration.DataFolder, folder)
}
logger.Debug("Creating folder: %s", path)
if err := os.MkdirAll(path, os.ModePerm); err != nil {
logger.Error("CreateDataFolderError", err)
}
}
}

@ -0,0 +1,112 @@
package config
import (
"bytes"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/asn1"
"encoding/pem"
"fmt"
"io/ioutil"
"os"
"npm/internal/logger"
)
var keysFolder string
var publicKeyFile string
var privateKeyFile string
func loadKeys() {
// check if keys folder exists in data folder
keysFolder = fmt.Sprintf("%s/keys", Configuration.DataFolder)
publicKeyFile = fmt.Sprintf("%s/public.key", keysFolder)
privateKeyFile = fmt.Sprintf("%s/private.key", keysFolder)
if _, err := os.Stat(keysFolder); os.IsNotExist(err) {
// nolint:errcheck,gosec
os.Mkdir(keysFolder, 0700)
}
// check if keys exist on disk
_, publicKeyErr := os.Stat(publicKeyFile)
_, privateKeyErr := os.Stat(privateKeyFile)
// generate keys if either one doesn't exist
if os.IsNotExist(publicKeyErr) || os.IsNotExist(privateKeyErr) {
generateKeys()
saveKeys()
}
// Load keys from disk
// nolint:gosec
publicKeyBytes, publicKeyBytesErr := ioutil.ReadFile(publicKeyFile)
// nolint:gosec
privateKeyBytes, privateKeyBytesErr := ioutil.ReadFile(privateKeyFile)
PublicKey = string(publicKeyBytes)
PrivateKey = string(privateKeyBytes)
if isError("PublicKeyReadError", publicKeyBytesErr) || isError("PrivateKeyReadError", privateKeyBytesErr) || PublicKey == "" || PrivateKey == "" {
logger.Warn("There was an error loading keys, proceeding to generate new RSA keys")
generateKeys()
saveKeys()
}
}
func generateKeys() {
reader := rand.Reader
bitSize := 4096
key, err := rsa.GenerateKey(reader, bitSize)
if isError("RSAGenerateError", err) {
return
}
privateKey := &pem.Block{
Type: "PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(key),
}
privateKeyBuffer := new(bytes.Buffer)
err = pem.Encode(privateKeyBuffer, privateKey)
if isError("PrivatePEMEncodeError", err) {
return
}
asn1Bytes, err2 := asn1.Marshal(key.PublicKey)
if isError("RSAMarshalError", err2) {
return
}
publicKey := &pem.Block{
Type: "PUBLIC KEY",
Bytes: asn1Bytes,
}
publicKeyBuffer := new(bytes.Buffer)
err = pem.Encode(publicKeyBuffer, publicKey)
if isError("PublicPEMEncodeError", err) {
return
}
PublicKey = publicKeyBuffer.String()
PrivateKey = privateKeyBuffer.String()
logger.Info("Generated new RSA keys")
}
func saveKeys() {
err := ioutil.WriteFile(publicKeyFile, []byte(PublicKey), 0600)
if err != nil {
logger.Error("PublicKeyWriteError", err)
} else {
logger.Info("Saved Public Key: %s", publicKeyFile)
}
err = ioutil.WriteFile(privateKeyFile, []byte(PrivateKey), 0600)
if err != nil {
logger.Error("PrivateKeyWriteError", err)
} else {
logger.Info("Saved Private Key: %s", privateKeyFile)
}
}

@ -0,0 +1,49 @@
package config
import (
"fmt"
"npm/internal/logger"
)
// Version is the version set by ldflags
var Version string
// Commit is the git commit set by ldflags
var Commit string
// IsSetup defines whether we have an admin user or not
var IsSetup bool
// ErrorReporting defines whether we will send errors to Sentry
var ErrorReporting bool
// PublicKey is the public key
var PublicKey string
// PrivateKey is the private key
var PrivateKey string
var logLevel logger.Level
type log struct {
Level string `json:"level" envconfig:"optional,default=info"`
Format string `json:"format" envconfig:"optional,default=nice"`
}
type acmesh struct {
Home string `json:"home" envconfig:"optional,default=/data/.acme.sh"`
ConfigHome string `json:"config_home" envconfig:"optional,default=/data/.acme.sh/config"`
CertHome string `json:"cert_home" envconfig:"optional,default=/data/.acme.sh/certs"`
}
// Configuration is the main configuration object
var Configuration struct {
DataFolder string `json:"data_folder" envconfig:"optional,default=/data"`
Acmesh acmesh `json:"acmesh"`
Log log `json:"log"`
}
// GetWellknown returns the well known path
func (a *acmesh) GetWellknown() string {
return fmt.Sprintf("%s/.well-known", a.Home)
}

@ -0,0 +1,46 @@
package database
import (
"fmt"
"strings"
"npm/internal/errors"
"npm/internal/model"
"npm/internal/util"
)
const (
// DateFormat for DateFormat
DateFormat = "2006-01-02"
// DateTimeFormat for DateTimeFormat
DateTimeFormat = "2006-01-02T15:04:05"
)
// GetByQuery returns a row given a query, populating the model given
func GetByQuery(model interface{}, query string, params []interface{}) error {
db := GetInstance()
if db != nil {
err := db.Get(model, query, params...)
return err
}
return errors.ErrDatabaseUnavailable
}
// BuildOrderBySQL takes a `Sort` slice and constructs a query fragment
func BuildOrderBySQL(columns []string, sort *[]model.Sort) (string, []model.Sort) {
var sortStrings []string
var newSort []model.Sort
for _, sortItem := range *sort {
if util.SliceContainsItem(columns, sortItem.Field) {
sortStrings = append(sortStrings, fmt.Sprintf("`%s` %s", sortItem.Field, sortItem.Direction))
newSort = append(newSort, sortItem)
}
}
if len(sortStrings) > 0 {
return fmt.Sprintf("ORDER BY %s", strings.Join(sortStrings, ", ")), newSort
}
return "", newSort
}

@ -0,0 +1,202 @@
package database
import (
"database/sql"
"fmt"
"io/fs"
"path"
"path/filepath"
"strings"
"sync"
"time"
"npm/embed"
"npm/internal/logger"
"npm/internal/util"
"github.com/jmoiron/sqlx"
)
// MigrationConfiguration options for the migrator.
type MigrationConfiguration struct {
Table string `json:"table"`
mux sync.Mutex
}
// Default migrator configuration
var mConfiguration = MigrationConfiguration{
Table: "migration",
}
// ConfigureMigrator and will return error if missing required fields.
func ConfigureMigrator(c *MigrationConfiguration) error {
// ensure updates to the config are atomic
mConfiguration.mux.Lock()
defer mConfiguration.mux.Unlock()
if c == nil {
return fmt.Errorf("a non nil Configuration is mandatory")
}
if strings.TrimSpace(c.Table) != "" {
mConfiguration.Table = c.Table
}
mConfiguration.Table = c.Table
return nil
}
type afterMigrationComplete func()
// Migrate will perform the migration from start to finish
func Migrate(followup afterMigrationComplete) bool {
logger.Info("Migration: Started")
// Try to connect to the database sleeping for 15 seconds in between
var db *sqlx.DB
for {
db = GetInstance()
if db == nil {
logger.Warn("Database is unavailable for migration, retrying in 15 seconds")
time.Sleep(15 * time.Second)
} else {
break
}
}
// Check for migration table existence
if !tableExists(db, mConfiguration.Table) {
err := createMigrationTable(db)
if err != nil {
logger.Error("MigratorError", err)
return false
}
logger.Info("Migration: Migration Table created")
}
// DO MIGRATION
migrationCount, migrateErr := performFileMigrations(db)
if migrateErr != nil {
logger.Error("MigratorError", migrateErr)
}
if migrateErr == nil {
logger.Info("Migration: Completed %v migration files", migrationCount)
followup()
return true
}
return false
}
// createMigrationTable performs a query to create the migration table
// with the name specified in the configuration
func createMigrationTable(db *sqlx.DB) error {
logger.Info("Migration: Creating Migration Table: %v", mConfiguration.Table)
// nolint:lll
query := fmt.Sprintf("CREATE TABLE IF NOT EXISTS `%v` (filename TEXT PRIMARY KEY, migrated_on INTEGER NOT NULL DEFAULT 0)", mConfiguration.Table)
_, err := db.Exec(query)
return err
}
// tableExists will check the database for the existence of the specified table.
func tableExists(db *sqlx.DB, tableName string) bool {
query := `SELECT CASE name WHEN $1 THEN true ELSE false END AS found FROM sqlite_master WHERE type='table' AND name = $1`
row := db.QueryRowx(query, tableName)
if row == nil {
logger.Error("MigratorError", fmt.Errorf("Cannot check if table exists, no row returned: %v", tableName))
return false
}
var exists *bool
if err := row.Scan(&exists); err != nil {
if err == sql.ErrNoRows {
return false
}
logger.Error("MigratorError", err)
return false
}
return *exists
}
// performFileMigrations will perform the actual migration,
// importing files and updating the database with the rows imported.
func performFileMigrations(db *sqlx.DB) (int, error) {
var importedCount = 0
// Grab a list of previously ran migrations from the database:
previousMigrations, prevErr := getPreviousMigrations(db)
if prevErr != nil {
return importedCount, prevErr
}
// List up the ".sql" files on disk
err := fs.WalkDir(embed.MigrationFiles, ".", func(file string, d fs.DirEntry, err error) error {
if !d.IsDir() {
shortFile := filepath.Base(file)
// Check if this file already exists in the previous migrations
// and if so, ignore it
if util.SliceContainsItem(previousMigrations, shortFile) {
return nil
}
logger.Info("Migration: Importing %v", shortFile)
sqlContents, ioErr := embed.MigrationFiles.ReadFile(path.Clean(file))
if ioErr != nil {
return ioErr
}
sqlString := string(sqlContents)
tx := db.MustBegin()
if _, execErr := tx.Exec(sqlString); execErr != nil {
return execErr
}
if commitErr := tx.Commit(); commitErr != nil {
return commitErr
}
if markErr := markMigrationSuccessful(db, shortFile); markErr != nil {
return markErr
}
importedCount++
}
return nil
})
return importedCount, err
}
// getPreviousMigrations will query the migration table for names
// of migrations we can ignore because they should have already
// been imported
func getPreviousMigrations(db *sqlx.DB) ([]string, error) {
var existingMigrations []string
// nolint:gosec
query := fmt.Sprintf("SELECT filename FROM `%v` ORDER BY filename", mConfiguration.Table)
rows, err := db.Queryx(query)
if err != nil {
if err == sql.ErrNoRows {
return existingMigrations, nil
}
return existingMigrations, err
}
for rows.Next() {
var filename *string
err := rows.Scan(&filename)
if err != nil {
return existingMigrations, err
}
existingMigrations = append(existingMigrations, *filename)
}
return existingMigrations, nil
}
// markMigrationSuccessful will add a row to the migration table
func markMigrationSuccessful(db *sqlx.DB, filename string) error {
// nolint:gosec
query := fmt.Sprintf("INSERT INTO `%v` (filename) VALUES ($1)", mConfiguration.Table)
_, err := db.Exec(query, filename)
return err
}

@ -0,0 +1,37 @@
package database
import (
"database/sql"
"npm/internal/config"
"npm/internal/errors"
"npm/internal/logger"
)
// CheckSetup Quick check by counting the number of users in the database
func CheckSetup() {
query := `SELECT COUNT(*) FROM "user" WHERE is_deleted = $1 AND is_disabled = $1 AND is_system = $1`
db := GetInstance()
if db != nil {
row := db.QueryRowx(query, false)
var totalRows int
queryErr := row.Scan(&totalRows)
if queryErr != nil && queryErr != sql.ErrNoRows {
logger.Error("SetupError", queryErr)
return
}
if totalRows == 0 {
logger.Warn("No users found, starting in Setup Mode")
} else {
config.IsSetup = true
logger.Info("Application is setup")
}
if config.ErrorReporting {
logger.Warn("Error reporting is enabled - Application Errors WILL be sent to Sentry, you can disable this in the Settings interface")
}
} else {
logger.Error("DatabaseError", errors.ErrDatabaseUnavailable)
}
}

@ -0,0 +1,74 @@
package database
import (
"fmt"
"os"
"npm/internal/config"
"npm/internal/logger"
"github.com/jmoiron/sqlx"
// Blank import for Sqlite
_ "github.com/mattn/go-sqlite3"
)
var dbInstance *sqlx.DB
// NewDB creates a new connection
func NewDB() {
logger.Info("Creating new DB instance")
db := SqliteDB()
if db != nil {
dbInstance = db
}
}
// GetInstance returns an existing or new instance
func GetInstance() *sqlx.DB {
if dbInstance == nil {
NewDB()
} else if err := dbInstance.Ping(); err != nil {
NewDB()
}
return dbInstance
}
// SqliteDB Create sqlite client
func SqliteDB() *sqlx.DB {
dbFile := fmt.Sprintf("%s/nginxproxymanager.db", config.Configuration.DataFolder)
autocreate(dbFile)
db, err := sqlx.Open("sqlite3", dbFile)
if err != nil {
logger.Error("SqliteError", err)
return nil
}
return db
}
// Commit will close and reopen the db file
func Commit() *sqlx.DB {
if dbInstance != nil {
err := dbInstance.Close()
if err != nil {
logger.Error("DatabaseCloseError", err)
}
}
NewDB()
return dbInstance
}
func autocreate(dbFile string) {
if _, err := os.Stat(dbFile); os.IsNotExist(err) {
// Create it
logger.Info("Creating Sqlite DB: %s", dbFile)
// nolint: gosec
_, err = os.Create(dbFile)
if err != nil {
logger.Error("FileCreateError", err)
}
Commit()
}
}

@ -1,461 +0,0 @@
const _ = require('lodash');
const error = require('../lib/error');
const deadHostModel = require('../models/dead_host');
const internalHost = require('./host');
const internalNginx = require('./nginx');
const internalAuditLog = require('./audit-log');
const internalCertificate = require('./certificate');
function omissions () {
return ['is_deleted'];
}
const internalDeadHost = {
/**
* @param {Access} access
* @param {Object} data
* @returns {Promise}
*/
create: (access, data) => {
let create_certificate = data.certificate_id === 'new';
if (create_certificate) {
delete data.certificate_id;
}
return access.can('dead_hosts:create', data)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
let domain_name_check_promises = [];
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name));
});
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
})
.then(() => {
// At this point the domains should have been checked
data.owner_user_id = access.token.getUserId(1);
data = internalHost.cleanSslHstsData(data);
return deadHostModel
.query()
.omit(omissions())
.insertAndFetch(data);
})
.then((row) => {
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, data)
.then((cert) => {
// update host with cert id
return internalDeadHost.update(access, {
id: row.id,
certificate_id: cert.id
});
})
.then(() => {
return row;
});
} else {
return row;
}
})
.then((row) => {
// re-fetch with cert
return internalDeadHost.get(access, {
id: row.id,
expand: ['certificate', 'owner']
});
})
.then((row) => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row)
.then(() => {
return row;
});
})
.then((row) => {
data.meta = _.assign({}, data.meta || {}, row.meta);
// Add to audit log
return internalAuditLog.add(access, {
action: 'created',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return row;
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @return {Promise}
*/
update: (access, data) => {
let create_certificate = data.certificate_id === 'new';
if (create_certificate) {
delete data.certificate_id;
}
return access.can('dead_hosts:update', data.id)
.then((/*access_data*/) => {
// Get a list of the domain names and check each of them against existing records
let domain_name_check_promises = [];
if (typeof data.domain_names !== 'undefined') {
data.domain_names.map(function (domain_name) {
domain_name_check_promises.push(internalHost.isHostnameTaken(domain_name, 'dead', data.id));
});
return Promise.all(domain_name_check_promises)
.then((check_results) => {
check_results.map(function (result) {
if (result.is_taken) {
throw new error.ValidationError(result.hostname + ' is already in use');
}
});
});
}
})
.then(() => {
return internalDeadHost.get(access, {id: data.id});
})
.then((row) => {
if (row.id !== data.id) {
// Sanity check that something crazy hasn't happened
throw new error.InternalValidationError('404 Host could not be updated, IDs do not match: ' + row.id + ' !== ' + data.id);
}
if (create_certificate) {
return internalCertificate.createQuickCertificate(access, {
domain_names: data.domain_names || row.domain_names,
meta: _.assign({}, row.meta, data.meta)
})
.then((cert) => {
// update host with cert id
data.certificate_id = cert.id;
})
.then(() => {
return row;
});
} else {
return row;
}
})
.then((row) => {
// Add domain_names to the data in case it isn't there, so that the audit log renders correctly. The order is important here.
data = _.assign({}, {
domain_names: row.domain_names
}, data);
data = internalHost.cleanSslHstsData(data, row);
return deadHostModel
.query()
.where({id: data.id})
.patch(data)
.then((saved_row) => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'updated',
object_type: 'dead-host',
object_id: row.id,
meta: data
})
.then(() => {
return _.omit(saved_row, omissions());
});
});
})
.then(() => {
return internalDeadHost.get(access, {
id: data.id,
expand: ['owner', 'certificate']
})
.then((row) => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row)
.then((new_meta) => {
row.meta = new_meta;
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
});
});
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {Array} [data.expand]
* @param {Array} [data.omit]
* @return {Promise}
*/
get: (access, data) => {
if (typeof data === 'undefined') {
data = {};
}
return access.can('dead_hosts:get', data.id)
.then((access_data) => {
let query = deadHostModel
.query()
.where('is_deleted', 0)
.andWhere('id', data.id)
.allowEager('[owner,certificate]')
.first();
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
// Custom omissions
if (typeof data.omit !== 'undefined' && data.omit !== null) {
query.omit(data.omit);
}
if (typeof data.expand !== 'undefined' && data.expand !== null) {
query.eager('[' + data.expand.join(', ') + ']');
}
return query;
})
.then((row) => {
if (row) {
row = internalHost.cleanRowCertificateMeta(row);
return _.omit(row, omissions());
} else {
throw new error.ItemNotFoundError(data.id);
}
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
delete: (access, data) => {
return access.can('dead_hosts:delete', data.id)
.then(() => {
return internalDeadHost.get(access, {id: data.id});
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
}
return deadHostModel
.query()
.where('id', row.id)
.patch({
is_deleted: 1
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('dead_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'deleted',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
enable: (access, data) => {
return access.can('dead_hosts:update', data.id)
.then(() => {
return internalDeadHost.get(access, {
id: data.id,
expand: ['certificate', 'owner']
});
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (row.enabled) {
throw new error.ValidationError('Host is already enabled');
}
row.enabled = 1;
return deadHostModel
.query()
.where('id', row.id)
.patch({
enabled: 1
})
.then(() => {
// Configure nginx
return internalNginx.configure(deadHostModel, 'dead_host', row);
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'enabled',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
});
},
/**
* @param {Access} access
* @param {Object} data
* @param {Number} data.id
* @param {String} [data.reason]
* @returns {Promise}
*/
disable: (access, data) => {
return access.can('dead_hosts:update', data.id)
.then(() => {
return internalDeadHost.get(access, {id: data.id});
})
.then((row) => {
if (!row) {
throw new error.ItemNotFoundError(data.id);
} else if (!row.enabled) {
throw new error.ValidationError('Host is already disabled');
}
row.enabled = 0;
return deadHostModel
.query()
.where('id', row.id)
.patch({
enabled: 0
})
.then(() => {
// Delete Nginx Config
return internalNginx.deleteConfig('dead_host', row)
.then(() => {
return internalNginx.reload();
});
})
.then(() => {
// Add to audit log
return internalAuditLog.add(access, {
action: 'disabled',
object_type: 'dead-host',
object_id: row.id,
meta: _.omit(row, omissions())
});
});
})
.then(() => {
return true;
});
},
/**
* All Hosts
*
* @param {Access} access
* @param {Array} [expand]
* @param {String} [search_query]
* @returns {Promise}
*/
getAll: (access, expand, search_query) => {
return access.can('dead_hosts:list')
.then((access_data) => {
let query = deadHostModel
.query()
.where('is_deleted', 0)
.groupBy('id')
.omit(['is_deleted'])
.allowEager('[owner,certificate]')
.orderBy('domain_names', 'ASC');
if (access_data.permission_visibility !== 'all') {
query.andWhere('owner_user_id', access.token.getUserId(1));
}
// Query is used for searching
if (typeof search_query === 'string') {
query.where(function () {
this.where('domain_names', 'like', '%' + search_query + '%');
});
}
if (typeof expand !== 'undefined' && expand !== null) {
query.eager('[' + expand.join(', ') + ']');
}
return query;
})
.then((rows) => {
if (typeof expand !== 'undefined' && expand !== null && expand.indexOf('certificate') !== -1) {
return internalHost.cleanAllRowsCertificateMeta(rows);
}
return rows;
});
},
/**
* Report use
*
* @param {Number} user_id
* @param {String} visibility
* @returns {Promise}
*/
getCount: (user_id, visibility) => {
let query = deadHostModel
.query()
.count('id as count')
.where('is_deleted', 0);
if (visibility !== 'all') {
query.andWhere('owner_user_id', user_id);
}
return query.first()
.then((row) => {
return parseInt(row.count, 10);
});
}
};
module.exports = internalDeadHost;

@ -0,0 +1,135 @@
package dnsproviders
import (
"errors"
"npm/internal/util"
)
type providerField struct {
Name string `json:"name"`
Type string `json:"type"`
IsRequired bool `json:"is_required"`
IsSecret bool `json:"is_secret"`
MetaKey string `json:"meta_key"`
EnvKey string `json:"-"` // not exposed in api
}
// Provider is a simple struct
type Provider struct {
AcmeshName string `json:"acmesh_name"`
Schema string `json:"-"`
Fields []providerField `json:"fields"`
}
// GetAcmeEnvVars will map the meta given to the env var required for
// acme.sh to use this dns provider
func (p *Provider) GetAcmeEnvVars(meta interface{}) map[string]string {
res := make(map[string]string)
for _, field := range p.Fields {
if acmeShEnvValue, found := util.FindItemInInterface(field.MetaKey, meta); found {
res[field.EnvKey] = acmeShEnvValue.(string)
}
}
return res
}
// List returns an array of providers
func List() []Provider {
return []Provider{
getDNSAd(),
getDNSAli(),
getDNSAws(),
getDNSCf(),
getDNSCloudns(),
getDNSCx(),
getDNSCyon(),
getDNSDgon(),
getDNSDNSimple(),
getDNSDp(),
getDNSDuckDNS(),
getDNSDyn(),
getDNSDynu(),
getDNSFreeDNS(),
getDNSGandiLiveDNS(),
getDNSGd(),
getDNSHe(),
getDNSInfoblox(),
getDNSIspconfig(),
getDNSLinodeV4(),
getDNSLua(),
getDNSMe(),
getDNSNamecom(),
getDNSOne(),
getDNSPDNS(),
getDNSUnoeuro(),
getDNSVscale(),
getDNSYandex(),
}
}
// GetAll returns all the configured providers
func GetAll() map[string]Provider {
mp := make(map[string]Provider)
items := List()
for _, item := range items {
mp[item.AcmeshName] = item
}
return mp
}
// Get returns a single provider by name
func Get(provider string) (Provider, error) {
all := GetAll()
if item, found := all[provider]; found {
return item, nil
}
return Provider{}, errors.New("provider_not_found")
}
// GetAllSchemas returns a flat array with just the schemas
func GetAllSchemas() []string {
items := List()
mp := make([]string, 0)
for _, item := range items {
mp = append(mp, item.Schema)
}
return mp
}
const commonKeySchema = `
{
"type": "object",
"required": [
"api_key"
],
"additionalProperties": false,
"properties": {
"api_key": {
"type": "string",
"minLength": 1
}
}
}
`
// nolint: gosec
const commonKeySecretSchema = `
{
"type": "object",
"required": [
"api_key",
"secret"
],
"additionalProperties": false,
"properties": {
"api_key": {
"type": "string",
"minLength": 1
},
"secret": {
"type": "string",
"minLength": 1
}
}
}
`

@ -0,0 +1,17 @@
package dnsproviders
func getDNSAd() Provider {
return Provider{
AcmeshName: "dns_ad",
Schema: commonKeySchema,
Fields: []providerField{
{
Name: "API Key",
Type: "password",
MetaKey: "api_key",
EnvKey: "AD_API_KEY",
IsRequired: true,
},
},
}
}

@ -0,0 +1,25 @@
package dnsproviders
func getDNSAli() Provider {
return Provider{
AcmeshName: "dns_ali",
Schema: commonKeySecretSchema,
Fields: []providerField{
{
Name: "Key",
Type: "text",
MetaKey: "api_key",
EnvKey: "Ali_Key",
IsRequired: true,
},
{
Name: "Secret",
Type: "password",
MetaKey: "secret",
EnvKey: "Ali_Secret",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,56 @@
package dnsproviders
const route53Schema = `
{
"type": "object",
"required": [
"access_key_id",
"access_key"
],
"additionalProperties": false,
"properties": {
"access_key_id": {
"type": "string",
"minLength": 10
},
"access_key": {
"type": "string",
"minLength": 10
},
"slow_rate": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSAws() Provider {
return Provider{
AcmeshName: "dns_aws",
Schema: route53Schema,
Fields: []providerField{
{
Name: "Access Key ID",
Type: "text",
MetaKey: "access_key_id",
EnvKey: "AWS_ACCESS_KEY_ID",
IsRequired: true,
},
{
Name: "Secret Access Key",
Type: "password",
MetaKey: "access_key",
EnvKey: "AWS_SECRET_ACCESS_KEY",
IsRequired: true,
IsSecret: true,
},
{
Name: "Slow Rate",
Type: "number",
MetaKey: "slow_rate",
EnvKey: "AWS_DNS_SLOWRATE",
},
},
}
}

@ -0,0 +1,80 @@
package dnsproviders
const cloudflareSchema = `
{
"type": "object",
"required": [
"api_key",
"email",
"token",
"account_id"
],
"additionalProperties": false,
"properties": {
"api_key": {
"type": "string",
"minLength": 1
},
"email": {
"type": "string",
"minLength": 5
},
"token": {
"type": "string",
"minLength": 5
},
"account_id": {
"type": "string",
"minLength": 1
},
"zone_id": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSCf() Provider {
return Provider{
AcmeshName: "dns_cf",
Schema: cloudflareSchema,
Fields: []providerField{
{
Name: "API Key",
Type: "password",
MetaKey: "api_key",
EnvKey: "CF_Key",
IsRequired: true,
},
{
Name: "Email",
Type: "text",
MetaKey: "email",
EnvKey: "CF_Email",
IsRequired: true,
},
{
Name: "Token",
Type: "text",
MetaKey: "token",
EnvKey: "CF_Token",
IsRequired: true,
IsSecret: true,
},
{
Name: "Account ID",
Type: "text",
MetaKey: "account_id",
EnvKey: "CF_Account_ID",
IsRequired: true,
},
{
Name: "Zone ID",
Type: "string",
MetaKey: "zone_id",
EnvKey: "CF_Zone_ID",
},
},
}
}

@ -0,0 +1,54 @@
package dnsproviders
const clouDNSNetSchema = `
{
"type": "object",
"required": [
"password"
],
"additionalProperties": false,
"properties": {
"auth_id": {
"type": "string",
"minLength": 1
},
"sub_auth_id": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSCloudns() Provider {
return Provider{
AcmeshName: "dns_cloudns",
Schema: clouDNSNetSchema,
Fields: []providerField{
{
Name: "Auth ID",
Type: "text",
MetaKey: "auth_id",
EnvKey: "CLOUDNS_AUTH_ID",
},
{
Name: "Sub Auth ID",
Type: "text",
MetaKey: "sub_auth_id",
EnvKey: "CLOUDNS_SUB_AUTH_ID",
},
{
Name: "Password",
Type: "password",
MetaKey: "password",
EnvKey: "CLOUDNS_AUTH_PASSWORD",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,25 @@
package dnsproviders
func getDNSCx() Provider {
return Provider{
AcmeshName: "dns_cx",
Schema: commonKeySecretSchema,
Fields: []providerField{
{
Name: "Key",
Type: "text",
MetaKey: "api_key",
EnvKey: "CX_Key",
IsRequired: true,
},
{
Name: "Secret",
Type: "password",
MetaKey: "secret",
EnvKey: "CX_Secret",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,57 @@
package dnsproviders
const cyonChSchema = `
{
"type": "object",
"required": [
"user",
"password"
],
"additionalProperties": false,
"properties": {
"user": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string",
"minLength": 1
},
"otp_secret": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSCyon() Provider {
return Provider{
AcmeshName: "dns_cyon",
Schema: cyonChSchema,
Fields: []providerField{
{
Name: "User",
Type: "text",
MetaKey: "user",
EnvKey: "CY_Username",
IsRequired: true,
},
{
Name: "Password",
Type: "password",
MetaKey: "password",
EnvKey: "CY_Password",
IsRequired: true,
IsSecret: true,
},
{
Name: "OTP Secret",
Type: "password",
MetaKey: "otp_secret",
EnvKey: "CY_OTP_Secret",
IsSecret: true,
},
},
}
}

@ -0,0 +1,18 @@
package dnsproviders
func getDNSDgon() Provider {
return Provider{
AcmeshName: "dns_dgon",
Schema: commonKeySchema,
Fields: []providerField{
{
Name: "API Key",
Type: "password",
MetaKey: "api_key",
EnvKey: "DO_API_KEY",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,18 @@
package dnsproviders
func getDNSDNSimple() Provider {
return Provider{
AcmeshName: "dns_dnsimple",
Schema: commonKeySchema,
Fields: []providerField{
{
Name: "OAuth Token",
Type: "text",
MetaKey: "api_key",
EnvKey: "DNSimple_OAUTH_TOKEN",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,46 @@
package dnsproviders
const dnsPodCnSchema = `
{
"type": "object",
"required": [
"id",
"api_key"
],
"additionalProperties": false,
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"api_key": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSDp() Provider {
return Provider{
AcmeshName: "dns_dp",
Schema: dnsPodCnSchema,
Fields: []providerField{
{
Name: "ID",
Type: "text",
MetaKey: "id",
EnvKey: "DP_Id",
IsRequired: true,
},
{
Name: "Key",
Type: "password",
MetaKey: "api_key",
EnvKey: "DP_Key",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,18 @@
package dnsproviders
func getDNSDuckDNS() Provider {
return Provider{
AcmeshName: "dns_duckdns",
Schema: commonKeySchema,
Fields: []providerField{
{
Name: "Token",
Type: "password",
MetaKey: "api_key",
EnvKey: "DuckDNS_Token",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,58 @@
package dnsproviders
const dynSchema = `
{
"type": "object",
"required": [
"customer",
"username",
"password"
],
"additionalProperties": false,
"properties": {
"customer": {
"type": "string",
"minLength": 1
},
"username": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSDyn() Provider {
return Provider{
AcmeshName: "dns_dyn",
Schema: dynSchema,
Fields: []providerField{
{
Name: "Customer",
Type: "text",
MetaKey: "customer",
EnvKey: "DYN_Customer",
IsRequired: true,
},
{
Name: "Username",
Type: "text",
MetaKey: "username",
EnvKey: "DYN_Username",
IsRequired: true,
},
{
Name: "Password",
Type: "password",
MetaKey: "password",
EnvKey: "DYN_Password",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,25 @@
package dnsproviders
func getDNSDynu() Provider {
return Provider{
AcmeshName: "dns_dynu",
Schema: commonKeySecretSchema,
Fields: []providerField{
{
Name: "Client ID",
Type: "text",
MetaKey: "api_key",
EnvKey: "Dynu_ClientId",
IsRequired: true,
},
{
Name: "Secret",
Type: "password",
MetaKey: "secret",
EnvKey: "Dynu_Secret",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,46 @@
package dnsproviders
const freeDNSSchema = `
{
"type": "object",
"required": [
"user",
"password"
],
"additionalProperties": false,
"properties": {
"user": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSFreeDNS() Provider {
return Provider{
AcmeshName: "dns_freedns",
Schema: freeDNSSchema,
Fields: []providerField{
{
Name: "User",
Type: "text",
MetaKey: "user",
EnvKey: "FREEDNS_User",
IsRequired: true,
},
{
Name: "Password",
Type: "password",
MetaKey: "password",
EnvKey: "FREEDNS_Password",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,17 @@
package dnsproviders
func getDNSGandiLiveDNS() Provider {
return Provider{
AcmeshName: "dns_gandi_livedns",
Schema: commonKeySchema,
Fields: []providerField{
{
Name: "Key",
Type: "password",
MetaKey: "api_key",
EnvKey: "GANDI_LIVEDNS_KEY",
IsRequired: true,
},
},
}
}

@ -0,0 +1,25 @@
package dnsproviders
func getDNSGd() Provider {
return Provider{
AcmeshName: "dns_gd",
Schema: commonKeySecretSchema,
Fields: []providerField{
{
Name: "Key",
Type: "text",
MetaKey: "api_key",
EnvKey: "GD_Key",
IsRequired: true,
},
{
Name: "Secret",
Type: "password",
MetaKey: "secret",
EnvKey: "GD_Secret",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,47 @@
package dnsproviders
// nolint: gosec
const commonUserPassSchema = `
{
"type": "object",
"required": [
"username",
"password"
],
"additionalProperties": false,
"properties": {
"username": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSHe() Provider {
return Provider{
AcmeshName: "dns_he",
Schema: commonUserPassSchema,
Fields: []providerField{
{
Name: "Username",
Type: "text",
MetaKey: "username",
EnvKey: "HE_Username",
IsRequired: true,
},
{
Name: "Password",
Type: "password",
MetaKey: "password",
EnvKey: "HE_Password",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,46 @@
package dnsproviders
const infobloxSchema = `
{
"type": "object",
"required": [
"credentials",
"server"
],
"additionalProperties": false,
"properties": {
"credentials": {
"type": "string",
"minLength": 1
},
"server": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSInfoblox() Provider {
return Provider{
AcmeshName: "dns_infoblox",
Schema: infobloxSchema,
Fields: []providerField{
{
Name: "Credentials",
Type: "text",
MetaKey: "credentials",
EnvKey: "Infoblox_Creds",
IsRequired: true,
IsSecret: true,
},
{
Name: "Server",
Type: "text",
MetaKey: "server",
EnvKey: "Infoblox_Server",
IsRequired: true,
},
},
}
}

@ -0,0 +1,67 @@
package dnsproviders
const ispConfigSchema = `
{
"type": "object",
"required": [
"user",
"password",
"api_url"
],
"additionalProperties": false,
"properties": {
"user": {
"type": "string",
"minLength": 1
},
"password": {
"type": "string",
"minLength": 1
},
"api_url": {
"type": "string",
"minLength": 1
},
"insecure": {
"type": "string"
}
}
}
`
func getDNSIspconfig() Provider {
return Provider{
AcmeshName: "dns_ispconfig",
Schema: ispConfigSchema,
Fields: []providerField{
{
Name: "User",
Type: "text",
MetaKey: "user",
EnvKey: "ISPC_User",
IsRequired: true,
},
{
Name: "Password",
Type: "password",
MetaKey: "password",
EnvKey: "ISPC_Password",
IsRequired: true,
IsSecret: true,
},
{
Name: "API URL",
Type: "text",
MetaKey: "api_url",
EnvKey: "ISPC_Api",
IsRequired: true,
},
{
Name: "Insecure",
Type: "bool",
MetaKey: "insecure",
EnvKey: "ISPC_Api_Insecure",
},
},
}
}

@ -0,0 +1,20 @@
package dnsproviders
// Note: https://github.com/acmesh-official/acme.sh/wiki/dnsapi#14-use-linode-domain-api
// needs 15 minute sleep, not currently implemented
func getDNSLinodeV4() Provider {
return Provider{
AcmeshName: "dns_linode_v4",
Schema: commonKeySchema,
Fields: []providerField{
{
Name: "API Key",
Type: "text",
MetaKey: "api_key",
EnvKey: "LINODE_V4_API_KEY",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,46 @@
package dnsproviders
const luaDNSSchema = `
{
"type": "object",
"required": [
"api_key",
"email"
],
"additionalProperties": false,
"properties": {
"api_key": {
"type": "string",
"minLength": 1
},
"email": {
"type": "string",
"minLength": 5
}
}
}
`
func getDNSLua() Provider {
return Provider{
AcmeshName: "dns_lua",
Schema: luaDNSSchema,
Fields: []providerField{
{
Name: "Key",
Type: "text",
MetaKey: "api_key",
EnvKey: "LUA_Key",
IsRequired: true,
IsSecret: true,
},
{
Name: "Email",
Type: "text",
MetaKey: "email",
EnvKey: "LUA_Email",
IsRequired: true,
},
},
}
}

@ -0,0 +1,25 @@
package dnsproviders
func getDNSMe() Provider {
return Provider{
AcmeshName: "dns_me",
Schema: commonKeySecretSchema,
Fields: []providerField{
{
Name: "Key",
Type: "text",
MetaKey: "api_key",
EnvKey: "ME_Key",
IsRequired: true,
},
{
Name: "Secret",
Type: "password",
MetaKey: "secret",
EnvKey: "ME_Secret",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,46 @@
package dnsproviders
const nameComSchema = `
{
"type": "object",
"required": [
"username",
"token"
],
"additionalProperties": false,
"properties": {
"username": {
"type": "string",
"minLength": 1
},
"token": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSNamecom() Provider {
return Provider{
AcmeshName: "dns_namecom",
Schema: nameComSchema,
Fields: []providerField{
{
Name: "Username",
Type: "text",
MetaKey: "username",
EnvKey: "Namecom_Username",
IsRequired: true,
},
{
Name: "Token",
Type: "text",
MetaKey: "token",
EnvKey: "Namecom_Token",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,18 @@
package dnsproviders
func getDNSOne() Provider {
return Provider{
AcmeshName: "dns_nsone",
Schema: commonKeySchema,
Fields: []providerField{
{
Name: "Key",
Type: "password",
MetaKey: "api_key",
EnvKey: "NS1_Key",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,69 @@
package dnsproviders
const powerDNSSchema = `
{
"type": "object",
"required": [
"url",
"server_id",
"token",
"ttl"
],
"additionalProperties": false,
"properties": {
"url": {
"type": "string",
"minLength": 1
},
"server_id": {
"type": "string",
"minLength": 1
},
"token": {
"type": "string",
"minLength": 1
},
"ttl": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSPDNS() Provider {
return Provider{
AcmeshName: "dns_pdns",
Schema: powerDNSSchema,
Fields: []providerField{
{
Name: "URL",
Type: "text",
MetaKey: "url",
EnvKey: "PDNS_Url",
IsRequired: true,
},
{
Name: "Server ID",
Type: "text",
MetaKey: "server_id",
EnvKey: "PDNS_ServerId",
IsRequired: true,
},
{
Name: "Token",
Type: "text",
MetaKey: "token",
EnvKey: "PDNS_Token",
IsRequired: true,
},
{
Name: "TTL",
Type: "number",
MetaKey: "ttl",
EnvKey: "PDNS_Ttl",
IsRequired: true,
},
},
}
}

@ -0,0 +1,47 @@
package dnsproviders
const unoEuroSchema = `
{
"type": "object",
"required": [
"api_key",
"user"
],
"additionalProperties": false,
"properties": {
"api_key": {
"type": "string",
"minLength": 1
},
"user": {
"type": "string",
"minLength": 1
}
}
}
`
func getDNSUnoeuro() Provider {
return Provider{
AcmeshName: "dns_unoeuro",
Schema: unoEuroSchema,
Fields: []providerField{
{
Name: "Key",
Type: "password",
MetaKey: "api_key",
EnvKey: "UNO_Key",
IsRequired: true,
IsSecret: true,
},
{
Name: "User",
Type: "text",
MetaKey: "user",
EnvKey: "UNO_User",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,17 @@
package dnsproviders
func getDNSVscale() Provider {
return Provider{
AcmeshName: "dns_vscale",
Schema: commonKeySchema,
Fields: []providerField{
{
Name: "API Key",
Type: "password",
MetaKey: "api_key",
EnvKey: "VSCALE_API_KEY",
IsRequired: true,
},
},
}
}

@ -0,0 +1,18 @@
package dnsproviders
func getDNSYandex() Provider {
return Provider{
AcmeshName: "dns_yandex",
Schema: commonKeySchema,
Fields: []providerField{
{
Name: "Token",
Type: "password",
MetaKey: "api_key",
EnvKey: "PDD_Token",
IsRequired: true,
IsSecret: true,
},
},
}
}

@ -0,0 +1,82 @@
package auth
import (
goerrors "errors"
"fmt"
"npm/internal/database"
)
// GetByID finds a auth by ID
func GetByID(id int) (Model, error) {
var m Model
err := m.LoadByID(id)
return m, err
}
// GetByUserIDType finds a user by email
func GetByUserIDType(userID int, authType string) (Model, error) {
var m Model
err := m.LoadByUserIDType(userID, authType)
return m, err
}
// Create will create a Auth from this model
func Create(auth *Model) (int, error) {
if auth.ID != 0 {
return 0, goerrors.New("Cannot create auth when model already has an ID")
}
auth.Touch(true)
db := database.GetInstance()
// nolint: gosec
result, err := db.NamedExec(`INSERT INTO `+fmt.Sprintf("`%s`", tableName)+` (
created_on,
modified_on,
user_id,
type,
secret,
is_deleted
) VALUES (
:created_on,
:modified_on,
:user_id,
:type,
:secret,
:is_deleted
)`, auth)
if err != nil {
return 0, err
}
last, lastErr := result.LastInsertId()
if lastErr != nil {
return 0, lastErr
}
return int(last), nil
}
// Update will Update a Auth from this model
func Update(auth *Model) error {
if auth.ID == 0 {
return goerrors.New("Cannot update auth when model doesn't have an ID")
}
auth.Touch(false)
db := database.GetInstance()
// nolint: gosec
_, err := db.NamedExec(`UPDATE `+fmt.Sprintf("`%s`", tableName)+` SET
created_on = :created_on,
modified_on = :modified_on,
user_id = :user_id,
type = :type,
secret = :secret,
is_deleted = :is_deleted
WHERE id = :id`, auth)
return err
}

@ -0,0 +1,98 @@
package auth
import (
goerrors "errors"
"fmt"
"time"
"npm/internal/database"
"npm/internal/types"
"golang.org/x/crypto/bcrypt"
)
const (
tableName = "auth"
// TypePassword is the Password Type
TypePassword = "password"
)
// Model is the user model
type Model struct {
ID int `json:"id" db:"id"`
UserID int `json:"user_id" db:"user_id"`
Type string `json:"type" db:"type"`
Secret string `json:"secret,omitempty" db:"secret"`
CreatedOn types.DBDate `json:"created_on" db:"created_on"`
ModifiedOn types.DBDate `json:"modified_on" db:"modified_on"`
IsDeleted bool `json:"is_deleted,omitempty" db:"is_deleted"`
}
func (m *Model) getByQuery(query string, params []interface{}) error {
return database.GetByQuery(m, query, params)
}
// LoadByID will load from an ID
func (m *Model) LoadByID(id int) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE id = ? LIMIT 1", tableName)
params := []interface{}{id}
return m.getByQuery(query, params)
}
// LoadByUserIDType will load from an ID
func (m *Model) LoadByUserIDType(userID int, authType string) error {
query := fmt.Sprintf("SELECT * FROM `%s` WHERE user_id = ? AND type = ? LIMIT 1", tableName)
params := []interface{}{userID, authType}
return m.getByQuery(query, params)
}
// Touch will update model's timestamp(s)
func (m *Model) Touch(created bool) {
var d types.DBDate
d.Time = time.Now()
if created {
m.CreatedOn = d
}
m.ModifiedOn = d
}
// Save will save this model to the DB
func (m *Model) Save() error {
var err error
if m.ID == 0 {
m.ID, err = Create(m)
} else {
err = Update(m)
}
return err
}
// SetPassword will generate a hashed password based on given string
func (m *Model) SetPassword(password string) error {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.MinCost+2)
if err != nil {
return err
}
m.Type = TypePassword
m.Secret = string(hash)
return nil
}
// ValidateSecret will check if a given secret matches the encrypted secret
func (m *Model) ValidateSecret(secret string) error {
if m.Type != TypePassword {
return goerrors.New("Could not validate Secret, auth type is not a Password")
}
err := bcrypt.CompareHashAndPassword([]byte(m.Secret), []byte(secret))
if err != nil {
return goerrors.New("Invalid Password")
}
return nil
}

@ -0,0 +1,25 @@
package certificate
import (
"npm/internal/entity"
)
var filterMapFunctions = make(map[string]entity.FilterMapFunction)
// getFilterMapFunctions is a map of functions that should be executed
// during the filtering process, if a field is defined here then the value in
// the filter will be given to the defined function and it will return a new
// value for use in the sql query.
func getFilterMapFunctions() map[string]entity.FilterMapFunction {
// if len(filterMapFunctions) == 0 {
// TODO: See internal/model/file_item.go:620 for an example
// }
return filterMapFunctions
}
// GetFilterSchema returns filter schema
func GetFilterSchema() string {
var m Model
return entity.GetFilterSchema(m)
}

Some files were not shown because too many files have changed in this diff Show More