diff --git a/nodejs/bin/www b/nodejs/bin/www deleted file mode 100755 index 69ccee79..00000000 --- a/nodejs/bin/www +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env node - -/** - * Module dependencies. - */ - -var app = require('../app'); -var debug = require('debug')('proxy-api:server'); -var http = require('http'); -const conf = require('../conf'); - -/** - * Get port from environment and store in Express. - */ - -var port = normalizePort(process.env.NODE_PORT || conf.port || '3000'); -app.set('port', port); - -/** - * Create HTTP server. - */ - -var server = http.createServer(app); - -var io = require('socket.io')(server); -app.io = io; - -/** - * Listen on provided port, on all network interfaces. - */ - -server.listen(port); -server.on('error', onError); -server.on('listening', onListening); - -/** - * Normalize a port into a number, string, or false. - */ - -function normalizePort(val) { - var port = parseInt(val, 10); - - if (isNaN(port)) { - // named pipe - return val; - } - - if (port >= 0) { - // port number - return port; - } - - return false; -} - -/** - * Event listener for HTTP server "error" event. - */ - -function onError(error) { - if (error.syscall !== 'listen') { - throw error; - } - - var bind = typeof port === 'string' - ? 'Pipe ' + port - : 'Port ' + port; - - // handle specific listen errors with friendly messages - switch (error.code) { - case 'EACCES': - console.error(bind + ' requires elevated privileges'); - process.exit(1); - break; - case 'EADDRINUSE': - console.error(bind + ' is already in use'); - process.exit(1); - break; - default: - throw error; - } -} - -/** - * Event listener for HTTP server "listening" event. - */ - -function onListening() { - var addr = server.address(); - var bind = typeof addr === 'string' - ? 'pipe ' + addr - : 'port ' + addr.port; - console.log('Listening on ' + bind); - - for(let listener of app.onListen){ - listener() - } -} \ No newline at end of file diff --git a/nodejs/models/cert.js b/nodejs/models/cert.js index 1392977d..3d727b92 100644 --- a/nodejs/models/cert.js +++ b/nodejs/models/cert.js @@ -1,17 +1,503 @@ 'use strict'; -const {createClient} = require('redis'); -const client = createClient({}); +const Table = require('.'); +let models = require('./').models; +const ModelPs = require('../utils/model_pubsub'); +const crypto = require("crypto"); -client.connect(); +const tldExtract = require('tld-extract').parse_host; +const LetsEncrypt = require('../utils/letsencrypt'); +const conf = require('../conf'); -async function getCert(host){ - try{ - console.log('looking for', host); - return JSON.parse(await client.GET(`${host}:latest`)); - }catch(error){ - return {} +const letsEncrypt = new LetsEncrypt({ + directoryUrl: conf.environment === "production" ? + LetsEncrypt.AcmeClient.directory.letsencrypt.production : + LetsEncrypt.AcmeClient.directory.letsencrypt.staging, +}); + + +class CertHost extends Table{ + static _key = 'host' + static _keyMap = { + host: {isRequired: true, type: 'string', max: 500}, + cert_id: {isRequired: true, type: 'string', max: 500}, + cert: {model: 'Cert', rel:'one', localKey: 'cert_id'}, + dnsProvider: {model: 'DnsProvider', rel: 'one', localKey: 'dnsProvider_id'}, + dnsProvider_id: {type: 'string'}, + } + + static async create(data, ...args){ + let instance = await super.create(data, ...args); + await this.buildLookUpObj(); + + return instance; + } + + static lookUpObj = {}; + static __lookUpIsReady = false; + + static async buildLookUpObj(){ + /* + Build a look up tree for domain records in the redis back end to allow + complex looks with wildcards. + */ + + // Hold lookUp ready while the look up object is being built. + this.__lookUpIsReady = false; + this.lookUpObj = {}; + + + // Loop over all the hosts in the redis. + for(let host of await this.list()){ + try{ + // Spit the hosts on "." into its fragments . + let fragments = host.split('.'); + + // Hold a pointer to the root of the lookup tree. + let pointer = this.lookUpObj; + + // Walk over each fragment, popping from right to left. + while(fragments.length){ + let fragment = fragments.pop(); + + // Add a branch to the lookup at the current position + if(!pointer[fragment]){ + pointer[fragment] = {}; + } + + // Add the record(leaf) when we hit the a full host name. + // #record denotes a leaf node on this tree. + if(fragments.length === 0){ + pointer[fragment]['#record'] = await this.get(host) + } + + // Advance the pointer to the next level of the tree. + pointer = pointer[fragment]; + } + }catch(error){ + console.error(error); + } + } + + // When the look up tree is finished, remove the ready hold. + this.__lookUpIsReady = true; + + } + + static lookUp(host){ + /* + Perform a complex lookup of @host on the look up tree. + */ + + + // Hold a pointer to the root of the look up tree + let place = this.lookUpObj; + + // Hold the last passed long wild card. + let last_resort = {}; + + // Walk over each fragment of the host, from right to left + for(let fragment of host.split('.').reverse()){ + + // If a long wild card is found on this level, hold on to it + if(place['**']) last_resort = place['**']; + + // If we have a match for the current fragment, update the current pointer + // A match in the lookup tree takes priority being a more exact match. + if({...last_resort, ...place}[fragment]){ + place = {...last_resort, ...place}[fragment]; + // If we have a not exact fragment match, a wild card will do. + }else if(place['*']){ + place = place['*'] + // If no fragment can be matched, continue with the long wild card branch. + }else if(last_resort){ + place = last_resort; + } + } + + // After the tree has been traversed, see if we have leaf node to return. + if(place && place['#record']) return place['#record']; + } + + static async lookUpReady(){ + /* + Wait for the lookup tree to be built. + */ + + // Check every 5ms to see if the look up tree is ready + while(!this.__lookUpIsReady) await new Promise(r => setTimeout(r, 5)); + return true; + } +} +CertHost.register(CertHost); + +// (async function(){ +// await CertHost.buildLookUpObj(); +// })(); + +class Cert extends Table{ + static _key = 'id'; + static _keyMap = { + 'created_by': {isRequired: true, type: 'string', min: 3, max: 500}, + 'created_on': {default: function(){return (new Date).getTime()}}, + 'updated_by': {default:"__NONE__", isRequired: false, type: 'string',}, + 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, + + 'id': {type: 'string', default: ()=>crypto.randomBytes(16).toString("hex")}, + 'name': {type: 'string', isRequired: true}, + 'is_active': {type: 'boolean', default: false}, + 'type': {isRequired: true, type: 'string', extends: true}, + 'hosts': {model: 'CertHost', rel: 'many', remoteKey: 'cert_id'}, + 'expires': {isRequired: false, type: 'number'}, + + 'has_error': {isRequired: false, type: 'boolean'}, + 'status': {isRequired: false, type: 'string', min: 3, max: 500}, + + 'cert_pem': {isPrivate: true}, + 'fullchain_pem': {isPrivate: true}, + 'privkey_pem': {isPrivate: true}, + } + + static async getByHost(host){ + try{ + let host = await CertHost.lookUp(host); + + return this.get(host.cert_id); + }catch{} + + try{ + + }catch{} + } + + static async findall(filterObj, ...args){ + let results = [...(await super.findall(filterObj, ...args))]; + + for(let [name, Cls] of Object.entries(this.__extendedModels.type)){ + if(Cls.__orginalMethods.includes('findall')){ + results.push(...(await Cls.findall(filterObj, ...args))) + } + } + + return results; + } + + static parseCert(cert){ + return LetsEncrypt.AcmeClient.crypto.readCertificateInfo(cert); + } + + static async cleanUp(){ + let countBad = 0 + for(let cert of await this.findall({type:'Auto'})){ + try{ + await models.Host.lookUp(cert.id) || await models.Host.get(cert.id); + }catch(error){ + console.log('deleting', cert.id) + await cert.remove(); + countBad++ + } + } + return countBad; + } +} +Cert.register(Cert, ModelPs); + +class Auto extends Cert{ + static _keyMap = { + hosts: {} + }; + + static async get(data, ...args){ + let index = typeof data === 'object' ? data[this._key] : data; + + let cert = JSON.parse(await this.redisClient.GET(`${index}:latest`)); + let parsed = this.parseCert(cert.cert_pem); + + return await this.__buildInstance({ + ...cert, + id: index, + is_active: true, + name: 'Auto', + hosts: [{host: index}], + type: 'Auto', + status: 'HTTP auth', + expires: cert.expiry*1000, + created_on: +parsed.notBefore, + updated_on: +parsed.notBefore, + created_by: 'System', + }); + } + + static async create(){ + throw new Error('Can not create this type of cert'); + } + + async getOthers(){ + let others = await this.constructor.redisClient.KEYS(`${this.id}:*`); + return others.filter(i=> !i.endsWith(':latest')); + } + + async update(){ + throw new Error('Can not update this type of cert'); + } + + async remove(...args){ + console.log('in remove') + try{ + for(let key of await this.getOthers()){ + await this.constructor.redisClient.DEL(key); + } + + await this.constructor.redisClient.DEL(`${this.id}:latest`); + }catch(error){ + console.log('error in remove', error); + } + } + + static async findall(filterObj, ...args){ + let results = []; + + for(let key of await this.redisClient.KEYS('*:latest')){ + let item = await this.get(key.split(':')[0]) + if(item.type !== 'Auto') continue; + + if(!filterObj) results.push(item); + let matchCount = 0; + for(let option in filterObj){ + if(item[option] === filterObj[option] && ++matchCount === Object.keys(filterObj).length){ + results.push(item); + break; + } + } + + results.push(item) + } + + return results + } + +} +Cert.extend('type', Auto); + +class Manual extends Cert{ +} + +class WildCard extends Cert{ + + async matchHostsToDns(data){ + let errors = []; + for(let idx in data.host){ + let domain; + try{ + if(data.host.slice(0,idx).includes(data.host[idx])){ + errors.push({key: `host`, keyIndex: idx, message: 'Please remove duplicate names.'}); + continue; + } + domain = await models.Domain.get(data.host[idx]); + }catch{ + errors.push({key: `host`, keyIndex: idx, message: 'No matching DNS provider.'}); + continue; + } + try{ + await this.hostsCreate({ + host: data.host[idx], + dnsProvider_id: domain.dnsProvider_id, + }); + }catch(error){ + console.log('add host error', error) + errors.push({key: `host`, keyIndex: idx, message: 'Used by another cert'}); + } + } + + if(errors.length) throw this.constructor.errors.ObjectValidateError(errors, ''); + } + + static async create(data, ...args){ + if(!Array.isArray(data.host)) data.host = [data.host]; + + let instance = await super.create({ + ...data, + status: 'Started', + }, ...args); + + try{ + await instance.matchHostsToDns(data); + await instance.startWildCardRequest(); + }catch(error){ + await instance.remove(); + throw error; + } + + return instance; + } + + async renew(data, ...args){ + await this.update({ + updated_by: data.username, + status: 'Renewing...', + }); + + try{ + await this.startWildCardRequest(); + }catch(error){ + await this.update({ + updated_by: data.username, + status: 'Renew failed', + }); + + throw error; + } + + return this; + } + + startWildCardRequest(){ + return new Promise(async(resolve, reject)=>{ + try{ + await this.createWildcardCert(resolve, reject); + }catch(error){ + reject(error); + } + }); + } + + async getPem(type){ + let types = ['cert_pem', 'fullchain_pem', 'privkey_pem']; + if(!this.is_active || !types.includes(type)) throw this.constructor.errors.EntryNotFound(this.id); + let cert = JSON.parse(await this.constructor.redisClient.GET(`${this.id}:latest`)); + return cert[type] + } + + async createWildcardCert(resolve, reject){ + try{ + let instance = this; + let cert = await letsEncrypt.dnsWildcard(this.hosts.map(i=>i.host), { + challengeCreateFn: async (authz, challenge, keyAuthorization) => { + resolve(); + try{ + let domain = await models.Domain.get(authz.identifier.value); + let parts = tldExtract(authz.identifier.value); + + await domain.createRecord({ + type:'TXT', + name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, + data: `${keyAuthorization}` + }); + }catch(error){ + await instance.update({ + has_error: true, + status: `challengeCreateFn: ${error}`, + }); + console.log('model Host challengeCreateFn error:', error) + } + }, + onDnsCheck: async(authz, checkCount)=>{ + await instance.update({ + status: `${checkCount} Checking DNS` + }); + }, + onDnsCheckFail: async(authz, error)=>{ + await instance.update({ + has_error: true, + status: `DNS check failed for ${authz.identifier.value}` + }); + }, + onDnsCheckFound: async(authz)=>{ + resolve(); + // await instance.update({ + // status: `DNS check found ${authz.identifier.value}` + // }); + }, + onDnsCheckSuccess: async(authz)=>{ + await instance.update({ + status: `DNS check success` + }); + }, + onDnsCheckRemove: async(authz)=>{ + await instance.update({ + status: `DNS remove record ${authz.identifier.value}` + }); + }, + challengeRemoveFn: async (authz, challenge, keyAuthorization)=>{ + try{ + let domain = await models.Domain.get(authz.identifier.value); + let parts = tldExtract(authz.identifier.value); + + await domain.deleteRecords({ + type:'TXT', + name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, + data: `${keyAuthorization}` + }); + }catch(error){ + console.log('challengeRemoveFn Error:', error); + await instance.update({ + + status: `DNS remove record ${authz.identifier.value}` + }); + } + }, + }); + + resolve(); + + let toAdd = { + cert_pem: cert.cert.split('\n\n')[0], + fullchain_pem: cert.cert, + privkey_pem: cert.key.toString(), + csr_pem: cert.csr.toString(), + expiry: 4120307657, + real_expiry: +LetsEncrypt.AcmeClient.crypto.readCertificateInfo(cert.cert).notAfter/1000, + }; + + await this.constructor.redisClient.SET(`${this.id}:latest`, JSON.stringify(toAdd)); + await this.update({ + status: `Done`, + is_active: true, + expires: toAdd.real_expiry*1000, + }); + + return this; + }catch(error){ + console.log('le failed', error) + reject(error); + throw error; + // this.update({ + // wildcard_status: `LE failed` + // }); + } } } +Cert.extend('type', WildCard); + +if(require.main === module){(async function(){try{ + + let certHosts = await CertHost.findall() + console.log('CertHost', certHosts) + + console.log(await Cert.cleanUp()) + + let certs = await Cert.findall() + + console.log('Certs', certs) + + // for(let cert of certs){ + // await cert.remove() + // } + // console.log('certs', certs) + // for(let cert of certs){ + // if(cert.real_expiry) console.log(cert) + // } + + + // console.log(await Cert.get('blah.test.cl.vm42.us')) + + + // console.log(await certs[0].remove()) + // console.log(LetsEncrypt.AcmeClient.crypto.readCertificateInfo(certs[0].cert_pem)) -module.exports = {getCert}; + // console.log(certs[0].host, await Host.lookUp(certs[0].host) || await Host.get(certs[0].host)) +}catch(error){ + console.log('IIFE Error:', error); +}finally{ + process.exit(0); +}})()} diff --git a/nodejs/models/dns_provider.js b/nodejs/models/dns_provider.js deleted file mode 100644 index dd8bb5c0..00000000 --- a/nodejs/models/dns_provider.js +++ /dev/null @@ -1,204 +0,0 @@ -'use strict'; - -const crypto = require("crypto"); - -const conf = require('../conf'); -const Table = require('../utils/redis_model'); -const ModelPs = require('../utils/model_pubsub'); - -const tldExtract = require('tld-extract').parse_host; - -const providers = { - Cloudflare: require('./dns_provider/cloudflare'), - DigitalOcean: require('./dns_provider/digitalocean'), - PorkBun: require('./dns_provider/porkbun'), -}; - -class Domain extends Table{ - static _key = 'domain'; - static _keyMap = { - 'created_by': {isRequired: true, type: 'string', min: 3, max: 500}, - 'created_on': {default: function(){return (new Date).getTime()}}, - 'updated_by': {default:"__NONE__", isRequired: false, type: 'string',}, - 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, - 'domain': {isRequired: true, type: 'string'}, - 'dnsProvider_id': {isRequired: true, type: 'string'}, - 'provider': {model: 'DnsProvider', rel:'one', localKey: 'dnsProvider_id'}, - 'zoneId': {isRequired: false, type: 'string'}, - } - - static async get(domain, ...args){ - try{ - domain = tldExtract(domain).domain; - }catch{} - - return await super.get(domain, ...args); - } - - async getRecords(...args){ - return await this.provider.api.getRecords(this, ...args); - } - - async createRecord(...args){ - return await this.provider.api.createRecord(this, ...args); - } - - async deleteRecords(...args){ - return await this.provider.api.deleteRecords(this, ...args); - } -} - -Domain.register(ModelPs(Domain)); - -class DnsProvider extends Table{ - static _key = 'id'; - static _keyMap = { - 'created_by': {isRequired: true, type: 'string', min: 3, max: 500}, - 'created_on': {default: function(){return (new Date).getTime()}}, - 'updated_by': {default:"__NONE__", isRequired: false, type: 'string',}, - 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, - 'id': {default: ()=>crypto.randomBytes(8).toString("hex")}, - 'name': {isRequired: true, type: 'string'}, - 'dnsProvider': {isRequired: true, type: 'string'}, - 'domains': {model:'Domain', rel: 'many', remoteKey: 'dnsProvider_id'} - } - - static __intraModel(provider){ - if(!Object.keys(providers).includes(provider)){ - throw new Error('Invalid DNS provider'); - } - - let Provider = providers[provider]; - let _keyMap = {...this._keyMap, ...Provider._keyMap}; - - return ({ - [this.name] : class extends this { - static _keyMap = _keyMap; - static Provider = Provider; - } - })[this.name]; - } - - static async create(data, ...args){ - let Provider; - try{ - let __intraModel = this.__intraModel(data.dnsProvider); - Provider = __intraModel.Provider; - - // This is here test if the given API key is valid - let provider = new __intraModel.Provider(data, ...args); - let domains = await provider.listDomains(); - - let instance = await super.create.call(__intraModel, data, ...args); - await instance.updateDomains(domains); - - return instance; - }catch(error){ - if(error.name === 'UnauthorizedDnsApi'){ - let keys = []; - console.log('Provider', Provider) - for(let key in Provider._keyMap){ - keys.push({'key': key, message: 'Invalid Key'}) - } - throw this.errors.ObjectValidateError(keys, "API rejected key"); - } - } - } - - static async get(data, ...args){ - let instance = await super.get(data, ...args); - let __intraModel = this.__intraModel(instance.dnsProvider); - - return await super.get.call(__intraModel, data, ...args); - } - - static listProviders(){ - let out = []; - for(let provider in providers){ - out.push({ - name: provider, - fields: providers[provider]._keyMap, - }); - } - return out; - } - - get api(){ - return new this.constructor.Provider(this); - } - - async listDomains(){ - return this.api.listDomains(); - } - - async updateDomains(domains){ - domains = domains || await this.listDomains(); - let currentDomains = this.domains.map(domain => domain.domain); - - - for(let domain of domains){ - if(currentDomains.includes(domain.domain)){ - delete currentDomains[currentDomains.indexOf(domain.domain)]; - continue; - } - await Domain.create({ - created_by: this.created_by, - domain: domain.domain, - dnsProvider_id: this.id, - zoneId: domain.zoneId, - }); - } - console.log('currentDomains:', currentDomains) - - for(let domain of currentDomains){ - if(!domain) continue - domain = await Domain.get(domain); - await domain.remove(); - } - } - - async remove(){ - for(let domain of await this.domains){ - await domain.remove(); - } - let instance = await super.remove(); - - return instance; - } - - toJSON(){ - return { - ...super.toJSON(), - ...this.constructor.Provider.toJSON() - }; - } -} - -DnsProvider.register(ModelPs(DnsProvider)) - - -if(require.main === module){(async function(){try{ - const conf = require('../conf'); - - // console.log(await DnsProvider.findall()); - - let provider = await DnsProvider.get('e8443e03ac503c7b'); - - console.log(await provider.listDomains()) - - let domain = await Domain.get('holycore.quest') // pork - // let domain = await Domain.get('rm-rf.stream') // DO - // let domain = await Domain.get('test.wtf') // CF - - // console.log(await domain.createRecord({type: 'TXT', name: 'apitewefweefwsewft222', data:'hiiiiiii'})) - - let txtRecords = await domain.getRecords({type: 'TXT'}); - console.log(txtRecords.map(i=>`${i.name}: ${i.data}`)) - // console.log(await domain.deleteRecords({type: 'TXT'})) - - -}catch(error){ - console.log('IIFE Error:', error); -}finally{ - process.exit(0); -}})()} diff --git a/nodejs/models/dns_provider/cloudflare.js b/nodejs/models/dns_provider/cloudflare.js index c44abe8d..1fd06a59 100644 --- a/nodejs/models/dns_provider/cloudflare.js +++ b/nodejs/models/dns_provider/cloudflare.js @@ -1,14 +1,10 @@ 'use strict'; const axios = require('axios'); -const {DnsApi} = require('./common'); +const {DnsProvider} = require('../').models; -//like the options obj will always use domain data and type -// change content to data -// change name to domain - -class CloudFlare extends DnsApi{ +class CloudFlare extends DnsProvider{ static _keyMap = { token: {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Token'}, } @@ -22,14 +18,16 @@ class CloudFlare extends DnsApi{ // Cloud icon for cloudflare static displayIconUni = '' - constructor(token){ - super() - this.token = token.token || token; - } + /* + The API and the generic class interface have different opinions of what keys + hold what data, the __parseOptions and __pastseRes normal the keys to what + the class expects - __typeCheck(type){ - if(!type) return; - if(!['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB'].includes(type)) throw new Error(`${this.constructor.name} API: Invalid 'type' passed`) + What the the API calls it : What the class wants it as. + */ + __apiKeyMap = { + 'content': 'data', + 'id': 'zoneId', } async axios(method, ...args){ @@ -50,32 +48,17 @@ class CloudFlare extends DnsApi{ } async listDomains(){ - let res = await this.axios('get'); + let res = await this.axios('get'); - for(let domain of res.data.result){ - domain.domain = domain.name - domain.zoneId = domain.id - } + console.log('cf domain red', res.data, '\n\n parsed res', this.__parseRes(res.data.result)) - return res.data.result; + return this.__parseRes(res.data.result); } - /* - The API and the generic class interface have different opinions of what keys - hold what data, the __parseOptions and __pastseRes normal the keys to what - the class expects - - What the the API calls it : What the class wants it as. - */ - __apiKeyMap = { - 'content': 'data', - } - - //get records async getRecords(domain, options){ - let res = await this.axios('get', - `${domain.zoneId}/dns_records`, - ); + let res = await this.axios('get', + `${domain.zoneId}/dns_records`, + ); let records = this.__parseRes(res.data.result); if(!options) return records; @@ -104,7 +87,6 @@ class CloudFlare extends DnsApi{ } throw error; } - } async deleteRecordById(domain, id){ @@ -121,28 +103,4 @@ class CloudFlare extends DnsApi{ } } -module.exports = CloudFlare; - - -if(require.main === module){(async function(){try{ - // let cf = new CloudFlare(""); - // let domain = { - // domain: "example.uk", - // zoneId: "5eb25c12cd7d22f11252330a29a0dd77" - // } - - // console.log(await cf.listDomains()) - - //content = ip - //name = domain - // console.log('get', await cf.getRecords(domain, {content: '172.206.221.130'})) - - // console.log('post', await cf.createRecord(domain, {name:'test', content: '10.0.0.1', type: "TXT"})) - - // console.log('delete', await cf.deleteRecordById(domain , "5c0e958c3406a34d011459933d538b78")) - - // console.log('delete', await cf.deleteRecords(domain, {type: 'A'})) - -}catch(error){ - console.log('IIFE Error:', error) -}})()} +DnsProvider.extend('dnsProvider', CloudFlare); diff --git a/nodejs/models/dns_provider/common.js b/nodejs/models/dns_provider/common.js deleted file mode 100644 index 32f88a2d..00000000 --- a/nodejs/models/dns_provider/common.js +++ /dev/null @@ -1,117 +0,0 @@ -'use strict'; - -const tldExtract = require('tld-extract').parse_host; - -class DnsApi{ - errors = { - unauthorized: ()=>{ - let error = new Error('UnauthorizedDnsApi'); - error.name = 'UnauthorizedDnsApi'; - error.message = `Unauthorized call to ${this.constructor.name}`; - error.status = 424; - - return error; - }, - invalidInput: (keys)=>{ - let error = new Error('InvalidInput'); - error.name = 'InvalidInput'; - error.message = `Required keys missing: ${keys.join(', ')}` - - return error - - }, - other: (status, message, APIcode)=>{ - let error = new Error('OtherDnsApiError'); - error.name = 'OtherDnsApiError'; - error.message = `DNS API Error ${this.constructor.name}: ${status} ${message}`; - error.status = 424; - error.APIcode = APIcode; - return error; - }, - } - - static info(){ - let svgDataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(this.displayIconHtml) - .replace(/'/g, '%27') - .replace(/"/g, '%22')}` - - return { - displayName: this.displayName, - displayIconUni: this.displayIconUni, - displayIconHtml: svgDataUrl, - fields: this._keyMap, - } - } - - static toJSON(){ - return { - ...this.info(), - } - } - - /* - No instance data should ever be shared, so just give the static level inf - */ - toJSON(){ - return this.constructor.toJSON(); - } - - - __typeCheck(type){ - if(!['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB'].includes(type)) throw new Error('PorkBun API: Invalid type passed') - } - - - /* - The API and the generic class interface have different opinions of what keys - hold what data, the __parseOptions and __pastseRes normal the keys to what - the class expects - - What the the API calls it : What the class wants it as. - */ - __apiKeyMap = {}; - - __parseOptions(options, keys){ - if(!options && !keys) return undefined; - - if(keys){ - let missingKeys = [] - for(let key of keys){ - if(!options[key]) missingKeys.push(key) - } - - if(missingKeys.length) throw this.errors.invalidInput(missingKeys); - } - - for(let [apiKey, clsKey] of Object.entries(this.__apiKeyMap)){ - if(options[clsKey]){ - options[apiKey] = options[clsKey]; - delete options[clsKey]; - } - } - - if(options.type) this.__typeCheck(options.type); - - return options; - } - - __parseRes(data){ - for(let item of data){ - for(let [apiKey, clsKey] of Object.entries(this.__apiKeyMap)){ - if(item[apiKey]){ - item[clsKey] = item[apiKey]; - } - } - try{ - item.name = tldExtract(item.name).sub - }catch{} - } - - return data; - } -} - -module.exports = { - DnsApi, -}; - diff --git a/nodejs/models/dns_provider/digitalocean.js b/nodejs/models/dns_provider/digitalocean.js index 264c827b..0434cb1a 100644 --- a/nodejs/models/dns_provider/digitalocean.js +++ b/nodejs/models/dns_provider/digitalocean.js @@ -1,28 +1,37 @@ 'use strict'; const axios = require('axios'); -const {DnsApi} = require('./common'); +const {DnsProvider} = require('../').models; -class DigitalOcean extends DnsApi{ + +class DigitalOcean extends DnsProvider{ static _keyMap = { token: {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Token'}, } static displayName = 'DigitalOcean' + static displayIconUni = '' static displayIconHtml = ` ` - // '' - static displayIconUni = '' - constructor(token){ - super() - this.token = token.token || token; + + /* + The API and the generic class interface have different opinions of what keys + hold what data, the __parseOptions and __pastseRes normal the keys to what + the class expects + + What the the API calls it : What the class wants it as. + */ + + __apiKeyMap = { + 'name': 'domain', } async axios(method, ...args){ + console.log('this', this.constructor) try{ let a = axios.create({ baseURL: 'https://api.digitalocean.com/v2/', @@ -82,4 +91,4 @@ class DigitalOcean extends DnsApi{ } } -module.exports = DigitalOcean; +DnsProvider.extend('dnsProvider', DigitalOcean) diff --git a/nodejs/models/dns_provider/index.js b/nodejs/models/dns_provider/index.js new file mode 100644 index 00000000..14963245 --- /dev/null +++ b/nodejs/models/dns_provider/index.js @@ -0,0 +1,266 @@ +'use strict'; + +const crypto = require("crypto"); +const tldExtract = require('tld-extract').parse_host; + +const conf = require('../../conf'); +const Table = require('../'); +const ModelPs = require('../../utils/model_pubsub'); + + +class DnsProvider extends Table{ + static _key = 'id'; + static _keyMap = { + 'created_by': {isRequired: true, type: 'string', min: 3, max: 500}, + 'created_on': {default: function(){return (new Date).getTime()}}, + 'updated_by': {default:"__NONE__", isRequired: false, type: 'string',}, + 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, + 'id': {default: ()=>crypto.randomBytes(8).toString("hex")}, + 'name': {isRequired: true, type: 'string'}, + 'domains': {model:'Domain', rel: 'many', remoteKey: 'dnsProvider_id'}, + 'dnsProvider': {isRequired: true, type: 'string', extends: true}, + } + + static errors = { + ...super.errors, + unauthorized: ()=>{ + let error = new Error('UnauthorizedDnsApi'); + error.name = 'UnauthorizedDnsApi'; + error.message = `Unauthorized call to ${this.constructor.name}`; + error.status = 424; + + return error; + }, + invalidInput: (keys)=>{ + let error = new Error('InvalidInput'); + error.name = 'InvalidInput'; + error.message = `Required keys missing: ${keys.join(', ')}`; + + return error + + }, + other: (status, message, APIcode)=>{ + let error = new Error('OtherDnsApiError'); + error.name = 'OtherDnsApiError'; + error.message = `DNS API Error ${this.constructor.name}: ${status} ${message}`; + error.status = 424; + error.APIcode = APIcode; + + return error; + }, + } + + static async create(data){ + // // Test if the provided key is valid by trying to get a list of domains + // // before adding the entry to the DB + // let api = new this(data); + // console.log('api', api) + // let domains = await api.listDomains(); + + // Create the entry in the backing DB, add the fetched domains and + // return the new instance + let instance = await super.create(data); + try{ + await instance.updateDomains(); + }catch(error){ + await instance.remove(); + throw error; + } + + return instance; + } + + async updateDomains(domains){ + /* + Use the remote API to get a list of known domains from the remote + service, add them to Domain table associated with the current DNS + provider. Remove any current associated domains not returned by the + remote API call. + */ + + domains = domains || await this.listDomains(); + + // Hold a list of current domains so when remove any that are not + // returned by the current API call + let currentDomains = this.domains.map(domain => domain.domain); + + // Walk the list of domains returned by the API + for(let domain of domains){ + // Reduce the list of currentDomains known and skip to the next + // domain + if(currentDomains.includes(domain.domain)){ + delete currentDomains[currentDomains.indexOf(domain.domain)]; + continue; + } + + // Add a new Domain entry + await this.domainsCreate({ + created_by: this.created_by, + domain: domain.domain, + zoneId: domain.zoneId, + }); + } + + // Walk the list of domains left in currentDomains and remove them. + for(let domain of currentDomains){ + if(!domain) continue; + domain = await Table.models.Domain.get(domain); + await domain.remove(); + } + } + + // async remove(){ + // for(let domain of await this.domains){ + // await domain.remove(); + // } + // let instance = await super.remove(); + + // return instance; + // } + + static info(){ + /* + Parse class info about the current DNS provider into something the front + end can use. + */ + let svgDataUrl = `data:image/svg+xml;charset=utf-8,${ + encodeURIComponent(this.displayIconHtml) + .replace(/'/g, '%27') + .replace(/"/g, '%22') + }`; + + return { + name: this.name, + displayName: this.displayName, + displayIconUni: this.displayIconUni, + displayIconHtml: svgDataUrl, + fields: this._keyMap, + }; + } + + static toJSON(){ + return { + ...this.info(), + ...(this.__isExtended ? {} :super.toJSON()), + }; + } + + /* + No instance data should ever be shared, so just give the static level inf + */ + toJSON(){ + return { + ...this.constructor.toJSON(), + ...super.toJSON(), + }; + } + + /* + Helper methods for DNS API interactions + */ + + __typeCheck(type){ + let validDnsRecordTypes = [ + 'A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', + 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB', + ]; + + if(!validDnsRecordTypes.includes(type)){ + // change this to throw a validation error + throw new Error('PorkBun API: Invalid type passed'); + } + } + + /* + The API and the generic class interface have different opinions of what keys + hold what data, the __parseOptions and __pastseRes normal the keys to what + the class expects + + What the the API calls it : What the class wants it as. + */ + __apiKeyMap = {}; + + __parseOptions(options, keys){ + if(!options && !keys) return undefined; + + if(keys){ + let missingKeys = [] + for(let key of keys){ + if(!options[key]) missingKeys.push(key) + } + + if(missingKeys.length) throw this.errors.invalidInput(missingKeys); + } + + for(let [apiKey, clsKey] of Object.entries(this.__apiKeyMap)){ + if(options[clsKey]){ + options[apiKey] = options[clsKey]; + delete options[clsKey]; + } + } + + if(options.type) this.__typeCheck(options.type); + + return options; + } + + __parseRes(data){ + for(let item of data){ + for(let [apiKey, clsKey] of Object.entries(this.__apiKeyMap)){ + if(item[apiKey]){ + item[clsKey] = item[apiKey]; + } + } + try{ + item.name = tldExtract(item.name).sub + }catch{} + } + + return data; + } +} + +DnsProvider.register(DnsProvider, ModelPs) + + +require('./cloudflare'); +require('./digitalocean'); +require('./porkbun'); + + +if(require.main === module){(async function(){try{ + + // console.log(await DnsProvider.subMobels['DigitalOcean'].get('fcaaa1f6b8ab405e')) + // console.log(JSON.stringify(DnsProvider, null, 2)); + + + let providers = await DnsProvider.findall(); + + // console.log(providers) + + console.log(JSON.stringify(providers[0], null, 2)) + + // let provider = await DnsProvider.get('cb9b1041bf92f668'); + + // await provider.update({dnsProvider: "CloudFlare"}) + + // console.log(provider) + + // console.log(await provider.listDomains()) + + // let domain = await Domain.get('holycore.quest') // pork + // let domain = await Domain.get('rm-rf.stream') // DO + // let domain = await Domain.get('test.wtf') // CF + + // console.log(await domain.createRecord({type: 'TXT', name: 'apitewefweefwsewft222', data:'hiiiiiii'})) + + // let txtRecords = await domain.getRecords({type: 'TXT'}); + // console.log(txtRecords.map(i=>`${i.name}: ${i.data}`)) + // console.log(await domain.deleteRecords({type: 'TXT'})) + + +}catch(error){ + console.log('IIFE Error:', error); +}finally{ + process.exit(0); +}})()} diff --git a/nodejs/models/dns_provider/porkbun.js b/nodejs/models/dns_provider/porkbun.js index 64347d81..17cd6522 100644 --- a/nodejs/models/dns_provider/porkbun.js +++ b/nodejs/models/dns_provider/porkbun.js @@ -1,10 +1,10 @@ 'use strict'; const axios = require('axios'); -const {DnsApi} = require('./common'); +const {DnsProvider} = require('../').models; -class PorkBun extends DnsApi{ +class PorkBun extends DnsProvider{ static _keyMap = { 'apiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API key'}, 'secretApiKey': {isRequired: true, type: 'string', isPrivate: true, displayName: 'API Secret key'}, @@ -26,10 +26,16 @@ class PorkBun extends DnsApi{ ` - constructor(args){ - super() - this.apiKey = args.apiKey; - this.secretApiKey = args.secretApiKey; + /* + The API and the generic class interface have different opinions of what keys + hold what data, the __parseOptions and __pastseRes normal the keys to what + the class expects + + What the the API calls it : What the class wants it as. + */ + + __apiKeyMap = { + 'content': 'data', } async post(url, data){ @@ -53,30 +59,6 @@ class PorkBun extends DnsApi{ } } - __typeCheck(type){ - if(!['A', 'MX', 'CNAME', 'ALIAS', 'TXT', 'NS', 'AAAA', 'SRV', 'TLSA', 'CAA', 'HTTPS', 'SVCB'].includes(type)) throw new Error('PorkBun API: Invalid type passed') - } - - __parseName(domain, name){ - if(name && !name.endsWith('.'+domain)){ - return `${name}.${domain}` - } - return name; - } - - - /* - The API and the generic class interface have different opinions of what keys - hold what data, the __parseOptions and __pastseRes normal the keys to what - the class expects - - What the the API calls it : What the class wants it as. - */ - - __apiKeyMap = { - 'content': 'data' - } - async getRecords(domain, options){ let res = await this.post(`/dns/retrieve/${domain}`); let records = this.__parseRes(res.data.records) @@ -130,4 +112,4 @@ class PorkBun extends DnsApi{ } } -module.exports = PorkBun; +DnsProvider.extend('dnsProvider', PorkBun); diff --git a/nodejs/models/domain.js b/nodejs/models/domain.js new file mode 100644 index 00000000..f3c55267 --- /dev/null +++ b/nodejs/models/domain.js @@ -0,0 +1,43 @@ +'use strict'; + +const tldExtract = require('tld-extract').parse_host; +const conf = require('../conf'); +const Table = require('../utils/redis_model'); +const ModelPs = require('../utils/model_pubsub'); + + +class Domain extends Table{ + static _key = 'domain'; + static _keyMap = { + 'created_by': {isRequired: true, type: 'string', min: 3, max: 500}, + 'created_on': {default: function(){return (new Date).getTime()}}, + 'updated_by': {default:"__NONE__", isRequired: false, type: 'string',}, + 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, + 'domain': {isRequired: true, type: 'string'}, + 'dnsProvider_id': {isRequired: true, type: 'string'}, + 'provider': {model: 'DnsProvider', rel:'one', localKey: 'dnsProvider_id'}, + 'zoneId': {isRequired: false, type: 'string'}, + } + + static async get(domain, ...args){ + try{ + domain = tldExtract(domain).domain; + }catch{} + + return await super.get(domain, ...args); + } + + async getRecords(...args){ + return await this.provider.getRecords(this, ...args); + } + + async createRecord(...args){ + return await this.provider.createRecord(this, ...args); + } + + async deleteRecords(...args){ + return await this.provider.deleteRecords(this, ...args); + } +} + +Domain.register(ModelPs(Domain)); diff --git a/nodejs/models/host.js b/nodejs/models/host.js index ca040140..42de48eb 100755 --- a/nodejs/models/host.js +++ b/nodejs/models/host.js @@ -1,7 +1,7 @@ 'use strict'; const Table = require('.'); -const {Domain} = require('.').models; +const {Domain, Cert} = require('.').models; const ModelPs = require('../utils/model_pubsub'); const tldExtract = require('tld-extract').parse_host; @@ -14,6 +14,7 @@ const letsEncrypt = new LetsEncrypt({ LetsEncrypt.AcmeClient.directory.letsencrypt.staging, }); + class Host extends Table{ static _key = 'host'; static _keyMap = { @@ -21,17 +22,17 @@ class Host extends Table{ 'created_on': {default: function(){return (new Date).getTime()}}, 'updated_by': {default:"__NONE__", isRequired: false, type: 'string',}, 'updated_on': {default: function(){return (new Date).getTime()}, always: true}, - 'host': {isRequired: true, type: 'string', min: 3, max: 500}, - 'ip': {isRequired: true, type: 'string', min: 3, max: 500}, + 'host': {isRequired: true, type: 'string', min: 1, max: 500}, + 'ip': {isRequired: true, type: 'string', min: 1, max: 500}, 'targetPort': {isRequired: true, type: 'number', min:0, max:65535}, 'forcessl': {isRequired: false, default: true, type: 'boolean'}, 'targetssl': {isRequired: false, default: false, type: 'boolean'}, 'is_cache': {default: false, isRequired: false, type: 'boolean',}, - 'is_wildcard': {default: false, isRequired: false, type: 'boolean',}, - 'wildcard_status': {isRequired: false, type: 'string', min: 3, max: 500}, - 'wildcard_parent': {isRequired: false, type: 'string', min: 3, max: 500}, - 'wildcard_expires': {isRequired: false, type: 'number'}, + // 'is_wildcard': {default: false, isRequired: false, type: 'boolean',}, + 'cert_id': {type: 'string', isRequired: false}, + // 'cert': {model: 'Cert', rel:'one', localKey: 'cert_id'}, 'domain': {model: 'Domain', rel: 'one'}, + 'prevent_remove': {type:'boolean', default: false}, } static lookUpObj = {}; @@ -112,108 +113,6 @@ class Host extends Table{ } } - async createWildcardCert(){ - if(!this.host.startsWith('*.')) throw new Error('not wild card'); - - try{ - let host = this; - - await host.update({ - wildcard_status: 'Requesting', - }); - let cert = await letsEncrypt.dnsWildcard(this.host, { - challengeCreateFn: async (authz, challenge, keyAuthorization) => { - await host.update({ - wildcard_status: `Adding record` - }); - try{ - let parts = tldExtract(authz.identifier.value); - - let res = await host.domain.createRecord( - { - type:'TXT', - name: `_acme-challenge${parts.sub ? `.${parts.sub}` : ''}`, - data: `${keyAuthorization}` - } - ); - }catch(error){ - console.log('model Host challengeCreateFn error:', error) - await host.update({ - wildcard_status: `Add DNS record failed` - }); - } - }, - onDnsCheck: async(authz, checkCount)=>{ - await host.update({ - wildcard_status: `${checkCount} Checking DNS` - }); - }, - onDnsCheckFail: async(authz, error)=>{ - await host.update({ - wildcard_status: `DNS check failed for ${authz.identifier.value}` - }); - }, - onDnsCheckFound: async(authz)=>{ - await host.update({ - wildcard_status: `DNS check found` - }); - }, - onDnsCheckSuccess: async(authz)=>{ - await host.update({ - wildcard_status: `DNS check success` - }) - }, - onDnsCheckRemove: async(authz)=>{ - await host.update({ - wildcard_status: `DNS remove record` - }) - }, - challengeRemoveFn: async (authz, challenge, keyAuthorization)=>{ - await host.update({ - wildcard_status: `DNS remove record` - }) - try{ - let parts = tldExtract(authz.identifier.value); - await host.domain.deleteRecords( - { - type:'TXT', - name: `_acme-challenge${parts.sub ? `.${parts.sub}` : '' - }`, - content: `${keyAuthorization}`} - ); - - }catch(error){ - await host.update({ - wildcard_status: `DNS remove record failed for ${authz.identifier.value}` - }) - } - }, - }); - - let toAdd = { - cert_pem: cert.cert.split('\n\n')[0], - fullchain_pem: cert.cert, - privkey_pem: cert.key.toString(), - csr_pem: cert.csr.toString(), - expiry: 4120307657, - real_expiry: +LetsEncrypt.AcmeClient.crypto.readCertificateInfo(cert.cert).notAfter/1000, - } - - - await this.constructor.redisClient.SET(`${this.host}:latest`, JSON.stringify(toAdd)); - await this.update({ - wildcard_status: `Done`, - wildcard_expires: toAdd.real_expiry*1000, - }); - - return this; - }catch(error){ - console.log('le failed', error) - this.update({ - wildcard_status: `LE failed` - }); - } - } async update(...args){ try{ @@ -227,8 +126,12 @@ class Host extends Table{ } } - async remove(...args){ + async remove(force, ...args){ try{ + if(this.prevent_remove && !force){ + // throw error or something here + return; + } let out = await super.remove(...args); await Host.buildLookUpObj(); await this.bustCache(this.host); @@ -333,7 +236,7 @@ class Host extends Table{ return true; } } -Host.register(ModelPs(Host)) +Host.register(ModelPs(Host)); class Cached extends Table{ @@ -344,19 +247,20 @@ class Cached extends Table{ } } -(async function(){ - await Host.buildLookUpObj(); -})(); +// (async function(){ +// await Host.buildLookUpObj(); +// })(); -module.exports = {Host: ModelPs(Host)}; if(require.main === module){(async function(){ try{ - await Host.lookUpReady(); - let host = await Host.get('*.new.test.wtf') + // await Host.lookUpReady(); + console.log(JSON.stringify(Host, null, 2)) + + // let host = await Host.get('*.new.test.wtf') - console.log('host', host.domain.provider.api); + // console.log('host', host.domain.provider.api); diff --git a/nodejs/models/index.js b/nodejs/models/index.js index 36b62cc8..5ce275e5 100644 --- a/nodejs/models/index.js +++ b/nodejs/models/index.js @@ -7,3 +7,5 @@ require('./dns_provider'); require('./host'); require('./token'); require('./user'); +require('./domain'); +require('./cert'); diff --git a/nodejs/models/user_ldap.js b/nodejs/models/user_ldap.js index 0c02db6d..5da86745 100644 --- a/nodejs/models/user_ldap.js +++ b/nodejs/models/user_ldap.js @@ -9,162 +9,65 @@ const client = new Client({ }); -const user_parse = function(data){ - if(data[conf.userNameAttribute]){ - data.username = data[conf.userNameAttribute] - delete data[conf.userNameAttribute]; - } - - if(data.uidNumber){ - data.uid = data.uidNumber; - delete data.uidNumber; - } +class LdapModel extends Model{ + static modelBacking = LdapModel; + static client = client; - return data; } -var User = {} -User.backing = "LDAP"; - -User.keyMap = { - 'username': {isRequired: true, type: 'string', min: 3, max: 500}, - 'password': {isRequired: true, type: 'string', min: 3, max: 500}, -} - -User.list = async function(){ - try{ - await client.bind(conf.bindDN, conf.bindPassword); - - const res = await client.search(conf.searchBase, { - scope: 'sub', - filter: conf.userFilter, - }); - - await client.unbind(); - - return res.searchEntries.map(function(user){return user.uid}); - }catch(error){ - throw error; - } -}; - -User.listDetail = async function(){ - try{ - await client.bind(conf.bindDN, conf.bindPassword); - - const res = await client.search(conf.searchBase, { - scope: 'sub', - filter: conf.userFilter, - }); - - await client.unbind(); - - let users = [] - - for(let user of res.searchEntries){ - let obj = Object.create(this); - Object.assign(obj, user_parse(user)); - - users.push(obj) +class User extends Model{ + static user_parse(data){ + if(data[conf.userNameAttribute]){ + data.username = data[conf.userNameAttribute] + delete data[conf.userNameAttribute]; } - return users; + if(data.uidNumber){ + data.uid = data.uidNumber; + delete data.uidNumber; + } - }catch(error){ - throw error; + return data; } -}; - -User.get = async function(data){ - try{ - if(typeof data !== 'object'){ - let username = data; - data = {}; - data.username = username; - } - - await client.bind(conf.bindDN, conf.bindPassword); - let filter = `(&${conf.userFilter}(${conf.userNameAttribute}=${data.username}))`; + static async get(data, ...args){ + try{ + if(typeof index === 'object'){ + index = index[this._key]; + } - const res = await client.search(conf.searchBase, { - scope: 'sub', - filter: filter, - }); + await this.client.bind(conf.bindDN, conf.bindPassword); - await client.unbind(); + const res = await this.client.search(conf.searchBase, { + scope: 'sub', + filter: `(&${conf.userFilter}(${conf.userNameAttribute}=${data.username}))`, + }); - let user = res.searchEntries[0] + await this.client.unbind(); - if(user){ - let obj = Object.create(this); - Object.assign(obj, user_parse(user)); + if(!res.searchEntries[0]) throw this.errors.EntryNotFound(index) - return obj; - }else{ - let error = new Error('UserNotFound'); - error.name = 'UserNotFound'; - error.message = `LDAP:${data.username} does not exists`; - error.status = 404; + return this(user_parse(res.searchEntries[0])); + }catch(error){ throw error; } - }catch(error){ - throw error; } -}; - -User.exists = async function(data){ - // Return true or false if the requested entry exists ignoring error's. - try{ - await this.get(data); - return true - }catch(error){ - return false; - } -}; + static async login(data){ + try{ + let user = await this.get(data.username); -User.invite = async function(){ - try{ - let token = await InviteToken.add({created_by: this.username}); - - return token; - - }catch(error){ - throw error; - } -}; + await client.bind(user.dn, data.password); -User.login = async function(data){ - try{ - let user = await this.get(data.username); + await client.unbind(); - await client.bind(user.dn, data.password); - - await client.unbind(); - - return user; - - }catch(error){ - throw error; - } -}; + return user; + }catch(error){ + throw error; + } + }; +} -module.exports = {User}; - - -// (async function(){ -// try{ -// console.log(await User.list()); - -// console.log(await User.listDetail()); - -// console.log(await User.get('wmantly')) - -// }catch(error){ -// console.error(error) -// } -// })() \ No newline at end of file diff --git a/nodejs/public/lib/js/app-base.js b/nodejs/public/lib/js/app-base.js index 486cb0c9..8c56d598 100644 --- a/nodejs/public/lib/js/app-base.js +++ b/nodejs/public/lib/js/app-base.js @@ -302,6 +302,8 @@ app.util = (function(app){ $target.html(message).slideDown('fast'); } setTimeout(callback,10) + + return $target; } $.fn.serializeObject = function(){ @@ -383,18 +385,20 @@ $( document ).ready(function(){ $('.momentFromNow').each((idx, el)=>{ var $el = $(el); try{ - $el.html(moment($(el).data('date')).fromNow()); + let date = $(el).data('date'); + if(data) $el.html(moment(date).fromNow()); }catch{} }) }, 30000,); }); + +// (function($){ $.fn.scrollTo = function(){ const yOffset = Number($('#spa-shell').css('margin-top').replace('px', '')); const y = this[0].getBoundingClientRect().top + window.scrollY - yOffset; - console.log('y', y) window.scrollTo({top: y, behavior: 'smooth'}); }; @@ -412,27 +416,25 @@ function formAJAX(btn){ return false; } - app.util.actionMessage( + var $actionTarget = app.util.actionMessage( '
Loading...
', $form, 'info' ); app.api[method]($form.attr('action'), formData, function(error, data){ - app.util.actionMessage(data.message, $form, error ? 'danger' : 'success'); //re-populate table + app.util.actionMessage(data.message, $actionTarget, error ? 'danger' : 'success'); $form.validateClear(); if(!error){ $form.trigger("reset"); eval($form.attr('evalAJAX')); //gets JS to run after completion }else{ - console.log('formAJAX res error', error, data) if(data && data.name === 'ObjectValidateError'){ - app.util.actionMessage('Please fix the form errors', $form, 'danger'); //re-populate table + app.util.actionMessage('Please fix the form errors', $actionTarget, 'danger'); } if(data && data.keys){ - console.log('form key errors', data.keys) for(let keyError of data.keys){ - $form.find(`[name=${keyError.key}]`).validateMessage(keyError.message); + $($form.find(`[name=${keyError.key}]`)[Number(keyError.keyIndex) || 0]).validateMessage(keyError.message); } } } diff --git a/nodejs/public/lib/js/jq-repeat_new.js b/nodejs/public/lib/js/jq-repeat_new.js index c4da5568..3e147bc0 100644 --- a/nodejs/public/lib/js/jq-repeat_new.js +++ b/nodejs/public/lib/js/jq-repeat_new.js @@ -81,6 +81,7 @@ MIT license if( i >= index){ this[i].__jq_$el.attr( 'jq-repeat-index', i+shift ); + this[i].__jq_$el.attr( 'jq-repeat-scope', this.__jqRepeatId ); } } @@ -97,6 +98,7 @@ MIT license //set call name and index keys to DOM element var $render = $( render ).addClass( 'jq-repeat-'+ this.__jqRepeatId ).attr( 'jq-repeat-index', key ); + $render.attr( 'jq-repeat-scope', this.__jqRepeatId ); //if add new elements in proper stop, or after the place holder. if( key === 0 ){ @@ -230,9 +232,11 @@ MIT license var $render = $(Mustache.render(this.__jqTemplate, this.__buildData(index, this[index]))); $render.attr('jq-repeat-index', index); + $render.attr('jq-repeat-scope', this.__jqRepeatId); this.__putUpdate(this[index].__jq_$el, $render, this[index], this); this[index].__jq_$el = $render; + return $render }; result.getByKey = function(key, value){ @@ -250,8 +254,8 @@ MIT license }; result.__putUpdate = function($el, $render, item, list){ + $render.show() $el.replaceWith($render); - $el.show(); }; result.__parseData = function(data){ @@ -347,6 +351,32 @@ MIT license }; + $.fn.scopeGetEl = function(){ + return this.closest('[jq-repeat-scope]'); + }; + + $.fn.scopeGet = function(){ + let $el = this.scopeGetEl() + if($el) return $.scope[$el.attr('jq-repeat-scope')]; + }; + + $.fn.scopeItem = function(){ + let $el = this.scopeGetEl() + if($el) return $.scope[$el.attr('jq-repeat-scope')][$el.attr('jq-repeat-index')]; + }; + + $.fn.scopeItemUpdate = function(data){ + console.log('args', arguments) + let $el = this.scopeGetEl(); + console.log('scope', $.scope[$el.attr('jq-repeat-scope')]) + $.scope[$el.attr('jq-repeat-scope')].update(Number($el.attr('jq-repeat-index')), data) ; + }; + + $.fn.scopeItemRemove = function(){ + let $el = this.scopeGetEl() + $.scope[$el.attr('jq-repeat-scope')].remove(Number($el.attr('jq-repeat-index'))) ; + }; + $( document ).ready( function(){ // Create an instance of MutationObserver and pass the callback function diff --git a/nodejs/public/lib/js/val.js b/nodejs/public/lib/js/val.js index b372555a..9b0df9c9 100755 --- a/nodejs/public/lib/js/val.js +++ b/nodejs/public/lib/js/val.js @@ -55,7 +55,7 @@ //checks if field is required, and length if(!isNaN(options) && value.length < options){ - message = `Must be ${options} characters`; + message = options == 1 ? 'Required' : `Must be ${options} characters`; } //checks if empty to stop processing diff --git a/nodejs/routes/cert.js b/nodejs/routes/cert.js index 40db3302..cb8bb7fd 100644 --- a/nodejs/routes/cert.js +++ b/nodejs/routes/cert.js @@ -1,15 +1,80 @@ 'use strict'; const router = require('express').Router(); -const {getCert} = require('../models/cert'); +const {Cert} = require('../models').models; -router.get('/:host', async function(req, res, next){ +router.get('/', async(req, res, next)=>{ try{ - return res.json(await getCert(req.params.host)); + return res.json({ + results: await Cert.findall() + }); }catch(error){ - return next(error); + next(error); } }); +router.get('/:item', async(req, res, next)=>{ + try{ + let item = await Cert.get(req.params.item); + return res.json({ + results: item, + }); + }catch(error){ + next(error); + } +}); + +router.get('/:item/cert/:type', async(req, res, next)=>{ + try{ + let item = await Cert.get(req.params.item); + return res.json({ + results: await item.getPem(req.params.type), + }); + }catch(error){ + next(error); + } +}); + +router.post('/', async (req, res, next)=>{ + try{ + req.body.created_by = req.user.username; + return res.json({ + results: await Cert.create({ + ...req.body + }), + ok: true, + }); + }catch(error){ + next(error); + } +}); + +router.put('/:item/renew', async (req, res, next)=>{ + try{ + let item = await Cert.get(req.params.item); + + return res.json({ + results: await item.renew({ + username: req.user.username, + }), + message: `Renewing certificate.`, + }); + }catch(error){ + next(error); + } +}); + +router.delete('/:item', async (req, res, next)=>{ + try{ + let item = await Cert.get(req.params.item); + return res.json({ + results: await item.remove(), + message: `${req.params.item} has been removed` + }); + }catch(error){ + next(error); + } +}) + module.exports = router; diff --git a/nodejs/routes/dns.js b/nodejs/routes/dns.js index 6fdedc4d..936c9834 100644 --- a/nodejs/routes/dns.js +++ b/nodejs/routes/dns.js @@ -18,7 +18,7 @@ router.get('/', async function(req, res, next){ router.options('/', async function(req, res, next){ try{ return res.json({ - results: await Model.listProviders() + results: Model, }); }catch(error){ return next(error); @@ -30,9 +30,10 @@ router.post('/', async function(req, res, next){ req.body.created_by = req.user.username; let item = await Model.create(req.body); + console.log('added', item) + return res.json({ - message: `"${item[Model._key]}" added.`, - ...item, + results: item }); } catch (error){ next(error); diff --git a/nodejs/routes/host.js b/nodejs/routes/host.js index 4e7be89e..64f1b2ba 100755 --- a/nodejs/routes/host.js +++ b/nodejs/routes/host.js @@ -41,6 +41,17 @@ router.get('/lookup/:item', async function(req, res, next){ } }); +router.get('/lookupobj', async function(req, res, next){ + try{ + return res.json({ + results: await Model.lookUpObj, + }); + + }catch(error){ + return next(error); + } +}); + router.get('/:item', async function(req, res, next){ try{ diff --git a/nodejs/routes/render.js b/nodejs/routes/render.js index b9876fc6..e81c9da9 100644 --- a/nodejs/routes/render.js +++ b/nodejs/routes/render.js @@ -33,11 +33,14 @@ router.get('/hosts', async function(req, res, next) { res.render('hosts', {...values}); }); +router.get('/cert', async function(req, res, next) { + res.render('cert', {...values}); +}); + router.get('/dns', async function(req, res, next) { res.render('dns', {...values}); }); - router.get('/users', async function(req, res, next) { res.render('users', {...values}); }); diff --git a/nodejs/utils/letsencrypt.js b/nodejs/utils/letsencrypt.js index 908aa774..1e5feaa1 100644 --- a/nodejs/utils/letsencrypt.js +++ b/nodejs/utils/letsencrypt.js @@ -37,16 +37,14 @@ class LetsEncrypt{ } } - async dnsWildcard(domain, options){ + async dnsWildcard(domains, options){ /* https://github.com/publishlab/node-acme-client/tree/master/examples/dns-01 */ try{ - domain = domain.replace(/^\*\./, ''); - const [key, csr] = await AcmeClient.crypto.createCsr({ - altNames: [domain, `*.${domain}`], + altNames: domains, }); let dnsToAdd = 0; @@ -84,12 +82,11 @@ class LetsEncrypt{ await sleep(10000); break; } - if(checkCount++ > 60) throw new Error('challengeCreateFn validation timed out'); - await sleep(1500); + if(checkCount++ > 100*dnsToAdd) throw new Error('challengeCreateFn validation timed out'); + await sleep(2000); } }catch(error){ - console.log('dns check failed error:', error) - options.onDnsCheckFail(authz, error) + throw error } }, challengeRemoveFn: options.challengeRemoveFn, @@ -102,7 +99,8 @@ class LetsEncrypt{ }; }catch(error){ - console.log('Error in LetsEncrypt.dnsChallenge', error) + console.log('LE dnsWildcard error:', error) + throw error } } } diff --git a/nodejs/utils/model_pubsub.js b/nodejs/utils/model_pubsub.js index e24c8c14..58120ab8 100644 --- a/nodejs/utils/model_pubsub.js +++ b/nodejs/utils/model_pubsub.js @@ -1,4 +1,5 @@ 'use strict'; + const ps = require('../controller/pubsub'); @@ -29,6 +30,7 @@ function ModelPs(model){ if(propKey == 'constructor') return target.constructor; const targetValue = Reflect.get(target, propKey, receiver); if (typeof targetValue === 'function') { + // console.log('prop called', propKey, target.name || target.constructor.name) return function(...args){ try{ // let res = targetValue.apply(this, args); // (A) @@ -37,8 +39,8 @@ function ModelPs(model){ res.then(function(res){ publish(propKey, res, ...args); }).catch(function(error){ - - console.log('toDo, publish errors...'); + // console.error('ASYNC error from proxy:', error) + console.log('toDo, publish async errors...'); }); }else{ publish(propKey, res, ...args); diff --git a/nodejs/utils/object_validate.js b/nodejs/utils/object_validate.js index f206df9a..aa21831c 100644 --- a/nodejs/utils/object_validate.js +++ b/nodejs/utils/object_validate.js @@ -99,8 +99,8 @@ function parseToString(data){ function ObjectValidateError(keys, message){ let error = new Error('ObjectValidateError') error.name = "ObjectValidateError" - error.message = message || `Invalid Keys: ${message}` - error.keys = (keys || {}); + error.message = `Invalid Keys` + error.keys = (keys || []); error.status = 422; return error diff --git a/nodejs/utils/redis_model.js b/nodejs/utils/redis_model.js index 9b624569..e5840ca6 100644 --- a/nodejs/utils/redis_model.js +++ b/nodejs/utils/redis_model.js @@ -12,9 +12,9 @@ function redisPrefix(key){ } class QueryHelper{ - hisroty = [] + hisroty = []; constructor(orgin){ - this.orgin = orgin + this.orgin = orgin; this.hisroty.push(orgin.constructor.name); } @@ -23,12 +23,12 @@ class QueryHelper{ if(queryHelper.hisroty.includes(modleName)){ return true; } - queryHelper.hisroty.push(modleName) + queryHelper.hisroty.push(modleName); } } } -class Table{ +class Model{ static errors = { ObjectValidateError: objValidate.ObjectValidateError, EntryNameUsed: ()=>{ @@ -42,14 +42,38 @@ class Table{ error.status = 409; return error; - } - } + }, + EntryNotFound: (index)=>{ + let error = new Error('EntryNotFound'); + error.name = 'EntryNotFound'; + error.message = `${this.name}:${index} does not exists`; + error.status = 404; - static redisClient = client; + return error; + }, + EntryNameUsed: (data)=>{ + let error = new Error('EntryNameUsed'); + error.name = 'EntryNameUsed'; + error.message = `${this.constructor.name}:${data[this.constructor._key]} already exists`; + error.keys = [{ + key: this.constructor._key, + message: `${this.constructor.name}:${data[this.constructor._key]} already exists` + }] + error.status = 409; + + return error; + }, + } static models = {} - static register = function(Model){ + + static register = function(Model, proxy){ Model = Model || this; + if(proxy){ + Model.__proxy = proxy; + Model = proxy(Model); + } + this.models[Model.name] = Model; } @@ -59,68 +83,177 @@ class Table{ } } - static async get(index, queryHelper){ - try{ - if(typeof index === 'object'){ - index = index[this._key]; - } - - let result = await client.HGETALL( - redisPrefix(`${this.prototype.constructor.name}_${index}`) - ); - - if(!Object.keys(result).length){ - let error = new Error('EntryNotFound'); - error.name = 'EntryNotFound'; - error.message = `${this.prototype.constructor.name}:${index} does not exists`; - error.status = 404; - throw error; + async buildRelations(queryHelper){ + + if(Object.values(this.constructor._keyMap).some(i=>i.extends)){ + for(let [key, options] of Object.entries(this.constructor._keyMap)){ + if(options.extends){ + if(this.constructor.__extendedModels && this[key] && !this.constructor.__isExtended){ + return this.constructor.__extendedModels[key][this[key]] + } + } } - - // Redis always returns strings, use the keyMap schema to turn them - // back to native values. - result = objValidate.parseFromString(this._keyMap, result); - - let instance = new this(result); - await instance.buildRelations(queryHelper); - - return instance; - }catch(error){ - throw error; } - } - - async buildRelations(queryHelper){ + // Loop over all the fields from the schema, matching any that have a + // relationship. for(let [key, options] of Object.entries(this.constructor._keyMap)){ if(options.model){ let remoteModel = this.constructor.models[options.model] try{ + + // Test if we are in a lookup cycle and bale if we are. if(QueryHelper.isNotCycle(remoteModel.name, queryHelper)) continue; + + // Swap the relationship key with the built relationship if(options.rel === 'one'){ - // console.log('relone:', this[key], queryHelper, remoteModel, await remoteModel.get(this[key], queryHelper || new QueryHelper(this))) - this[key] = await remoteModel.get(this[key] || this[options.localKey || this.constructor._key] , queryHelper || new QueryHelper(this)) + + this[key] = await remoteModel.get( + this[key] || this[options.localKey || this.constructor._key] , + queryHelper || new QueryHelper(this) + ); } if(options.rel === 'many'){ this[key] = await remoteModel.listDetail({ [options.remoteKey]: this[options.localKey || this.constructor._key], - },queryHelper || new QueryHelper(this)) + },queryHelper || new QueryHelper(this)); + + // Add a method to this instance to add values to the + // current many + this[`${key}Create`] = async (data, ...args)=>{ + let item = await remoteModel.create({ + ...data, + [options.remoteKey]: this[options.localKey || this.constructor._key], + }) + + this[key].push(item); + this.update(); + + return this; + } + + // do this better... + this.remove = (()=>{ + let currentRemove = this.remove; + return async (data, ...args)=>{ + try{ + for(let item of this[key]){ + await item.remove(); + } + }catch{} + return await currentRemove.call(this, data, ...args); + } + })() } - }catch{} + // if(options.rel === 'manyToMany'){ + // // Make through table + // let nameSort = [options.model, this.constructor.name]; + // let name = nameSort.sort().join('_join_'); + + // if(!this.constructor.models[name]){ + // this.constructor.models[name] = ({ + // [name] : class extends Model { + // static _keyMap = { + // 'created_on': {default: function(){return (new Date).getTime()}}, + // `${options.model}_key`: {type: 'string', isRequired: true} + // `${this.constructor.name}_key`: {type: 'string', isRequired: true} + // } + // } + // })[name]; + // } + + // remoteModel = this.constructor.models[name]; + + // this[key] = await remoteModel.findall({ + // `${this.constructor.name}_key`: this[options.localKey || this.constructor._key] + // }) + + // } + }catch(error){ + console.log('buildRelations error', error) + } } } } - static async exists(index){ - if(typeof index === 'object'){ - index = index[this._key]; - } + static extend(key, Model){ + if(!this.__extendedModels) this.__extendedModels = {}; + if(!this.__extendedModels[key]) this.__extendedModels[key] = {}; + + let originalKeyMap = Object.assign({}, Model._keyMap); + let _keyMap = {...this._keyMap, ...Model._keyMap}; + + let parentModel = this; + + let cls = ({ + [this.name] : class extends Model { + static _keyMap = _keyMap; + static _originalKeyMap = originalKeyMap; + static __isExtended = true; + static __orginalMethods = Object.getOwnPropertyNames(Model); + + static toJSON(){ + return { + fields: this._originalKeyMap, + name: Model.name, + ...super.toJSON(), + }; + }; + + static async get(...args){ + let instance = await super.get(...args); + if(parentModel.__proxy){ + instance = parentModel.__proxy(instance); + } + return instance; + } + } + })[this.name]; - return await client.SISMEMBER( - redisPrefix(this.prototype.constructor.name), - index - ); + this.__extendedModels[key][Model.name] = cls; + } + + static async __buildInstance(data, queryHelper){ + let instance = new this(data); + let newThis = await instance.buildRelations(queryHelper); + if(newThis) return await newThis.get(data, queryHelper); + + return instance; + } +} + +class Table extends Model{ + static modelBacking = Table; + static redisClient = client; + + static async get(data, queryHelper){ + try{ + let index; + if(typeof data === 'object'){ + if('type' in data){ + return await this.__buildInstance(data, queryHelper); + } + index = data[this._key]; + }else{ + index = data; + } + + let result = await client.HGETALL( + redisPrefix(`${this.name}_${index}`) + ); + + if(!Object.keys(result).length) throw this.errors.EntryNotFound(index) + + // Redis always returns strings, use the keyMap schema to turn them + // back to native values. + result = objValidate.parseFromString(this._keyMap, result); + + return await this.__buildInstance(result, queryHelper); + + }catch(error){ + throw error; + } } static async list(){ @@ -155,6 +288,17 @@ class Table{ return out; } + static async exists(index){ + if(typeof index === 'object'){ + index = index[this._key]; + } + + return await client.SISMEMBER( + redisPrefix(this.prototype.constructor.name), + index + ); + } + static findall(...args){ return this.listDetail(...args); } @@ -163,6 +307,16 @@ class Table{ // Add a entry to this redis table. try{ + // See if this can have an class has a key that calls for the model + // to be extended. + for(let [key, options] of Object.entries(this._keyMap)){ + if(options.extends){ + if(this.__extendedModels && data[key] && !this.__isExtended){ + return await this.__extendedModels[key][data[key]].create(data) + } + } + } + // Validate the passed data by the keyMap schema. data = objValidate.processKeys(this._keyMap, data); @@ -207,23 +361,14 @@ class Table{ // Update an existing entry. try{ // Validate the passed data, ignoring required fields. - data = objValidate.processKeys(this.constructor._keyMap, data, true); + data = objValidate.processKeys(this.constructor._keyMap, data || {}, true); // Check to see if entry name changed. if(data[this.constructor._key] && data[this.constructor._key] !== this[this.constructor._key]){ // Remove the index key from the tables members list. if(data[this.constructor._key] && await this.constructor.exists(data)){ - let error = new Error('EntryNameUsed'); - error.name = 'EntryNameUsed'; - error.message = `${this.constructor.name}:${data[this.constructor._key]} already exists`; - error.keys = [{ - key: this.constructor._key, - message: `${this.constructor.name}:${data[this.constructor._key]} already exists` - }] - error.status = 409; - - throw error; + throw this.constructor.errors.EntryNameUsed(data); } await client.SREM( @@ -254,7 +399,6 @@ class Table{ ); } - return this; } catch(error){ @@ -286,7 +430,17 @@ class Table{ } }; + static toJSON(){ + return { + name: this.name, + fields: this._keyMap, + pk: this._key, + extend: this.__extendedModels ? this.__extendedModels[Object.keys(this.__extendedModels)[0]] : undefined, + } + } + toJSON(){ + // Remove any value that is marked private let result = {}; for (const [key, value] of Object.entries(this)) { if(this.constructor._keyMap[key] && this.constructor._keyMap[key].isPrivate) continue; @@ -294,15 +448,12 @@ class Table{ } return result - - // return JSON.stringify(result); } toString(){ return this[this.constructor._key]; } - } -module.exports = Table; \ No newline at end of file +module.exports = Table; diff --git a/nodejs/views/cert.ejs b/nodejs/views/cert.ejs new file mode 100644 index 00000000..c4d25436 --- /dev/null +++ b/nodejs/views/cert.ejs @@ -0,0 +1,323 @@ +<%- include('top') %> + + + + + + +<%- include('bottom') %> diff --git a/nodejs/views/dns.ejs b/nodejs/views/dns.ejs index 2e0ea343..4bc9274e 100644 --- a/nodejs/views/dns.ejs +++ b/nodejs/views/dns.ejs @@ -26,16 +26,17 @@ - + +
+
+ @@ -201,12 +208,10 @@
-
+ + - - - - - + + <%- include('bottom') %> diff --git a/nodejs/views/hosts.ejs b/nodejs/views/hosts.ejs index 5652fc05..bddb1ca2 100755 --- a/nodejs/views/hosts.ejs +++ b/nodejs/views/hosts.ejs @@ -32,6 +32,8 @@ + + + + +
-
<%- include('bottom') %> diff --git a/nodejs/views/top.ejs b/nodejs/views/top.ejs index 2b3be7b2..288acf72 100755 --- a/nodejs/views/top.ejs +++ b/nodejs/views/top.ejs @@ -12,8 +12,6 @@ - - @@ -44,6 +42,12 @@ Hosts +