2020-03-13 17:49:51 -04:00
import fs , { writeFile , unlink } from 'fs-extra' ;
import axios from 'axios' ;
import { Message } from 'eris' ;
import { AccountInterface } from '../models' ;
import { Command , RichEmbed } from '../class' ;
import { Client } from '..' ;
import { parseCertificate } from '../functions' ;
export default class CWG_Create extends Command {
public urlRegex : RegExp ;
constructor ( client : Client ) {
super ( client ) ;
this . name = 'create' ;
this . description = 'Bind a domain to the CWG' ;
this . usage = ` ${ this . client . config . prefix } cwg create [User ID | Username] [Domain] [Port] <Cert Chain> <Private Key> || Use snippets raw URL ` ;
this . permissions = { roles : [ '525441307037007902' ] } ;
this . aliases = [ 'bind' ] ;
this . enabled = true ;
this . urlRegex = /^[a-zA-Z0-9\-._~:/?#[\]@!$&'()*+,;=]+$/ ;
}
public async run ( message : Message , args : string [ ] ) {
/ *
args [ 0 ] should be the user ' s ID OR account username ; required
args [ 1 ] should be the domain ; required
args [ 2 ] should be the port ; required
args [ 3 ] should be the path to the x509 certificate ; not required
args [ 4 ] should be the path to the x509 key ; not required
* /
try {
if ( ! args [ 2 ] ) return this . client . commands . get ( 'help' ) . run ( message , [ 'cwg' , this . name ] ) ;
let certs : { cert : string , key : string } ;
if ( ! this . urlRegex . test ( args [ 1 ] ) ) return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } ***Invalid URL*** ` ) ;
if ( Number ( args [ 2 ] ) < 1024 || Number ( args [ 2 ] ) > 65535 ) return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } ***Port must be greater than 1024 and less than 65535*** ` ) ;
if ( ! args [ 1 ] . endsWith ( '.cloud.libraryofcode.org' ) && ! args [ 4 ] ) return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } ***Certificate Chain and Private Key are required for custom domains*** ` ) ;
const account = await this . client . db . Account . findOne ( { $or : [ { username : args [ 0 ] } , { userID : args [ 0 ] } ] } ) ;
if ( ! account ) return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } Cannot locate account, please try again. ` ) ;
if ( await this . client . db . Domain . exists ( { domain : args [ 1 ] } ) ) return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } ***This domain already exists*** ` ) ;
if ( await this . client . db . Domain . exists ( { port : Number ( args [ 2 ] ) } ) ) {
let answer : Message ;
try {
answer = await this . client . util . messageCollector (
message ,
` *** ${ this . client . stores . emojis . error } ***This port is already binded to a domain. Do you wish to continue? (y/n)*** ` ,
30000 , true , [ 'y' , 'n' ] , ( msg ) = > msg . author . id === message . author . id && msg . channel . id === message . channel . id ,
) ;
} catch ( error ) {
return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } ***Bind request cancelled*** ` ) ;
}
if ( answer . content === 'n' ) return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } ***Bind request cancelled*** ` ) ;
}
const edit = await message . channel . createMessage ( ` ${ this . client . stores . emojis . loading } ***Binding domain...*** ` ) ;
if ( ! args [ 1 ] . endsWith ( '.cloud.libraryofcode.org' ) ) {
const urls = args . slice ( 3 , 5 ) ;
if ( urls . some ( ( l ) = > ! l . includes ( 'snippets.cloud.libraryofcode.org/raw/' ) ) ) return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } ***Invalid snippets URL*** ` ) ;
const tasks = urls . map ( ( l ) = > axios ( { method : 'GET' , url : l } ) ) ;
const response = await Promise . all ( tasks ) ;
const certAndPrivateKey : string [ ] = response . map ( ( r ) = > r . data ) ;
if ( ! this . isValidCertificateChain ( certAndPrivateKey [ 0 ] ) ) return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } ***Invalid Certificate Chain*** ` ) ;
if ( ! this . isValidPrivateKey ( certAndPrivateKey [ 1 ] ) ) return message . channel . createMessage ( ` ${ this . client . stores . emojis . error } ***Invalid Private Key*** ` ) ;
certs = { cert : certAndPrivateKey [ 0 ] , key : certAndPrivateKey [ 1 ] } ;
}
const domain = await this . createDomain ( account , args [ 1 ] , Number ( args [ 2 ] ) , certs ) ;
const tasks = [ message . delete ( ) , this . client . util . exec ( 'systemctl reload' ) ] ;
// @ts-ignore
await Promise . all ( tasks ) ;
const embed = new RichEmbed ( )
. setTitle ( 'Domain Creation' )
. setColor ( 3066993 )
. addField ( 'Account Username' , account . username , true )
. addField ( 'Account ID' , account . id , true )
. addField ( 'Engineer' , ` <@ ${ message . author . id } > ` , true )
. addField ( 'Domain' , domain . domain , true )
. addField ( 'Port' , String ( domain . port ) , true ) ;
const cert = await parseCertificate ( this . client , domain . x509 . cert ) ;
embed . addField ( 'Certificate Issuer' , cert . issuer . organizationName , true )
. addField ( 'Certificate Subject' , cert . subject . commonName , true )
. setFooter ( this . client . user . username , this . client . user . avatarURL )
. setTimestamp ( new Date ( message . timestamp ) ) ;
const completed = [
edit . edit ( ` *** ${ this . client . stores . emojis . success } Successfully binded ${ domain . domain } to port ${ domain . port } for ${ account . userID } .*** ` ) ,
this . client . createMessage ( '580950455581147146' , { embed } ) ,
this . client . getDMChannel ( account . userID ) . then ( ( r ) = > r . createMessage ( { embed } ) ) ,
this . client . util . transport . sendMail ( {
to : account.emailAddress ,
from : 'Library of Code sp-us | Support Team <help@libraryofcode.org>' ,
subject : 'Your domain has been binded' ,
html : `
< h1 > Library of Code sp - us | Cloud Services < / h1 >
< p > Hello , this is an email informing you that a new domain under your account has been binded .
Information is below . < / p >
< b > Domain : < / b > $ { domain . domain } < br >
< b > Port : < / b > $ { domain . port } < br >
< b > Certificate Issuer : < / b > $ { cert . issuer . organizationName } < br >
< b > Certificate Subject : < / b > $ { cert . subject . commonName } < br >
< b > Responsible Engineer : < / b > $ { message . author . username } # $ { message . author . discriminator } < br > < br >
If you have any questions about additional setup , you can reply to this email or send a message in # cloud - support in our Discord server . < br >
< b > < i > Library of Code sp - us | Support Team < / i > < / b >
` ,
} ) ,
] ;
if ( ! domain . domain . includes ( 'cloud.libraryofcode.org' ) ) {
const content = ` __**DNS Record Setup**__ \ nYou recently a binded a custom domain to your Library of Code sp-us Account. You'll have to update your DNS records. We've provided the records below. \ n \ n \` ${ domain . domain } IN CNAME cloud.libraryofcode.org AUTO/500 \` \ nThis basically means you need to make a CNAME record with the key/host of ${ domain . domain } and the value/point to cloud.libraryofcode.org. If you have any questions, don't hesitate to ask us. ` ;
completed . push ( this . client . getDMChannel ( account . userID ) . then ( ( r ) = > r . createMessage ( content ) ) ) ;
}
return Promise . all ( completed ) ;
} catch ( err ) {
await fs . unlink ( ` /etc/nginx/sites-available/ ${ args [ 1 ] } ` ) ;
await fs . unlink ( ` /etc/nginx/sites-enabled/ ${ args [ 1 ] } ` ) ;
await this . client . db . Domain . deleteMany ( { domain : args [ 1 ] } ) ;
return this . client . util . handleError ( err , message , this ) ;
}
}
/ * *
* This function binds a domain to a port on the CWG .
* @param account The account of the user .
* @param subdomain The domain to use . ` mydomain.cloud.libraryofcode.org `
* @param port The port to use , must be between 1024 and 65535 .
* @param x509Certificate The contents the certificate and key files .
* @example await CWG . createDomain ( account , 'mydomain.cloud.libraryofcode.org' , 6781 ) ;
* /
public async createDomain ( account : AccountInterface , domain : string , port : number , x509Certificate : { cert? : string , key? : string } ) {
try {
if ( port <= 1024 || port >= 65535 ) throw new RangeError ( ` Port range must be between 1024 and 65535, received ${ port } . ` ) ;
if ( await this . client . db . Domain . exists ( { domain } ) ) throw new Error ( ` Domain ${ domain } already exists in the database. ` ) ;
if ( ! await this . client . db . Account . exists ( { userID : account.userID } ) ) throw new Error ( ` Cannot find account ${ account . userID } . ` ) ;
let x509 : { cert : string , key : string } ;
if ( x509Certificate ) {
x509 = await this . createCertAndPrivateKey ( domain , x509Certificate . cert , x509Certificate . key ) ;
}
let cfg = await fs . readFile ( '/var/CloudServices/dist/static/nginx.conf' , { encoding : 'utf8' } ) ;
cfg = cfg . replace ( /\[DOMAIN]/g , domain ) ;
cfg = cfg . replace ( /\[PORT]/g , String ( port ) ) ;
cfg = cfg . replace ( /\[CERTIFICATE]/g , x509 . cert ) ;
cfg = cfg . replace ( /\[KEY]/g , x509 . key ) ;
await fs . writeFile ( ` /etc/nginx/sites-available/ ${ domain } ` , cfg , { encoding : 'utf8' } ) ;
await fs . symlink ( ` /etc/nginx/sites-available/ ${ domain } ` , ` /etc/nginx/sites-enabled/ ${ domain } ` ) ;
const entry = new this . client . db . Domain ( {
account ,
domain ,
port ,
x509 ,
enabled : true ,
} ) ;
if ( domain . includes ( 'cloud.libraryofcode.org' ) ) {
const dmn = domain . split ( '.' ) ;
await axios ( {
method : 'post' ,
url : 'https://api.cloudflare.com/client/v4/zones/5e82fc3111ed4fbf9f58caa34f7553a7/dns_records' ,
headers : { Authorization : ` Bearer ${ this . client . config . cloudflare } ` , 'Content-Type' : 'application/json' } ,
data : JSON.stringify ( { type : 'CNAME' , name : ` ${ dmn [ 0 ] } . ${ dmn [ 1 ] } ` , content : 'cloud.libraryofcode.org' , proxied : false } ) ,
} ) ;
}
return entry . save ( ) ;
} catch ( error ) {
await fs . unlink ( ` /etc/nginx/sites-enabled/ ${ domain } ` ) ;
await fs . unlink ( ` /etc/nginx/sites-available/ ${ domain } ` ) ;
await this . client . db . Domain . deleteMany ( { domain } ) ;
throw error ;
}
}
public async createCertAndPrivateKey ( domain : string , certChain : string , privateKey : string ) {
if ( ! this . isValidCertificateChain ( certChain ) ) throw new Error ( 'Invalid Certificate Chain' ) ;
if ( ! this . isValidPrivateKey ( privateKey ) ) throw new Error ( 'Invalid Private Key' ) ;
const path = ` /var/CloudServices/temp/ ${ domain } ` ;
const temp = [ writeFile ( ` ${ path } .chain.crt ` , certChain ) , writeFile ( ` ${ path } .key.pem ` , privateKey ) ] ;
const removeFiles = [ unlink ( ` ${ path } .chain.crt ` ) , unlink ( ` ${ path } .key.pem ` ) ] ;
await Promise . all ( temp ) ;
if ( ! this . isMatchingPair ( ` ${ path } .chain.crt ` , ` ${ path } .key.pem ` ) ) {
await Promise . all ( removeFiles ) ;
throw new Error ( 'Certificate and Private Key do not match' ) ;
}
const tasks = [ writeFile ( ` /etc/nginx/ssl/ ${ domain } .chain.crt ` , certChain ) , writeFile ( ` /etc/nginx/ssl/ ${ domain } .key.pem ` , privateKey ) ] ;
await Promise . all ( tasks ) ;
return { cert : ` /etc/nginx/ssl/ ${ domain } .chain.crt ` , key : ` /etc/nginx/ssl/ ${ domain } .key.pem ` } ;
}
public checkOccurance ( text : string , query : string ) {
return ( text . match ( new RegExp ( query , 'g' ) ) || [ ] ) . length ;
}
public isValidCertificateChain ( cert : string ) {
if ( ! cert . replace ( /^\s+|\s+$/g , '' ) . startsWith ( '-----BEGIN CERTIFICATE-----' ) ) return false ;
if ( ! cert . replace ( /^\s+|\s+$/g , '' ) . endsWith ( '-----END CERTIFICATE-----' ) ) return false ;
if ( this . checkOccurance ( cert . replace ( /^\s+|\s+$/g , '' ) , '-----BEGIN CERTIFICATE-----' ) !== 2 ) return false ;
if ( this . checkOccurance ( cert . replace ( /^\s+|\s+$/g , '' ) , '-----END CERTIFICATE-----' ) !== 2 ) return false ;
return true ;
}
public isValidPrivateKey ( key : string ) {
if ( ! key . replace ( /^\s+|\s+$/g , '' ) . startsWith ( '-----BEGIN PRIVATE KEY-----' ) ) return false ;
if ( ! key . replace ( /^\s+|\s+$/g , '' ) . endsWith ( '-----END PRIVATE KEY-----' ) ) return false ;
if ( this . checkOccurance ( key . replace ( /^\s+|\s+$/g , '' ) , '-----BEGIN PRIVATE KEY-----' ) !== 1 ) return false ;
if ( this . checkOccurance ( key . replace ( /^\s+|\s+$/g , '' ) , '-----END PRIVATE KEY-----' ) !== 1 ) return false ;
return true ;
}
public async isMatchingPair ( cert : string , privateKey : string ) {
const result : string = await this . client . util . exec ( ` ${ __dirname } /../bin/checkCertSignatures ${ cert } ${ privateKey } ` ) ;
const { ok } : { ok : boolean } = JSON . parse ( result ) ;
return ok ;
}
}