Viewable container logs

You can now view a containers logs. 
Code clean-up.
This commit is contained in:
lllllllillllllillll 2023-12-03 14:24:30 -08:00 committed by GitHub
parent fb3fb34532
commit da0a5b8401
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 478 additions and 72 deletions

17
app.js
View file

@ -11,11 +11,11 @@ const redisClient = redis.createClient({ url: "redis://DweebCache:6379", passwor
redisClient.connect().catch(console.log);
let redisStore = new RedisStore({ client: redisClient });
// Routes
// Router
const routes = require("./routes");
// Functions and variables
const { serverStats, containerList, containerStats, containerAction } = require('./functions/system');
const { serverStats, containerList, containerStats, containerAction, containerLogs } = require('./functions/system');
let sentList, clicked;
app.locals.site_list = '';
@ -98,6 +98,19 @@ io.on('connection', (socket) => {
clicked = false;
});
// Container logs
socket.on('logs', (data) => {
containerLogs(data.container)
.then(logs => {
socket.emit('logString', logs);
})
.catch(err => {
console.error(err);
});
});
// On disconnect
socket.on('disconnect', () => {
clearInterval(ServerStats);
clearInterval(ContainerList);

View file

@ -140,7 +140,7 @@ module.exports.dashCard = function dashCard(data) {
</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item" data-bs-toggle="modal" data-bs-target="#${name}_modal-details" href="#">Details</a>
<a class="dropdown-item" onclick="viewLogs(this)" name="${name}" data-bs-toggle="modal" data-bs-target="#${name}_logs" href="#">Logs</a>
<a class="dropdown-item" onclick="viewLogs(this)" name="${name}" data-bs-toggle="modal" data-bs-target="#log_view" href="#">Logs</a>
<a class="dropdown-item" href="#">Edit</a>
<a class="dropdown-item text-primary" href="#">Update</a>
<a class="dropdown-item text-danger" data-bs-toggle="modal" data-bs-target="#${name}_modal-danger" href="#">Remove</a>
@ -255,67 +255,7 @@ module.exports.dashCard = function dashCard(data) {
</div>
</div>
<div class="modal modal-blur fade" id="${name}_logs" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">${name} Logs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card-body">
<h4>
Log File:
</h4>
<div>
<pre><code>GET <a class="text-reset" target="_blank" href="https://tabler.io/demo">https://tabler.io/demo</a></code></pre>
</div>
<h4>Logs:</h4>
<div>
<pre>Effective URL&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a class="text-reset" target="_blank" href="https://tabler.io/demo">https://tabler.io/demo</a><br>Redirect count&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0<br>Name lookup time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;3.4e-05<br>Connect time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0.000521<br>Pre-transfer time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0.0<br>Start-transfer time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0.0<br>App connect time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0.0<br>Redirect time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0.0<br>Total time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;28.000601<br>Response code&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;0<br>Return keyword&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;operation_timedout</pre>
</div>
<h4>Response Headers</h4>
<div>
<pre>HTTP/1.1 200 Connection established</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-info">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</button>
</div>
</div>
</div>
</div>
<div class="modal modal-blur fade" id="${name}_modal-details" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">

150
controllers/auth.js Normal file
View file

@ -0,0 +1,150 @@
const User = require('../database/UserModel');
const bcrypt = require('bcrypt');
exports.Login = function(req,res){
// check whether we have a session
if(req.session.user){
// Redirect to log out.
res.redirect("/logout");
}else{
// Render the login page.
res.render("pages/login",{
"error":"",
"isLoggedIn": false
});
}
}
exports.processLogin = async function(req,res){
// get the data.
let email = req.body.email;
let password = req.body.password;
// check if we have data.
if(email && password){
// check if the user exists.
let existingUser = await User.findOne({ where: {email:email}});
if(existingUser){
// compare the password.
let match = await bcrypt.compare(password,existingUser.password);
if(match){
// set the session.
req.session.user = existingUser.username;
req.session.UUID = existingUser.UUID;
req.session.role = existingUser.role;
// Redirect to the home page.
res.redirect("/");
}else{
// return an error.
res.render("pages/login",{
"error":"Invalid password",
isLoggedIn: false
});
}
}else{
// return an error.
res.render("pages/login",{
"error":"User with that email does not exist.",
isLoggedIn:false
});
}
}else{
res.status(400);
res.render("pages/login",{
"error":"Please fill in all the fields.",
isLoggedIn:false
});
}
}
exports.Logout = function(req,res){
// clear the session.
req.session.destroy();
// Redirect to the login page.
res.redirect("/login");
}
exports.Register = function(req,res){
// Check whether we have a session
if(req.session.user){
// Redirect to log out.
res.redirect("/logout");
} else {
// Render the signup page.
res.render("pages/register",{
"error":"",
isLoggedIn:false
});
}
}
exports.processRegister = async function(req,res){
// Get the data.
let { first_name, last_name, username, email, password, avatar, tos } = req.body;
let role = "user";
// Check the data.
if(first_name && last_name && email && password && username && tos){
// Check if there is an existing user with that username.
let existingUser = await User.findOne({ where: {username:username}});
let adminUser = await User.findOne({ where: {role:"admin"}});
if(!existingUser){
// hash the password.
let hashedPassword = bcrypt.hashSync(password,10);
if(!adminUser){
console.log('Creating admin User');
role = "admin";
}
try {
const user = await User.create({
first_name: first_name,
last_name: last_name,
username: username,
email: email,
password: hashedPassword,
role: role,
group: 'all',
avatar: `<img src="./static/avatars/${avatar}">`
});
// set the session.
req.session.user = user.username;
req.session.UUID = user.UUID;
req.session.role = user.role;
// Redirect to the home page.
res.redirect("/");
}
catch (err) {
// return an error.
res.render("pages/register",{
"error":"Something went wrong when creating account.",
isLoggedIn:false
});
}
}else{
// return an error.
res.render("pages/register",{
"error":"User with that username already exists.",
isLoggedIn:false
});
}
}else{
// Redirect to the signup page.
res.render("pages/register",{
"error":"Please fill in all the fields and accept TOS.",
isLoggedIn:false
});
}
}

View file

@ -1,4 +1,10 @@
const User = require('../database/UserModel');
const { readFileSync, writeFileSync, appendFileSync, readdirSync } = require('fs');
const { execSync } = require("child_process");
const { siteCard } = require('../components/siteCard');
const { containerExec } = require('../functions/system')
exports.Dashboard = async function (req, res) {
@ -26,4 +32,216 @@ exports.Dashboard = async function (req, res) {
// Redirect to the login page
res.redirect("/login");
}
}
exports.AddSite = async function (req, res) {
let { domain, type, host, port } = req.body;
if ((req.session.role == "admin") && ( domain && type && host && port)) {
let { domain, type, host, port } = req.body;
// build caddyfile
let caddyfile = `${domain} {`
caddyfile += `\n\t${type} ${host}:${port}`
caddyfile += `\n\theader {`
caddyfile += `\n\t\tStrict-Transport-Security "max-age=31536000; includeSubDomains; preload"`
caddyfile += `\n\t}`
caddyfile += `\n}`
// save caddyfile
writeFileSync(`./caddyfiles/sites/${domain}.Caddyfile`, caddyfile, function (err) { console.log(err) });
// format caddyfile
let format = {
container: 'DweebProxy',
command: `caddy fmt --overwrite /etc/caddy/sites/${domain}.Caddyfile`
}
await containerExec(format, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Formatted ${domain}.Caddyfile`);
});
///////////////// convert caddyfile to json
let convert = {
container: 'DweebProxy',
command: `caddy adapt --config /etc/caddy/sites/${domain}.Caddyfile --pretty >> /etc/caddy/sites/${domain}.json`
}
await containerExec(convert, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Converted ${domain}.Caddyfile to JSON`);
});
////////////// reload caddy
let reload = {
container: 'DweebProxy',
command: `caddy reload --config /etc/caddy/Caddyfile`
}
await containerExec(reload, function(err, data) {
if (err) {
console.error(err);
return;
}
console.log(`Reloaded Caddy Config`);
});
let site = siteCard(type, domain, host, port, 0);
req.app.locals.site_list += site;
res.redirect("/");
} else {
// Redirect
console.log('not admin or missing info')
res.redirect("/");
}
}
exports.RemoveSite = async function (req, res) {
if (req.session.role == "admin") {
for (const [key, value] of Object.entries(req.body)) {
execSync(`rm ./caddyfiles/sites/${value}.Caddyfile`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`removed ${value}.Caddyfile`);
});
}
let reload = {
container: 'DweebProxy',
command: `caddy reload --config /etc/caddy/Caddyfile`
}
await containerExec(reload);
console.log('Removed Site(s)')
res.redirect("/refreshsites");
} else {
res.redirect("/");
}
}
exports.RefreshSites = async function (req, res) {
let domain, type, host, port;
let id = 1;
if (req.session.role == "admin") {
// Clear site_list.ejs
req.app.locals.site_list = "";
// check if ./caddyfiles/sites contains any .json files, then delete them
try {
let files = readdirSync('./caddyfiles/sites/');
files.forEach(file => {
if (file.includes(".json")) {
execSync(`rm ./caddyfiles/sites/${file}`, (err, stdout, stderr) => {
if (err) { console.error(`error: ${err.message}`); return; }
if (stderr) { console.error(`stderr: ${stderr}`); return; }
console.log(`removed ${file}`);
});
}
});
} catch (error) { console.log("No .json files to delete") }
// get list of Caddyfiles
let sites = readdirSync('./caddyfiles/sites/');
sites.forEach(site_name => {
// convert the caddyfile of each site to json
let convert = {
container: 'DweebProxy',
command: `caddy adapt --config ./caddyfiles/sites/${site_name} --pretty >> ./caddyfiles/sites/${site_name}.json`
}
containerExec(convert);
try {
// read the json file
let site_file = readFileSync(`./caddyfiles/sites/${site_name}.json`, 'utf8');
// fix whitespace and parse the json file
site_file = site_file.replace(/ /g, " ");
site_file = JSON.parse(site_file);
} catch (error) { console.log("No .json file to read") }
// get the domain, type, host, and port from the json file
try { domain = site_file.apps.http.servers.srv0.routes[0].match[0].host[0] } catch (error) { console.log("No Domain") }
try { type = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].handler } catch (error) { console.log("No Type") }
try { host = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[0] } catch (error) { console.log("Not Localhost") }
try { port = site_file.apps.http.servers.srv0.routes[0].handle[0].routes[0].handle[1].upstreams[0].dial.split(":")[1] } catch (error) { console.log("No Port") }
// build the site card
let site = siteCard(type, domain, host, port, id);
// append the site card to site_list
req.app.locals.site_list += site;
id++;
});
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/");
}
}
exports.DisableSite = async function (req, res) {
if (req.session.role == "admin") {
console.log(req.body)
console.log('Disable Site')
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}
exports.EnableSite = async function (req, res) {
if (req.session.role == "admin") {
console.log(req.body)
console.log('Enable Site')
res.redirect("/");
} else {
// Redirect to the login page
res.redirect("/login");
}
}

View file

@ -2,6 +2,7 @@ const { currentLoad, mem, networkStats, fsSize, dockerContainerStats } = require
var Docker = require('dockerode');
var docker = new Docker({ socketPath: '/var/run/docker.sock' });
const { dashCard } = require('../components/dashCard');
const { Readable } = require('stream');
// export docker
module.exports.docker = docker;
@ -254,3 +255,41 @@ module.exports.containerExec = async function (data) {
module.exports.containerLogs = function (data) {
return new Promise((resolve, reject) => {
let logString = '';
var options = {
follow: false,
stdout: true,
stderr: false,
timestamps: false
};
var containerName = docker.getContainer(data);
containerName.logs(options, function (err, stream) {
if (err) {
reject(err);
return;
}
const readableStream = Readable.from(stream);
readableStream.on('data', function (chunk) {
logString += chunk.toString('utf8');
});
readableStream.on('end', function () {
resolve(logString);
});
});
});
};

View file

@ -22,7 +22,7 @@ const diskBar = document.getElementById('disk-bar');
const dockerCards = document.getElementById('cards');
// create
const logViewer = document.getElementById('logView');
//Update usage bars
socket.on('metrics', (data) => {
@ -115,10 +115,25 @@ function buttonAction(button) {
socket.emit('clicked', {container: button.name, state: button.id, action: button.value});
}
let containerLogs;
function viewLogs(button) {
console.log(`trying to view logs for ${button.name}`);
if (button.name != 'refresh') {
containerLogs = button.name;
}
socket.emit('logs', {container: containerLogs});
}
socket.on('logString', (data) => {
logViewer.innerHTML = `<pre>${data}</pre>`;
});
socket.on('cards', (data) => {
console.log('cards deleted');

View file

@ -1,17 +1,13 @@
const express = require("express");
const router = express.Router();
const { Dashboard } = require("../controllers/dashboard");
const { AddSite, RemoveSite, RefreshSites, DisableSite, EnableSite } = require("../controllers/site_actions");
const { Dashboard, AddSite, RemoveSite, RefreshSites, DisableSite, EnableSite } = require("../controllers/dashboard");
const { Login, processLogin, Logout, Register, processRegister } = require("../controllers/auth");
const { Apps, searchApps, Install, Uninstall } = require("../controllers/apps");
const { Users } = require("../controllers/users");
const { Account } = require("../controllers/account");
const { Settings } = require("../controllers/settings");
const { Logout } = require("../controllers/logout");
const { Login, processLogin } = require("../controllers/login");
const { Register, processRegister } = require("../controllers/register");
@ -44,4 +40,7 @@ router.post("/register",processRegister); // Process Register
router.get("/logout",Logout); // Logout
module.exports = router;

View file

@ -126,6 +126,38 @@
</div>
</div>
<div class="modal modal-blur fade" id="log_view" tabindex="-1" style="display: none;" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Logs</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card-body">
<h4>
Log File:
</h4>
<h4>Logs:</h4>
<div id="logView">
<pre>No logs available</pre>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn me-auto" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-info" onclick="viewLogs(this)" name="refresh">
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-tabler icon-tabler-refresh" width="24" height="24" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" fill="none" stroke-linecap="round" stroke-linejoin="round"> <path stroke="none" d="M0 0h24v24H0z" fill="none"></path> <path d="M20 11a8.1 8.1 0 0 0 -15.5 -2m-.5 -4v4h4"></path> <path d="M4 13a8.1 8.1 0 0 0 15.5 2m.5 4v-4h-4"></path> </svg>
Refresh
</button>
</div>
</div>
</div>
</div>
<div class="col-12 mt-12 <%- caddy %>">
<div class="card">
<div class="card-header">

View file

@ -24,7 +24,7 @@
</li>
<li class="list-inline-item">
<a href="#" class="link-secondary" rel="noopener">
v0.06
v0.07
</a>
</li>
</ul>