Merge branch 'master' into donate

This commit is contained in:
Luke Berndt
2024-02-22 22:12:49 -05:00
committed by GitHub
23 changed files with 2997 additions and 281 deletions

View File

@@ -14,7 +14,7 @@ There are a bunch of experiments lurking in the code. There is some code for add
I haven't done a great job of keeping all the packages up to date... and I never got around to adding tests. Both of these would be great things for folks to go after. I haven't done a great job of keeping all the packages up to date... and I never got around to adding tests. Both of these would be great things for folks to go after.
## PROD vs DEV Env ## PROD vs TEST Env
The Prod environment expects you to be using HTTPS for your domains. It is pretty easy to use Let's Encrypt to grab Certs for the domains you are using. If not, just run things using the `docker-test.sh` script. The Prod environment expects you to be using HTTPS for your domains. It is pretty easy to use Let's Encrypt to grab Certs for the domains you are using. If not, just run things using the `docker-test.sh` script.
## Path Forward ## Path Forward
@@ -43,8 +43,9 @@ CNAMEs also need to be created for the various services. Create the CNAMEs below
- api - api
- account - account
- admin - admin
- www
After doing this, you should have the following domains: `api.domain.com`, `account.domain.com`, `admin.domain.com` After doing this, you should have the following domains: `api.domain.com`, `account.domain.com`, `admin.domain.com`, `www.domain.com`
### S3 Storage ### S3 Storage
Currently, both **test** and **prod** expect to use S3-based storage instead of local storage. Switching to use local storage would be relatively easy - but for the sake of testing, let's just say use something S3-compatible. Make sure that ~/.aws/credentials has the credentials you'd like to use with your S3-compatible storage provider.. IE: Currently, both **test** and **prod** expect to use S3-based storage instead of local storage. Switching to use local storage would be relatively easy - but for the sake of testing, let's just say use something S3-compatible. Make sure that ~/.aws/credentials has the credentials you'd like to use with your S3-compatible storage provider.. IE:
@@ -54,6 +55,29 @@ aws_access_key_id = [..]
aws_secret_access_key = [..] aws_secret_access_key = [..]
``` ```
# Automatically renewing SSL Certificates
SSL certs are automatically fetched from Let's Encrypt using the CertBot tool. The approach taken is based on this [Medium post](https://pentacent.medium.com/nginx-and-lets-encrypt-with-docker-in-less-than-5-minutes-b4b8a60d3a71) and accompanying [GitHub repo](https://github.com/wmnnd/nginx-certbot/tree/master).
You do need to jump start the process and do an initial fetch. To get started, make sure you have your `prod.env` file filled out. If you don't, copy prod.env.example to prod.env and fill in the details. Make sure **DOMAIN_NAME** and **REACT_APP_ADMIN_EMAIL** are correct and are the version you want to use in production. Those values will be used when requesting an SSL cert from Let's Encrypt.
This script uses Let's Encrypt's [Certbot tool](https://eff-certbot.readthedocs.io/en/latest/).
In the main directory of the **Trunk Server** repo, run the following commands:
```bash
source prod.env
docker compose -f certbot-compose.yml up
```
Check the output from CertBot - and when it is done, just hit `ctrl + c` to exit.
And then run the following to make sure everything has stopped:
```bash
docker compose -f certbot-compose.yml down
```
### Configure ### Configure
The configurations for both **test** and **prod** come from environment variable files that are read in before the containers are started. The configurations for both **test** and **prod** come from environment variable files that are read in before the containers are started.
Copy the example files and fill in the required info: Copy the example files and fill in the required info:
@@ -218,14 +242,14 @@ It is a huge pain to upgrade MongoDB in place. It turns out to be easier to dump
Rough Playbook (use common sense, I may not have this exact): Rough Playbook (use common sense, I may not have this exact):
- get into the shell of the mongo container - get into the shell of the mongo container
- `mongodump --db scanner —out /data/db/backup` - `mongodump --uri="mongodb://127.0.0.1" --db scanner --out /data/db/backup`
- exit contianer and go back to host machine - exit contianer and go back to host machine
- `cd data/db` - `cd data/db`
- `rm *` erase everything... but not the sub-directories because that is where the backup is - `rm *` erase everything... but not the sub-directories because that is where the backup is
- upgrade to the latest version of mongo - upgrade to the latest version of mongo
- build and launch the mongo container, which will create an empty DB - build and launch the mongo container, which will create an empty DB
- get into the shell of the mongo container - get into the shell of the mongo container
- `mongorestore --db scanner --drop /data/db/backup/` - `mongorestore --db scanner --drop /data/db/backup/scanner`
### Privlege Port Error ### Privlege Port Error
@@ -237,7 +261,7 @@ https://docs.docker.com/engine/security/rootless/#exposing-privileged-ports
## Setting up Logging ## Setting up Logging
I have had good luck with Loggly. Their free tier provides enough capabilities for most small sites. I have had good luck with Loggly. Their free tier provides enough capabilities for most small sites.
The Doocker Logging Driver works well and is easy to install: The Docker Logging Driver works well and is easy to install:
https://documentation.solarwinds.com/en/Success_Center/loggly/Content/admin/docker-logging-driver.htm https://documentation.solarwinds.com/en/Success_Center/loggly/Content/admin/docker-logging-driver.htm

View File

@@ -110,7 +110,7 @@ exports.deleteGroup = async function (req, res, next) {
exports.reorderGroups = function (req, res, next) { exports.reorderGroups = function (req, res, next) {
console.log(req.body)
//router.post('/system/:shortName/group_order', isLoggedIn, function(req, res) { //router.post('/system/:shortName/group_order', isLoggedIn, function(req, res) {
process.nextTick(async function () { process.nextTick(async function () {
const system = await System.findOne({ shortName: req.params.shortName.toLowerCase() }); const system = await System.findOne({ shortName: req.params.shortName.toLowerCase() });
@@ -131,7 +131,7 @@ exports.reorderGroups = function (req, res, next) {
}); });
return; return;
} }
console.log(req.body)
if (!req.body.groupOrder) { if (!req.body.groupOrder) {
res.status(500); res.status(500);
res.json({ res.json({
@@ -142,8 +142,6 @@ exports.reorderGroups = function (req, res, next) {
} }
var groupOrder = JSON.parse(req.body.groupOrder); var groupOrder = JSON.parse(req.body.groupOrder);
for (var i = 0; i < groupOrder.length; i++) { for (var i = 0; i < groupOrder.length; i++) {
console.log("in group: " + groupOrder[i] + " position: " + i);
const group = await Group.findOneAndUpdate( const group = await Group.findOneAndUpdate(
{ {
_id: new mongoose.Types.ObjectId(groupOrder[i]), _id: new mongoose.Types.ObjectId(groupOrder[i]),
@@ -177,10 +175,8 @@ exports.reorderGroups = function (req, res, next) {
}; };
exports.upsertGroup = function (req, res, next) { exports.upsertGroup = function (req, res, next) {
console.log(req.body)
//router.post('/system/:shortName/group/:groupId?', isLoggedIn, function(req, res) { //router.post('/system/:shortName/group/:groupId?', isLoggedIn, function(req, res) {
process.nextTick(async function () { process.nextTick(async function () {
console.log(req.body)
let system = await System.findOne({ shortName: req.params.shortName.toLowerCase() }); let system = await System.findOne({ shortName: req.params.shortName.toLowerCase() });
if (!system) { if (!system) {

View File

@@ -58,7 +58,6 @@ exports.listUserSystems = async function (req,res,next) {
} }
const users = await User.find({}, "_id email firstName lastName lastLogin"); const users = await User.find({}, "_id email firstName lastName lastLogin");
let list = []; let list = [];
console.log(users);
for (let index = 0; index < users.length; index++) { for (let index = 0; index < users.length; index++) {
let user = users[index].toObject(); let user = users[index].toObject();
const userId = new mongoose.Types.ObjectId(user._id); const userId = new mongoose.Types.ObjectId(user._id);

View File

@@ -94,7 +94,6 @@ async function csv_import(shortName, filename, callback) {
for (var i = 0; i < data.length; i++) { for (var i = 0; i < data.length; i++) {
var row = data[i]; var row = data[i];
console.log(row)
if (!( "Decimal" in row) || !("Description" in row) || !("Alpha Tag" in row)) { if (!( "Decimal" in row) || !("Description" in row) || !("Alpha Tag" in row)) {
console.error("Error: csv import column header error"); console.error("Error: csv import column header error");
callback("Column Headers are required - Required Columns: 'Decimal' 'Decscription' 'Alpha Tag' Optional Column: 'Priority' ", null); callback("Column Headers are required - Required Columns: 'Decimal' 'Decscription' 'Alpha Tag' Optional Column: 'Priority' ", null);

View File

@@ -0,0 +1,23 @@
// tracing.js
'use strict';
const { HoneycombSDK } = require('@honeycombio/opentelemetry-node');
const {
getNodeAutoInstrumentations,
} = require('@opentelemetry/auto-instrumentations-node');
// uses the HONEYCOMB_API_KEY and OTEL_SERVICE_NAME environment variables
const sdk = new HoneycombSDK({
sampleRate: 5,
instrumentations: [
getNodeAutoInstrumentations({
// we recommend disabling fs autoinstrumentation since it can be noisy
// and expensive during startup
'@opentelemetry/instrumentation-fs': {
enabled: false,
},
}),
],
});
sdk.start();

View File

@@ -2,7 +2,7 @@
const { ObjectId } = require('mongodb'); const { ObjectId } = require('mongodb');
var { callModel: Call } = require("../models/call"); var { callModel: Call } = require("../models/call");
const Group = require("../models/group"); const Group = require("../models/group");
const opentelemetry = require('@opentelemetry/api');
var defaultNumResults = 50; var defaultNumResults = 50;
var channels = {}; var channels = {};

View File

@@ -1,10 +1,12 @@
var path = require('path'); var path = require('path');
var fs = require('fs'); var fs = require('fs');
const opentelemetry = require('@opentelemetry/api');
var sysStats = require("../sys_stats"); var sysStats = require("../sys_stats");
var System = require("../models/system"); var System = require("../models/system");
var Talkgroup = require("../models/talkgroup"); var Talkgroup = require("../models/talkgroup");
var { callModel: Call } = require("../models/call"); var { callModel: Call } = require("../models/call");
const { PutObjectCommand, S3Client } = require("@aws-sdk/client-s3"); const { PutObjectCommand, S3Client } = require("@aws-sdk/client-s3");
const { fromIni } = require("@aws-sdk/credential-providers"); const { fromIni } = require("@aws-sdk/credential-providers");
@@ -18,183 +20,236 @@ const client = new S3Client({
credentials: fromIni({ profile: s3_profile }), credentials: fromIni({ profile: s3_profile }),
endpoint: s3_endpoint, endpoint: s3_endpoint,
region: s3_region, region: s3_region,
maxAttempts: 1 maxAttempts: 3
}); });
exports.upload = async function (req, res, next) { exports.upload = async function (req, res, next) {
process.nextTick( async () => {
if (!req.file || ((path.extname(req.file.originalname) != '.m4a') && (path.extname(req.file.originalname) != '.mp3'))) {
console.warn("[" + req.params.shortName + "] Error file name is wrong or file does not exist");
res.status(500);
res.send("Error, invalid filename:\n");
return;
}
var shortName = req.params.shortName.toLowerCase();
var apiKey = req.body.api_key;
let item = null;
try { process.nextTick(async () => {
item = await System.findOne({ 'shortName': shortName }, ["key", "ignoreUnknownTalkgroup"]); if (!req.file || ((path.extname(req.file.originalname) != '.m4a') && (path.extname(req.file.originalname) != '.mp3'))) {
} catch (err) { console.warn("[" + req.params.shortName + "] Error file name is wrong or file does not exist");
console.warn("[" + req.params.shortName + "] Error /:shortName/upload - Error: " + err); res.status(500);
res.status(500); res.send("Error, invalid filename:\n");
res.send("Error, invalid shotName:\n" + err); return;
return; }
} var shortName = req.params.shortName.toLowerCase();
var apiKey = req.body.api_key;
var talkgroupNum = parseInt(req.body.talkgroup_num);
var freq = parseFloat(req.body.freq);
var time = new Date(parseInt(req.body.start_time) * 1000);
var stopTime = new Date();
if (req.body.stop_time) {
var stopTime = new Date(parseInt(req.body.stop_time) * 1000);
}
var startTime = req.body.start_time;
var emergency = parseInt(req.body.emergency);
if (!item) { let errorCount = parseInt(req.body.error_count);
console.info("[" + req.params.shortName + "] Error /:shortName/upload ShortName does not exist"); let spikeCount = parseInt(req.body.spike_count);
res.status(500);
res.send("ShortName does not exist: " + shortName + "\n");
return;
}
if (apiKey != item.key) {
console.warn("[" + req.params.shortName + "] Error /:shortName/upload API Key Mismatch - Provided key: " + apiKey);
res.status(500);
res.send("API Keys do not match!\n");
return;
}
var talkgroupNum = parseInt(req.body.talkgroup_num); if (Number.isNaN(errorCount)) {
var freq = parseFloat(req.body.freq); errorCount = 0;
var time = new Date(parseInt(req.body.start_time) * 1000); }
var stopTime = new Date(); if (Number.isNaN(spikeCount)) {
if (req.body.stop_time) { spikeCount = 0;
var stopTime = new Date(parseInt(req.body.stop_time) * 1000); }
}
var startTime = req.body.start_time;
var emergency = parseInt(req.body.emergency);
// Blocking sensitive talkgroups try {
if ((shortName == "hennearmer") && ((talkgroupNum == 3421) || (talkgroupNum == 3423))) { var srcList = JSON.parse(req.body.source_list);
res.status(200).end(); } catch (err) {
return; var srcList = [];
} console.warn("[" + req.params.shortName + "] Error /:shortName/upload Parsing Source/Freq List - Error: " + err);
res.status(500);
res.send("Error parsing sourcelist " + err);
return;
}
let item = null;
const span = opentelemetry.trace.getActiveSpan();
span.setAttribute('call.shortName', shortName);
span.setAttribute('call.talkgroupNum', talkgroupNum);
span.setAttribute('call.startTime', startTime);
if (item.ignoreUnknownTalkgroup == true) {
talkgroupExists = await Talkgroup.exists({ // Validate that the system exists and the API key is correct
'shortName': shortName,
'num': talkgroupNum try {
item = await System.findOne({ 'shortName': shortName }, ["key", "ignoreUnknownTalkgroup"]);
} catch (err) {
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: "Error /:shortName/upload - Error: " + err,
});
console.warn("[" + req.params.shortName + "] Error /:shortName/upload - Error: " + err);
res.status(500);
res.send("Error, invalid shortName:\n" + err);
return;
}
if (!item) {
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: "Error /:shortName/upload API Key Mismatch - Provided key: " + apiKey,
});
console.info("[" + req.params.shortName + "] Error /:shortName/upload ShortName does not exist");
res.status(500);
res.send("ShortName does not exist: " + shortName + "\n");
return;
}
if (apiKey != item.key) {
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: "Error /:shortName/upload API Key Mismatch - Provided key: " + apiKey,
});
console.warn("[" + req.params.shortName + "] Error /:shortName/upload API Key Mismatch - Provided key: " + apiKey);
res.status(500);
res.send("API Keys do not match!\n");
return;
}
// Blocking sensitive talkgroups
if ((shortName == "hennearmer") && ((talkgroupNum == 3421) || (talkgroupNum == 3423))) {
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: "Sensitive Talkgroup",
});
res.status(200).end();
return;
}
if (item.ignoreUnknownTalkgroup == true) {
talkgroupExists = await Talkgroup.exists({
'shortName': shortName,
'num': talkgroupNum
});
if (!talkgroupExists) {
try {
fs.unlinkSync(req.file.path)
//file removed
} catch (err) {
console.log("[" + call.shortName + "] error deleting: " + req.file.path);
}
res.status(500);
res.send("Talkgroup does not exist, skipping.\n");
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: "Ignore Unknown Talkgroup Set - Talkgroup does not exist: " + talkgroupNum + " ShortName: " + shortName,
});
return;
}
}
// Add in an API Key check
res.status(200).end();
var local_path = "/" + shortName + "/" + time.getFullYear() + "/" + (time.getMonth() + 1) + "/" + time.getDate() + "/";
var object_key = "media/" + shortName + "-" + talkgroupNum + "-" + startTime + path.extname(req.file.originalname);
var endpoint = s3_endpoint;
var bucket = s3_bucket;
var url = s3_endpoint + "/" + s3_bucket + "/" + object_key;
var call = new Call({
shortName: shortName,
talkgroupNum: talkgroupNum,
objectKey: object_key,
endpoint: endpoint,
bucket: bucket,
time: time,
name: talkgroupNum + "-" + startTime + ".m4a",
freq: freq,
errorCount: errorCount,
spikeCount: spikeCount,
url: url,
emergency: emergency,
path: local_path,
srcList: srcList,
len: -1
}); });
if (!talkgroupExists) {
try { try {
fs.unlinkSync(req.file.path) if (req.body.call_length) {
//file removed call.len = parseFloat(req.body.call_length);
} catch (err) { } else {
console.log("[" + call.shortName + "] error deleting: " + req.file.path); call.len = (stopTime - time) / 1000;
} }
} catch (err) {
res.status(500); console.error(err);
res.send("Talkgroup does not exist, skipping.\n"); }
let fileContent;
try {
fileContent = fs.readFileSync(req.file.path);
//s3Src = fs.createReadStream(req.file.path);
} catch (err) {
span.setStatus({
code: opentelemetry.SpanStatusCode.ERROR,
message: "Unable to open file: " + req.file.path + " Error: " + err,
});
console.error("[" + call.shortName + "] Unable to open file: " + req.file.path + " Error: " + err);
return; return;
} }
} const command = new PutObjectCommand({
// Add in an API Key check Bucket: s3_bucket,
Key: object_key,
Body: fileContent,
ACL: 'public-read'
});
let errorCount = parseInt(req.body.error_count); try {
let spikeCount = parseInt(req.body.spike_count); await client.send(command);
if (Number.isNaN(errorCount)) { } catch (err) {
errorCount = 0; span.setStatus({
} code: opentelemetry.SpanStatusCode.ERROR,
if (Number.isNaN(spikeCount)) { message: "Upload Error: " + err + " Filename: " + call.name + " content-length: " + req.headers['content-length'] + " Key: " + object_key + " Bucket: " + s3_bucket,
spikeCount = 0; });
} console.warn("[" + call.shortName + "] Upload Error: " + err + " Filename: " + call.name + " content-length: " + req.headers['content-length'] + " Key: " + object_key + " Bucket: " + s3_bucket);
/*res.status(500);
try { sysStats.addError(call.toObject());
var srcList = JSON.parse(req.body.source_list); res.contentType('json');
} catch (err) { res.status(500);
var srcList = []; res.send(JSON.stringify({
console.warn("[" + req.params.shortName + "] Error /:shortName/upload Parsing Source/Freq List - Error: " + err); success: false,
res.status(500); error: "File Upload"
res.send("Error parsing sourcelist " + err); }));*/
return;
}
res.status(200).end();
var local_path = "/" + shortName + "/" + time.getFullYear() + "/" + (time.getMonth() + 1) + "/" + time.getDate() + "/";
var object_key = "media/" + shortName + "-" + talkgroupNum + "-" + startTime + path.extname(req.file.originalname);
var endpoint = s3_endpoint;
var bucket = s3_bucket;
var url = s3_endpoint + "/" + s3_bucket + "/" + object_key;
var call = new Call({
shortName: shortName,
talkgroupNum: talkgroupNum,
objectKey: object_key,
endpoint: endpoint,
bucket: bucket,
time: time,
name: talkgroupNum + "-" + startTime + ".m4a",
freq: freq,
errorCount: errorCount,
spikeCount: spikeCount,
url: url,
emergency: emergency,
path: local_path,
srcList: srcList,
len: -1
});
try {
if (req.body.call_length) {
call.len = parseFloat(req.body.call_length);
} else {
call.len = (stopTime - time) / 1000;
} }
} catch (err) {
console.error(err); try {
} await call.save();
let fileContent; sysStats.addCall(call.toObject());
try { // we only want to notify clients if the clip is longer than 1 second.
fileContent = fs.readFileSync(req.file.path); if (call.len >= 1) {
//s3Src = fs.createReadStream(req.file.path); req.call = call.toObject();
} catch (err) { next();
console.error("[" + call.shortName + "] Unable to open file: " + req.file.path + " Error: " + err); }
return; } catch (err) {
} console.warn("[" + call.shortName + "] Error saving call: " + err);
const command = new PutObjectCommand({ span.setStatus({
Bucket: s3_bucket, code: opentelemetry.SpanStatusCode.ERROR,
Key: object_key, message: "Error saving call: " + err,
Body: fileContent, });
ACL: 'public-read' /*res.status(500);
}); sysStats.addError(call.toObject());
res.contentType('json');
try { res.status(500);
await client.send(command); res.send(JSON.stringify({
await call.save(); success: false,
sysStats.addCall(call.toObject()); error: "File Upload"
// we only want to notify clients if the clip is longer than 1 second. }));*/
if (call.len >= 1) {
req.call = call.toObject();
next();
} }
} catch (err) {
console.warn("[" + call.shortName + "] Upload Error: " + err + " Filename: " + call.name + " content-length: " + req.headers['content-length'] + " Key: " + object_key + " Bucket: " + s3_bucket);
/*res.status(500);
sysStats.addError(call.toObject());
res.contentType('json');
res.status(500);
res.send(JSON.stringify({
success: false,
error: "File Upload"
}));*/
}
try { try {
fs.unlinkSync(req.file.path); fs.unlinkSync(req.file.path);
} catch (err) { } catch (err) {
console.warn("There was an Error uploading an deleting: " + req.file.path); console.warn("There was an Error uploading an deleting: " + req.file.path);
}; };
}); });
} }

View File

@@ -1,4 +1,4 @@
const tracing = require("./agents/honeycomb-tracing");
var express = require("express"); var express = require("express");
var configureExpress = require("./config/express"); var configureExpress = require("./config/express");

2631
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -6,6 +6,9 @@
"@aws-sdk/client-s3": "^3.309.0", "@aws-sdk/client-s3": "^3.309.0",
"@aws-sdk/credential-provider-process": "^3.306.0", "@aws-sdk/credential-provider-process": "^3.306.0",
"@aws-sdk/credential-providers": "^3.309.0", "@aws-sdk/credential-providers": "^3.309.0",
"@honeycombio/opentelemetry-node": "^0.6.1",
"@opentelemetry/api": "^1.7.0",
"@opentelemetry/auto-instrumentations-node": "^0.41.0",
"adm-zip": "^0.5.10", "adm-zip": "^0.5.10",
"aws-sdk": "^2.1353", "aws-sdk": "^2.1353",
"axios": "1.3.5", "axios": "1.3.5",
@@ -22,12 +25,12 @@
"ffprobe-static": "3.1.0", "ffprobe-static": "3.1.0",
"find-remove": "4.1.0", "find-remove": "4.1.0",
"get-folder-size": "^4.0.0", "get-folder-size": "^4.0.0",
"node-mailjet": "6.0.2",
"mkdirp": "^2.1.6", "mkdirp": "^2.1.6",
"mongodb": "5.2.0", "mongodb": "5.2.0",
"mongoose": "7.0.3", "mongoose": "7.0.3",
"multer": "1.3.0", "multer": "1.3.0",
"node-ffprobe": "https://github.com/ListenerApproved/node-ffprobe", "node-ffprobe": "https://github.com/ListenerApproved/node-ffprobe",
"node-mailjet": "6.0.2",
"node-schedule": "2.1.1", "node-schedule": "2.1.1",
"passport": "0.6.0", "passport": "0.6.0",
"passport-local": "1.0.0", "passport-local": "1.0.0",

View File

@@ -10,10 +10,7 @@ var spots = (24 * 60) / timePeriod; // the number of spots needed to keep track
async function updateActiveSystems() { async function updateActiveSystems() {
// Go through all of the Systems // Go through all of the Systems
for await (let item of System.find()) { for await (let item of System.find()) {
// go through all the systems // go through all the systems
// if you have recieved some calls during that last period, make the system active // if you have recieved some calls during that last period, make the system active

37
certbot-compose.yml Normal file
View File

@@ -0,0 +1,37 @@
version: "3"
services:
nginx:
build:
context: ./nginx-proxy
args:
STAGE: "cert"
environment:
- DOMAIN_NAME
image: "nginx:${TAG}-cert"
networks:
- proxy
ports:
- "80:80"
restart: always
volumes:
- ${PWD}/data/log/syslog:/var/log/syslog
- ${PWD}/data/log/nginx:/var/log/nginx
- ${PWD}/data/certbot/www/:/var/www/certbot/:ro
- ${PWD}/data/certbot/conf/:/etc/letsencrypt/:ro
command: /bin/bash -c "envsubst '$$DOMAIN_NAME' < /etc/nginx/conf.d/site.template > /etc/nginx/conf.d/default.conf && while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g 'daemon off;'"
certbot:
image: certbot/certbot:latest
volumes:
- ${PWD}/data/certbot/www/:/var/www/certbot/:rw
- ${PWD}/data/certbot/conf/:/etc/letsencrypt/:rw
entrypoint: "certbot certonly --webroot -w /var/www/certbot \
--email ${REACT_APP_ADMIN_EMAIL} \
-d ${DOMAIN_NAME} -d www.${DOMAIN_NAME} -d api.${DOMAIN_NAME} -d admin.${DOMAIN_NAME} -d account.${DOMAIN_NAME} \
--rsa-key-size 4096 \
--agree-tos \
--force-renewal"
networks:
proxy:
node:

View File

@@ -33,6 +33,10 @@ services:
- ${PWD}/data/certbot/www/:/var/www/certbot/:rw - ${PWD}/data/certbot/www/:/var/www/certbot/:rw
- ${PWD}/data/certbot/conf/:/etc/letsencrypt/:rw - ${PWD}/data/certbot/conf/:/etc/letsencrypt/:rw
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'" entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
logging:
driver: syslog
options:
tag: "certbot"
mongo: mongo:
build: build:
@@ -73,6 +77,8 @@ services:
- S3_BUCKET - S3_BUCKET
- S3_PROFILE - S3_PROFILE
- S3_REGION - S3_REGION
- OTEL_SERVICE_NAME
- HONEYCOMB_API_KEY
volumes: volumes:
- "~/.aws:/home/app/.aws" - "~/.aws:/home/app/.aws"
depends_on: depends_on:

View File

@@ -1,3 +1,5 @@
source test.env source test.env
echo "Domains: " $DOMAIN_NAME
echo "Protocol: " $PROTOCOL
echo "Docker Compose Command: " $@ echo "Docker Compose Command: " $@
docker compose -f docker-compose.yml -f test-compose.yml $@ docker compose -f docker-compose.yml -f test-compose.yml $@

View File

@@ -66,15 +66,17 @@ const MediaPlayer = (props) => {
const audio = audioRef.current.audioEl.current; const audio = audioRef.current.audioEl.current;
setSourceIndex(0); setSourceIndex(0);
navigator.mediaSession.metadata = new MediaMetadata({ if ('mediaSession' in navigator) {
title: "Waiting for Call...", navigator.mediaSession.metadata = new MediaMetadata({
album: 'OpenMHz', title: "Waiting for Call...",
artwork: [ album: 'OpenMHz',
{ src: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' }, artwork: [
{ src: '/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' }, { src: '/favicon-32x32.png', sizes: '32x32', type: 'image/png' },
{ src: '/android-chrome-192x192.png', sizes: '512x512', type: 'image/png' }, { src: '/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' },
] { src: '/android-chrome-192x192.png', sizes: '512x512', type: 'image/png' },
}); ]
});
}
audio.src = "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////////////////////////////////////////AAAAAExhdmM1OC4xMwAAAAAAAAAAAAAAACQDkAAAAAAAAAGw9wrNaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAAAANIAAAAAExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxDsAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxHYAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV"; audio.src = "data:audio/mpeg;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4Ljc2LjEwMAAAAAAAAAAAAAAA/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////////////////////////////////////////AAAAAExhdmM1OC4xMwAAAAAAAAAAAAAAACQDkAAAAAAAAAGw9wrNaQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAAAANIAAAAAExBTUUzLjEwMFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxDsAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV/+MYxHYAAANIAAAAAFVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVVV";
const playPromise = audio.play(); const playPromise = audio.play();

View File

@@ -1,91 +0,0 @@
#!/bin/bash
# from https://github.com/wmnnd/nginx-certbot/blob/master/init-letsencrypt.sh
# Load environment variables from prod.env.example
if [ -f prod.env ]; then
source test.env
fi
# Rest of the script...
if ! [ -x "$(command -v docker compose)" ]; then
echo 'Error: docker compose is not installed.' >&2
exit 1
fi
domains=("$DOMAIN_NAME" "account.$DOMAIN_NAME" "admin.$DOMAIN_NAME" "api.$DOMAIN_NAME" "www.$DOMAIN_NAME")
rsa_key_size=4096
data_path="./data/certbot"
email=$REACT_APP_ADMIN_EMAIL
staging=0 # Set to 1 if you're testing your setup to avoid hitting request limits
if [ -d "$data_path" ]; then
read -p "Existing data found for $domains. Continue and replace existing certificate? (y/N) " decision
if [ "$decision" != "Y" ] && [ "$decision" != "y" ]; then
exit
fi
fi
if [ ! -e "$data_path/conf/options-ssl-nginx.conf" ] || [ ! -e "$data_path/conf/ssl-dhparams.pem" ]; then
echo "### Downloading recommended TLS parameters ..."
mkdir -p "$data_path/conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot-nginx/certbot_nginx/_internal/tls_configs/options-ssl-nginx.conf > "$data_path/conf/options-ssl-nginx.conf"
curl -s https://raw.githubusercontent.com/certbot/certbot/master/certbot/certbot/ssl-dhparams.pem > "$data_path/conf/ssl-dhparams.pem"
echo
fi
echo "### Creating dummy certificate for $domains ..."
path="/etc/letsencrypt/live/$domains"
mkdir -p "$data_path/conf/live/$domains"
docker compose -f docker-compose.yml -f test-compose.yml run --rm --entrypoint "\
openssl req -x509 -nodes -newkey rsa:$rsa_key_size -days 1\
-keyout '$path/privkey.pem' \
-out '$path/fullchain.pem' \
-subj '/CN=localhost'" certbot
echo
echo "### Starting nginx ..."
docker compose -f docker-compose.yml -f test-compose.yml up -d nginx
echo
echo "### Deleting dummy certificate for $domains ..."
docker compose -f docker-compose.yml -f test-compose.yml run --rm --entrypoint "\
rm -Rf /etc/letsencrypt/live/$domains && \
rm -Rf /etc/letsencrypt/archive/$domains && \
rm -Rf /etc/letsencrypt/renewal/$domains.conf" certbot
echo
echo "### Requesting Let's Encrypt certificate for $domains ..."
#Join $domains to -d args
domain_args=""
for domain in "${domains[@]}"; do
domain_args="$domain_args -d $domain"
done
# Select appropriate email arg
case "$email" in
"") email_arg="--register-unsafely-without-email" ;;
*) email_arg="--email $email" ;;
esac
# Enable staging mode if needed
if [ $staging != "0" ]; then staging_arg="--staging"; fi
docker compose -f docker-compose.yml -f test-compose.yml run --rm --entrypoint "\
certbot certonly --webroot -w /var/www/certbot \
$staging_arg \
$email_arg \
$domain_args \
--rsa-key-size $rsa_key_size \
--agree-tos \
--force-renewal" certbot
echo
echo "### Reloading nginx ..."
docker compose -f docker-compose.yml -f test-compose.yml exec nginx nginx -s reload

View File

@@ -2,7 +2,7 @@
# #
# docker build -t mongo . # docker build -t mongo .
FROM mongo:5.0 FROM mongo:7-jammy
MAINTAINER Luke Berndt <lukekb@gmail.com> MAINTAINER Luke Berndt <lukekb@gmail.com>

View File

@@ -21,7 +21,6 @@ RUN apt-get update \
# Add main NGINX config # Add main NGINX config
COPY *.conf /etc/nginx/ COPY *.conf /etc/nginx/
COPY ${STAGE}/certs/ /etc/nginx/certs/
# Add virtual hosts # Add virtual hosts
COPY ${STAGE}/vhosts/ /etc/nginx/conf.d/ COPY ${STAGE}/vhosts/ /etc/nginx/conf.d/

View File

@@ -0,0 +1,8 @@
server {
listen 80;
server_name account.${DOMAIN_NAME} api.${DOMAIN_NAME} admin.${DOMAIN_NAME} www.${DOMAIN_NAME} ${DOMAIN_NAME};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
}

View File

@@ -1 +0,0 @@
# Copy your certs here

View File

@@ -27,6 +27,10 @@ server {
server_name ${DOMAIN_NAME} www.${DOMAIN_NAME}; server_name ${DOMAIN_NAME} www.${DOMAIN_NAME};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location /static { location /static {
proxy_pass http://frontend:3000; proxy_pass http://frontend:3000;
access_log off; access_log off;
@@ -63,6 +67,10 @@ server {
server_name api.${DOMAIN_NAME}; server_name api.${DOMAIN_NAME};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location /marcsip/upload { location /marcsip/upload {
deny all; deny all;
} }
@@ -100,6 +108,10 @@ server {
server_name account.${DOMAIN_NAME}; server_name account.${DOMAIN_NAME};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / { location / {
proxy_pass http://account:3009; proxy_pass http://account:3009;
proxy_http_version 1.1; proxy_http_version 1.1;
@@ -127,6 +139,10 @@ server {
server_name admin.${DOMAIN_NAME}; server_name admin.${DOMAIN_NAME};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / { location / {
proxy_pass http://admin:3008; proxy_pass http://admin:3008;
proxy_http_version 1.1; proxy_http_version 1.1;

View File

@@ -1 +0,0 @@
# Copy your certs here

View File

@@ -1,8 +1,14 @@
server { server {
listen 80; listen 80;
server_name ${DOMAIN_NAME} www.${DOMAIN_NAME}; server_name ${DOMAIN_NAME} www.${DOMAIN_NAME};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location /static { location /static {
proxy_pass http://frontend:3000; proxy_pass http://frontend:3000;
access_log off; access_log off;
@@ -30,6 +36,10 @@ server {
server_name account.${DOMAIN_NAME}; server_name account.${DOMAIN_NAME};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / { location / {
proxy_pass http://account:3009; proxy_pass http://account:3009;
include /etc/nginx/proxy.conf; include /etc/nginx/proxy.conf;
@@ -42,6 +52,10 @@ server {
server_name admin.${DOMAIN_NAME}; server_name admin.${DOMAIN_NAME};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / { location / {
proxy_pass http://admin:3008; proxy_pass http://admin:3008;
include /etc/nginx/proxy.conf; include /etc/nginx/proxy.conf;
@@ -54,6 +68,10 @@ server {
server_name api.${DOMAIN_NAME}; server_name api.${DOMAIN_NAME};
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / { location / {
proxy_pass http://backend:3005; proxy_pass http://backend:3005;
include /etc/nginx/proxy.conf; include /etc/nginx/proxy.conf;