Compare commits

...

71 Commits
v0.0.1 ... main

Author SHA1 Message Date
Florian Walther
e8ba7f7390 mein bereinigt 2026-02-11 20:12:09 +01:00
Florian Walther
5d05592a90 updated screenshot 2026-02-09 18:54:12 +01:00
Florian Walther
ae081b12df added load time to HTML template
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 55s
2026-02-09 18:51:06 +01:00
Florian Walther
c69acecfd7 environment added to docker-compose.yml 2026-02-09 18:16:09 +01:00
Florian Walther
3101529fa7 debug modus im HTML eingebaut
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 56s
2026-02-09 18:13:13 +01:00
Florian Walther
25f5fae505 added env config COUNTER_FILE, DEBUG
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 59s
2026-02-09 17:16:06 +01:00
Florian Walther
1d4849acff adding new features to readme 2026-02-08 23:24:36 +01:00
Florian Walther
58205c20a0 deleted empty line in listing 2026-02-08 23:19:35 +01:00
Florian Walther
2a2f99dd76 updated build instructions 2026-02-08 23:18:42 +01:00
Florian Walther
6b93307f9e fixed log printf
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 53s
2026-02-08 22:56:50 +01:00
Florian Walther
a029a38787 changed log to combined log format
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 54s
2026-02-08 22:52:51 +01:00
Florian Walther
4a83ab6bd6 logging middleware added
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 56s
2026-02-08 22:42:31 +01:00
Florian Walther
2c09ab8f87 added getRealIP
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 1m1s
2026-02-08 22:26:02 +01:00
Florian Walther
75be4d3015 added getClientIP
Some checks failed
Docker Release Build / push_to_registry (push) Failing after 49s
2026-02-08 21:17:08 +01:00
Florian Walther
c208afabf1 updated screenshot to v0.7.2 2026-02-08 11:20:27 +01:00
Florian Walther
7c7a0dcf15 added debug var, changed title
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 1m0s
2026-02-08 10:59:18 +01:00
Florian Walther
90f4b9a0e3 screenshot updated 2026-02-08 00:42:30 +01:00
Florian Walther
0e7f3be529 design updates
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 51s
2026-02-08 00:24:00 +01:00
Florian Walther
13c791b1ae added password counter
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 58s
2026-02-07 23:45:57 +01:00
Florian Walther
8869db0f6b screenshot auf v0.6.0 altualisiert 2026-02-07 16:49:46 +01:00
Florian Walther
c9296beb5f footer und AppVersion eingebaut
All checks were successful
Docker Release Build / push_to_registry (push) Successful in 1m2s
2026-02-07 16:45:02 +01:00
Florian Walther
dd8eab7975 fixed URL 2026-02-07 00:34:05 +01:00
Florian Walther
3e2f4cf104 fixed spelling error 2026-02-07 00:32:20 +01:00
Florian Walther
9d4c816642 fixed spelling error 2026-02-07 00:31:40 +01:00
Florian Walther
2a8d343ff4 fixed spelling error 2026-02-07 00:29:41 +01:00
Florian Walther
c990d8a335 mention Dark- and Light-Mode in README 2026-02-06 13:08:16 +01:00
Florian Walther
e99bf45be9 favicon geändert
All checks were successful
/ push_to_registry (push) Successful in 53s
2026-02-06 13:04:52 +01:00
Florian Walther
3c324d355d favicon added 2026-02-06 12:43:48 +01:00
Florian Walther
149cdf2e63 new screenshot added 2026-02-06 12:40:50 +01:00
Florian Walther
3ab261d777 added templates, more solid DarkMode
All checks were successful
/ push_to_registry (push) Successful in 1m13s
2026-02-06 12:34:10 +01:00
Florian Walther
623cfd3a50 Merge branch 'main' of gitea.scu.si:Florian.Walther/Web-Password
All checks were successful
/ push_to_registry (push) Successful in 58s
2026-02-04 00:00:04 +01:00
Florian Walther
2f9ee42071 added darkmode 2026-02-03 23:55:35 +01:00
33fe4b2b80 added log statement to APIHandler
All checks were successful
/ push_to_registry (push) Successful in 51s
2026-02-02 22:06:42 +01:00
21f512c2d7 fixed log statement
All checks were successful
/ push_to_registry (push) Successful in 51s
2026-02-02 22:03:08 +01:00
dfda16f8e1 fixed update howto 2026-02-02 22:01:54 +01:00
44bb35abac added basic logging
Some checks failed
/ push_to_registry (push) Failing after 46s
2026-02-02 22:00:01 +01:00
1ae5f9c679 corrected pull url 2026-02-02 21:59:43 +01:00
9a052b3ef7 added more usage tips 2026-02-02 21:53:40 +01:00
9a906ec55c http to https changed in API page
All checks were successful
/ push_to_registry (push) Successful in 51s
2026-02-02 21:32:30 +01:00
8b93585422 workflow angepasst
All checks were successful
/ push_to_registry (push) Successful in 1m14s
2026-01-24 17:52:15 +01:00
59dd16d4ac fixed code URL
Some checks failed
/ push_to_registry (push) Failing after 1m38s
2026-01-24 17:42:32 +01:00
d7678274be added pull info
Some checks failed
/ push_to_registry (push) Failing after 1m38s
2026-01-17 22:57:04 +01:00
4a8f7525e7 updated README 2026-01-17 22:54:57 +01:00
fb55f47e5c fixed spelling 2026-01-17 22:32:35 +01:00
0251b9dc69 undo centered image, did not work 2026-01-17 22:30:58 +01:00
2b505b0d9d centered image 2026-01-17 22:30:00 +01:00
cb3545f261 updated claim in README.md 2026-01-17 22:29:08 +01:00
d673b97b4b deleted obsolete lines in misc/docker-compose.traefik.yml 2026-01-17 22:26:58 +01:00
1be4aeb6b8 updated MoreUsage.md 2026-01-17 22:24:16 +01:00
0a3f4ab5ef added dynamic hostname to helpHandler
Some checks failed
/ push_to_registry (push) Failing after 1m34s
2026-01-17 21:59:54 +01:00
36ce60aa28 container sized down to 500px width
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-17 21:44:01 +01:00
Florian Walther
d21b959104 58er Zeichensatz hinzugefügt 2026-01-17 13:13:09 +01:00
Florian Walther
2b822af907 deleted deprecated workflow 2026-01-17 12:57:04 +01:00
Florian Walther
2aa636409a Screenshot aktualisiert 2026-01-17 12:47:28 +01:00
Florian Walther
819af38886 added link to code repository
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-17 12:09:26 +01:00
Florian Walther
65fe245e9f updated README 2026-01-17 11:58:02 +01:00
Florian Walther
8ebde5ffbc reorganising, added docker-compose.traefik.yml, split README 2026-01-17 11:51:02 +01:00
Florian Walther
eef425d7b7 3rd try with registry_url non hard coded
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-17 10:23:37 +01:00
Florian Walther
490ec5b151 2nd try with registry_url non hard coded
Some checks failed
/ push_to_registry (push) Failing after 12s
2026-01-17 10:22:09 +01:00
Florian Walther
1e38f4b842 add title to README 2026-01-17 10:20:30 +01:00
Florian Walther
827d6fecec add pic 2026-01-16 23:01:34 +01:00
Florian Walther
e3a76baec7 README erweitert 2026-01-16 22:54:38 +01:00
Florian Walther
f41eb6de51 removed latex code for better readability 2026-01-16 22:51:10 +01:00
Florian Walther
90829b054c added SECURITY.md 2026-01-16 22:46:46 +01:00
Florian Walther
8db6c5af9f hard coded registry url
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-16 22:35:14 +01:00
Florian Walther
e8d34d1bc2 fixed typo 2
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-16 22:33:23 +01:00
Florian Walther
520420f863 fixed typo
Some checks failed
/ push_to_registry (push) Failing after 13s
2026-01-16 22:31:04 +01:00
Florian Walther
656c3ab073 workflow docker-build-push set to workflow_dispatch 2026-01-16 22:29:41 +01:00
Florian Walther
162d69cb93 Variablen angepasst
Some checks failed
/ push_to_registry (push) Failing after 12s
2026-01-16 22:26:22 +01:00
Florian Walther
c6506dde33 docker-compose.yml added 2026-01-16 22:23:59 +01:00
Florian Walther
11dc42574b workflow docker-build-push deaktiviert
workflow docker-release aktiviert
2026-01-16 22:23:37 +01:00
16 changed files with 971 additions and 279 deletions

