initial commit
This commit is contained in:
33
Dockerfile
Normal file
33
Dockerfile
Normal file
@ -0,0 +1,33 @@
|
||||
# ---------- Dockerfile ----------
|
||||
# Base image – PHP + Apache
|
||||
FROM php:apache
|
||||
|
||||
# Install Python, NGINX, Supervisor and pip
|
||||
RUN apt-get update && apt-get install -y \
|
||||
python3 \
|
||||
python3-pip \
|
||||
nginx \
|
||||
supervisor \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Python dependencies
|
||||
# (add any other libraries you need here)
|
||||
RUN pip3 install --no-cache-dir \
|
||||
flask \
|
||||
pyyaml
|
||||
|
||||
# Copy your application code & config files
|
||||
# Website Files
|
||||
COPY www/ /var/www/html
|
||||
# Python files
|
||||
COPY api/ /usr/src/app/
|
||||
# Config Files
|
||||
COPY apache_ports.conf /etc/apache2/ports.conf
|
||||
COPY apache_vhost.conf /etc/apache2/sites-available/000-default.conf
|
||||
COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
|
||||
|
||||
# Expose the ports you care about
|
||||
EXPOSE 80
|
||||
|
||||
# Start Supervisor
|
||||
CMD ["/usr/bin/supervisord", "-n"]
|
||||
5
apache_ports.conf
Normal file
5
apache_ports.conf
Normal file
@ -0,0 +1,5 @@
|
||||
# Listen on 8080 inside the container
|
||||
Listen 8080
|
||||
|
||||
# If you still want Apache to listen on 80 (rare), add it back:
|
||||
# Listen 80
|
||||
14
apache_vhost.conf
Normal file
14
apache_vhost.conf
Normal file
@ -0,0 +1,14 @@
|
||||
<VirtualHost *:8080>
|
||||
ServerAdmin webmaster@localhost
|
||||
DocumentRoot /var/www/html
|
||||
|
||||
# Log files
|
||||
ErrorLog ${APACHE_LOG_DIR}/error.log
|
||||
CustomLog ${APACHE_LOG_DIR}/access.log combined
|
||||
|
||||
# If you need PHP processing
|
||||
<Directory /var/www/html>
|
||||
AllowOverride All
|
||||
Require all granted
|
||||
</Directory>
|
||||
</VirtualHost>
|
||||
430
api/app.py
Normal file
430
api/app.py
Normal file
@ -0,0 +1,430 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# yo dawg i heard you like libraries
|
||||
from __future__ import annotations
|
||||
import random
|
||||
import string
|
||||
import yaml
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import List, Iterable, Set, Dict, Any
|
||||
from flask import Flask, jsonify, request, Response
|
||||
import json, time, hashlib
|
||||
|
||||
# System Variables
|
||||
words = []
|
||||
simple_words = []
|
||||
HASH_FILE = "/opt/pwdgen/hash_record.txt"
|
||||
password_hashes =set()
|
||||
SPECIAL_SET = "!@#$%^&*(),.<>?~`;:|][}{=-+_"
|
||||
WORDS_FILE = "dict.yaml"
|
||||
password_types = [
|
||||
"generate_standard_password",
|
||||
"generate_windows_ad_password",
|
||||
"generate_simple_password"
|
||||
]
|
||||
|
||||
#################################################
|
||||
# Hash Record Functions
|
||||
#################################################
|
||||
if not HASH_FILE.exists():
|
||||
# Nothing to load – create an empty file for future use
|
||||
HASH_FILE.touch(exist_ok=True)
|
||||
try:
|
||||
with HASH_FILE.open("r", encoding="utf-8") as fh:
|
||||
password_hashes = {line.strip() for line in fh if line.strip()}
|
||||
except OSError as exc:
|
||||
# If we cannot read the file we fall back to an empty set; the
|
||||
# application will continue to work, but any persisted hashes
|
||||
# will be lost.
|
||||
print(f"Warning: cannot read hash file {HASH_FILE!s}: {exc}")
|
||||
password_hashes = set()
|
||||
|
||||
def save_hashes() -> None:
|
||||
# Write to a temporary file first
|
||||
tmp_path = HASH_FILE.with_name(HASH_FILE.name + ".tmp")
|
||||
try:
|
||||
with tmp_path.open("w", encoding="utf-8") as fh:
|
||||
for h in sorted(password_hashes):
|
||||
fh.write(f"{h}\n")
|
||||
# Replace the original file atomically
|
||||
tmp_path.replace(HASH_FILE)
|
||||
except OSError as exc:
|
||||
print(f"Error: could not write hash file {HASH_FILE!s}: {exc}")
|
||||
|
||||
#################################################
|
||||
# YAML Handler
|
||||
#################################################
|
||||
with open(WORDS_FILE, "r", encoding="utf-8") as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
|
||||
# Ensure the key exists and is a list; otherwise return empty list.
|
||||
raw_words = data.get("words") if isinstance(data, dict) else None
|
||||
if not isinstance(words, list):
|
||||
raise ValueError(f"YAML file {path!s} must contain a top-level 'words' list")
|
||||
|
||||
# Ensure the key exists and is a list; otherwise return empty list.
|
||||
raw_simple_words = data.get("simple_words") if isinstance(data, dict) else None
|
||||
if not isinstance(words, list):
|
||||
raise ValueError(f"YAML file {path!s} must contain a top-level 'words' list")
|
||||
|
||||
# Strip whitespace and keep only non-empty words
|
||||
words = [w.strip() for w in raw_words if isinstance(w, str) and w.strip()]
|
||||
simple_words = [w.strip() for w in raw_simple_words if isinstance(w, str) and w.strip()]
|
||||
total_words = len(words) + len(simple_words)
|
||||
|
||||
# Declare Descriptor after loading words
|
||||
|
||||
password_function_descriptor= {
|
||||
"generate_standard_password": {
|
||||
"name": "Standard Password",
|
||||
"type": "0",
|
||||
"description": f"From word lists totalling {total_words} word this algorithm selects 60 of these words that are less than 13 characters. From these 60, it uses the one with the index of the current second, and depending on this word length, it may or may not select additional words from the 60. It then randomly selects an integer and some special characters and randomly puts these all together into one string that is this password."
|
||||
}
|
||||
,
|
||||
"generate_windows_ad_password": {
|
||||
"name": "Windows AD Password",
|
||||
"type": "1",
|
||||
"description": "This password is always in the following format: $Word1Word2Number$<br> - Each word is less than 7 characters, the number is 3 digits, and the $ represents a Special Charater."
|
||||
}
|
||||
,
|
||||
"generate_simple_password": {
|
||||
"name": "Simple Password",
|
||||
"type": "2",
|
||||
"description": f"This simple password is in the following format: !Password123 - this pulls from a list of {len(simple_words)} simple words."
|
||||
}
|
||||
}
|
||||
|
||||
#################################################
|
||||
# Flask Routes and Helpers
|
||||
#################################################
|
||||
app = Flask(__name__)
|
||||
|
||||
@app.route('/get_password', methods=['GET'])
|
||||
def get_password():
|
||||
pwd_index = request.args.get('pwd_index')
|
||||
# Check for presence
|
||||
if pwd_index is None:
|
||||
print("Missing required query parameter 'pwd_index'")
|
||||
return jsonify({
|
||||
"error": "Missing required query parameter 'pwd_index'"
|
||||
}), 400 # 400 Bad Request
|
||||
# Check for int
|
||||
try:
|
||||
index = int(pwd_index)
|
||||
except ValueError:
|
||||
print(f"Parameter 'index' must be an integer, got '{pwd_index}'")
|
||||
return jsonify({
|
||||
"error": f"Parameter 'index' must be an integer, got '{pwd_index}'"
|
||||
}), 400
|
||||
# Check bounds
|
||||
if index < 0 or index >= len(password_types):
|
||||
print(f"Index {index} is out of bounds (0 ≤ index < {len(password_types)})")
|
||||
return jsonify({
|
||||
"error": f"Index {index} is out of bounds (0 ≤ index < {len(password_types)})",
|
||||
"password_types": password_types
|
||||
}), 404 # 404 Not Found
|
||||
|
||||
# Success – return the password
|
||||
return jsonify({
|
||||
"password": get_password_by_index(index)
|
||||
}), 200
|
||||
|
||||
@app.route('/verbose_password', methods=['GET'])
|
||||
def verbose_password():
|
||||
pwd_index = request.args.get('pwd_index')
|
||||
# Check for presence
|
||||
if pwd_index is None:
|
||||
return jsonify({
|
||||
"error": "Missing required query parameter 'pwd_index'"
|
||||
}), 400 # 400 Bad Request
|
||||
# Check for int
|
||||
try:
|
||||
index = int(pwd_index)
|
||||
except ValueError:
|
||||
return jsonify({
|
||||
"error": f"Parameter 'index' must be an integer, got '{pwd_index}'"
|
||||
}), 400
|
||||
# Check bounds
|
||||
if index < 0 or index >= len(password_types):
|
||||
return jsonify({
|
||||
"error": f"Index {index} is out of bounds (0 ≤ index < {len(password_types)})",
|
||||
"password_types": password_types
|
||||
}), 404 # 404 Not Found
|
||||
# Success – return the result
|
||||
result = {
|
||||
"password": get_password_by_index(index),
|
||||
"descriptor": password_function_descriptor[password_types[index]],
|
||||
"password count": len(password_hashes)
|
||||
}
|
||||
return jsonify(result), 200
|
||||
|
||||
@app.route('/custom_password', methods=['POST'])
|
||||
def custom_password():
|
||||
if not request.is_json:
|
||||
return jsonify(error="Request body must be JSON"), 400
|
||||
|
||||
payload = request.get_json()
|
||||
|
||||
# Basic presence check – you can relax this if you want defaults
|
||||
required_keys = {"w_min", "w_max", "w_count", "s_char", "num_len"}
|
||||
missing = required_keys - payload.keys()
|
||||
if missing:
|
||||
return jsonify(error=f"Missing keys: {', '.join(missing)}"), 400
|
||||
|
||||
try:
|
||||
password = generate_custom_password(payload)
|
||||
except ValueError as exc:
|
||||
return jsonify(error=str(exc)), 400
|
||||
|
||||
return jsonify({
|
||||
"password": password
|
||||
}), 200
|
||||
|
||||
@app.route('/get_types', methods=['GET'])
|
||||
def get_types():
|
||||
return jsonify(password_function_descriptor)
|
||||
|
||||
@app.route('/get_count', methods=['GET'])
|
||||
def get_count():
|
||||
password_count = len(password_hashes)
|
||||
result = {
|
||||
"total_passwords": password_count
|
||||
}
|
||||
return jsonify(result)
|
||||
|
||||
|
||||
def get_password_by_index(pwd_index: int):
|
||||
if pwd_index < 0 or pwd_index >= len(password_types):
|
||||
raise IndexError(f"Index {pwd_index} is out of range for list of length {len(password_types)}")
|
||||
func_name = password_types[pwd_index]
|
||||
# Look up the actual function object in the module namespace.
|
||||
func = globals().get(func_name)
|
||||
if not callable(func):
|
||||
raise ValueError(f"Function name '{func_name}' does not refer to a callable")
|
||||
return func()
|
||||
|
||||
#################################################
|
||||
# Password Generators
|
||||
#################################################
|
||||
# Standard Password Generator
|
||||
# This is excessively complicated
|
||||
def generate_standard_password() -> str:
|
||||
candidates = [w for w in words if len(w) < 13]
|
||||
if not candidates:
|
||||
raise ValueError("No words of length < 13 found")
|
||||
# Shuffle the list and grab the first 60
|
||||
random.shuffle(candidates)
|
||||
candidates = candidates[:60]
|
||||
# Grab a random-ish word based on the time
|
||||
sec = int(time.time())
|
||||
word1 = candidates[sec % len(candidates)]
|
||||
len1 = len(word1)
|
||||
# Start building the password
|
||||
password = ""
|
||||
# If it picked a short word, add more stuff
|
||||
if len1 < 5:
|
||||
word2 = random.choice(words)
|
||||
len2 = len(word2)
|
||||
# If this picked a really short word, add moar things
|
||||
if len2 < 4:
|
||||
word3 = random.choice(words)
|
||||
return type3(word1, word2, word3)
|
||||
password = type2(word1, word2)
|
||||
# If it picked a really long word, this will be fine
|
||||
if len1 > 8:
|
||||
password = type1(word1)
|
||||
# If the first word is medium-sized, get one more
|
||||
word2 = random.choice(words)
|
||||
password = type2(word1, word2)
|
||||
# Check, and return password
|
||||
if check_and_add_hash(password):
|
||||
return password
|
||||
else:
|
||||
return "Somehow a password was duplicated"
|
||||
|
||||
# Standard Password Helper Functions
|
||||
# For one really long word
|
||||
def type1(w1: str) -> str:
|
||||
num1 = rand_int(100, 9999)
|
||||
sym = rand_symbol_set(SPECIAL_SET, 3)
|
||||
components = [ucfirst(w1), str(num1)] + sym
|
||||
shuffle_list(components)
|
||||
return "".join(components)
|
||||
# For two medium-sized words
|
||||
def type2(w1: str, w2: str) -> str:
|
||||
num2 = rand_int(100, 9999)
|
||||
sym = rand_symbol_set(SPECIAL_SET, 3)
|
||||
components = [ucfirst(w1), ucfirst(w2), str(num2)] + sym
|
||||
shuffle_list(components)
|
||||
return "".join(components)
|
||||
# At least two tiny words
|
||||
def type3(w1: str, w2: str, w3: str) -> str:
|
||||
num3 = rand_int(100, 9999)
|
||||
sym = rand_symbol_set(SPECIAL_SET, 4)
|
||||
components = [ucfirst(w1), ucfirst(w2), ucfirst(w3), str(num3)] + sym
|
||||
shuffle_list(components)
|
||||
return "".join(components)
|
||||
|
||||
# Windows AD Password
|
||||
def generate_windows_ad_password() -> str:
|
||||
short_words = [w for w in simple_words if len(w) < 7]
|
||||
if len(short_words) < 2:
|
||||
raise ValueError("Need at least two words of length < 7")
|
||||
# Get two words
|
||||
w1, w2 = random.sample(short_words, 2)
|
||||
# Get two symbols and a 3-digit number
|
||||
sym_set = "!@#$%^&*()[]{}|-+<>?"
|
||||
symbols = rand_symbol_set(sym_set, 2)
|
||||
number = rand_int(100, 999)
|
||||
# Build, check, and return password
|
||||
password = f"{symbols[0]}{ucfirst(w1)}{ucfirst(w2)}{number}{symbols[1]}"
|
||||
if check_and_add_hash(password):
|
||||
return password
|
||||
else:
|
||||
return "Somehow a password was duplicated"
|
||||
|
||||
# Simple Password
|
||||
def generate_simple_password() -> str:
|
||||
candidates = [w for w in words if 7 <= len(w) <= 12]
|
||||
if not candidates:
|
||||
raise ValueError("No words of length 7-12 found")
|
||||
# Get a random word, number, and SC
|
||||
word = random.choice(candidates)
|
||||
symbol = random.choice("!@#$%^&*()")
|
||||
number = rand_int(100, 999)
|
||||
# Build, check, and return password
|
||||
password = f"{symbol}{ucfirst(word)}{number}"
|
||||
if check_and_add_hash(password):
|
||||
return password
|
||||
else:
|
||||
return "Somehow a password was duplicated"
|
||||
|
||||
|
||||
#################################################
|
||||
# Custom Password Generator
|
||||
#################################################
|
||||
|
||||
def generate_custom_password(params: Dict[str, Any]) -> str:
|
||||
|
||||
# Helper function for range
|
||||
def _num_range(num_len: int) -> tuple[int, int] | None:
|
||||
mapping = {
|
||||
0: None,
|
||||
1: (0, 9),
|
||||
2: (10, 99),
|
||||
3: (100, 999),
|
||||
4: (1000, 9999),
|
||||
5: (10000, 99999),
|
||||
6: (100000, 999999),
|
||||
7: (1000000, 9999999),
|
||||
8: (10000000, 99999999),
|
||||
}
|
||||
return mapping.get(num_len)
|
||||
|
||||
# 2.1 Pull and validate parameters
|
||||
w_min = int(params.get('w_min', 3))
|
||||
w_max = int(params.get('w_max', 10))
|
||||
w_count = int(params.get('w_count', 2))
|
||||
s_char = int(params.get('s_char', 2))
|
||||
num_len = int(params.get('num_len', 3))
|
||||
words_raw = list(params.get('words', []))
|
||||
|
||||
# Basic sanity checks
|
||||
if not (3 <= w_min <= 10):
|
||||
raise ValueError("w_min must be between 3 and 10")
|
||||
if not (3 <= w_max <= 10):
|
||||
raise ValueError("w_max must be between 3 and 10")
|
||||
if w_min > w_max:
|
||||
raise ValueError("w_min cannot be greater than w_max")
|
||||
if not (1 <= w_count <= 5):
|
||||
raise ValueError("w_count must be between 1 and 5")
|
||||
if not (0 <= s_char <= 4):
|
||||
raise ValueError("s_char must be between 0 and 4")
|
||||
if not (0 <= num_len <= 8):
|
||||
raise ValueError("num_len must be between 0 and 8")
|
||||
|
||||
# 2.2 Filter the word list by length
|
||||
candidates = [w for w in words if w_min <= len(w) <= w_max]
|
||||
if len(candidates) < w_count:
|
||||
raise ValueError(
|
||||
f"Not enough words of length {w_min}-{w_max}. "
|
||||
f"Need {w_count}, found {len(candidates)}."
|
||||
)
|
||||
|
||||
# 2.3 Randomly pick words and capitalize first letter
|
||||
rng = random.SystemRandom() # cryptographically secure PRNG
|
||||
selected_words = rng.sample(candidates, w_count)
|
||||
selected_words = [w.capitalize() for w in selected_words]
|
||||
|
||||
# Generate the numeric block (if required)
|
||||
num_part = ""
|
||||
num_range = _num_range(num_len)
|
||||
if num_range:
|
||||
low, high = num_range
|
||||
num_part = str(rng.randint(low, high))
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Pick special characters
|
||||
# --------------------------------------------------------------------- #
|
||||
special_chars = "!@#$%^&*()[]{}|-+<>?"
|
||||
shuffled_specials = list(special_chars)
|
||||
rng.shuffle(shuffled_specials)
|
||||
specials = "".join(shuffled_specials[:s_char])
|
||||
|
||||
# --------------------------------------------------------------------- #
|
||||
# Assemble all pieces into a list, shuffle, and join
|
||||
# --------------------------------------------------------------------- #
|
||||
all_parts: List[str] = selected_words
|
||||
if num_part:
|
||||
all_parts.append(num_part)
|
||||
if specials:
|
||||
# We insert specials as individual characters (mirroring PHP code)
|
||||
all_parts.extend(list(specials))
|
||||
|
||||
rng.shuffle(all_parts)
|
||||
password = "".join(all_parts)
|
||||
|
||||
return password
|
||||
|
||||
#################################################
|
||||
# Password helper functions
|
||||
#################################################
|
||||
|
||||
# Return the word with its first character capitalised
|
||||
def ucfirst(word: str) -> str:
|
||||
return word[0].upper() + word[1:] if word else ""
|
||||
|
||||
def shuffle_list(items: List[str]) -> None:
|
||||
random.shuffle(items)
|
||||
|
||||
def rand_int(low: int, high: int) -> int:
|
||||
return random.randint(low, high)
|
||||
|
||||
def rand_symbol_set(symbols: str, n: int) -> List[str]:
|
||||
return random.sample(symbols, n)
|
||||
|
||||
# Return True when password is unused
|
||||
def check_and_add_hash(password: str) -> bool:
|
||||
def sha256_hex(s: str) -> str:
|
||||
return hashlib.sha256(s.encode('utf-8')).hexdigest()
|
||||
hash_value = sha256_hex(password)
|
||||
if hash_value in password_hashes:
|
||||
return False
|
||||
# Hash was missing – add it and report that it was newly inserted.
|
||||
password_hashes.add(hash_value)
|
||||
save_hashes()
|
||||
return True
|
||||
|
||||
#################################################
|
||||
# Main Function
|
||||
#################################################
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
######################################
|
||||
# Flask API
|
||||
######################################
|
||||
|
||||
app.run(debug=False, host='0.0.0.0', port=5000)
|
||||
|
||||
9074
api/dict.yaml
Normal file
9074
api/dict.yaml
Normal file
File diff suppressed because it is too large
Load Diff
1
build.sh
Executable file
1
build.sh
Executable file
@ -0,0 +1 @@
|
||||
docker build -t pwdgen-v2 /opt/containers/pwdgen-v2
|
||||
12
docker-compose.yaml
Normal file
12
docker-compose.yaml
Normal file
@ -0,0 +1,12 @@
|
||||
# docker compose
|
||||
|
||||
services:
|
||||
pwdgen:
|
||||
build: .
|
||||
image: pwdgen_v2:latest
|
||||
container_name: pwd.matt-cloud.com
|
||||
ports:
|
||||
- "80:80"
|
||||
volumes:
|
||||
- ./pwdgen:/opt/pwdgen
|
||||
restart: always
|
||||
67
nginx.conf
Normal file
67
nginx.conf
Normal file
@ -0,0 +1,67 @@
|
||||
# nginx.conf
|
||||
# This file will be mounted into /etc/nginx/conf.d/default.conf inside the container
|
||||
|
||||
# Enable proxy buffers (optional but recommended)
|
||||
proxy_buffering on;
|
||||
proxy_buffers 16 16k;
|
||||
proxy_buffer_size 32k;
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name pwdgwn_v2;
|
||||
|
||||
# ---------------------------------------
|
||||
# API Routes
|
||||
# ---------------------------------------
|
||||
location = /get_password {
|
||||
proxy_pass http://0.0.0.0:5000/get_password;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /verbose_password {
|
||||
proxy_pass http://0.0.0.0:5000/verbose_password;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /custom_password {
|
||||
proxy_pass http://0.0.0.0:5000/custom_password;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /get_count {
|
||||
proxy_pass http://0.0.0.0:5000/get_count;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location = /get_info {
|
||||
proxy_pass http://0.0.0.0:5000/get_info;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
# ---------------------------------------
|
||||
# All other paths → Apache (PHP)
|
||||
# ---------------------------------------
|
||||
location / {
|
||||
proxy_pass http://0.0.0.0:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
36
supervisord.conf
Normal file
36
supervisord.conf
Normal file
@ -0,0 +1,36 @@
|
||||
[supervisord]
|
||||
nodaemon=true
|
||||
logfile=/dev/stdout ; Supervisor itself → stdout
|
||||
logfile_maxbytes=0 ; (no rotation – keeps it simple)
|
||||
logfile_backups=0
|
||||
loglevel=info
|
||||
|
||||
[program:apache]
|
||||
command=/usr/sbin/apache2ctl -D FOREGROUND
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout ; Apache → stdout
|
||||
stderr_logfile=/dev/stderr ; Apache → stderr
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
|
||||
[program:python]
|
||||
command=python3 /usr/src/app/app.py
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout ; Python → stdout
|
||||
stderr_logfile=/dev/stderr ; Python → stderr
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
|
||||
[program:nginx]
|
||||
command=/usr/sbin/nginx -g 'daemon off;'
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stdout_logfile=/dev/stdout ; Nginx → stdout
|
||||
stderr_logfile=/dev/stderr ; Nginx → stderr
|
||||
stdout_logfile_maxbytes=0
|
||||
stderr_logfile_maxbytes=0
|
||||
redirect_stderr=true
|
||||
470
www/index.php
Normal file
470
www/index.php
Normal file
@ -0,0 +1,470 @@
|
||||
<?php
|
||||
ob_start(); // start buffering so we can set cookies before any output
|
||||
|
||||
// helper for settings
|
||||
function getSetting(
|
||||
string $cookieName,
|
||||
string $getName,
|
||||
int $default,
|
||||
?int $min = null,
|
||||
?int $max = null
|
||||
): int {
|
||||
if (isset($_GET[$getName])) {
|
||||
$value = intval($_GET[$getName]);
|
||||
|
||||
if (!is_null($min) && $value < $min) $value = $min;
|
||||
if (!is_null($max) && $value > $max) $value = $max;
|
||||
|
||||
$cookieOptions = [
|
||||
'expires' => time() + 86400 * 365,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
];
|
||||
setcookie($cookieName, $value, $cookieOptions);
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (isset($_COOKIE[$cookieName])) {
|
||||
return intval($_COOKIE[$cookieName]);
|
||||
}
|
||||
|
||||
$cookieOptions = [
|
||||
'expires' => time() + 86400 * 365,
|
||||
'path' => '/',
|
||||
'secure' => true,
|
||||
'httponly' => true,
|
||||
'samesite' => 'Strict',
|
||||
];
|
||||
setcookie($cookieName, $default, $cookieOptions);
|
||||
return $default;
|
||||
}
|
||||
|
||||
// ---- PASS TYPE COOKIE ----
|
||||
$defaultPassType = 0;
|
||||
if (isset($_GET['pt'])) {
|
||||
$passType = intval($_GET['pt']);
|
||||
setcookie('passtype', $passType, time() + 86400 * 365, '/');
|
||||
} elseif (isset($_COOKIE['passtype'])) {
|
||||
$passType = intval($_COOKIE['passtype']);
|
||||
} else {
|
||||
setcookie('passtype', $defaultPassType, time() + 86400 * 365, '/');
|
||||
$passType = $defaultPassType;
|
||||
}
|
||||
|
||||
// ---- CUSTOM PASSWORD SETTINGS ----
|
||||
$wMin = getSetting('wMin', 'wMinIn', 6, 3, 10);
|
||||
$wMax = getSetting('wMax', 'wMaxIn', 12, 3, 10);
|
||||
$wCount = getSetting('wCount', 'wCountIn', 2, 1, 5);
|
||||
$sChar = getSetting('sChar', 'sCharIn', 2, 0, 4);
|
||||
$numLen = getSetting('numLen', 'numLenIn', 3, 0, 8);
|
||||
|
||||
function curlHelper($url, $APIKey){
|
||||
// Initialise cURL
|
||||
$ch = curl_init($url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // return the response as a string
|
||||
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); // follow redirects if any
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10); // timeout after 10 seconds
|
||||
// Execute the request
|
||||
$response = curl_exec($ch);
|
||||
// Handle cURL errors
|
||||
if ($response === false) {
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
throw new Exception("cURL error while calling API: {$error}");
|
||||
}
|
||||
// Check HTTP status code
|
||||
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpStatus !== 200) {
|
||||
throw new Exception("API returned HTTP status {$httpStatus} (expected 200).");
|
||||
}
|
||||
$decoded = json_decode($response, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
if (isset($decoded[$APIKey])) {
|
||||
return $decoded[$APIKey];
|
||||
}
|
||||
}
|
||||
return trim($response);
|
||||
}
|
||||
|
||||
// Password Generator API Function
|
||||
function getStandardPasswordFromAPI($passType){
|
||||
$apiUrl = "http://172.17.0.1:8189/get_password";
|
||||
// Build the query string and full URL
|
||||
$query = http_build_query(['pwd_index' => $passType]);
|
||||
$url = rtrim($apiUrl, '?') . '?' . $query;
|
||||
return curlHelper($url, "password");
|
||||
}
|
||||
|
||||
// Password Generator API Function for Custom Password
|
||||
function getCustomPasswordFromAPI($passType, $payload){
|
||||
$url = 'http://172.17.0.1:8189/custom_password';
|
||||
|
||||
// Initialise a cURL handle
|
||||
$ch = curl_init($url);
|
||||
|
||||
// Tell cURL we want to send a POST request with a JSON body
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
||||
|
||||
// Tell cURL what headers to send
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Content-Length: ' . strlen($payload),
|
||||
]);
|
||||
|
||||
// We want the response body back, not the HTTP headers
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
|
||||
// Optional: if you need to trust self‑signed certs (rare for production)
|
||||
// curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
|
||||
// curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
||||
|
||||
// Execute the request
|
||||
$response = curl_exec($ch);
|
||||
|
||||
// Basic error handling
|
||||
if ($response === false) {
|
||||
// Something went wrong with the cURL call
|
||||
error_log('cURL error: ' . curl_error($ch));
|
||||
curl_close($ch);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get HTTP status code to confirm the request succeeded
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
// Non‑200 responses are treated as errors
|
||||
error_log("Password API returned HTTP {$httpCode}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Decode the JSON response
|
||||
$data = json_decode($response, true);
|
||||
|
||||
if ($data === null || !isset($data['password'])) {
|
||||
error_log('Password API returned malformed JSON');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Return the password string
|
||||
return $data['password'];
|
||||
|
||||
}
|
||||
|
||||
// Password Count API Function
|
||||
function getPasswordCountFromAPI(){
|
||||
$apiUrl = "http://172.17.0.1:8189/get_count";
|
||||
// Build the query string and full URL
|
||||
$url = rtrim($apiUrl, '?') ;
|
||||
return curlHelper($url, "total_passwords");
|
||||
}
|
||||
|
||||
function passwordTest_strength($passwordTest) {
|
||||
$strength = 0;
|
||||
$possible_points = 12;
|
||||
$length = strlen($passwordTest);
|
||||
|
||||
if (detect_any_uppercase($passwordTest)) $strength += 1;
|
||||
if (detect_any_lowercase($passwordTest)) $strength += 1;
|
||||
$strength += min(count_numbers($passwordTest), 2);
|
||||
$strength += min(count_symbols($passwordTest), 2);
|
||||
|
||||
if ($length >= 8) {
|
||||
$strength += 2;
|
||||
$strength += min(($length - 8) * 0.5, 4);
|
||||
}
|
||||
|
||||
$strength_percent = $strength / (float)$possible_points;
|
||||
return floor($strength_percent * 10);
|
||||
}
|
||||
|
||||
|
||||
function detect_any_uppercase($string) {
|
||||
return strtolower($string) != $string;
|
||||
}
|
||||
|
||||
function detect_any_lowercase($string) {
|
||||
return strtoupper($string) != $string;
|
||||
}
|
||||
|
||||
function count_numbers($string) {
|
||||
return preg_match_all('/[0-9]/', $string);
|
||||
}
|
||||
|
||||
function count_symbols($string) {
|
||||
$regex = '/[' . preg_quote('!@£$%^&*-_+=?') . ']/';
|
||||
return preg_match_all($regex, $string);
|
||||
}
|
||||
|
||||
function returnActualPassword($passType){
|
||||
if ($passType == 3){
|
||||
|
||||
$payload = json_encode([
|
||||
'w_min' => getSetting('wMin', 'wMinIn', 6, 3, 10),
|
||||
'w_max' => getSetting('wMax', 'wMinIn', 6, 3, 10),
|
||||
'w_count' => getSetting('wCount', 'wCountIn', 2, 1, 5),
|
||||
's_char' => getSetting('sChar', 'sCharIn', 2, 0, 4),
|
||||
'num_len' => getSetting('numLen', 'numLenIn', 3, 0, 8),
|
||||
]);
|
||||
return getCustomPasswordFromAPI($passType, $payload);
|
||||
|
||||
}
|
||||
else{
|
||||
return getStandardPasswordFromAPI($passType);
|
||||
}
|
||||
}
|
||||
|
||||
$final = returnActualPassword($passType);
|
||||
#$final = getStandardPasswordFromAPI($passType);
|
||||
$total = getPasswordCountFromAPI();
|
||||
$rating = passwordTest_strength($final);
|
||||
|
||||
// ---- OUTPUT ----
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Matt-Cloud Password Generator</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="card">
|
||||
<h2>Matt-Cloud Password Generator</h2>
|
||||
<p>Hello folks.<br>
|
||||
This here is a nice little human-readable password generator. <br>
|
||||
You've got a few different modes and sometimes it is accidentally funny.<br>
|
||||
|
||||
<div class="help-link" id="helpToggle" >API</div>
|
||||
</div>
|
||||
|
||||
<div id="helpText" class="card">
|
||||
<strong>Matt-Cloud Password API</strong><p>
|
||||
To get passwords, you may:<p>
|
||||
<code>
|
||||
curl -s https://<?php echo $_SERVER['SERVER_NAME'] ?>/get_password?pwd_index=N<br>
|
||||
{<br>
|
||||
"password": "-`(UncoloredSwiftly2099"<br>
|
||||
}
|
||||
</code><p>
|
||||
Where N is an integer 0,1, or 2 for now.<p>
|
||||
To get verbose passwords, you may:<p>
|
||||
<code>
|
||||
curl -s https://<?php echo $_SERVER['SERVER_NAME'] ?>/verbose_password?pwd_index=N<br>
|
||||
{<br>
|
||||
"descriptor": {<br>
|
||||
"description": "This simple password is in the following format: !Password123 - this pulls from a list of 1291 simple words.",<br>
|
||||
"name": "Simple Password",<br>
|
||||
"type": "2"<br>
|
||||
},<br>
|
||||
"password": "&Keenness887",<br>
|
||||
"password count": 9<br>
|
||||
}
|
||||
</code><p>
|
||||
To get custom passwords, you may:<p>
|
||||
<code>
|
||||
curl -X POST https://<?php echo $_SERVER['SERVER_NAME'] ?>/custom_password \ <br>
|
||||
H "Content-Type: application/json" \ <br>
|
||||
d '{ <br>
|
||||
"w_min":5, <br>
|
||||
"w_max":8, <br>
|
||||
"w_count":3, <br>
|
||||
"s_char":2, <br>
|
||||
"num_len":3, <br>
|
||||
}' <br>
|
||||
{<br>
|
||||
"password": "Copier+ViolinBoned632*"<br>
|
||||
}
|
||||
</code><p>
|
||||
To get the API password count (but why tho?), you may:<p>
|
||||
<code>
|
||||
curl -s https://<?php echo $_SERVER['SERVER_NAME'] ?>/get_count<br>
|
||||
{<br>
|
||||
"total_passwords": 10<br>
|
||||
}
|
||||
</code><p>
|
||||
To view the password descriptor, you may <br>
|
||||
<code>
|
||||
curl -s https://<?php echo $_SERVER['SERVER_NAME'] ?>/get_info<br>
|
||||
</code>
|
||||
This will return the entire JSON descriptor variable <br>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<?php
|
||||
// only show the form if a password was generated
|
||||
if ($passType === 0 || $passType === 1 || $passType === 2 || $passType === 3) {
|
||||
echo '<form action="#" method="GET">';
|
||||
// Password output
|
||||
echo '<input type="text" onClick="this.select();" style="font-size:20pt;border:none;" value="' . htmlspecialchars($final) . '" id="myInput"><br><p>';
|
||||
echo 'Your password strength is: ' . $rating . '<br><p>';
|
||||
echo '<button onclick="myFunction()">Copy text</button>';
|
||||
echo '<input type="button" value="Generate Password" onClick="window.location.reload();">';
|
||||
|
||||
// Pass type selector
|
||||
echo '<select name="pt" onchange="this.form.submit()">';
|
||||
$types = ['Standard' => 0, 'Windows AD' => 1, 'Simple' => 2, 'Custom' => 3];
|
||||
foreach ($types as $label => $value) {
|
||||
echo '<option value="' . $value . '"' . ($passType == $value ? ' selected' : '') . '>' . $label . '</option>';
|
||||
}
|
||||
echo '</select>';
|
||||
|
||||
//////////////////////////////////////////
|
||||
// Meter Code
|
||||
echo '<div id=meter>';
|
||||
for($i=0; $i < 10; $i++) {
|
||||
echo "<div";
|
||||
if($rating > $i) {
|
||||
echo " class=\"rating-{$rating}\"";
|
||||
}
|
||||
echo "></div>";
|
||||
}
|
||||
echo '</div>';
|
||||
////////////////////////////////////////////
|
||||
|
||||
// Total count
|
||||
echo '<br><p>There have been ' . $total . ' total passwords generated thus far.</p>';
|
||||
if($passType === 0 || $passType === 1 || $passType === 2) {
|
||||
|
||||
echo '<button id="info_panel" class="collapsible" type="button">';
|
||||
echo ' Click here for Password Rules';
|
||||
echo '</button>';
|
||||
|
||||
}
|
||||
// Custom slider UI (only for type 3)
|
||||
if ($passType == 3) {
|
||||
echo '</div><div class="card">';
|
||||
echo 'Hey folks, this thing finally works.<br><p>';
|
||||
|
||||
echo '<table width="500"><tr><td width="200">Minimum Word Length:</td>';
|
||||
echo '<td width="100"><input type="range" name="wMinIn" id="wMinIn" min="3" max="10" value="' . $wMin . '" oninput="updateTextInput1(this.value);"></td>';
|
||||
echo '<td width="50"><input type="number" name="wMin" id="wMin" min="3" max="10" value="' . $wMin . '" readonly></td></tr>';
|
||||
|
||||
echo '<tr><td width="200">Maximum Word Length:</td>';
|
||||
echo '<td width="100"><input type="range" name="wMaxIn" id="wMaxIn" min="3" max="10" value="' . $wMax . '" oninput="updateTextInput2(this.value);"></td>';
|
||||
echo '<td width="50"><input type="number" name="wMax" id="wMax" min="3" max="10" value="' . $wMax . '" readonly></td></tr>';
|
||||
|
||||
echo '<tr><td width="200">Number of Words:</td>';
|
||||
echo '<td width="100"><input type="range" name="wCountIn" id="wCountIn" min="1" max="5" value="' . $wCount . '" oninput="updateTextInput3(this.value);"></td>';
|
||||
echo '<td width="50"><input type="number" name="wCount" id="wCount" min="1" max="5" value="' . $wCount . '" readonly></td></tr>';
|
||||
|
||||
echo '<tr><td width="200">Special Characters:</td>';
|
||||
echo '<td width="100"><input type="range" name="sCharIn" id="sCharIn" min="0" max="4" value="' . $sChar . '" oninput="updateTextInput4(this.value);"></td>';
|
||||
echo '<td width="50"><input type="number" name="sChar" id="sChar" min="0" max="4" value="' . $sChar . '" readonly></td></tr>';
|
||||
|
||||
echo '<tr><td width="200">Number Length:</td>';
|
||||
echo '<td width="100"><input type="range" name="numLenIn" id="numLenIn" min="0" max="8" value="' . $numLen . '" oninput="updateTextInput5(this.value);"></td>';
|
||||
echo '<td width="50"><input type="number" name="numLen" id="numLen" min="0" max="8" value="' . $numLen . '" readonly></td></tr>';
|
||||
|
||||
echo '</table><br>';
|
||||
echo '<button type="submit" class="btn-gen">Generate Password</button>';
|
||||
//echo '<input type="button" value="Generate Password" onClick="window.location.reload()">';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
if ($passType == 0) {
|
||||
|
||||
echo '</div><div id="hidden_info" class="content">';
|
||||
echo '<p>I have a list of about 20k english words. When you go to this page, I select 60 of these words that are less than 13 characters, ';
|
||||
echo 'and use the one whose index matches the current second. Then, depending on the length of this word, ';
|
||||
echo 'this may or may not grab a couple more words from the list, and then it will generate a random number.';
|
||||
echo 'Then, it shuffles a list of special characters and sprinkles a few of those in with the words and numbers. ';
|
||||
echo 'Then, once all that is generated, it will shuffle all these things it generated and spit them back. ';
|
||||
echo 'It takes a hash of the generated password and compares it to a list of all hashes generated in the past. ';
|
||||
echo 'If the newly generated password is unique, it will print it out under here, otherwise it will try again. ';
|
||||
echo 'If it generates a bad password, just refresh until you get one you like. This site can never generate the same password twice. ';
|
||||
echo 'Also, this hash is a one way encryption, so the passwords cannot be re-created from the hashes.</p>';
|
||||
echo '</div>';
|
||||
}
|
||||
|
||||
if ($passType == 1) {
|
||||
echo '</div><div id="hidden_info" class="content"><p>';
|
||||
echo 'This password is always in the following format: <br>';
|
||||
echo '$Word1Word2Number$<br>';
|
||||
echo 'Where each word is less than 7 characters, the number is <br>';
|
||||
echo '3 digits, and the $ represents a Special Charater.</p></div><p>';
|
||||
}
|
||||
|
||||
if ($passType == 2) {
|
||||
echo '</div><div id="hidden_info" class="content"><br>';
|
||||
echo 'This simple password is in the following format:<br>';
|
||||
echo '!Password123<p>';
|
||||
echo 'Also, this draws from a simpler list of about 3k words.<p></div>';
|
||||
}
|
||||
|
||||
echo '</form>';
|
||||
} else {
|
||||
// duplicate hash case
|
||||
echo '<p>OH FUCK DUPLICATE PASSWORD!!!!1!!<br>';
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const panel = document.getElementById('info_panel');
|
||||
if (panel) {
|
||||
panel.addEventListener('click', function () {
|
||||
const help = document.getElementById('hidden_info');
|
||||
if (help.style.display === 'none' || help.style.display === '') {
|
||||
help.style.display = 'block';
|
||||
} else {
|
||||
help.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function myFunction() {
|
||||
const copyText = document.getElementById("myInput");
|
||||
copyText.select();
|
||||
document.execCommand("copy");
|
||||
alert("Copied the text: " + copyText.value);
|
||||
}
|
||||
|
||||
// limit min/max slider relationship
|
||||
const elX = document.getElementById("wMaxIn");
|
||||
const elY = document.getElementById("wMinIn");
|
||||
function limit() {
|
||||
if (elX && elY) {
|
||||
if (elY.value > elX.value) {
|
||||
elY.value = elX.value;
|
||||
}
|
||||
document.getElementById("wMin").value = elY.value;
|
||||
}
|
||||
}
|
||||
if (elX && elY) {
|
||||
elX.onchange = limit;
|
||||
elY.onchange = limit;
|
||||
}
|
||||
|
||||
// helper to sync range to number input (already done in form via oninput)
|
||||
// but keep for safety
|
||||
function updateTextInput1(val) { document.getElementById('wMin').value = val; }
|
||||
function updateTextInput2(val) { document.getElementById('wMax').value = val; }
|
||||
function updateTextInput3(val) { document.getElementById('wCount').value = val; }
|
||||
function updateTextInput4(val) { document.getElementById('sChar').value = val; }
|
||||
function updateTextInput5(val) { document.getElementById('numLen').value = val; }
|
||||
|
||||
//Toggle the help text when the link is clicked
|
||||
document.getElementById('helpToggle').addEventListener('click', function () {
|
||||
const help = document.getElementById('helpText');
|
||||
if (help.style.display === 'none' || help.style.display === '') {
|
||||
help.style.display = 'block';
|
||||
} else {
|
||||
help.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
ob_end_flush();
|
||||
?>
|
||||
217
www/style.css
Normal file
217
www/style.css
Normal file
@ -0,0 +1,217 @@
|
||||
|
||||
|
||||
.active, .collapsible:hover {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
width: 550px;
|
||||
padding: 0 px;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
.password_field {
|
||||
font-size:20pt;
|
||||
border:none;
|
||||
background-color: #ecf0f1;
|
||||
|
||||
}
|
||||
.button {
|
||||
|
||||
color: var(--clr-text);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Meter - dark‑mode look
|
||||
------------------------------------------------------------------ */
|
||||
#meter {
|
||||
display: flex;
|
||||
gap: 4px; /* doubled spacing between bars */
|
||||
padding: 8px; /* doubled padding inside container */
|
||||
align-items: center;
|
||||
|
||||
/* Card‑style background + border - same as before */
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--clr-border);
|
||||
border-radius: 4px;
|
||||
max-width: 420px; /* doubled max‑width */
|
||||
box-shadow: 0 1px 3px rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Bar - light‑dark contrast
|
||||
------------------------------------------------------------------ */
|
||||
#meter div {
|
||||
width: 36px; /* doubled width */
|
||||
height: 36px; /* doubled height */
|
||||
flex-shrink: 0;
|
||||
|
||||
/* Base colour (dark grey) - can be overridden via --bg */
|
||||
background: var(--bg, #4a5b6c);
|
||||
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 1px 2px rgba(0,0,0,.15);
|
||||
transition: background .25s ease, transform .2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Hover lift (still subtle on dark background) */
|
||||
#meter div:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Rating colour tiers - same vibrant colours
|
||||
------------------------------------------------------------------ */
|
||||
#meter div.rating-1,
|
||||
#meter div.rating-2 { --bg: #e74c3c; } /* red - poor */
|
||||
#meter div.rating-3,
|
||||
#meter div.rating-4 { --bg: #e67e22; } /* orange - fair */
|
||||
#meter div.rating-5,
|
||||
#meter div.rating-6 { --bg: #f1c40f; } /* yellow - average */
|
||||
#meter div.rating-7,
|
||||
#meter div.rating-8 { --bg: #2ecc71; } /* greenyellow - good */
|
||||
#meter div.rating-9,
|
||||
#meter div.rating-10{ --bg: #27ae60; } /* green - excellent */
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
Optional - if you want to add a “selected” state
|
||||
------------------------------------------------------------------ */
|
||||
#meter div.selected {
|
||||
box-shadow: 0 0 0 2px var(--clr-accent);
|
||||
}
|
||||
|
||||
/* Standard Matt-Cloud CSS */
|
||||
|
||||
/* -------------------------------------------------
|
||||
1. Global settings & color palette
|
||||
------------------------------------------------- */
|
||||
:root {
|
||||
/* Dark theme - body & card backgrounds */
|
||||
--bg-body: #2c3e50; /* main page background */
|
||||
--bg-card: #34495e; /* card / panel background */
|
||||
--bg-sidebar: #3d566e; /* sidebar background (slightly lighter) */
|
||||
/* Accent / link colour */
|
||||
--clr-accent: #3498db; /* blue accent for links */
|
||||
/* Text colour */
|
||||
--clr-text: #ecf0f1; /* light whiteish text */
|
||||
/* Borders / accents */
|
||||
--clr-border: #1f2b38; /* dark border colour */
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
/* Body */
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg-body);
|
||||
color: var(--clr-text);
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
|
||||
/* Links */
|
||||
a { color: var(--clr-accent); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* -------------------------------------------------
|
||||
2. Layout - wrapper, sidebar, main
|
||||
------------------------------------------------- */
|
||||
.wrapper { display: flex; min-height: 100vh; }
|
||||
.main { flex: 1; padding: 1rem; }
|
||||
|
||||
/* -------------------------------------------------
|
||||
3. Card components
|
||||
------------------------------------------------- */
|
||||
.card {
|
||||
max-width: 550px;
|
||||
margin: 20px auto 1rem auto;
|
||||
padding: 20px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--clr-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,.3);
|
||||
}
|
||||
|
||||
.collapsible {
|
||||
max-width: 550px;
|
||||
margin: 20px auto 1rem auto;
|
||||
padding: 20px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--clr-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,.3);
|
||||
text-align: left;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.active, .collapsible:hover {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.content {
|
||||
max-width: 550px;
|
||||
margin: 20px auto 1rem auto;
|
||||
padding: 20px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--clr-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,.3);
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* -------------------------------------------------
|
||||
4. Tables
|
||||
------------------------------------------------- */
|
||||
table, th, td {
|
||||
border: 2px solid var(--clr-border);
|
||||
border-collapse: collapse;
|
||||
}
|
||||
th, td { padding: 10px; }
|
||||
|
||||
/* Alternate row colour for metrics table */
|
||||
#host_metrics_table tbody tr td:nth-of-type(even) {
|
||||
background: #3e5c78; /* slight contrast */
|
||||
}
|
||||
|
||||
/* -------------------------------------------------
|
||||
5. Lists & headings
|
||||
------------------------------------------------- */
|
||||
h1, h2, h3, h4 { color: var(--clr-text); margin: 0 0 .4rem 0; }
|
||||
ul { list-style: none; padding: 0; }
|
||||
ol { list-style: none; padding: 0; }
|
||||
li { margin-bottom: 10px; color: var(--clr-text); }
|
||||
|
||||
/* -------------------------------------------------
|
||||
6. Components grid
|
||||
------------------------------------------------- */
|
||||
.components {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
}
|
||||
.component {
|
||||
padding: 10px;
|
||||
border: 1px solid var(--clr-border);
|
||||
border-radius: 4px;
|
||||
}
|
||||
.component h3 { margin: 0 0 5px; }
|
||||
|
||||
/* -------------------------------------------------
|
||||
7. Help toggle / modal
|
||||
------------------------------------------------- */
|
||||
.help-link {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
color: var(--clr-accent);
|
||||
text-align: right;
|
||||
}
|
||||
.help-link:hover { text-decoration: underline; }
|
||||
#helpText { display: none; }
|
||||
|
||||
/* -------------------------------------------------
|
||||
8. Misc helpers
|
||||
------------------------------------------------- */
|
||||
/* Hide numeric markers in metric columns (if any) */
|
||||
#host_metrics_column td { list-style: none; padding-left: 0; margin-left: 0; }
|
||||
Reference in New Issue
Block a user