Compare commits
10 Commits
4960132d1d
...
bf530217f3
| Author | SHA1 | Date | |
|---|---|---|---|
| bf530217f3 | |||
| 69c52fdf8c | |||
| d61c137c12 | |||
| 8a8d793199 | |||
| 011be07e03 | |||
| 518ee7c366 | |||
| f31e97b014 | |||
| 7f0cf36419 | |||
| d206cad0c5 | |||
| bf4fd38202 |
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/user.txt
|
||||||
76
README.md
76
README.md
@ -1,7 +1,79 @@
|
|||||||
# MALP - Monitor Active Linux Pulses
|
# MALP - Monitor A Linux Platform
|
||||||
|
|
||||||
A lightweight Ruby CGI status page for home server monitoring.
|
A lightweight Ruby CGI status page for home server monitoring.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
|
|
||||||
Copy repository directory to `/var/www/malp`.
|
Copy this repository directory to `/var/www`, so `/var/www/malp/cgi-bin` exists.
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir /var/www/malp/data
|
||||||
|
chown apache:apache /var/www/malp/data
|
||||||
|
```
|
||||||
|
|
||||||
|
### If using SELinux (e.g. AlmaLinux)
|
||||||
|
|
||||||
|
```
|
||||||
|
chcon -R -t httpd_sys_script_exec_t /var/www/malp/cgi-bin
|
||||||
|
chcon -R -t httpd_sys_rw_content_t /var/www/malp/data
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Apache HTTPD Setup
|
||||||
|
|
||||||
|
Here is my example setup.
|
||||||
|
My server's name is `anubis`.
|
||||||
|
Replace as desired.
|
||||||
|
|
||||||
|
#### Create self-signed TLS certificate
|
||||||
|
|
||||||
|
```
|
||||||
|
mkdir /etc/httpd/tls
|
||||||
|
cd /etc/httpd/tls
|
||||||
|
openssl ecparam -name secp384r1 -genkey -noout -out anubis.key
|
||||||
|
openssl req -new -x509 -key anubis.key -out anubis.crt -days 3650 -sha384
|
||||||
|
```
|
||||||
|
|
||||||
|
#### /etc/httpd/conf.d/anubis.conf
|
||||||
|
|
||||||
|
```
|
||||||
|
<VirtualHost *:80>
|
||||||
|
ServerName anubis
|
||||||
|
ServerAlias anubis
|
||||||
|
|
||||||
|
# Permanent redirect to the same URI on HTTPS
|
||||||
|
Redirect permanent / https://anubis/
|
||||||
|
</VirtualHost>
|
||||||
|
|
||||||
|
<VirtualHost *:443>
|
||||||
|
ServerName anubis
|
||||||
|
DocumentRoot /var/www/html
|
||||||
|
|
||||||
|
SSLEngine on
|
||||||
|
|
||||||
|
SSLCertificateFile /etc/httpd/tls/anubis.crt
|
||||||
|
SSLCertificateKeyFile /etc/httpd/tls/anubis.key
|
||||||
|
|
||||||
|
# Modern TLS Security (Recommended for ECDSA)
|
||||||
|
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
|
||||||
|
SSLCipherSuite ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384
|
||||||
|
SSLHonorCipherOrder off
|
||||||
|
SSLSessionTickets off
|
||||||
|
|
||||||
|
ErrorLog /var/log/httpd/anubis-error.log
|
||||||
|
CustomLog /var/log/httpd/anubis-access.log combined
|
||||||
|
|
||||||
|
<Directory /var/www/html>
|
||||||
|
Options Indexes FollowSymLinks
|
||||||
|
AllowOverride All
|
||||||
|
Require all granted
|
||||||
|
</Directory>
|
||||||
|
|
||||||
|
ScriptAlias / /var/www/malp/cgi-bin/malp.rb
|
||||||
|
</VirtualHost>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set user name and password
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/www/malp/bin/setpasswd
|
||||||
|
```
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Home Server Status</title>
|
<title><%= hostname %> status - MALP</title>
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: #0f1117;
|
background: #0f1117;
|
||||||
@ -115,7 +115,7 @@
|
|||||||
<body>
|
<body>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<h1><span>homelab</span> · status</h1>
|
<h1><span><%= hostname %></span> status - MALP</h1>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
@ -123,7 +123,7 @@
|
|||||||
<form method="post">
|
<form method="post">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input type="text" id="username" name="username" autocomplete="username" required>
|
<input type="text" id="username" name="username" autocomplete="username" required autofocus>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
29
bin/setpasswd
Executable file
29
bin/setpasswd
Executable file
@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env ruby
|
||||||
|
|
||||||
|
require "digest"
|
||||||
|
require "fileutils"
|
||||||
|
require "io/console"
|
||||||
|
require "securerandom"
|
||||||
|
|
||||||
|
DATA_DIR = File.join(__dir__, "../data")
|
||||||
|
USER_FILE = File.join(DATA_DIR, "user.txt")
|
||||||
|
|
||||||
|
print "User name: "
|
||||||
|
username = $stdin.gets.chomp
|
||||||
|
|
||||||
|
unless username =~ /^[a-zA-Z0-9_]+$/
|
||||||
|
$stderr.puts "Invalid characters in user name"
|
||||||
|
exit(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
print "Password: "
|
||||||
|
password = $stdin.noecho(&:gets).chomp
|
||||||
|
|
||||||
|
salt = SecureRandom.hex(8)
|
||||||
|
input = salt + password
|
||||||
|
hash = Digest::SHA256.hexdigest(input)
|
||||||
|
hashhex = hash.chars.map {|c| sprintf("%02X", c.ord)}.join
|
||||||
|
user_file_contents = "#{username}:#{salt}:#{hashhex}\n"
|
||||||
|
|
||||||
|
File.binwrite(USER_FILE, user_file_contents)
|
||||||
|
puts "User and password set"
|
||||||
@ -2,13 +2,74 @@
|
|||||||
|
|
||||||
require "cgi"
|
require "cgi"
|
||||||
require "erb"
|
require "erb"
|
||||||
|
require "digest"
|
||||||
|
require "securerandom"
|
||||||
|
|
||||||
ASSETS_DIR = File.join(__dir__, "../assets")
|
ASSETS_DIR = File.join(__dir__, "../assets")
|
||||||
|
DATA_DIR = File.join(__dir__, "../data")
|
||||||
|
SESSIONS_FILE = File.join(DATA_DIR, "sessions.txt")
|
||||||
|
USER_FILE = File.join(DATA_DIR, "user.txt")
|
||||||
|
|
||||||
cgi = CGI.new
|
cgi = CGI.new
|
||||||
|
|
||||||
template = ERB.new(File.read(File.join(ASSETS_DIR, "login.erb")))
|
hostname = File.read("/etc/hostname").strip rescue "localhost"
|
||||||
|
|
||||||
cgi.out("type" => "text/html", "charset" => "UTF-8") do
|
def load_sessions
|
||||||
|
return [] unless File.exist?(SESSIONS_FILE)
|
||||||
|
now = Time.now.to_i
|
||||||
|
max_age = 3 * 7 * 24 * 60 * 60 # 3 weeks
|
||||||
|
sessions = File.readlines(SESSIONS_FILE).filter_map do |line|
|
||||||
|
token, timestamp = line.strip.split(":", 2)
|
||||||
|
next if token.nil? || token.empty?
|
||||||
|
[token, timestamp.to_i]
|
||||||
|
end
|
||||||
|
active, expired = sessions.partition { |_, ts| now - ts < max_age }
|
||||||
|
if expired.any?
|
||||||
|
File.write(SESSIONS_FILE, active.map { |t, ts| "#{t}:#{ts}" }.join("\n") + "\n")
|
||||||
|
end
|
||||||
|
active
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid_session?(token)
|
||||||
|
return false if token.nil? || token.empty?
|
||||||
|
load_sessions.any? { |t, _| t == token }
|
||||||
|
end
|
||||||
|
|
||||||
|
def check_credentials(username, password)
|
||||||
|
return false unless File.exist?(USER_FILE)
|
||||||
|
stored_user, salt, stored_hash_hex2 = File.read(USER_FILE).strip.split(":", 3)
|
||||||
|
return false unless username == stored_user
|
||||||
|
stored_hash = [stored_hash_hex2].pack("H*")
|
||||||
|
computed_hash = Digest::SHA256.hexdigest(salt + password)
|
||||||
|
stored_hash == computed_hash
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_session
|
||||||
|
token = SecureRandom.hex(32)
|
||||||
|
File.open(SESSIONS_FILE, "a") { |f| f.puts("#{token}:#{Time.now.to_i}") }
|
||||||
|
token
|
||||||
|
end
|
||||||
|
|
||||||
|
session_token = (cgi.cookies["MALP"] || []).first
|
||||||
|
authenticated = valid_session?(session_token)
|
||||||
|
cookie = nil
|
||||||
|
|
||||||
|
if cgi.request_method == "POST" && !authenticated
|
||||||
|
username = cgi.params["username"]&.first.to_s
|
||||||
|
password = cgi.params["password"]&.first.to_s
|
||||||
|
|
||||||
|
if check_credentials(username, password)
|
||||||
|
token = create_session
|
||||||
|
cookie = CGI::Cookie.new("name" => "MALP", "value" => token, "path" => "/")
|
||||||
|
authenticated = true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
template = ERB.new(File.read(File.join(ASSETS_DIR, "page.erb")))
|
||||||
|
|
||||||
|
out_params = { "type" => "text/html", "charset" => "UTF-8" }
|
||||||
|
out_params["cookie"] = cookie if cookie
|
||||||
|
|
||||||
|
cgi.out(out_params) do
|
||||||
template.result(binding)
|
template.result(binding)
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user