View File

@@ -1,27 +0,0 @@
name: Docker Build and Push
on: [push]
jobs:
build-and-push:
runs-on: docker
if: branch == 'main'
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Login to Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login ${{ secrets.REGISTRY_URL }} -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin
- name: Build Docker Image
run: |
docker build -t ${{ secrets.REGISTRY_URL }}/FlorianWalther/password-generator:latest .
- name: Push Docker Image
run: |
docker push ${{ secrets.REGISTRY_URL }}/FlorianWalther/password-generator:latest
- name: Cleanup
run: |
docker system prune -f

View File

@@ -0,0 +1,32 @@
name: Docker Release Build
on:
push:
tags:
- 'v*' # Reagiert auf Tags die mit 'v' beginnen, z.B. v1.0.2
jobs:
push_to_registry:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Login to Gitea
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_URL }} # gitea.scu.si
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
# Hier wird die Git-Referenz automatisch als Docker-Tag genutzt
#tags: gitea.scu.si/florianwalther/password-generator:${{ gitea.ref_name }}
build-args: |
APP_VERSION=${{ gitea.ref_name }}
tags: |
gitea.scu.si/florian.walther/password-generator:${{ gitea.ref_name }}
gitea.scu.si/florian.walther/password-generator:latest

View File

@@ -8,7 +8,8 @@ WORKDIR /app
COPY . .
# Baue die Anwendung
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /app/password-generator
ARG APP_VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w -X 'main.AppVersion=${APP_VERSION}'" -o /app/password-generator
# Verwende ein minimales Image für die finale Stage
FROM scratch
@@ -16,9 +17,14 @@ FROM scratch
# Kopiere die gebaute Binärdatei
COPY --from=builder /app/password-generator /password-generator
# Kopiere die templates
COPY templates/ /templates/
# Kopiere die static files
COPY static/ /static/
# Setze die Umgebungsvariable für die Ports
ENV PORT=8080
# Starte die Anwendung
CMD ["/password-generator"]

View File

