Skip to content

Commit

Permalink
Initial release
Browse files Browse the repository at this point in the history
HomebrewDIY derivative for AWS Lightsail.
  • Loading branch information
CanyonCasa committed Nov 12, 2022
1 parent 2aaa7fc commit 7653546
Show file tree
Hide file tree
Showing 17 changed files with 3,173 additions and 1 deletion.
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
restricted/*.js*
bin/node_modules
bin/x*.js
*workspace
181 changes: 180 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,181 @@
# HomebrewCloud
HomebrewDIY derivative for AWS Lightsail
HomebrewDIY derivative for AWS Lightsail.

## Instance Setup

### Prerequisites
* Create an AWS account
* Setup AWS account users
* Create a Lightsail Instance
* Select a region
* Linux distro
* Node app

Additional customization may be needed as desired such as setting your timezone.

### SSH Via browser
From the Lightsail instance in the management console click the _'prompt >_ icon'_ to open a connection window in a browser window directly to the instance.

## SSH Via Putty
From the Lightsail instance in the management console click the _'vertical ... icon'_ to open the instance management window.

Note the CONNECT TO public IP address. This is the host to which Putty will connect. Enter into the Putty configuration. **Note: May change if not a static IP.**

Scroll to the bottom and click the _'Download default key'_. Save the file to a **secure** location. Use PuttyGen to convert the _.pem_ file to a _.ppk_ file. Load the file then Save as Private Key.

Enter the file file location into Putty settings under _Connections | SSH | Auth | Private key file for authentication_.

Under _Connections | Data | Auto-login username_ enter _bitnami_.

Set any other desired settings such as colors then save settings.

Open to create a connection.

## Putting Homebrew in the Cloud
The Lightsail Linux/NodeJS blueprint defaults to running the Apache web server at the standard http (80) and https (443) ports.

You have at least 4 ways to run the Homebrew code in the AWS Lightsail cloud environemnt. Basic setup for each requires dealing with this default Apache setup in one way or another. Each method has pros and cons as outlined in the descriptions below.

## METHOD 1: Use Apache as a Proxy
In this setup you configure Apache to proxy requests sent to the standard ports onto the NodeJS app configured ports.

Pros/Cons
* Apache dependency
* Minimal impact/changes to default setup
* Automatic certificate handling
* Multiple NodeJS endpoints with more involved/complex virtual hosts setup.

### How To
For this method you configure Apache to proxy requests to the localhost:port defined for the NodeJS app. Create the _/opt/bitnami/apache/conf/vhosts/app-https-vhost.conf_ file with the following contents...

<VirtualHost _default_:443>
ServerName YOUR_SERVER_NAME
#ServerAlias *
SSLEngine on
SSLCertificateFile "/opt/bitnami/apache/conf/bitnami/certs/server.crt"
SSLCertificateKeyFile "/opt/bitnami/apache/conf/bitnami/certs/server.key"
#DocumentRoot /opt/bitnami/projects/sample
#<Directory "/opt/bitnami/projects/sample">
# Options -Indexes +FollowSymLinks -MultiViews
# Require all granted
#</Directory>
ProxyPass / http://localhost:PORT/
ProxyPassReverse / http://localhost:PORT/
</VirtualHost>

Be sure to change YOUR_SERVER_NAME and PORT to those of your app.

After defining your domain/hosts and ensuring proper DNS setup, run the following command:

sudo /opt/bitnami/bncert-tool

This will create a Let's Encrypt certificate, setup automatic renewal, and redirect http requests to https.

## METHOD 2: Reassign Default Apache Ports
In this setup you configure Apache to use alternate ports so the NodeJS app can use the standard ports.

Pros/Cons
* Apache dependency
* Minimal impact/changes to default setup
* Certificate handling?
* NodeJS app run as root or IP tables configured to forward privileged ports to non-privileged ports. (Note: by design the Homebrew apps all assume non-root operation.)

### How To
Apache uses the default web hosting ports, which you will want to reassign so user can access your app using the defaults ports, 80 and 443.

Edit the _/opt/bitnami/apache2/conf/httpd.conf_ file and change the line

Listen 80

to the lines

#Listen 80
Listen 8080

Where port 8080 (or other port of your choice) is the new Apache port. Save and exit.

Then edit the _/opt/bitnami/apache2/conf/bitnami/bitnami.conf_ file and change the line

<VirtualHost _default_:80>

to

<VirtualHost _default_:8080>

Where the port number matches the value set above in the _httpd.conf_ file.

Then restart apache using the bitnami control script

sudo /opt/bitnami/ctlscript.sh restart apache

Then open the firewall for the specific port... Open the Lightsail management console for the instance and click Networking. Scroll down to IPv4 Firewall and add a rule for port 8080 (or port of choice defined above).

#################################

**TBD: Then same must be done for port 443.**

#################################

Ports 80 and 443 are then available for the Homebrew app use. Note, Linux considered ports below 1024 as privileged requiring root permissions. The iptables command can be used (as described elsewhere in Homebrew documentation) to map the privileged ports to non-privileged ports.

## METHOD 3: Bypass Apache
In this setup you disable the Apache server altogether and use the HomebrewDIY code to proxy to one or more backend NodeJS apps.

Pros/Cons
* No Apache dependency
* Greater changes to the default setup
* Certificate handling?
* NodeJS app run as root or IP tables configured to forward privileged ports to non-privileged ports. (Note: by design the Homebrew apps all assume non-root operation.)
* Multiple NodeJS apps or services run from a single IP address.

### How To
To disable Apache, first stop the server by running

sudo /opt/bitnami/ctlscript.sh stop

Then disable the service from running again at startup

sudo systemctl disable apache

This totally disables Apache. In this case you will need a fully featured NodeJS app such as HomebrewDIY to manage everything.

## METHOD 4: Use Alternate Ports
In this setup you leave the default Apache server setup and run the NodeJS at alternate ports such as the common 8080. While this method is not user friendly (since external users must include the port number in the request URL, it does offer a simple setup for internal API operations.

Pros/Cons
* No Apache dependency
* No changes to the default setup
* Certificate handling?
* NodeJS app runs on alternate ports, which must be specified in http/s requests.
* Multiple NodeJS apps or services run from a single IP address.
* Requires opening additional ports, which creates a large security cross-section.

## How To
This method requires no changes to Apache. Instead simply install your NodeJS app on port(s) of choice. Access it with http://SERVER-IP:PORT.

Then open the firewall for the specific port... Open the Lightsail management console for the instance and click Networking. Scroll down to IPv4 Firewall and add a rule for port 8080 (or port of choice defined above).


#################################

#################################

## Sample (Default) NodeJS App
Install your NodeJS app or a temporary one. Install the default sample as follows.

sudo mkdir /opt/bitnami/projects
sudo chown $USER /opt/bitnami/projects
cd /opt/bitnami/projects
express --view pug sample
cd sample
npm install

Start this app with

DEBUG=sample:* ./bin/www

Access it with http://SERVER-IP:3000/ via a SSH tunnel




151 changes: 151 additions & 0 deletions bin/Extensions2JS.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @module Extensions2JS
*
* Personal JavaScript language extensions...
* (c) 2020 Enchanted Engineering, MIT license
* All code in this module directly modifies JavaScript primitives, as such, the module has no exports
* This module only needs loaded once per application
*
* @example
* require('./Extensions2JS');
*/


