initial commit

This commit is contained in:
2026-03-24 14:44:15 -07:00
commit dac995a33e
11 changed files with 10359 additions and 0 deletions

33
Dockerfile Normal file
View 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
View 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
View 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
View 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

File diff suppressed because it is too large Load Diff

1
build.sh Executable file
View File

@ -0,0 +1 @@
docker build -t pwdgen-v2 /opt/containers/pwdgen-v2

12
docker-compose.yaml Normal file
View 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
View 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
View 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
View 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 selfsigned 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) {
// Non200 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
View 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 - darkmode look
------------------------------------------------------------------ */
#meter {
display: flex;
gap: 4px; /* doubled spacing between bars */
padding: 8px; /* doubled padding inside container */
align-items: center;
/* Cardstyle background + border - same as before */
background: var(--bg-card);
border: 1px solid var(--clr-border);
border-radius: 4px;
max-width: 420px; /* doubled maxwidth */
box-shadow: 0 1px 3px rgba(0,0,0,.2);
}
/* ------------------------------------------------------------------
Bar - lightdark 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; }