@@ -1,26 +1,43 @@
# Web-Password
# Funktionsweise
_a web based password generator, with an API endpoint_
* Passwortgenerierung: Die Anwendung generiert ein 32-stelliges Passwort mit Großbuchstaben, Kleinbuchstaben und Ziffern (entspricht dem Befehl `apg -a 1 -m 32 -n 1 -M NCL`).
* Zwischenablage: Mit dem Button "In Zwischenablage kopieren" wird das Passwort in die Zwischenablage kopiert.
* Docker: Der Container enthält nur die Go-Anwendung und keine zusätzliche Linux-Distribution.
![App Screenshot](img/screenshot.png)
## Features
# Baue die Go-Anwendung
* generates long and random, secure passwords (read about the [security considerations](SECURITY.md))
* copy to clipboard
* very small docker container, that only contains the application and has minimum attack surface
* supports DarkMode and LightMode, you can toggle
* prepared to run behind a reverse-proxy, like traefik.
* logs in combined log format
## Demo
There is a demo at [https://passwd.scu.si](https://passwd.scu.si)
## Usage
The following example shows how to get up your own instance with `docker compose`.
```
go build -o password-generator ./
git clone https://gitea.scu.si/FlorianWalther/Web-Password.git
cd Web-Password
cp misc/docker-compose.yml ./
docker compose pull
docker compose up -d
```
# Baue das Docker-Image
## Docker image
The latest official docker image is at [https://gitea.scu.si/Florian.Walther/-/packages/container/password-generator/latest](https://gitea.scu.si/Florian.Walther/-/packages/container/password-generator/latest)
You can pull it like this:
```
docker build -t password-generator .
docker pull gitea.scu.si/florian.walther/password-generator:latest
```
# Starte den Docker Container
```
docker run -p 8080:8080 password-generator
```
## more usage examples
There are some more usage example in [misc/MoreUsage.md](misc/MoreUsage.md)

110
SECURITY.md Normal file
View File

@@ -0,0 +1,110 @@
# Security Considerations
---
## 1. Overview
This document analyzes the security of passwords generated by the application, which uses the following parameters:
- Length: 32 characters
- Character set: Uppercase letters (A-Z), lowercase letters (a-z), digits (0-9)
- No special characters (equivalent to `apg -a 1 -m 32 -n 1 -M NCL`)
---
## 2. Keyspace Analysis
### 2.1. Character Set and Length
- Character set size: 26 (uppercase) + 26 (lowercase) + 10 (digits) = **62 possible characters per position**.
- Password length: 32 characters.
### 2.2. Total Keyspace
The total number of possible passwords is calculated as:
62^32 ≈ 1.46 × 10^57
This means there are **1.46 decillion** possible combinations.
---
## 3. Brute-Force Resistance
### 3.1. Average Number of Guesses
On average, an attacker would need to try half of the keyspace to guess the correct password:
(62^32) / 2 ≈ 7.3 × 10^56 attempts
### 3.2. Time to Crack on Modern Hardware
| Hardware | Hashes per Second | Time to Exhaust Keyspace |
|-------------------|-------------------|--------------------------------|
| Modern CPU | 10 billion | 7.3 × 10^46 seconds | ≈ 2.3 × 10^39 years |
| Modern GPU | 100 billion | 7.3 × 10^45 seconds | ≈ 2.3 × 10^38 years |
**Note**: Even with massive parallelization (e.g., botnets or supercomputers), brute-forcing a 32-character password from this keyspace is practically infeasible.
---
## 4. Comparison with Shorter Passwords
| Length | Keyspace (62 Characters) | Average Guesses | Time on GPU (100 GigaHashes/s) |
|--------|--------------------------|-----------------------|-------------------------------|
| 16 | 4.7 × 10^28 | 2.35 × 10^28 | ~74 years |
| 24 | 1.3 × 10^43 | 6.5 × 10^42 | ~2.1 million years |
| 32 | 1.46 × 10^57 | 7.3 × 10^56 | ~2.3 trillion years |
---
## 5. Threat Model
### 5.1. Brute-Force Attacks
- **Conclusion**: Brute-force attacks are not a viable threat for 32-character passwords.
- **Mitigation**: Ensure the system enforces rate-limiting to prevent automated guessing.
### 5.2. Social Engineering and Side-Channel Attacks
- **Social Engineering**: Phishing, keyloggers, or shoulder surfing are more realistic threats than brute-force attacks.
- **Side-Channel Attacks**: Timing attacks or power analysis could theoretically reduce security if the password verification is poorly implemented.
- **Mitigation**: Use constant-time comparison functions for password verification.
### 5.3. Password Storage
- **Hashing**: Always store passwords using strong, adaptive hashing algorithms like:
- Argon2 (recommended for new systems)
- bcrypt or PBKDF2 (with high work factors)
- **Salting**: Use a unique salt per password to prevent rainbow table attacks.
---
## 6. Practical Recommendations
### 6.1. For Users
- **Password Managers**: Encourage the use of password managers to store and manage generated passwords.
- **Multi-Factor Authentication (MFA)**: Always enable MFA where possible to add an extra layer of security.
### 6.2. For Developers
- **Rate Limiting**: Implement rate limiting on authentication endpoints to slow down brute-force attempts.
- **Secure Transmission**: Ensure passwords are transmitted over TLS/SSL to prevent interception.
- **Password Policies**: Enforce policies that discourage password reuse and encourage regular updates.
### 6.3. For DFIR and Incident Response
- **Logging and Monitoring**: Log failed login attempts and monitor for unusual activity.
- **Incident Response Plan**: Have a plan in place for compromised accounts, including forced password resets and user notification.
---
## 7. Additional Considerations
### 7.1. Extended Character Set
If special characters are included (e.g., !@#$%^&*), the keyspace increases to:
72^32 ≈ 1.9 × 10^60
This further improves security but is not necessary for most use cases given the already massive keyspace.
### 7.2. Entropy Calculation
The entropy of a 32-character password from a 62-character set is:
log2(62^32) ≈ 192.6 bits
This exceeds the 128-bit security level recommended by NIST for cryptographic applications.
---
## 8. Conclusion
The passwords generated by this application are extremely secure against brute-force attacks due to their length and character diversity. The primary risks lie in human factors (e.g., phishing, reuse) and implementation flaws (e.g., weak hashing, lack of rate limiting).
For DFIR and high-security environments, combine these passwords with:
- Multi-Factor Authentication (MFA)
- Regular audits of authentication logs
- User education on social engineering risks
---
## 9. References
- [NIST Special Publication 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html) (Digital Identity Guidelines)
- [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html)
- [Argon2: The Memory-Hard Function for Password Hashing](https://github.com/P-H-C/phc-winner-argon2)

2
go.mod
View File

@@ -1,3 +1,3 @@
module gitea.scu.si/FlorianWalther/Web-Password
module gitea.scu.si/Florian.Walther/Web-Password
go 1.24.1

BIN
img/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

475
main.go
View File

@@ -2,268 +2,271 @@ package main
import (
"crypto/rand"
"encoding/json"
"fmt"
"html/template"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
)
const (
passwordLength = 32
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
//chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!?$%&=#+<>-:,.;_*@"
passwordLength = 32
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
startTimeKey contextKey = "startTime"
)
var (
debug = false
templates = make(map[string]*template.Template)
AppVersion = "development"
counterFile = "/data/counter.txt"
mu sync.Mutex
)
type contextKey string
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
func newResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{w, http.StatusOK} // Default 200 OK
}
func initConfig() {
if envFile := os.Getenv("COUNTER_FILE"); envFile != "" {
counterFile = envFile
log.Printf("counterFile st to %s, by ENV\n", envFile)
// Prüfen, ob das Verzeichnis für die Datei existiert
dir := filepath.Dir(counterFile)
if _, err := os.Stat(dir); os.IsNotExist(err) {
log.Printf("WARNUNG: Verzeichnis %s existiert nicht. Counter wird evtl. fehlschlagen.", dir)
}
}
envDebug := strings.ToLower(os.Getenv("DEBUG"))
if envDebug == "true" || envDebug == "1" {
debug = true
log.Println("DEBUG-Modus ist aktiviert")
}
}
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := newResponseWriter(w)
next.ServeHTTP(rw, r)
duration := time.Since(start)
clientIP := getClientIP(r)
userAgent := r.UserAgent()
// Format: IP - - [Datum] "Method Path Proto" Status Duration User-Agent
// "Combined Log Format"
log.Printf("%s - - [%s] \"%s %s %s\" %d %v \"%s\"\n",
clientIP,
time.Now().Format("02/Jan/2006:15:04:05 -0700"),
r.Method,
r.URL.Path,
r.Proto,
rw.statusCode,
duration,
userAgent,
)
})
}
func getClientIP(r *http.Request) string {
// 1. Prüfe den X-Forwarded-For Header (Standard für Proxies)
xForwardedFor := r.Header.Get("X-Forwarded-For")
if xForwardedFor != "" {
// Der Header kann eine Liste von IPs sein (Client, Proxy1, Proxy2)
// Die erste IP in der Liste ist die echte Client-IP
ips := strings.Split(xForwardedFor, ",")
return strings.TrimSpace(ips[0])
}
// 2. Fallback auf X-Real-IP (oft von Traefik/Nginx gesetzt)
xRealIP := r.Header.Get("X-Real-IP")
if xRealIP != "" {
return xRealIP
}
// 3. Letzter Ausweg: Die direkte IP (wird in deinem Fall die Traefik-IP sein)
// RemoteAddr enthält oft auch den Port (z.B. "127.0.0.1:1234")
ip, _, _ := net.SplitHostPort(r.RemoteAddr)
return ip
}
// Diese Funktion wird nur intern aufgerufen, wenn der Mutex bereits gesperrt ist
func getCount() int {
data, err := os.ReadFile(counterFile)
if err != nil {
return 0
}
count, _ := strconv.Atoi(strings.TrimSpace(string(data)))
return count
}
// Öffentliche Funktion für das Template (mit Lock)
func GetPasswordCount() int {
mu.Lock()
defer mu.Unlock()
return getCount()
}
// Öffentliche Funktion zum Erhöhen (mit Lock)
func IncrementPasswordCount() {
mu.Lock()
defer mu.Unlock()
// Wir rufen jetzt die interne Funktion auf, die NICHT versucht,
// den Mutex erneut zu sperren
count := getCount()
count++
os.WriteFile(counterFile, []byte(strconv.Itoa(count)), 0644)
}
func loadTemplates() {
funcMap := template.FuncMap{
"getAppVersion": func() string { return AppVersion },
"getPassCount": func() int { return GetPasswordCount() },
"isDebug": func() bool { return debug },
"dt": func(startTime time.Time) string {
duration := time.Since(startTime)
// Gibt die Zeit in Millisekunden mit 2 Nachkommastellen aus, z.B. "1.45ms"
return fmt.Sprintf("%.2fms", float64(duration.Nanoseconds())/1e6)
},
}
templates["index.html"] = template.Must(template.New("base.html").Funcs(funcMap).ParseFiles(
"templates/base.html",
"templates/index.html",
))
templates["help.html"] = template.Must(template.New("base.html").Funcs(funcMap).ParseFiles(
"templates/base.html",
"templates/help.html",
))
log.Printf("Alle Templates erfolgreich geladen")
}
func generatePassword() string {
if debug {
log.Printf("called generatePassword\n")
}
password := make([]byte, passwordLength)
_, err := rand.Read(password)
if err != nil {
log.Fatal(err)
}
for i := 0; i < passwordLength; i++ {
password[i] = chars[int(password[i])%len(chars)]
}
IncrementPasswordCount()
return string(password)
}
func passwordHandler(w http.ResponseWriter, r *http.Request) {
if debug {
log.Printf("called passwordHandler\n")
}
password := generatePassword()
fmt.Fprint(w, password)
currentCount := GetPasswordCount()
response := map[string]interface{}{
"password": password,
"count": currentCount,
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(response)
if err != nil {
log.Printf("Fehler beim Senden des JSON: %v", err)
http.Error(w, "Interner Fehler", http.StatusInternalServerError)
return
}
}
func passwordAPIHandler(w http.ResponseWriter, r *http.Request) {
if debug {
log.Printf("called passwordHandler\n")
}
password := generatePassword()
w.Header().Set("Content-Type", "text/plain")
w.Write([]byte(password))
}
func indexHandler(w http.ResponseWriter, r *http.Request) {
if debug {
log.Printf("call indexHandler: Request %s %s\n", r.Method, r.URL)
}
startTime, ok := r.Context().Value(startTimeKey).(time.Time)
if !ok {
startTime = time.Now() // Fallback, falls die Middleware mal fehlt
}
password := generatePassword()
data := struct {
Password string
StartTime time.Time
Request *http.Request
RealIP string
}{
Password: password,
StartTime: startTime,
Request: r,
RealIP: getClientIP(r),
}
if debug {
log.Printf("prepare template for index\n")
}
err := templates["index.html"].ExecuteTemplate(w, "base.html", data)
if err != nil {
log.Printf("Fehler beim Rendern des Templates: %v", err)
http.Error(w, "Interner Serverfehler", http.StatusInternalServerError)
}
}
func helpHandler(w http.ResponseWriter, r *http.Request) {
helpHTML := `
<!DOCTYPE html>
<html>
<head>
<title>Hilfe</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
color: #333;
}
.help-container {
text-align: left;
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
max-width: 800px;
width: 90%;
min-width: 600px;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: #444;
}
pre {
font-family: 'Courier New', Courier, monospace;
background: #f0f0f0;
padding: 0.8rem;
border-radius: 4px;
overflow-x: auto;
}
a {
color: #007BFF;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
</style>
</head>
<body>
<div class="help-container">
<h1>Hilfe: API-Endpunkt</h1>
<p>
Diese Anwendung bietet einen API-Endpunkt, um Passwörter direkt über die Kommandozeile abzurufen.
Der Endpunkt gibt das Passwort im Plain-Text-Format zurück.
</p>
<h2>Endpunkt:</h2>
<p><code>http://localhost:8080/api/password</code></p>
<h2>Beispiele:</h2>
<h3>Mac/Linux (Terminal):</h3>
<pre>echo $(curl -s http://localhost:8080/api/password)</pre>
<h3>Windows (PowerShell):</h3>
<pre>Invoke-RestMethod -Uri http://localhost:8080/api/password</pre>
<h3>Windows (cmd):</h3>
<pre>curl http://localhost:8080/api/password</pre>
<p>
<a href="/">Zurück zur Passwort-Generierung</a>
</p>
</div>
</body>
</html>
`
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, helpHTML)
}
func webHandler(w http.ResponseWriter, r *http.Request) {
password := generatePassword()
html := fmt.Sprintf(
`<DOCTYPE html>
<html>
<head>
<title>Passwort-Generator</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f5f5f5;
color: #333;
}
.container {
text-align: center;
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 90%%;
min-width: 600px;
position: relative;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1.5rem;
color: #444;
}
#password {
font-family: 'Courier New', Courier, monospace;
font-size: 1.2rem;
letter-spacing: 1px;
margin: 1rem auto;
padding: 0.8rem;
background: #f0f0f0;
border-radius: 4px;
border: 1px solid #ddd;
width: 90%%;
word-break: break-all;
}
.copy-button {
background: #4CAF50;
color: white;
border: none;
padding: 0.6rem 1.2rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
margin: 0.3rem;
}
.copy-button:hover {
background: #45a049;
}
.renew-button {
background: #007BFF;
color: white;
border: none;
padding: 0.6rem 1.2rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
margin: 0.3rem;
}
.renew-button:hover {
background: #0056b3;
}
.help-link {
position: absolute;
top: 1rem;
right: 1rem;
font-size: 1.2rem;
color: #999;
text-decoration: none;
}
.help-link:hover {
color: #444;
}
.buttons {
display: flex;
justify-content: center;
gap: 0.5rem;
}
#toast {
visibility: hidden;
min-width: 150px;
background-color: #4CAF50;
color: white;
text-align: center;
border-radius: 4px;
padding: 0.5rem;
position: fixed;
top: 20px;
right: 20px;
z-index: 1;
font-size: 0.9rem;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
}
</style>
</head>
<body>
<div class="container">
<a href="/help" class="help-link">API</a>
<h1>Generiertes Passwort</h1>
<div id="password">%s</div>
<div class="buttons">
<button class="copy-button" onclick="copyToClipboard()">In Zwischenablage kopieren</button>
<button class="renew-button" onclick="generateNewPassword()">Neues Passwort generieren</button>
</div>
<div id="toast">✓ Kopiert!</div>
</div>
<script>
function copyToClipboard() {
const password = document.getElementById("password").innerText;
navigator.clipboard.writeText(password).then(() => {
const toast = document.getElementById("toast");
toast.style.visibility = "visible";
setTimeout(() => {
toast.style.visibility = "hidden";
}, 1500); // Toast verschwindet nach 1,5 Sekunden
}).catch(err => {
console.error("Fehler beim Kopieren: ", err);
alert("Kopieren fehlgeschlagen. Bitte manuell kopieren: " + password);
});
}
function generateNewPassword() {
fetch("/api/password")
.then(response => response.text())
.then(password => {
document.getElementById("password").innerText = password;
})
.catch(error => console.error("Fehler:", error));
}
</script>
</body>
</html>`,
password,
)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprint(w, html)
if debug {
log.Printf("call helpHandler\n")
}
err := templates["help.html"].ExecuteTemplate(w, "base.html", nil)
if err != nil {
log.Printf("Fehler beim Rendern des Templates: %v", err)
http.Error(w, "Interner Serverfehler", http.StatusInternalServerError)
}
}
func main() {
http.HandleFunc("/", webHandler)
http.HandleFunc("/api/password", passwordHandler)
http.HandleFunc("/help", helpHandler)
log.Println("Server läuft auf http://localhost:8080")
log.Println("Plain-Text-Passwort: curl http://localhost:8080/api/password")
log.Fatal(http.ListenAndServe(":8080", nil))
}
initConfig()
loadTemplates()
mux := http.NewServeMux()
fs := http.FileServer(http.Dir("static"))
mux.Handle("/static/", http.StripPrefix("/static/", fs))
mux.HandleFunc("/", indexHandler)
mux.HandleFunc("/api/password", passwordAPIHandler)
mux.HandleFunc("/json/password", passwordHandler)
mux.HandleFunc("/help", helpHandler)
loggingRouter := LoggingMiddleware(mux)
log.Println("Server läuft auf http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", loggingRouter))
}