///*************************************************************
/// Date Style Extension ...
const DAYS = ["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"];
const MONTHS = ["January","February","March","April","May","June","July","August","September","October","November","December"];
const DSTYLE = /Y(?:YYY|Y)?|S[MDZ]|0?([MDNhms])\1?|[aexz]|(['"])(.*?)\2/g; // Date.prototpye.style parsing pattern
if (!Date.prototype.style)
/**
* @lends Date#
* @function style extends Date object defining a function for creating formated date strings
* @param {string|'iso'|'form'} format - output format
* format string meta-characters...
* Y: 4 digit year, i.e. 2016
* M: month, i.e. 2
* D: day of month, i.e. 4
* N: day of the week, i.e. 0-6
* SM: long month name string, i.e. February
* SD: long day name string, i.e. Sunday
* LY: leap year flag, true/false (not usable in format)
* h: hour of the day, 12 hour format, unpadded, i.e. 9
* hh: hour of the day, 24 hour format, padded, i.e. 09
* m: minutes part hour, i.e. 7
* s: seconds past minute, i.e. 5
* x: milliseconds, i.e. 234
* a: short meridiem flag, i.e. A or P
* z: short time zone, i.e. MST
* e: Unix epoch, seconds past midnight Jan 1, 1970
* dst: Daylight Savings Time flag, true/false (not usable in format)
* ofs: Local time offset (not usable in format)
* 'text': quoted text preserved, as well as non-meta characters such as spaces
* defined format keywords ...
* 'form': ["YYYY-MM-DD","hh:mm:ss"], needed by form inputs for date and time (defaults to local realm)
* 'http': HTTP Date header format, per RFC7231
* 'iso': "YYYY-MM-DD'T'hh:mm:ssZ", JavaScript standard
* 'stamp: filespec safe timestamp string, '20161207T21-22-11Z'
* notes:
* 1. Add a leading 0 or duplicate field character to pad result as 2 character field [MDNhms], i.e. 0M or MM
* 2. Use Y or YYYY for 4 year or YY for 2 year
* 3. An undefined or empty format returns an object of all fields
* @param {'local'|'utc'} realm - flag to adjust input time to local or UTC time before styling
* 'local': treats input as UTC time and adjusts to local time before styling (default)
* 'utc': treats input as local time and adjusts to UTC before styling
* undefined: leaves time unchanged, unless frmt = 'form', which assumes local
* @return {string} - date string formatted as specified
*
* @example...
* d = new Date(); // 2016-12-07T21:22:11.262Z
* d.style(); // { Y: 2016, M: 12, D: 7, h: 21, m: 22, s: 11, x: 262, z: 'MST', e:1481145731.262, a:'PM', N:3,
* SM: 'December', SD: 'Wednesday', SZ: 'Mountain Daylight Time', LY:true, dst:false, ofs: -420 }
* d.style().e; // 1481145731.262
* d.style("MM/DD/YY"); // '12/07/16'
* d.style('hh:mm:ss','local') // '14:22:11', adjusts UTC input time (d) to local time (e.g. h = 22 - 7 = 14 )
* d.style('hh:mm:ss','utc') // '04:22:11', treats input time as local and adjusts to UTC (e.g. h = 21+7 % 24 = 4)
* d.style('SD, DD SM YYYY hh:mm:ss "GMT"').replace(/[a-z]{4,}/gi,($0)=>$0.slice(0,3))
* // HTTP header date, RFC7231: 'Wed, 07 Dec 2016 21:22:11 GMT'
*
*/
Date.prototype.style = function(frmt,realm) {
let sign = (realm || frmt=='form') ? (String(realm).toLowerCase()=='utc' ? -1 : 1) : 0; // to utc, to local, or no change
let dx = sign ? new Date(this-sign*this.getTimezoneOffset()*60*1000) : this;
let zone = dx.toString().split('(')[1].replace(')','');
let zx = zone.replace(/[a-z ]/g,'');
let base = dx.toISOString();
switch (frmt||'') {
case 'form': return dx.style('YYYY-MM-DD hh:mm').split(' '); // values for form inputs
case 'http': return dx.style('SD, DD SM YYYY hh:mm:ss "GMT"').replace(/([a-z]{3})[a-z]+/gi,'$1');
case 'iso': return (realm && sign==1) ? base.replace(/z/i,zx) : base; // ISO (Zulu time) or ISO-like localtime
case 'stamp': return dx.style(`YMMDDThh-mm-ss${(realm && sign==1)?'z':'Z'}`); // filespec safe timestamp
case '': // object of date field values
let [Y,M,D,h,m,s,ms] = base.split(/[\-:\.TZ]/);
return { Y:+Y, M:+M, D:+D, h:+h, m:+m, s:+s, x:+ms, z:zx, e:dx.valueOf()*0.001, a:h<12 ?"AM":"PM", N:dx.getDay(),
SM: MONTHS[M-1], SD: DAYS[dx.getDay()], SZ:zone, LY: Y%4==0&&(Y%100==Y%400), ofs: -dx.getTimezoneOffset(),
dst: !!(new Date(1970,1,1).getTimezoneOffset()-dx.getTimezoneOffset()), iso: dx.toISOString() };
default: // any format string
let pad = (s) => ('0'+s).slice(-2);
let tkn = dx.style(); tkn['YYYY']=tkn.Y; tkn['hh']=('0'+tkn['h']).substr(-2); if (tkn['h']>12) tkn['h']%=12;
return (frmt).replace(DSTYLE,$0=>$0 in tkn ? tkn[$0] : $0.slice(1) in tkn ? pad(tkn[$0.slice(1)]) : $0.slice(1,-1));
};
};

///*************************************************************
/// Object Extensions...
/**
* @function filterByKey object equivalent of Array.prototype.filter - calls user function with value, key, and source object
* @memberof Object
* @param {function} f - function called for each object field
* @return {{}} - Modified object (does not mutate input unless filterFunc does)
* @info result will reference source object if value is an object
*/
if (!Object.filterByKey) Object.defineProperty(Object.prototype,'filterByKey', {
value:
function(f) {
let [ obj, tmp ] = [ this, {} ];
for (let key in obj) if (f(obj[key],key,obj)) tmp[key] = obj[key];
return tmp;
},
enumerable: false
});

/**
* @function mapByKey object equivalent of Array.prototype.map - calls user function with value, key, and source object
* @memberof Object
* @param {function} f - function called for each object field
* @return {{}} - Modified object (does not mutate input unless mapFunc does)
* @info result will reference source object if value is an object
*/
if (!Object.mapByKey) Object.defineProperty(Object.prototype,'mapByKey', {
value:
function(f) {
let [ obj, tmp ] = [ this, {} ];
for (let key in obj) tmp[key] = f(obj[key],key,obj);
return tmp;
},
enumerable: false
});

/**
* @function mergekeys recursively merge keys of an object into an existing object with merged object having precedence
* @param {{}} merged - object merged into source object, MUST NOT BE CIRCULAR!
* @return {{}} - object representing merger of source and merged (mutates source, but has no reference to merged)
*/
if (!Object.mergekeys) Object.defineProperty(Object.prototype,'mergekeys', {
value:
function(merged={},except=[]) {
const isObj = (obj) => (typeof obj==='object') && (obj!==null) && !(obj instanceof RegExp);
if (isObj(merged)) {
Object.keys(merged).filter(k=>!except.includes(k)).forEach(key=>{
if (isObj(merged[key])) {
this[key] = this[key] || (merged[key] instanceof Array ? [] : {}); // init new object if doesn't exist
this[key].mergekeys(merged[key]); // new object so recursively merge keys
} else {
this[key] = merged[key]; // just replace with or insert merged key, even if null
};
});
};
return this;
},
enumerable: false
});
Loading

0 comments on commit 7653546

Please sign in to comment.