98
misc/MoreUsage.md Normal file
View File

@@ -0,0 +1,98 @@
## bash alias
You can configure an bash alias in your `~/.bashrc` like this:
```
## genpasswd alias
alias genpasswd='echo $(curl -s https://passwd.scu.si/api/password)'
```
After making above changes you have to reload your ~/bashrc, in order to activate your changes.
```
. ~/.bashrc
```
Now you can enter `genpasswd` and get a fresh password from the API Endpoint.
## get 10 fresh passwords
```bash
for i in {1..10}; do echo $(curl -s https://passwd.scu.si/api/password); done
```
# building the app
you can build the app yourself like this:
```
go build ./
```
NOTE: If you build the app manually in go, like shown in this example, it will probably not run, since it misses a writeable `/data` directory.
You can set the counterFile by environment variable `COUNTER_FILE`, like this:
```
COUNTER_FILE=./counter.txt ./Web-Password
```
## debuging the app
You can turn on debug mode via environment variable `DEBUG`
```
DEBUG=true ./Web-Password
```
# build a docker container
```
docker build -t web-password:dev .
```
# start the docker container
```
docker run -p 8080:8080 -v app_data:/data web-password:dev
```
## docker-compose
There are two example docker-compose files in the [misc](./) directory.
### docker-compose.yml
A basic variant that just brings up the container and export port 8080.
The basic variant can be used without modifications.
### docker-compose.traefik.yml
The other one is meant to be used behind a traefik reverse proxy.
This variant has lables to configure traefik accordingly.
The traefik variant needs to be adjusted to your environment before
you can use it successfully.
### initial pull
```
docker compose pull
```
### start up
```
docker compose up -d
```
### bring down
```
docker compose down
```
### update container
In order to update your container to the current version, do this:
```
docker compose pull
docker compose up -d
```

View File

@@ -0,0 +1,24 @@
services:
password-generator:
image: gitea.scu.si/florian.walther/password-generator:latest
container_name: password-generator
restart: always
volumes:
- ./app_data:/data
expose:
- "8080:8080"
labels:
- "traefik.enable=true"
- "traefik.docker.network=traefik_backend"
- "traefik.http.routers.webpass.rule=Host(`passwd.scu.si`)"
- "traefik.http.routers.webpass.entrypoints=web,websecure"
- "traefik.http.routers.webpass.tls=true"
- "traefik.http.routers.webpass.tls.certresolver=myresolver"
- "traefik.http.services.webpass.loadbalancer.server.port=8080"
networks:
- traefik_backend
networks:
traefik_backend:
external: true

14
misc/docker-compose.yml Normal file
View File

@@ -0,0 +1,14 @@
services:
password-generator:
image: gitea.scu.si/florian.walther/password-generator:latest
container_name: password-generator
restart: always
environment:
- DEBUG=false
- COUNTER_FILE=/data/counter.txt
volumes:
- ./app_data:/data
ports:
- "8080:8080"
# Falls die Registry privat ist, muss der Host zuvor mit
# 'docker login gitea.scu.si' angemeldet worden sein.

BIN
static/key.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

237
static/style.css Normal file
View File

@@ -0,0 +1,237 @@
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--highlight-color: #1d4ed8;
--container-bg: white;
--button-bg: #007BFF;
--button-hover: #0056b3;
--copy-button-bg: #4CAF50;
--copy-button-hover: #45a049;
--password-bg: #f0f0f0;
--border-color: #ddd;
--shadow-color: rgba(0, 0, 0, 0.1);
}
.dark {
--bg-color: #121212;
--text-color: #e0e0e0;
--highlight-color: #fcf803;
--container-bg: #1e1e1e;
--button-bg: #2a7df4;
--button-hover: #1a5fb4;
--copy-button-bg: #4caf60;
--copy-button-hover: #3d8b40;
--password-bg: #2d2d2d;
--border-color: #444;
--shadow-color: rgba(0, 0, 0, 0.3);
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: var(--bg-color);
color: var(--text-color);
transition: background-color 0.3s, color 0.3s;
margin-bottom: 60px;
}
.container {
text-align: center;
background: var(--container-bg);
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px var(--shadow-color);
width: 50%;
min-width: 600px;
position: relative;
}
footer {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
background-color: var(--bg-color);
border-top: 1px solid var(--border-color);
padding: 10px 20px;
box-sizing: border-box;
box-shadow: 0 2px 10px var(--shadow-color);
z-index: 1000;
}
.footer-container {
display: flex;
justify-content: flex-end; /* Schiebt alles nach rechts */
gap: 20px; /* Abstand zwischen Counter und Version */
align-items: center;
}
.footer-item {
display: flex;
align-items: center;
gap: 8px;
font-family: monospace; /* Monospace sieht für Versionen oft "technischer" aus */
font-size: 13px;
color:var(--text-color);
}
@keyframes pulse {
0% { transform: scale(1); color: var(--shadow-color); }
50% { transform: scale(1.2); color: var(--highlight-color); font-weight: bold; }
100% { transform: scale(1); color: var(--shadow-color); }
}
.counter-update {
animation: pulse 0.4s ease-out;
}
#password {
font-family: 'Courier New', Courier, monospace;
font-size: 1.2rem;
letter-spacing: 1px;
margin: 1rem auto;
padding: 0.8rem;
background: var(--password-bg);
border-radius: 4px;
border: 1px solid var(--border-color);
width: 90%;
word-break: break-all;
color: var(--text-color);
}
.buttons {
display: flex;
justify-content: center;
gap: 0.5rem;
}
.copy-button {
background: var(--copy-button-bg);
color: white;
border: none;
padding: 0.6rem 1.2rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.copy-button:hover {
background: var(--copy-button-hover);
}
.renew-button {
background: var(--button-bg);
color: white;
border: none;
padding: 0.6rem 1.2rem;
font-size: 1rem;
border-radius: 4px;
cursor: pointer;
transition: background 0.2s;
}
.renew-button:hover {
background: var(--button-hover);
}
.code-link {
position: absolute;
top: 1rem;
left: 1rem;
font-size: 1.2rem;
color: var(--text-color);
opacity: 0.7;
text-decoration: none;
}
.code-link:hover {
opacity: 1;
}
.help-link {
position: absolute;
top: 1rem;
right: 1rem;
font-size: 1.2rem;
color: var(--text-color);
opacity: 0.7;
text-decoration: none;
}
.help-link:hover {
opacity: 1;
}
#toast {
visibility: hidden;
min-width: 150px;
background-color: var(--copy-button-bg);
color: white;
text-align: center;
border-radius: 4px;
padding: 0.5rem;
position: fixed;
top: 20px;
right: 20px;
z-index: 1;
font-size: 0.9rem;
box-shadow: 0 2px 10px var(--shadow-color);
}
#theme-toggle {
position: absolute;
top: 1rem;
left: 1rem;
background: transparent;
border: none;
color: var(--text-color);
font-size: 1.2rem;
cursor: pointer;
}
pre {
font-family: 'Courier New', Courier, monospace;
background: var(--password-bg);
padding: 0.8rem;
border-radius: 4px;
color: var(--text-color);
border: 1px solid var(--border-color);
}
a {
color: var(--button-bg);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.debug-banner {
position: absolute;
top: 1rem;
left: 4rem;
font-size: 1.2rem;
border-radius: 4px;
border: 1px solid var(--border-color);
}
.debug-only {
display: none;
}
.debug-footer {
position: absolute;
bottom: 4em;
left: 0;
}
body.is-debug .debug-only {
display: inline-block;
border: 1px dashed red;
}

88
templates/base.html Normal file
View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ block "title" . }}Passwort-Generator{{ end }}</title>
<link rel="icon" href="/static/key.png" type="image/png">
<link rel="stylesheet" href="/static/style.css">
<script>
document.addEventListener('DOMContentLoaded', function() {
const root = document.documentElement;
const savedTheme = localStorage.getItem('theme');
const themeToggle = document.getElementById('theme-toggle');
// Setze das Theme basierend auf localStorage oder Systemeinstellung
if (savedTheme === 'dark') {
root.classList.add('dark');
} else if (savedTheme === 'light') {
root.classList.remove('dark');
} else if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
root.classList.add('dark');
}
// Toggle-Button-Logik
themeToggle.addEventListener('click', function() {
if (root.classList.contains('dark')) {
root.classList.remove('dark');
localStorage.setItem('theme', 'light');
} else {
root.classList.add('dark');
localStorage.setItem('theme', 'dark');
}
});
// Systemtheme-Änderungen abhören
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
if (!localStorage.getItem('theme')) {
e.matches ? root.classList.add('dark') : root.classList.remove('dark');
}
});
});
</script>
{{ block "head" . }}{{ end }}
</head>
<body>
<button id="theme-toggle">🌓</button>
{{if isDebug}}
<div class="debug-banner" style="background: #ffeb3b; color: #000; text-align: center; font-size: 12px; padding: 5px; font-weight: bold;">
⚠️ DEBUG-MODUS AKTIVIERT
</div>
{{end}}
{{ block "body" . }}{{end}}
{{if isDebug}}
<div class="debug-footer" style="background: #333; color: #0f0; font-family: monospace; font-size: 11px; padding: 10px; border-top: 2px solid #0f0;">
<div>
<strong>DEBUG INFO:</strong>
<span>Ladezeit: {{dt .StartTime}}</span> |
<span>Method: {{.Request.Method}}</span> |
<span>Path: {{.Request.URL.Path}}</span> |
<span>Remote: {{.Request.RemoteAddr}}</span> |
<span>Real IP: {{.RealIP}}</span>
</div>
<div style="margin-top: 5px; color: #888;">
User-Agent: {{.Request.UserAgent}}
</div>
</div>
{{end}}
<!-- <footer>Version: {{getAppVersion}} | made with golang and ♥️ {{ block "footer" . }}{{ end }}</footer> -->
<footer>
<div class="footer-container">
<div class="footer-item">
<span>Ladezeit: {{dt .StartTime}}</span>
</div>
<div class="footer-item">
Passwörter generiert: <span id="global-counter">{{getPassCount}}</span>
</div>
<div class="footer-item">
Version: {{getAppVersion}}
</div>
<div class="footer-item">
made with golang and ♥️
</div>
</div>
</footer>
</body>
</html>

35
templates/help.html Normal file
View File

@@ -0,0 +1,35 @@
{{ define "help.html" }}
{{ block "title" . }}Hilfe{{ end }}
{{ block "head" . }}
<script>
document.addEventListener('DOMContentLoaded', function() {
const currentHost = window.location.host;
const apiEndpoint = window.location.protocol + "//" + currentHost + "/api/password";
document.getElementById("api-endpoint").textContent = apiEndpoint;
document.getElementById("curl-example").textContent = "curl " + apiEndpoint;
document.getElementById("powershell-example").textContent = "Invoke-RestMethod -Uri " + apiEndpoint;
document.getElementById("cmd-example").textContent = "curl " + apiEndpoint;
});
</script>
{{ end }}
{{ block "body" . }}
<div class="container">
<h1>Hilfe: API-Endpunkt</h1>
<p>Diese Anwendung bietet einen API-Endpunkt, um Passwörter direkt über die Kommandozeile abzurufen.</p>
<h2>Endpunkt:</h2>
<p><code id="api-endpoint"></code></p>
<h2>Beispiele:</h2>
<h3>Mac/Linux (Terminal):</h3>
<pre id="curl-example"></pre>
<h3>Windows (PowerShell):</h3>
<pre id="powershell-example"></pre>
<h3>Windows (cmd):</h3>
<pre id="cmd-example"></pre>
<p><a href="/">Zurück zur Passwort-Generierung</a></p>
</div>
{{ end }}
{{ end }}

55
templates/index.html Normal file
View File

@@ -0,0 +1,55 @@
{{ define "index.html" }}
{{ block "title" . }}Passwort-Generator{{ end }}
{{ block "head" . }}
<script>
function copyToClipboard() {
const password = document.getElementById("password").innerText;
navigator.clipboard.writeText(password).then(() => {
const toast = document.getElementById("toast");
toast.style.visibility = "visible";
setTimeout(() => { toast.style.visibility = "hidden"; }, 1500);
}).catch(err => {
console.error("Fehler beim Kopieren: ", err);
alert("Kopieren fehlgeschlagen. Bitte manuell kopieren: " + password);
});
}
function generateNewPassword() {
fetch("/json/password")
.then(response => response.json()) // Jetzt .json() statt .text()
.then(data => {
// Passwort aktualisieren
document.getElementById("password").innerText = data.password;
// Counter im Footer aktualisieren
// Wir suchen das Element mit der Klasse 'badge-blue' (oder gib ihm eine ID)
const counterElement = document.getElementById("global-counter");
if (counterElement) {
document.getElementById("global-counter").innerText = data.count;
// Animation triggern
counterElement.classList.remove("counter-update"); // Vorherige Animation zurücksetzen
void counterElement.offsetWidth; // Trick, um CSS-Reflow zu erzwingen
counterElement.classList.add("counter-update");
}
})
.catch(error => console.error("Fehler:", error));
}
</script>
{{ end }}
{{ block "body" . }}
<div class="container">
<a href="/help" class="help-link">?</a>
<a href="https://gitea.scu.si/Florian.Walther/Web-Password" class="code-link">Sourcecode</a>
<h1>Passwort Generator</h1>
<div id="password">{{ .Password }}</div>
<div class="buttons">
<button class="copy-button" onclick="copyToClipboard()">In Zwischenablage kopieren</button>
<button class="renew-button" onclick="generateNewPassword()">Neues Passwort generieren</button>
</div>
<div id="toast">✓ Kopiert!</div>
</div>
{{ end }}
{{ end }}