up
This commit is contained in:
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
/go.mod
|
||||||
|
/go.sum
|
||||||
|
/ImageMagick
|
||||||
|
wordlist.txt
|
||||||
149
README.md
Normal file
149
README.md
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
# Email Header Analyzer - Build & Usage Instructions
|
||||||
|
|
||||||
|
## Building a Single-File Executable (Windows & Linux)
|
||||||
|
|
||||||
|
This app uses Go's `embed` package to bundle all static files (web UI, icons, CSS, etc.) into a single executable. You do **not** need to distribute the `web/` folder separately.
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- [Go 1.16+](https://golang.org/dl/) (required for `embed`)
|
||||||
|
- (Optional) [Python 3](https://www.python.org/) with Pillow for icon conversion
|
||||||
|
|
||||||
|
### 1. Prepare Icons
|
||||||
|
- Place your tray icon and favicon in `web/icon.png` and `web/favicon.png`.
|
||||||
|
- For Windows tray icon, you **must** use a `.ico` file. Use the provided script:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Convert PNG to ICO (requires Python Pillow)
|
||||||
|
python web/convert_icon.py web/icon.png web/icon.ico
|
||||||
|
# using ImageMagick convertor
|
||||||
|
magick envelope.jpg -define icon:auto-resize=16,32,48,64 favicon.ico
|
||||||
|
ImageMagick\magick.exe web\icon.png -define icon:auto-resize=16,32,48,64 web\favicon.ico
|
||||||
|
ImageMagick\magick.exe identify web\favicon.ico
|
||||||
|
# App Icon:
|
||||||
|
|
||||||
|
ImageMagick\magick.exe web\icon.png -resize 256x256 -define icon:format=bmp icon.ico
|
||||||
|
go install github.com/akavel/rsrc@latest
|
||||||
|
rsrc -arch amd64 -ico icon.ico -o icon.syso
|
||||||
|
go clean -cache
|
||||||
|
```
|
||||||
|
|
||||||
|
- For Linux, PNG is fine for the tray icon.
|
||||||
|
|
||||||
|
### 2. Build for Your Platform
|
||||||
|
|
||||||
|
#### Windows (from Windows PowerShell or Command Prompt):
|
||||||
|
```powershell
|
||||||
|
# PowerShell (set environment variables before the command)
|
||||||
|
$env:GOOS="windows"; $env:GOARCH="amd64"; go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
|
||||||
|
```
|
||||||
|
```cmd
|
||||||
|
REM Command Prompt (set environment variables before the command)
|
||||||
|
set GOOS=windows
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Linux/macOS (from Bash):
|
||||||
|
```sh
|
||||||
|
# Build 64-bit Linux executable
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o headeranalyzer main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
- The resulting executable contains all static files and icons.
|
||||||
|
|
||||||
|
### 2.1. Add an Icon to the Windows Executable
|
||||||
|
|
||||||
|
By default, Go does not embed an icon in the .exe. To add your tray/web icon as the Windows executable icon:
|
||||||
|
|
||||||
|
1. Install the `rsrc` tool (one-time):
|
||||||
|
```powershell
|
||||||
|
go install github.com/akavel/rsrc@latest
|
||||||
|
```
|
||||||
|
2. Generate a Windows resource file with your icon:
|
||||||
|
```powershell
|
||||||
|
rsrc -ico web/icon.ico -o icon.syso
|
||||||
|
```
|
||||||
|
This creates `icon.syso` in your project root. Go will automatically include it when building for Windows.
|
||||||
|
3. Build your app for Windows (see below for PowerShell/CMD syntax).
|
||||||
|
|
||||||
|
### 2.2. Build for Your Platform
|
||||||
|
|
||||||
|
#### Windows (from Windows PowerShell or Command Prompt):
|
||||||
|
```powershell
|
||||||
|
# PowerShell (set environment variables before the command)
|
||||||
|
$env:GOOS="windows"; $env:GOARCH="amd64"; go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
|
||||||
|
```
|
||||||
|
```cmd
|
||||||
|
REM Command Prompt (set environment variables before the command)
|
||||||
|
set GOOS=windows
|
||||||
|
set GOARCH=amd64
|
||||||
|
go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
- The `-ldflags "-H=windowsgui"` flag prevents a console window from opening when you run the app.
|
||||||
|
- The `icon.syso` file ensures your executable uses the same icon as the tray and web UI.
|
||||||
|
|
||||||
|
#### Linux/macOS (from Bash):
|
||||||
|
```sh
|
||||||
|
# Build 64-bit Linux executable
|
||||||
|
GOOS=linux GOARCH=amd64 go build -o headeranalyzer main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
- The resulting executable contains all static files and icons.
|
||||||
|
|
||||||
|
### 3. Run the App
|
||||||
|
|
||||||
|
- Double-click or run from terminal:
|
||||||
|
- On Windows: `headeranalyzer.exe`
|
||||||
|
- On Linux: `./headeranalyzer`
|
||||||
|
- The app will start a web server (default: http://localhost:8080) and show a system tray icon.
|
||||||
|
- Use the tray menu to open the web UI or quit the app.
|
||||||
|
|
||||||
|
### 4. Usage Notes
|
||||||
|
- All static files (HTML, CSS, JS, icons) are embedded. No need to copy the `web/` folder.
|
||||||
|
- For custom icons, always convert to `.ico` for Windows tray compatibility.
|
||||||
|
- The favicon is served from the embedded files.
|
||||||
|
|
||||||
|
### 5. Troubleshooting
|
||||||
|
- If icons do not appear in the tray, ensure you used a `.ico` file for Windows.
|
||||||
|
- If you update static files, rebuild the executable to embed the latest changes.
|
||||||
|
|
||||||
|
### 5.1. If the Executable Still Has No Icon
|
||||||
|
|
||||||
|
If you followed all steps and the icon still does not appear:
|
||||||
|
|
||||||
|
- **Check icon.syso location:** It must be in the same folder as `main.go` when you run `go build`.
|
||||||
|
- **Check for multiple .syso files:** Only one `.syso` file should be present in your project root. Delete any others.
|
||||||
|
- **Try a different icon:** Some ICO files may be malformed or missing required sizes. Use a simple 32x32 or 64x64 PNG, convert again, and re-run `rsrc`.
|
||||||
|
- **Clear Windows icon cache:** Sometimes Windows caches icons. Move the `.exe` to a new folder, or restart Explorer.
|
||||||
|
- **Verify with Resource Hacker:** Download [Resource Hacker](http://www.angusj.com/resourcehacker/) and open your `.exe` to see if the icon is embedded. If not, the build did not pick up `icon.syso`.
|
||||||
|
- **Try building without cross-compiling:** If you are cross-compiling, try building directly on Windows.
|
||||||
|
- **Try go build without -ldflags:** Rarely, the `-ldflags` flag can interfere. Try building with and without it:
|
||||||
|
```powershell
|
||||||
|
go build -o headeranalyzer.exe main.go
|
||||||
|
go build -ldflags "-H=windowsgui" -o headeranalyzer.exe main.go
|
||||||
|
```
|
||||||
|
- **Try go generate:** If you use `go generate`, ensure it does not overwrite or remove `icon.syso`.
|
||||||
|
|
||||||
|
If none of these work, please report your Go version, OS, and the exact steps you used.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Favicon and Tray Icon Support
|
||||||
|
|
||||||
|
- The app uses both `favicon.png` and `icon.ico` for browser and tray compatibility.
|
||||||
|
- The browser will use `icon.ico` if available (for Windows/Edge/PWA), and `favicon.png` for other platforms.
|
||||||
|
- The tray icon is loaded from the embedded `icon.ico`.
|
||||||
|
- If you update icons, place the new files in `web/` and rebuild.
|
||||||
|
|
||||||
|
### How to Add or Update Icons
|
||||||
|
|
||||||
|
1. Place your PNG icon (32x32 or 64x64 recommended) in `web/favicon.png`.
|
||||||
|
2. Convert it to ICO for Windows tray support:
|
||||||
|
```sh
|
||||||
|
python web/convert_icon.py web/favicon.png web/icon.ico
|
||||||
|
```
|
||||||
|
3. Both files must be present in `web/` before building.
|
||||||
|
|
||||||
|
## License
|
||||||
|
MIT
|
||||||
33
build-resource.bat
Normal file
33
build-resource.bat
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
@echo off
|
||||||
|
echo Compiling Windows resource file...
|
||||||
|
|
||||||
|
REM Check if windres is available (part of MinGW/MSYS2)
|
||||||
|
where windres >nul 2>&1
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo Error: windres not found. Please install MinGW-w64 or MSYS2.
|
||||||
|
echo Download from: https://www.msys2.org/
|
||||||
|
echo After installation, run: pacman -S mingw-w64-x86_64-toolchain
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Check if favicon.ico exists
|
||||||
|
if not exist "web\favicon.ico" (
|
||||||
|
echo Error: web\favicon.ico not found!
|
||||||
|
echo Please ensure your favicon.ico file is in the web folder.
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Compile resource file to .syso
|
||||||
|
windres -i resource.rc -o resource.syso -O coff
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo Error: Failed to compile resource file
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Resource file compiled successfully: resource.syso
|
||||||
|
echo Now you can build the executable with: go build -ldflags="-H=windowsgui" -o headeranalyzer.exe
|
||||||
|
|
||||||
|
pause
|
||||||
32
build-resource.sh
Normal file
32
build-resource.sh
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
echo "Compiling Windows resource file..."
|
||||||
|
|
||||||
|
# Check if x86_64-w64-mingw32-windres is available (for cross-compilation)
|
||||||
|
if command -v x86_64-w64-mingw32-windres >/dev/null 2>&1; then
|
||||||
|
WINDRES="x86_64-w64-mingw32-windres"
|
||||||
|
elif command -v windres >/dev/null 2>&1; then
|
||||||
|
WINDRES="windres"
|
||||||
|
else
|
||||||
|
echo "Error: windres not found. Please install MinGW-w64."
|
||||||
|
echo "On Ubuntu/Debian: sudo apt-get install mingw-w64"
|
||||||
|
echo "On macOS: brew install mingw-w64"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if favicon.ico exists
|
||||||
|
if [ ! -f "web/favicon.ico" ]; then
|
||||||
|
echo "Error: web/favicon.ico not found!"
|
||||||
|
echo "Please ensure your favicon.ico file is in the web folder."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Compile resource file to .syso
|
||||||
|
$WINDRES -i resource.rc -o resource.syso -O coff
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to compile resource file"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Resource file compiled successfully: resource.syso"
|
||||||
|
echo "Now you can build the executable with:"
|
||||||
|
echo "GOOS=windows GOARCH=amd64 go build -ldflags=\"-H=windowsgui\" -o headeranalyzer.exe"
|
||||||
39
build-windows.bat
Normal file
39
build-windows.bat
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
@echo off
|
||||||
|
echo Building HeaderAnalyzer for Windows...
|
||||||
|
|
||||||
|
REM Clean previous builds
|
||||||
|
if exist "headeranalyzer.exe" del "headeranalyzer.exe"
|
||||||
|
|
||||||
|
REM Check if resource.syso exists
|
||||||
|
if not exist "resource.syso" (
|
||||||
|
echo Warning: resource.syso not found. Building resource file first...
|
||||||
|
call build-resource.bat
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo Failed to build resource file. Continuing without icon...
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
REM Set environment variables for Windows build
|
||||||
|
set GOOS=windows
|
||||||
|
set GOARCH=amd64
|
||||||
|
|
||||||
|
REM Build the executable with Windows GUI subsystem (no console window)
|
||||||
|
echo Building executable...
|
||||||
|
go build -ldflags="-H=windowsgui -s -w" -o headeranalyzer.exe
|
||||||
|
if %ERRORLEVEL% NEQ 0 (
|
||||||
|
echo Error: Build failed
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
echo Build completed successfully: headeranalyzer.exe
|
||||||
|
echo The executable includes:
|
||||||
|
echo - Embedded web assets
|
||||||
|
echo - Application icon (if resource.syso exists)
|
||||||
|
echo - No console window
|
||||||
|
echo - System tray support
|
||||||
|
|
||||||
|
REM Check file size
|
||||||
|
for %%A in (headeranalyzer.exe) do echo File size: %%~zA bytes
|
||||||
|
|
||||||
|
pause
|
||||||
547
main.go
Normal file
547
main.go
Normal file
@@ -0,0 +1,547 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"embed"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"headeranalyzer/parser"
|
||||||
|
"headeranalyzer/passwordgenerator"
|
||||||
|
|
||||||
|
"github.com/getlantern/systray"
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
addr = flag.String("host", "127.0.0.1", "IP to bind")
|
||||||
|
port = flag.Int("port", 5555, "Port to run on")
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed web/*
|
||||||
|
var embeddedFiles embed.FS
|
||||||
|
|
||||||
|
func onReady(addrPort string, shutdownCh chan struct{}) {
|
||||||
|
var iconPath string
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
iconPath = "web/favicon.ico"
|
||||||
|
} else {
|
||||||
|
iconPath = "web/favicon.png"
|
||||||
|
}
|
||||||
|
|
||||||
|
iconData, err := fs.ReadFile(embeddedFiles, iconPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to load system tray icon (%s): %v", iconPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(iconData) == 0 {
|
||||||
|
log.Printf("System tray icon (%s) is empty", iconPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Loaded system tray icon (%s): %d bytes", iconPath, len(iconData))
|
||||||
|
|
||||||
|
// SetIcon does not return an error, so call it directly
|
||||||
|
systray.SetIcon(iconData)
|
||||||
|
|
||||||
|
systray.SetTitle("HeaderAnalyzer")
|
||||||
|
systray.SetTooltip("Email Header Analyzer")
|
||||||
|
mOpen := systray.AddMenuItem("Open Web UI", "Open the web interface")
|
||||||
|
mQuit := systray.AddMenuItem("Quit", "Quit the app")
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-mOpen.ClickedCh:
|
||||||
|
url := "http://" + addrPort
|
||||||
|
openBrowser(url)
|
||||||
|
case <-mQuit.ClickedCh:
|
||||||
|
systray.Quit()
|
||||||
|
close(shutdownCh)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func openBrowser(url string) {
|
||||||
|
switch runtime.GOOS {
|
||||||
|
case "windows":
|
||||||
|
exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||||
|
case "darwin":
|
||||||
|
exec.Command("open", url).Start()
|
||||||
|
default:
|
||||||
|
exec.Command("xdg-open", url).Start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Initialize password generator word list
|
||||||
|
passwordgenerator.InitWordList()
|
||||||
|
|
||||||
|
tmpl := template.Must(template.New("index.html").Funcs(template.FuncMap{
|
||||||
|
"splitString": func(s, delimiter string) []string {
|
||||||
|
return strings.Split(s, delimiter)
|
||||||
|
},
|
||||||
|
"contains": func(s, substr string) bool {
|
||||||
|
return strings.Contains(s, substr)
|
||||||
|
},
|
||||||
|
}).ParseFS(embeddedFiles, "web/index.html"))
|
||||||
|
|
||||||
|
// Use embedded static files for web assets
|
||||||
|
staticFS, err := fs.Sub(embeddedFiles, "web")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// Serve static files from embedded FS
|
||||||
|
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(staticFS))))
|
||||||
|
|
||||||
|
// Serve favicon and tray icon from embedded FS
|
||||||
|
http.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data, err := fs.ReadFile(staticFS, "favicon.ico")
|
||||||
|
if err != nil {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "image/x-icon")
|
||||||
|
w.Write(data)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method == http.MethodPost {
|
||||||
|
headers := r.FormValue("headers")
|
||||||
|
report := parser.Analyze(headers)
|
||||||
|
tmpl.Execute(w, report)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
tmpl.Execute(w, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/dns", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
dnsTmpl := template.Must(template.ParseFS(embeddedFiles, "web/dns.html"))
|
||||||
|
dnsTmpl.Execute(w, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/api/dns", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
query := r.URL.Query().Get("query")
|
||||||
|
typeq := r.URL.Query().Get("type")
|
||||||
|
if query == "" || typeq == "" {
|
||||||
|
w.WriteHeader(400)
|
||||||
|
w.Write([]byte("Missing query or type"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dnsServer := r.URL.Query().Get("server")
|
||||||
|
var result string
|
||||||
|
switch typeq {
|
||||||
|
case "WHOIS":
|
||||||
|
resp, err := http.Get("https://rdap.org/domain/" + query)
|
||||||
|
if err != nil {
|
||||||
|
result = "WHOIS lookup failed: " + err.Error()
|
||||||
|
} else {
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, _ := io.ReadAll(resp.Body)
|
||||||
|
// Try to parse JSON and extract key info
|
||||||
|
var data map[string]interface{}
|
||||||
|
if err := json.Unmarshal(body, &data); err == nil {
|
||||||
|
var lines []string
|
||||||
|
if v, ok := data["ldhName"]; ok {
|
||||||
|
lines = append(lines, fmt.Sprintf("Domain: %v", v))
|
||||||
|
}
|
||||||
|
if v, ok := data["status"]; ok {
|
||||||
|
if arr, ok := v.([]interface{}); ok {
|
||||||
|
lines = append(lines, fmt.Sprintf("Status: %v", strings.Join(func() []string {
|
||||||
|
s := make([]string, len(arr))
|
||||||
|
for i, x := range arr {
|
||||||
|
s[i] = fmt.Sprintf("%v", x)
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}(), ", ")))
|
||||||
|
} else {
|
||||||
|
lines = append(lines, fmt.Sprintf("Status: %v", v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var registrar, registrarIANA string
|
||||||
|
var registrant, registrantEmail, registrantPhone, registrantOrg, registrantCountry string
|
||||||
|
if v, ok := data["entities"]; ok {
|
||||||
|
if ents, ok := v.([]interface{}); ok {
|
||||||
|
for _, ent := range ents {
|
||||||
|
if entmap, ok := ent.(map[string]interface{}); ok {
|
||||||
|
var rolestr string
|
||||||
|
if roles, ok := entmap["roles"]; ok {
|
||||||
|
if rlist, ok := roles.([]interface{}); ok {
|
||||||
|
for _, r := range rlist {
|
||||||
|
rolestr = fmt.Sprintf("%v", r)
|
||||||
|
if rolestr == "registrar" {
|
||||||
|
if v, ok := entmap["vcardArray"]; ok {
|
||||||
|
if vcard, ok := v.([]interface{}); ok && len(vcard) > 1 {
|
||||||
|
if props, ok := vcard[1].([]interface{}); ok {
|
||||||
|
for _, prop := range props {
|
||||||
|
if arr, ok := prop.([]interface{}); ok && len(arr) > 3 {
|
||||||
|
if arr[0] == "fn" {
|
||||||
|
registrar = fmt.Sprintf("%v", arr[3])
|
||||||
|
}
|
||||||
|
if arr[0] == "org" {
|
||||||
|
registrar = fmt.Sprintf("%v", arr[3])
|
||||||
|
}
|
||||||
|
if arr[0] == "ianaRegistrarId" {
|
||||||
|
registrarIANA = fmt.Sprintf("%v", arr[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rolestr == "registrant" {
|
||||||
|
if v, ok := entmap["vcardArray"]; ok {
|
||||||
|
if vcard, ok := v.([]interface{}); ok && len(vcard) > 1 {
|
||||||
|
if props, ok := vcard[1].([]interface{}); ok {
|
||||||
|
for _, prop := range props {
|
||||||
|
if arr, ok := prop.([]interface{}); ok && len(arr) > 3 {
|
||||||
|
if arr[0] == "fn" {
|
||||||
|
registrant = fmt.Sprintf("%v", arr[3])
|
||||||
|
}
|
||||||
|
if arr[0] == "email" {
|
||||||
|
registrantEmail = fmt.Sprintf("%v", arr[3])
|
||||||
|
}
|
||||||
|
if arr[0] == "tel" {
|
||||||
|
registrantPhone = fmt.Sprintf("%v", arr[3])
|
||||||
|
}
|
||||||
|
if arr[0] == "org" {
|
||||||
|
registrantOrg = fmt.Sprintf("%v", arr[3])
|
||||||
|
}
|
||||||
|
if arr[0] == "adr" {
|
||||||
|
if adr, ok := arr[3].([]interface{}); ok && len(adr) > 6 {
|
||||||
|
registrantCountry = fmt.Sprintf("%v", adr[6])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if registrar != "" {
|
||||||
|
lines = append(lines, "Registrar: "+registrar)
|
||||||
|
}
|
||||||
|
if registrarIANA != "" {
|
||||||
|
lines = append(lines, "Registrar IANA ID: "+registrarIANA)
|
||||||
|
}
|
||||||
|
if registrant != "" {
|
||||||
|
lines = append(lines, "Registrant: "+registrant)
|
||||||
|
}
|
||||||
|
if registrantOrg != "" {
|
||||||
|
lines = append(lines, "Registrant Org: "+registrantOrg)
|
||||||
|
}
|
||||||
|
if registrantEmail != "" {
|
||||||
|
lines = append(lines, "Registrant Email: "+registrantEmail)
|
||||||
|
}
|
||||||
|
if registrantPhone != "" {
|
||||||
|
lines = append(lines, "Registrant Phone: "+registrantPhone)
|
||||||
|
}
|
||||||
|
if registrantCountry != "" {
|
||||||
|
lines = append(lines, "Registrant Country: "+registrantCountry)
|
||||||
|
}
|
||||||
|
if v, ok := data["nameservers"]; ok {
|
||||||
|
if nsarr, ok := v.([]interface{}); ok && len(nsarr) > 0 {
|
||||||
|
nslist := make([]string, 0, len(nsarr))
|
||||||
|
for _, ns := range nsarr {
|
||||||
|
if nsmap, ok := ns.(map[string]interface{}); ok {
|
||||||
|
if ldh, ok := nsmap["ldhName"]; ok {
|
||||||
|
nslist = append(nslist, fmt.Sprintf("%v", ldh))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(nslist) > 0 {
|
||||||
|
lines = append(lines, "Nameservers: "+strings.Join(nslist, ", "))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := data["secureDNS"]; ok {
|
||||||
|
if sec, ok := v.(map[string]interface{}); ok {
|
||||||
|
if ds, ok := sec["delegationSigned"]; ok {
|
||||||
|
lines = append(lines, fmt.Sprintf("DNSSEC: %v", ds))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := data["events"]; ok {
|
||||||
|
if evs, ok := v.([]interface{}); ok {
|
||||||
|
for _, ev := range evs {
|
||||||
|
if evm, ok := ev.(map[string]interface{}); ok {
|
||||||
|
if action, ok := evm["eventAction"]; ok {
|
||||||
|
if date, ok := evm["eventDate"]; ok {
|
||||||
|
lines = append(lines, fmt.Sprintf("%v: %v", action, date))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := data["remarks"]; ok {
|
||||||
|
if rems, ok := v.([]interface{}); ok {
|
||||||
|
for _, rem := range rems {
|
||||||
|
if remm, ok := rem.(map[string]interface{}); ok {
|
||||||
|
if desc, ok := remm["description"]; ok {
|
||||||
|
if descarr, ok := desc.([]interface{}); ok && len(descarr) > 0 {
|
||||||
|
lines = append(lines, fmt.Sprintf("Remark: %v", descarr[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = strings.Join(lines, "\n")
|
||||||
|
} else {
|
||||||
|
result = string(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Special handling for SPF, DKIM, DMARC
|
||||||
|
var answers []string
|
||||||
|
switch strings.ToUpper(typeq) {
|
||||||
|
case "SPF":
|
||||||
|
// Query TXT records and filter for SPF
|
||||||
|
target := dns.Fqdn(query)
|
||||||
|
resolvers := []string{"1.1.1.1:53", "8.8.8.8:53"}
|
||||||
|
if dnsServer != "" {
|
||||||
|
if !strings.Contains(dnsServer, ":") {
|
||||||
|
dnsServer = dnsServer + ":53"
|
||||||
|
}
|
||||||
|
resolvers = []string{dnsServer}
|
||||||
|
}
|
||||||
|
for _, resolverAddr := range resolvers {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(target, dns.TypeTXT)
|
||||||
|
resp, err := dns.Exchange(m, resolverAddr)
|
||||||
|
if err == nil && resp != nil && len(resp.Answer) > 0 {
|
||||||
|
for _, ans := range resp.Answer {
|
||||||
|
if t, ok := ans.(*dns.TXT); ok {
|
||||||
|
for _, txt := range t.Txt {
|
||||||
|
if strings.HasPrefix(txt, "v=spf1") {
|
||||||
|
answers = append(answers, txt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(answers) == 0 {
|
||||||
|
result = "No SPF record found."
|
||||||
|
} else {
|
||||||
|
result = "SPF record(s):\n" + strings.Join(answers, "\n")
|
||||||
|
}
|
||||||
|
case "DMARC":
|
||||||
|
// Query _dmarc.<domain> TXT
|
||||||
|
dmarc := "_dmarc." + query
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(dns.Fqdn(dmarc), dns.TypeTXT)
|
||||||
|
resolvers := []string{"1.1.1.1:53", "8.8.8.8:53"}
|
||||||
|
if dnsServer != "" {
|
||||||
|
if !strings.Contains(dnsServer, ":") {
|
||||||
|
dnsServer = dnsServer + ":53"
|
||||||
|
}
|
||||||
|
resolvers = []string{dnsServer}
|
||||||
|
}
|
||||||
|
for _, resolverAddr := range resolvers {
|
||||||
|
resp, err := dns.Exchange(m, resolverAddr)
|
||||||
|
if err == nil && resp != nil && len(resp.Answer) > 0 {
|
||||||
|
for _, ans := range resp.Answer {
|
||||||
|
if t, ok := ans.(*dns.TXT); ok {
|
||||||
|
answers = append(answers, strings.Join(t.Txt, ""))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(answers) == 0 {
|
||||||
|
result = "No DMARC record found."
|
||||||
|
} else {
|
||||||
|
result = "DMARC record(s):\n" + strings.Join(answers, "\n")
|
||||||
|
}
|
||||||
|
case "DKIM":
|
||||||
|
// Query <selector>._domainkey.<domain> TXT, if not found, check CNAME and then TXT at CNAME target
|
||||||
|
domain := query
|
||||||
|
selector := r.URL.Query().Get("selector")
|
||||||
|
if strings.Contains(query, ":") {
|
||||||
|
parts := strings.SplitN(query, ":", 2)
|
||||||
|
domain = parts[0]
|
||||||
|
selector = parts[1]
|
||||||
|
}
|
||||||
|
if selector == "" {
|
||||||
|
selector = "default"
|
||||||
|
}
|
||||||
|
if selector == "default" && !strings.Contains(query, ":") {
|
||||||
|
answers = append(answers, "Tip: For DKIM, specify selector as domain:selector (e.g. example.com:selector1)")
|
||||||
|
}
|
||||||
|
dkim := selector + "._domainkey." + domain
|
||||||
|
resolvers := []string{"1.1.1.1:53", "8.8.8.8:53"}
|
||||||
|
if dnsServer != "" {
|
||||||
|
if !strings.Contains(dnsServer, ":") {
|
||||||
|
dnsServer = dnsServer + ":53"
|
||||||
|
}
|
||||||
|
resolvers = []string{dnsServer}
|
||||||
|
}
|
||||||
|
foundTXT := false
|
||||||
|
var cnameTarget string
|
||||||
|
for _, resolverAddr := range resolvers {
|
||||||
|
// 1. Query TXT at DKIM name
|
||||||
|
mTXT := new(dns.Msg)
|
||||||
|
mTXT.SetQuestion(dns.Fqdn(dkim), dns.TypeTXT)
|
||||||
|
txtResp, txtErr := dns.Exchange(mTXT, resolverAddr)
|
||||||
|
if txtErr == nil && txtResp != nil && len(txtResp.Answer) > 0 {
|
||||||
|
for _, ans := range txtResp.Answer {
|
||||||
|
if t, ok := ans.(*dns.TXT); ok {
|
||||||
|
answers = append(answers, strings.Join(t.Txt, ""))
|
||||||
|
foundTXT = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundTXT {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// 2. If no TXT, query CNAME at DKIM name
|
||||||
|
mCNAME := new(dns.Msg)
|
||||||
|
mCNAME.SetQuestion(dns.Fqdn(dkim), dns.TypeCNAME)
|
||||||
|
cnameResp, cnameErr := dns.Exchange(mCNAME, resolverAddr)
|
||||||
|
if cnameErr == nil && cnameResp != nil && len(cnameResp.Answer) > 0 {
|
||||||
|
for _, ans := range cnameResp.Answer {
|
||||||
|
if c, ok := ans.(*dns.CNAME); ok {
|
||||||
|
cnameTarget = c.Target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cnameTarget != "" {
|
||||||
|
// 3. Query TXT at CNAME target
|
||||||
|
m2 := new(dns.Msg)
|
||||||
|
m2.SetQuestion(cnameTarget, dns.TypeTXT)
|
||||||
|
resp2, err2 := dns.Exchange(m2, resolverAddr)
|
||||||
|
if err2 == nil && resp2 != nil && len(resp2.Answer) > 0 {
|
||||||
|
for _, ans2 := range resp2.Answer {
|
||||||
|
if t2, ok := ans2.(*dns.TXT); ok {
|
||||||
|
answers = append(answers, strings.Join(t2.Txt, ""))
|
||||||
|
foundTXT = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundTXT {
|
||||||
|
answers = append(answers, "(via CNAME: "+cnameTarget+")")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if foundTXT {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(answers) == 0 || (len(answers) == 1 && strings.HasPrefix(answers[0], "Tip:")) {
|
||||||
|
result = "No DKIM record found for selector '" + selector + "'."
|
||||||
|
if len(answers) > 0 {
|
||||||
|
result += "\n" + answers[0]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result = "DKIM record(s) for selector '" + selector + "':\n" + strings.Join(answers, "\n")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(dns.Fqdn(query), dns.StringToType[typeq])
|
||||||
|
resolvers := []string{"1.1.1.1:53", "8.8.8.8:53"}
|
||||||
|
if dnsServer != "" {
|
||||||
|
if !strings.Contains(dnsServer, ":") {
|
||||||
|
dnsServer = dnsServer + ":53"
|
||||||
|
}
|
||||||
|
resolvers = []string{dnsServer}
|
||||||
|
}
|
||||||
|
for _, resolverAddr := range resolvers {
|
||||||
|
resp, err := dns.Exchange(m, resolverAddr)
|
||||||
|
if err == nil && resp != nil && len(resp.Answer) > 0 {
|
||||||
|
for _, ans := range resp.Answer {
|
||||||
|
result += ans.String() + "\n"
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if result == "" {
|
||||||
|
result = "No result found."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Write([]byte(result))
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/password", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
passwordTmpl := template.Must(template.ParseFS(embeddedFiles, "web/password.html"))
|
||||||
|
passwordTmpl.Execute(w, nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/api/password", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var config passwordgenerator.Config
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&config); err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte("Invalid JSON"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
password, err := passwordgenerator.GeneratePassword(config)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusBadRequest)
|
||||||
|
w.Write([]byte(err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||||
|
w.Write([]byte(password))
|
||||||
|
})
|
||||||
|
|
||||||
|
http.HandleFunc("/api/password/info", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
count, source, lastUpdate := passwordgenerator.GetWordListInfo()
|
||||||
|
|
||||||
|
info := map[string]interface{}{
|
||||||
|
"wordCount": count,
|
||||||
|
"source": source,
|
||||||
|
"lastUpdate": lastUpdate.Format("2006-01-02 15:04:05"),
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(info)
|
||||||
|
})
|
||||||
|
|
||||||
|
addrPort := fmt.Sprintf("%s:%d", *addr, *port)
|
||||||
|
fmt.Printf("Listening on http://%s\n", addrPort)
|
||||||
|
|
||||||
|
srv := &http.Server{Addr: addrPort}
|
||||||
|
shutdownCh := make(chan struct{})
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Fatal(srv.ListenAndServe())
|
||||||
|
}()
|
||||||
|
|
||||||
|
systray.Run(func() { onReady(addrPort, shutdownCh) }, func() {})
|
||||||
|
|
||||||
|
<-shutdownCh
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
srv.Shutdown(ctx)
|
||||||
|
}
|
||||||
1132
parser/analyzer.go
Normal file
1132
parser/analyzer.go
Normal file
File diff suppressed because it is too large
Load Diff
518
passwordgenerator/generator.go
Normal file
518
passwordgenerator/generator.go
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
package passwordgenerator
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
Length int `json:"length"`
|
||||||
|
IncludeUpper bool `json:"includeUpper"`
|
||||||
|
IncludeLower bool `json:"includeLower"`
|
||||||
|
NumberCount int `json:"numberCount"`
|
||||||
|
SpecialChars string `json:"specialChars"`
|
||||||
|
NoConsecutive bool `json:"noConsecutive"`
|
||||||
|
UsePassphrase bool `json:"usePassphrase"`
|
||||||
|
WordCount int `json:"wordCount"`
|
||||||
|
NumberPosition string `json:"numberPosition"` // "start", "end", "each"
|
||||||
|
PassphraseUseNumbers bool `json:"passphraseUseNumbers"`
|
||||||
|
PassphraseUseSpecial bool `json:"passphraseUseSpecial"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback word list in case online fetch fails
|
||||||
|
var fallbackWordList = []string{
|
||||||
|
"apple", "brave", "chair", "dance", "eagle", "flame", "grape", "happy", "island", "jungle",
|
||||||
|
"kite", "lemon", "magic", "night", "ocean", "piano", "quiet", "river", "storm", "tiger",
|
||||||
|
"under", "voice", "water", "young", "zebra", "beach", "cloud", "dream", "fresh", "glass",
|
||||||
|
"heart", "light", "money", "paper", "quick", "royal", "smile", "table", "violet", "world",
|
||||||
|
"bright", "castle", "gentle", "honest", "knight", "marble", "orange", "purple", "silver", "yellow",
|
||||||
|
"ancient", "crystal", "distant", "emerald", "fantasy", "harmony", "journey", "mystery", "outdoor", "perfect",
|
||||||
|
"rainbow", "serenity", "thunder", "universe", "victory", "whisper", "amazing", "balance", "courage", "dignity",
|
||||||
|
"elegant", "freedom", "golden", "holiday", "inspire", "justice", "kindness", "library", "mountain", "natural",
|
||||||
|
"peaceful", "quality", "respect", "sunshine", "triumph", "unique", "wonderful", "adventure", "beautiful", "creative",
|
||||||
|
"delicate", "exciting", "friendly", "generous", "hilarious", "incredible", "joyful", "lovely", "magnificent", "optimistic",
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global word list cache
|
||||||
|
var (
|
||||||
|
wordList []string
|
||||||
|
wordListMux sync.RWMutex
|
||||||
|
lastFetch time.Time
|
||||||
|
fetchTimeout = 10 * time.Second
|
||||||
|
cacheExpiry = 24 * time.Hour // Cache for 24 hours
|
||||||
|
wordListFile = "wordlist.txt" // Local file path
|
||||||
|
)
|
||||||
|
|
||||||
|
// InitWordList initializes the word list cache. Call this at app startup.
|
||||||
|
func InitWordList() {
|
||||||
|
go func() {
|
||||||
|
// Fetch word list in background to warm up cache
|
||||||
|
getWordList()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWordListInfo returns information about the current word list
|
||||||
|
func GetWordListInfo() (count int, source string, lastUpdate time.Time) {
|
||||||
|
wordListMux.RLock()
|
||||||
|
defer wordListMux.RUnlock()
|
||||||
|
|
||||||
|
count = len(wordList)
|
||||||
|
if count == 0 {
|
||||||
|
count = len(fallbackWordList)
|
||||||
|
source = "fallback"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we have a local file
|
||||||
|
if _, err := os.Stat(wordListFile); err == nil {
|
||||||
|
fileTime := getFileModTime(wordListFile)
|
||||||
|
if time.Since(fileTime) > cacheExpiry {
|
||||||
|
source = "local file (expired)"
|
||||||
|
} else if len(wordList) > len(fallbackWordList) {
|
||||||
|
source = "local file (from MIT)"
|
||||||
|
} else {
|
||||||
|
source = "local file"
|
||||||
|
}
|
||||||
|
return count, source, fileTime
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Since(lastFetch) > cacheExpiry {
|
||||||
|
source = "cached (expired)"
|
||||||
|
} else if len(wordList) > len(fallbackWordList) {
|
||||||
|
source = "MIT online"
|
||||||
|
} else {
|
||||||
|
source = "fallback"
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, source, lastFetch
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWordList returns the current word list, fetching from online source if needed
|
||||||
|
func getWordList() []string {
|
||||||
|
wordListMux.RLock()
|
||||||
|
|
||||||
|
// Check if we have a cached word list that's still valid
|
||||||
|
if len(wordList) > 0 && time.Since(lastFetch) < cacheExpiry {
|
||||||
|
defer wordListMux.RUnlock()
|
||||||
|
return wordList
|
||||||
|
}
|
||||||
|
wordListMux.RUnlock()
|
||||||
|
|
||||||
|
// Need to fetch or refresh
|
||||||
|
wordListMux.Lock()
|
||||||
|
defer wordListMux.Unlock()
|
||||||
|
|
||||||
|
// Double-check in case another goroutine already fetched
|
||||||
|
if len(wordList) > 0 && time.Since(lastFetch) < cacheExpiry {
|
||||||
|
return wordList
|
||||||
|
}
|
||||||
|
|
||||||
|
// First try to load from local file
|
||||||
|
if localWords, err := loadWordListFromFile(); err == nil && len(localWords) >= 100 {
|
||||||
|
wordList = localWords
|
||||||
|
lastFetch = getFileModTime(wordListFile)
|
||||||
|
return wordList
|
||||||
|
}
|
||||||
|
|
||||||
|
// If local file doesn't exist or is invalid, try to fetch from MIT
|
||||||
|
newWordList, err := fetchWordListFromMIT()
|
||||||
|
if err != nil || len(newWordList) < 100 { // Sanity check
|
||||||
|
// Use fallback if fetch failed or list is too small
|
||||||
|
if len(wordList) == 0 {
|
||||||
|
wordList = make([]string, len(fallbackWordList))
|
||||||
|
copy(wordList, fallbackWordList)
|
||||||
|
}
|
||||||
|
return wordList
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save downloaded word list to local file
|
||||||
|
saveWordListToFile(newWordList)
|
||||||
|
|
||||||
|
wordList = newWordList
|
||||||
|
lastFetch = time.Now()
|
||||||
|
return wordList
|
||||||
|
}
|
||||||
|
|
||||||
|
// loadWordListFromFile loads the word list from local file
|
||||||
|
func loadWordListFromFile() ([]string, error) {
|
||||||
|
file, err := os.Open(wordListFile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var words []string
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
word := strings.TrimSpace(scanner.Text())
|
||||||
|
if len(word) > 0 && len(word) <= 15 {
|
||||||
|
words = append(words, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return words, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// saveWordListToFile saves the word list to local file
|
||||||
|
func saveWordListToFile(words []string) error {
|
||||||
|
file, err := os.Create(wordListFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
for _, word := range words {
|
||||||
|
if _, err := fmt.Fprintln(file, word); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getFileModTime returns the modification time of a file
|
||||||
|
func getFileModTime(filename string) time.Time {
|
||||||
|
if info, err := os.Stat(filename); err == nil {
|
||||||
|
return info.ModTime()
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fetchWordListFromMIT fetches the word list from MIT's server
|
||||||
|
// fetchWordListFromMIT fetches the word list from MIT's server
|
||||||
|
func fetchWordListFromMIT() ([]string, error) {
|
||||||
|
client := &http.Client{
|
||||||
|
Timeout: fetchTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Get("https://www.mit.edu/~ecprice/wordlist.10000")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to fetch word list: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var words []string
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
word := strings.TrimSpace(scanner.Text())
|
||||||
|
if len(word) > 0 && len(word) <= 15 { // Filter out empty lines and very long words
|
||||||
|
words = append(words, word)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("error reading word list: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(words) < 100 {
|
||||||
|
return nil, fmt.Errorf("word list too small: got %d words", len(words))
|
||||||
|
}
|
||||||
|
|
||||||
|
return words, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
Length: 12,
|
||||||
|
IncludeUpper: true,
|
||||||
|
IncludeLower: true,
|
||||||
|
NumberCount: 1,
|
||||||
|
SpecialChars: "!@#$%^&*-_=+",
|
||||||
|
NoConsecutive: false,
|
||||||
|
UsePassphrase: true, // Default to passphrase
|
||||||
|
WordCount: 3,
|
||||||
|
NumberPosition: "end",
|
||||||
|
PassphraseUseNumbers: true,
|
||||||
|
PassphraseUseSpecial: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func GeneratePassword(config Config) (string, error) {
|
||||||
|
if config.UsePassphrase {
|
||||||
|
return generatePassphrase(config)
|
||||||
|
}
|
||||||
|
return generateRandomPassword(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomPassword(config Config) (string, error) {
|
||||||
|
if config.Length < 1 {
|
||||||
|
return "", fmt.Errorf("password length must be at least 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
var charset string
|
||||||
|
var required []string
|
||||||
|
|
||||||
|
// Build character set
|
||||||
|
if config.IncludeLower {
|
||||||
|
charset += "abcdefghijklmnopqrstuvwxyz"
|
||||||
|
required = append(required, "abcdefghijklmnopqrstuvwxyz")
|
||||||
|
}
|
||||||
|
if config.IncludeUpper {
|
||||||
|
charset += "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||||
|
required = append(required, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")
|
||||||
|
}
|
||||||
|
if config.NumberCount > 0 {
|
||||||
|
charset += "0123456789"
|
||||||
|
required = append(required, "0123456789")
|
||||||
|
}
|
||||||
|
if len(config.SpecialChars) > 0 {
|
||||||
|
charset += config.SpecialChars
|
||||||
|
required = append(required, config.SpecialChars)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(charset) == 0 {
|
||||||
|
return "", fmt.Errorf("no character types selected")
|
||||||
|
}
|
||||||
|
|
||||||
|
password := make([]byte, config.Length)
|
||||||
|
|
||||||
|
// Ensure at least one character from each required set
|
||||||
|
usedPositions := make(map[int]bool)
|
||||||
|
|
||||||
|
// First, place required numbers
|
||||||
|
numbersPlaced := 0
|
||||||
|
for numbersPlaced < config.NumberCount && numbersPlaced < config.Length {
|
||||||
|
pos, err := rand.Int(rand.Reader, big.NewInt(int64(config.Length)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
posInt := int(pos.Int64())
|
||||||
|
|
||||||
|
if !usedPositions[posInt] {
|
||||||
|
char, err := rand.Int(rand.Reader, big.NewInt(10))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
password[posInt] = byte('0' + char.Int64())
|
||||||
|
usedPositions[posInt] = true
|
||||||
|
numbersPlaced++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then place at least one from each other required character set
|
||||||
|
for _, reqSet := range required {
|
||||||
|
if reqSet == "0123456789" {
|
||||||
|
continue // Already handled numbers
|
||||||
|
}
|
||||||
|
|
||||||
|
placed := false
|
||||||
|
attempts := 0
|
||||||
|
for !placed && attempts < config.Length*2 {
|
||||||
|
pos, err := rand.Int(rand.Reader, big.NewInt(int64(config.Length)))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
posInt := int(pos.Int64())
|
||||||
|
|
||||||
|
if !usedPositions[posInt] {
|
||||||
|
char, err := rand.Int(rand.Reader, big.NewInt(int64(len(reqSet))))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
password[posInt] = reqSet[char.Int64()]
|
||||||
|
usedPositions[posInt] = true
|
||||||
|
placed = true
|
||||||
|
}
|
||||||
|
attempts++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill remaining positions
|
||||||
|
for i := 0; i < config.Length; i++ {
|
||||||
|
if !usedPositions[i] {
|
||||||
|
char, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
password[i] = charset[char.Int64()]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle no consecutive characters requirement
|
||||||
|
if config.NoConsecutive {
|
||||||
|
return ensureNoConsecutive(string(password), charset)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(password), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generatePassphrase(config Config) (string, error) {
|
||||||
|
if config.WordCount < 1 {
|
||||||
|
return "", fmt.Errorf("word count must be at least 1")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current word list (online or fallback)
|
||||||
|
currentWordList := getWordList()
|
||||||
|
|
||||||
|
words := make([]string, config.WordCount)
|
||||||
|
for i := 0; i < config.WordCount; i++ {
|
||||||
|
wordIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(currentWordList))))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
word := currentWordList[wordIndex.Int64()]
|
||||||
|
|
||||||
|
// Capitalize first letter if upper case is enabled
|
||||||
|
if config.IncludeUpper {
|
||||||
|
word = strings.Title(word)
|
||||||
|
}
|
||||||
|
words[i] = word
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate numbers if enabled and needed
|
||||||
|
var numbers []string
|
||||||
|
if config.PassphraseUseNumbers && config.NumberCount > 0 {
|
||||||
|
for i := 0; i < config.NumberCount; i++ {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(10))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
numbers = append(numbers, fmt.Sprintf("%d", num.Int64()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to get random separator
|
||||||
|
getSeparator := func() string {
|
||||||
|
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||||
|
sepIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(config.SpecialChars))))
|
||||||
|
if err != nil {
|
||||||
|
return "-" // fallback
|
||||||
|
}
|
||||||
|
return string(config.SpecialChars[sepIndex.Int64()])
|
||||||
|
}
|
||||||
|
return "-"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default separator for non-random cases
|
||||||
|
defaultSeparator := "-"
|
||||||
|
|
||||||
|
// Combine based on number position
|
||||||
|
var result string
|
||||||
|
if len(numbers) == 0 {
|
||||||
|
// No numbers, join words with random separators
|
||||||
|
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||||
|
var parts []string
|
||||||
|
for i, word := range words {
|
||||||
|
parts = append(parts, word)
|
||||||
|
if i < len(words)-1 { // Don't add separator after last word
|
||||||
|
parts = append(parts, getSeparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = strings.Join(parts, "")
|
||||||
|
} else {
|
||||||
|
result = strings.Join(words, defaultSeparator)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
switch config.NumberPosition {
|
||||||
|
case "start":
|
||||||
|
numberStr := strings.Join(numbers, "")
|
||||||
|
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||||
|
var parts []string
|
||||||
|
parts = append(parts, numberStr)
|
||||||
|
for i, word := range words {
|
||||||
|
parts = append(parts, getSeparator(), word)
|
||||||
|
if i < len(words)-1 {
|
||||||
|
parts = append(parts, getSeparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = strings.Join(parts, "")
|
||||||
|
} else {
|
||||||
|
result = numberStr + defaultSeparator + strings.Join(words, defaultSeparator)
|
||||||
|
}
|
||||||
|
case "end":
|
||||||
|
numberStr := strings.Join(numbers, "")
|
||||||
|
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||||
|
var parts []string
|
||||||
|
for i, word := range words {
|
||||||
|
parts = append(parts, word)
|
||||||
|
if i < len(words)-1 {
|
||||||
|
parts = append(parts, getSeparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = append(parts, getSeparator(), numberStr)
|
||||||
|
result = strings.Join(parts, "")
|
||||||
|
} else {
|
||||||
|
result = strings.Join(words, defaultSeparator) + defaultSeparator + numberStr
|
||||||
|
}
|
||||||
|
case "each":
|
||||||
|
var parts []string
|
||||||
|
for i, word := range words {
|
||||||
|
parts = append(parts, word)
|
||||||
|
|
||||||
|
// Add the specified number of digits after each word
|
||||||
|
for j := 0; j < config.NumberCount; j++ {
|
||||||
|
num, err := rand.Int(rand.Reader, big.NewInt(10))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
parts = append(parts, fmt.Sprintf("%d", num.Int64()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add separator after each word+numbers (except last)
|
||||||
|
if i < len(words)-1 {
|
||||||
|
parts = append(parts, getSeparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = strings.Join(parts, "")
|
||||||
|
default:
|
||||||
|
numberStr := strings.Join(numbers, "")
|
||||||
|
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||||
|
var parts []string
|
||||||
|
for i, word := range words {
|
||||||
|
parts = append(parts, word)
|
||||||
|
if i < len(words)-1 {
|
||||||
|
parts = append(parts, getSeparator())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = append(parts, getSeparator(), numberStr)
|
||||||
|
result = strings.Join(parts, "")
|
||||||
|
} else {
|
||||||
|
result = strings.Join(words, defaultSeparator) + defaultSeparator + numberStr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureNoConsecutive(password, charset string) (string, error) {
|
||||||
|
passwordRunes := []rune(password)
|
||||||
|
maxAttempts := 1000
|
||||||
|
|
||||||
|
for attempt := 0; attempt < maxAttempts; attempt++ {
|
||||||
|
hasConsecutive := false
|
||||||
|
|
||||||
|
for i := 0; i < len(passwordRunes)-1; i++ {
|
||||||
|
if passwordRunes[i] == passwordRunes[i+1] {
|
||||||
|
// Replace the second character
|
||||||
|
newChar, err := rand.Int(rand.Reader, big.NewInt(int64(len(charset))))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
passwordRunes[i+1] = rune(charset[newChar.Int64()])
|
||||||
|
hasConsecutive = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasConsecutive {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(passwordRunes), nil
|
||||||
|
}
|
||||||
88
resolver/dnscheck.go
Normal file
88
resolver/dnscheck.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package resolver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/miekg/dns"
|
||||||
|
)
|
||||||
|
|
||||||
|
var resolvers = []string{"1.1.1.1:53", "8.8.8.8:53"}
|
||||||
|
|
||||||
|
func lookupTXT(domain string) ([]string, error) {
|
||||||
|
for _, server := range resolvers {
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(dns.Fqdn(domain), dns.TypeTXT)
|
||||||
|
resp, err := dns.Exchange(m, server)
|
||||||
|
if err != nil || resp == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var results []string
|
||||||
|
for _, ans := range resp.Answer {
|
||||||
|
if txt, ok := ans.(*dns.TXT); ok {
|
||||||
|
results = append(results, txt.Txt...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(results) > 0 {
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("no TXT records found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckSPF(domain string) (string, bool) {
|
||||||
|
txts, err := lookupTXT(domain)
|
||||||
|
if err != nil {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
for _, txt := range txts {
|
||||||
|
if strings.HasPrefix(txt, "v=spf1") {
|
||||||
|
return txt, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckDMARC(domain string) (string, bool) {
|
||||||
|
dmarc := "_dmarc." + domain
|
||||||
|
txts, err := lookupTXT(dmarc)
|
||||||
|
if err != nil || len(txts) == 0 {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return txts[0], true
|
||||||
|
}
|
||||||
|
|
||||||
|
var rbls = []string{
|
||||||
|
"zen.spamhaus.org",
|
||||||
|
"bl.spamcop.net",
|
||||||
|
"b.barracudacentral.org",
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckBlacklists(ip string) []string {
|
||||||
|
var listed []string
|
||||||
|
parts := strings.Split(ip, ".")
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return listed
|
||||||
|
}
|
||||||
|
reversed := fmt.Sprintf("%s.%s.%s.%s", parts[3], parts[2], parts[1], parts[0])
|
||||||
|
for _, rbl := range rbls {
|
||||||
|
query := fmt.Sprintf("%s.%s.", reversed, rbl)
|
||||||
|
m := new(dns.Msg)
|
||||||
|
m.SetQuestion(query, dns.TypeA)
|
||||||
|
for _, resolver := range resolvers {
|
||||||
|
resp, err := dns.Exchange(m, resolver)
|
||||||
|
if err == nil && resp != nil && len(resp.Answer) > 0 {
|
||||||
|
for _, ans := range resp.Answer {
|
||||||
|
if a, ok := ans.(*dns.A); ok {
|
||||||
|
ip := a.A.String()
|
||||||
|
if strings.HasPrefix(ip, "127.0.0.") {
|
||||||
|
listed = append(listed, rbl)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return listed
|
||||||
|
}
|
||||||
35
resource.rc
Normal file
35
resource.rc
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Windows Resource File for HeaderAnalyzer
|
||||||
|
// This file defines the icon that will be embedded in the executable
|
||||||
|
|
||||||
|
// Application icon (ID 1 is the default for main application icon)
|
||||||
|
1 ICON "icon.ico"
|
||||||
|
|
||||||
|
// Version information
|
||||||
|
1 VERSIONINFO
|
||||||
|
FILEVERSION 1,0,0,0
|
||||||
|
PRODUCTVERSION 1,0,0,0
|
||||||
|
FILEFLAGSMASK 0x3fL
|
||||||
|
FILEFLAGS 0x0L
|
||||||
|
FILEOS 0x40004L
|
||||||
|
FILETYPE 0x1L
|
||||||
|
FILESUBTYPE 0x0L
|
||||||
|
BEGIN
|
||||||
|
BLOCK "StringFileInfo"
|
||||||
|
BEGIN
|
||||||
|
BLOCK "040904b0"
|
||||||
|
BEGIN
|
||||||
|
VALUE "CompanyName", "HeaderAnalyzer"
|
||||||
|
VALUE "FileDescription", "Email Header Analyzer"
|
||||||
|
VALUE "FileVersion", "1.0.0.0"
|
||||||
|
VALUE "InternalName", "headeranalyzer"
|
||||||
|
VALUE "LegalCopyright", "MIT License"
|
||||||
|
VALUE "OriginalFilename", "headeranalyzer.exe"
|
||||||
|
VALUE "ProductName", "HeaderAnalyzer"
|
||||||
|
VALUE "ProductVersion", "1.0.0.0"
|
||||||
|
END
|
||||||
|
END
|
||||||
|
BLOCK "VarFileInfo"
|
||||||
|
BEGIN
|
||||||
|
VALUE "Translation", 0x409, 1200
|
||||||
|
END
|
||||||
|
END
|
||||||
92
web/dns.html
Normal file
92
web/dns.html
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>DNS Tools</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<style>
|
||||||
|
.dns-tools-container { max-width: 900px; margin: 0 auto; }
|
||||||
|
.dns-query-form { display: flex; gap: 10px; margin-bottom: 10px; }
|
||||||
|
.dns-query-form input, .dns-query-form select { padding: 7px; border-radius: 4px; border: 1px solid #444; background: #232323; color: #e0e0e0; }
|
||||||
|
.dns-query-form button { padding: 7px 16px; }
|
||||||
|
.dns-results { margin-top: 10px; }
|
||||||
|
.dns-result-block { background: #232323; border-radius: 6px; margin-bottom: 12px; padding: 12px; box-shadow: 0 2px 4px rgba(0,0,0,0.12); }
|
||||||
|
.dns-result-block pre { white-space: pre-wrap; word-break: break-word; font-size: 1em; }
|
||||||
|
.save-btns { margin-bottom: 10px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Analyze New Header</a>
|
||||||
|
<a href="/dns">DNS Tools</a>
|
||||||
|
<a href="/password">Password Generator</a>
|
||||||
|
</nav>
|
||||||
|
<div class="dns-tools-container">
|
||||||
|
<h1>DNS Tools</h1>
|
||||||
|
<form class="dns-query-form" id="dnsForm" onsubmit="return false;">
|
||||||
|
<input type="text" id="dnsInput" placeholder="Enter domain or IP" required>
|
||||||
|
<select id="dnsType">
|
||||||
|
<option value="A">A</option>
|
||||||
|
<option value="AAAA">AAAA</option>
|
||||||
|
<option value="MX">MX</option>
|
||||||
|
<option value="TXT">TXT</option>
|
||||||
|
<option value="NS">NS</option>
|
||||||
|
<option value="CNAME">CNAME</option>
|
||||||
|
<option value="PTR">PTR</option>
|
||||||
|
<option value="SOA">SOA</option>
|
||||||
|
<option value="SPF">SPF</option>
|
||||||
|
<option value="DKIM">DKIM</option>
|
||||||
|
<option value="DMARC">DMARC</option>
|
||||||
|
<option value="WHOIS">WHOIS</option>
|
||||||
|
</select>
|
||||||
|
<input type="text" id="dnsServer" placeholder="Custom DNS server (optional)" style="width:180px;" autocomplete="off">
|
||||||
|
<button type="submit">Query</button>
|
||||||
|
</form>
|
||||||
|
<div class="save-btns">
|
||||||
|
<button onclick="saveResults('csv')">Save as CSV</button>
|
||||||
|
<button onclick="saveResults('txt')">Save as TXT</button>
|
||||||
|
</div>
|
||||||
|
<div class="dns-results" id="dnsResults"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
let results = [];
|
||||||
|
|
||||||
|
function renderResults() {
|
||||||
|
const container = document.getElementById('dnsResults');
|
||||||
|
container.innerHTML = '';
|
||||||
|
results.forEach(r => {
|
||||||
|
const block = document.createElement('div');
|
||||||
|
block.className = 'dns-result-block';
|
||||||
|
block.innerHTML = `<b>${r.type} for ${r.query}</b><br><pre>${r.result}</pre>`;
|
||||||
|
container.appendChild(block);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('dnsForm').addEventListener('submit', async function() {
|
||||||
|
const query = document.getElementById('dnsInput').value.trim();
|
||||||
|
const type = document.getElementById('dnsType').value;
|
||||||
|
let url = `/api/dns?query=${encodeURIComponent(query)}&type=${encodeURIComponent(type)}`;
|
||||||
|
const dnsServer = document.getElementById('dnsServer').value.trim();
|
||||||
|
if (dnsServer) url += `&server=${encodeURIComponent(dnsServer)}`;
|
||||||
|
let res = await fetch(url);
|
||||||
|
let data = await res.text();
|
||||||
|
results.unshift({query, type, result: data});
|
||||||
|
renderResults();
|
||||||
|
});
|
||||||
|
|
||||||
|
function saveResults(format) {
|
||||||
|
let content = '';
|
||||||
|
if (format === 'csv') {
|
||||||
|
content = 'Type,Query,Result\n' + results.map(r => `${r.type},${r.query},"${r.result.replace(/"/g, '""')}"`).join('\n');
|
||||||
|
} else {
|
||||||
|
content = results.map(r => `${r.type} for ${r.query}\n${r.result}\n`).join('\n');
|
||||||
|
}
|
||||||
|
const blob = new Blob([content], {type: 'text/plain'});
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = URL.createObjectURL(blob);
|
||||||
|
link.download = `dnswhois_results.${format}`;
|
||||||
|
link.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
web/favicon.ico
Normal file
BIN
web/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
web/icon.png
Normal file
BIN
web/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 85 KiB |
374
web/index.html
Normal file
374
web/index.html
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Email Header Analyzer</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<script src="https://html2canvas.hertzen.com/dist/html2canvas.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<a href="/">Analyze New Header</a>
|
||||||
|
<a href="/dns">DNS Tools</a>
|
||||||
|
<a href="/password">Password Generator</a>
|
||||||
|
</nav>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Email Header Analyzer</h1>
|
||||||
|
{{if not .From}}
|
||||||
|
<form method="POST">
|
||||||
|
<textarea name="headers" placeholder="Paste email headers here..."></textarea>
|
||||||
|
<br>
|
||||||
|
<button type="submit">Analyze Headers</button>
|
||||||
|
</form>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .From}}
|
||||||
|
<div id="report" class="container">
|
||||||
|
<div class="section" style="display: flex; align-items: flex-start; justify-content: space-between; gap: 30px;">
|
||||||
|
<div style="flex: 1 1 0; min-width: 0;">
|
||||||
|
<h2>Sender Identification</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<p><b>Envelope Sender (Return-Path):</b> {{.EnvelopeSender}}</p>
|
||||||
|
<p><b>From Domain:</b> {{.FromDomain}}</p>
|
||||||
|
<p><b>Sending Server:</b> {{.SendingServer}}</p>
|
||||||
|
</div>
|
||||||
|
<div class="score-indicators">
|
||||||
|
<span class="status {{if .SPFPass}}good{{else}}error{{end}}" title="SPF">SPF {{if .SPFPass}}✓{{else}}✗{{end}}</span>
|
||||||
|
<span class="status {{if .DMARCPass}}good{{else}}error{{end}}" title="DMARC">DMARC {{if .DMARCPass}}✓{{else}}✗{{end}}</span>
|
||||||
|
<span class="status {{if .DKIMPass}}good{{else}}error{{end}}" title="DKIM">DKIM {{if .DKIMPass}}✓{{else}}✗{{end}}</span>
|
||||||
|
<span class="status {{if .Encrypted}}good{{else}}error{{end}}" title="Encrypted">Encrypted {{if .Encrypted}}✓{{else}}✗{{end}}</span>
|
||||||
|
{{if .Blacklists}}
|
||||||
|
<span class="status error" title="{{range .Blacklists}}{{.}}, {{end}}">Blacklisted {{len .Blacklists}} times</span>
|
||||||
|
{{else}}
|
||||||
|
<span class="status good">Not listed on major blacklists</span>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{if .SenderRep}}
|
||||||
|
<div>
|
||||||
|
<b><span>Sender Reputation: </span></b><div class="status {{if contains .SenderRep "EXCELLENT"}}good{{else if contains .SenderRep "GOOD"}}good{{else if contains .SenderRep "FAIR"}}warning{{else}}error{{end}}">
|
||||||
|
{{.SenderRep}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="explanation">
|
||||||
|
<small>
|
||||||
|
<b>Envelope Sender</b> is the real sender used for delivery (can differ from From).<br>
|
||||||
|
<b>From Domain</b> is the domain shown to the recipient.<br>
|
||||||
|
<b>Sending Server</b> is the host or IP that actually sent the message (from first Received header).<br>
|
||||||
|
If these differ, the message may be sent on behalf of another user or via a third-party service.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<details id="all-headers" class="section" style="margin-top:10px;">
|
||||||
|
<summary><b style="font-size: 1.5em;">All Email Headers Table</b></summary>
|
||||||
|
<div style="margin-bottom:10px;">
|
||||||
|
<input type="text" id="headerSearch" placeholder="Search headers..." style="width: 100%; max-width: 350px; padding: 5px; border-radius: 4px; border: 1px solid #444; background: #232323; color: #e0e0e0;">
|
||||||
|
</div>
|
||||||
|
<div style="overflow-x:auto;">
|
||||||
|
<table id="headersTable" style="width:100%; border-collapse:collapse; border:1px solid #444;">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="text-align:left; padding:4px 8px; border:1px solid #444; width: 180px; background:#232323;">Header Name</th>
|
||||||
|
<th style="text-align:left; padding:4px 8px; border:1px solid #444; background:#232323;">Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $k, $v := .AllHeaders}}
|
||||||
|
<tr>
|
||||||
|
<td style="vertical-align:top; padding:4px 8px; border:1px solid #444; word-break:break-word;">{{$k}}</td>
|
||||||
|
<td style="vertical-align:top; padding:4px 8px; border:1px solid #444; white-space:pre-wrap; word-break:break-word;">{{$v}}</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Basic Information</h2>
|
||||||
|
<div class="grid">
|
||||||
|
<div>
|
||||||
|
<p><b>From:</b> {{.From}}</p>
|
||||||
|
<p><b>To:</b> {{.To}}</p>
|
||||||
|
<p><b>Subject:</b> {{.Subject}}</p>
|
||||||
|
<p><b>Date:</b> {{.Date}}</p>
|
||||||
|
{{if .ReplyTo}}<p><b>Reply-To:</b> {{.ReplyTo}}</p>{{end}}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p><b>Message-ID:</b> {{.MessageID}}</p>
|
||||||
|
<p><b>Priority:</b> {{.Priority}}</p>
|
||||||
|
<p><b>Content Type:</b> {{.ContentType}}</p>
|
||||||
|
<p><b>Encoding:</b> {{.Encoding}}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Mail Flow</h2>
|
||||||
|
<ul class="mail-flow">
|
||||||
|
{{range .Received}}<li>{{.}}</li>{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if ne .DeliveryDelay "Insufficient data for delay analysis"}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Delivery Analysis</h2>
|
||||||
|
<p><b>Delivery Timing:</b> {{.DeliveryDelay}}</p>
|
||||||
|
{{if .GeoLocation}}<p><b>Geographic Info:</b> {{.GeoLocation}}</p>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Security Analysis</h2>
|
||||||
|
<div class="security-analysis-vertical">
|
||||||
|
<div class="section">
|
||||||
|
<h3>SPF Authentication</h3>
|
||||||
|
<div class="status {{if .SPFPass}}good{{else}}error{{end}}">
|
||||||
|
{{if .SPFPass}}✓ Passed{{else}}✗ Failed{{end}}
|
||||||
|
</div>
|
||||||
|
<p>{{.SPFDetails}}</p>
|
||||||
|
{{if .SPFRecord}}<pre>{{.SPFRecord}}</pre>{{end}}
|
||||||
|
{{if .SPFHeader}}
|
||||||
|
<details class="details"><summary>Show SPF Header</summary><pre>{{.SPFHeader}}</pre></details>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>DMARC Policy</h3>
|
||||||
|
<div class="status {{if .DMARCPass}}good{{else}}error{{end}}">
|
||||||
|
{{if .DMARCPass}}✓ Passed{{else}}✗ Failed{{end}}
|
||||||
|
</div>
|
||||||
|
<p>{{.DMARCDetails}}</p>
|
||||||
|
{{if .DMARCRecord}}<pre>{{.DMARCRecord}}</pre>{{end}}
|
||||||
|
{{if .DMARCHeader}}
|
||||||
|
<details class="details"><summary>Show DMARC Header</summary><pre>{{.DMARCHeader}}</pre></details>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h3>DKIM Signature</h3>
|
||||||
|
<div class="status {{if .DKIMPass}}good{{else}}error{{end}}">
|
||||||
|
{{if .DKIMPass}}✓ Present{{else}}✗ Missing{{end}}
|
||||||
|
</div>
|
||||||
|
<p>{{.DKIMDetails}}</p>
|
||||||
|
{{if .DKIM}}
|
||||||
|
<details class="details"><summary>Show DKIM Header</summary><pre>{{.DKIM}}</pre></details>
|
||||||
|
{{else if .DKIMHeader}}
|
||||||
|
<details class="details"><summary>Show DKIM Header</summary><pre>{{.DKIMHeader}}</pre></details>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>Encryption</h2>
|
||||||
|
<div class="status {{if .Encrypted}}good{{else}}error{{end}}">
|
||||||
|
{{if .Encrypted}}Encrypted (TLS){{else}}Not Encrypted{{end}}
|
||||||
|
</div>
|
||||||
|
<details><summary>Show Encryption Details</summary><pre>{{.EncryptionDetail}}</pre></details>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{if .Warnings}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Warnings</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .Warnings}}<li class="status warning">⚠️ {{.}}</li>{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .SecurityFlags}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Security Flags</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .SecurityFlags}}<li class="status">🔒 {{.}}</li>{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Blacklists}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Blacklist Status</h2>
|
||||||
|
<div style="margin-bottom: 6px;">
|
||||||
|
<b>Checked:</b>
|
||||||
|
{{if .SendingServer}}IP {{.SendingServer}}{{else if .FromDomain}}Domain {{.FromDomain}}{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="status error">⚠️ Listed on the following blacklists:</div>
|
||||||
|
<ul>
|
||||||
|
{{range .Blacklists}}<li>{{.}}</li>{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
|
||||||
|
{{if .SpamFlags}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Spam Analysis</h2>
|
||||||
|
<div class="status {{if gt (len .SpamFlags) 0}}warning{{else}}good{{end}}">
|
||||||
|
{{if gt (len .SpamFlags) 0}}⚠️ Spam Indicators Found{{else}}✓ No Spam Indicators{{end}}
|
||||||
|
</div>
|
||||||
|
{{if .SpamScore}}<p><b>Spam Score:</b> {{.SpamScore}}</p>{{end}}
|
||||||
|
{{if .SpamFlags}}
|
||||||
|
<ul>
|
||||||
|
{{range .SpamFlags}}<li>{{.}}</li>{{end}}
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if ne .VirusInfo "No virus scanning information found"}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Virus Scanning</h2>
|
||||||
|
<div class="status good">🛡️ Virus Scanning Information</div>
|
||||||
|
<p>{{.VirusInfo}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .PhishingRisk}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Security Risk Assessment</h2>
|
||||||
|
<div class="security-analysis-vertical">
|
||||||
|
<div class="section">
|
||||||
|
<h3>Phishing Risk</h3>
|
||||||
|
<div class="status {{if eq (index (splitString .PhishingRisk " ") 0) "HIGH"}}error{{else if eq (index (splitString .PhishingRisk " ") 0) "MEDIUM"}}warning{{else}}good{{end}}">
|
||||||
|
{{.PhishingRisk}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="section">
|
||||||
|
<h3>Spoofing Risk</h3>
|
||||||
|
<div class="status {{if contains .SpoofingRisk "POTENTIAL"}}warning{{else}}good{{end}}">
|
||||||
|
{{.SpoofingRisk}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .ListInfo}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Mailing List Information</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .ListInfo}}<li>{{.}}</li>{{end}}
|
||||||
|
</ul>
|
||||||
|
{{if .AutoReply}}<p class="status">📧 Auto-reply message detected</p>{{end}}
|
||||||
|
{{if .BulkEmail}}<p class="status">📬 Bulk/marketing email detected</p>{{end}}
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Compliance}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Compliance Information</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .Compliance}}<li class="status good">✓ {{.}}</li>{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .ARC}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>ARC (Authenticated Received Chain)</h2>
|
||||||
|
<details><summary>Show ARC Headers</summary>
|
||||||
|
<ul>
|
||||||
|
{{range .ARC}}<li><pre>{{.}}</pre></li>{{end}}
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if ne .BIMI "No BIMI record found"}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Brand Indicators (BIMI)</h2>
|
||||||
|
<p>{{.BIMI}}</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .Attachments}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Attachment Information</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .Attachments}}<li>{{.}}</li>{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if .URLs}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>URL Information</h2>
|
||||||
|
<ul>
|
||||||
|
{{range .URLs}}<li>{{.}}</li>{{end}}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
{{if ne .ThreadInfo "No threading information available"}}
|
||||||
|
<div class="section">
|
||||||
|
<h2>Message Threading</h2>
|
||||||
|
<details><summary>Show Threading Information</summary>
|
||||||
|
<pre>{{.ThreadInfo}}</pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<button onclick="exportPDF()" type="button">Export as PDF</button>
|
||||||
|
<button onclick="exportImage()" type="button">Save as Image</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Template helper functions for the enhanced features
|
||||||
|
function splitString(str, delimiter) {
|
||||||
|
return str.split(delimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
function contains(str, substr) {
|
||||||
|
return str.includes(substr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportImage() {
|
||||||
|
html2canvas(document.querySelector("#report")).then(canvas => {
|
||||||
|
let link = document.createElement("a");
|
||||||
|
link.download = "email-analysis.png";
|
||||||
|
link.href = canvas.toDataURL();
|
||||||
|
link.click();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportPDF() {
|
||||||
|
// Expand all details before export
|
||||||
|
document.querySelectorAll('#report details').forEach(d => d.open = true);
|
||||||
|
document.getElementById('all-headers').open = true;
|
||||||
|
const element = document.getElementById('report');
|
||||||
|
const opt = {
|
||||||
|
margin: 0.1,
|
||||||
|
filename: 'email-analysis.pdf',
|
||||||
|
image: { type: 'jpeg', quality: 0.98 },
|
||||||
|
html2canvas: { scale: 2, useCORS: true },
|
||||||
|
jsPDF: { unit: 'in', format: 'a4', orientation: 'portrait', putOnlyUsedFonts:true }
|
||||||
|
};
|
||||||
|
html2pdf().set(opt).from(element).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header table search
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
var search = document.getElementById('headerSearch');
|
||||||
|
if (search) {
|
||||||
|
search.addEventListener('input', function() {
|
||||||
|
var filter = search.value.toLowerCase();
|
||||||
|
var rows = document.querySelectorAll('#headersTable tbody tr');
|
||||||
|
rows.forEach(function(row) {
|
||||||
|
var text = row.textContent.toLowerCase();
|
||||||
|
row.style.display = text.indexOf(filter) > -1 ? '' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
664
web/password.html
Normal file
664
web/password.html
Normal file
@@ -0,0 +1,664 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Password Generator - HeaderAnalyzer</title>
|
||||||
|
<link rel="stylesheet" href="/static/style.css">
|
||||||
|
<style>
|
||||||
|
.password-generator {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-output {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 2px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
word-break: break-all;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-text {
|
||||||
|
color: #00ff88;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
background: #007acc;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background: #005999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn.copied {
|
||||||
|
background: #00aa44;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 30px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #00ff88;
|
||||||
|
border-bottom: 1px solid #333;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
margin: 15px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row label {
|
||||||
|
flex: 1;
|
||||||
|
color: #ccc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input[type="number"],
|
||||||
|
.form-row input[type="text"],
|
||||||
|
.form-row select {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #444;
|
||||||
|
color: #fff;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input[type="text"] {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row input[type="checkbox"] {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn {
|
||||||
|
background: #00aa44;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 15px 30px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: bold;
|
||||||
|
width: 100%;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.generate-btn:hover {
|
||||||
|
background: #008833;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-buttons {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-radius: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn {
|
||||||
|
flex: 1;
|
||||||
|
background: #2a2a2a;
|
||||||
|
color: #ccc;
|
||||||
|
border: none;
|
||||||
|
padding: 15px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn.active {
|
||||||
|
background: #007acc;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-btn:hover:not(.active) {
|
||||||
|
background: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passphrase-controls {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.passphrase-controls.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-share {
|
||||||
|
background: #2a2a2a;
|
||||||
|
border: 1px solid #444;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 15px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url-share input {
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 1px solid #333;
|
||||||
|
color: #ccc;
|
||||||
|
padding: 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.controls {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav>
|
||||||
|
<div class="nav-container">
|
||||||
|
<div class="nav-links">
|
||||||
|
<a href="/">Email Analysis</a>
|
||||||
|
<a href="/dns">DNS Tools</a>
|
||||||
|
<a href="/password" class="active">Password Generator</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container password-generator">
|
||||||
|
<h1>🔐 Password Generator</h1>
|
||||||
|
|
||||||
|
<div class="tab-buttons">
|
||||||
|
<button class="tab-btn" id="randomTab">Random Password</button>
|
||||||
|
<button class="tab-btn active" id="passphraseTab">Passphrase</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="password-output">
|
||||||
|
<div class="password-text" id="passwordDisplay">Click "Generate Password" to create a secure password</div>
|
||||||
|
<button class="copy-btn" id="copyBtn" onclick="copyPassword()" style="display: none;">Copy to Clipboard</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="generate-btn" onclick="generatePassword()">🎲 Generate Password</button>
|
||||||
|
|
||||||
|
<div class="controls">
|
||||||
|
<div class="control-group">
|
||||||
|
<h3>🔧 Basic Settings</h3>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="length">Password Length:</label>
|
||||||
|
<input type="number" id="length" min="4" max="128" value="12">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="includeUpper">Include Uppercase (A-Z):</label>
|
||||||
|
<input type="checkbox" id="includeUpper" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="includeLower">Include Lowercase (a-z):</label>
|
||||||
|
<input type="checkbox" id="includeLower" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="numberCount">Number of Digits:</label>
|
||||||
|
<input type="number" id="numberCount" min="0" max="20" value="1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="specialChars">Special Characters:</label>
|
||||||
|
<input type="text" id="specialChars" value="!@#$%^&*-_=+">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="noConsecutive">No consecutive identical characters:</label>
|
||||||
|
<input type="checkbox" id="noConsecutive">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<h3>🎯 Advanced Settings</h3>
|
||||||
|
|
||||||
|
<div class="passphrase-controls active" id="passphraseControls">
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="wordCount">Number of Words:</label>
|
||||||
|
<input type="number" id="wordCount" min="2" max="10" value="3">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="passphraseUseNumbers">Include Numbers:</label>
|
||||||
|
<input type="checkbox" id="passphraseUseNumbers" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="passphraseUseSpecial">Include Special Characters:</label>
|
||||||
|
<input type="checkbox" id="passphraseUseSpecial" checked>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="numberPosition">Number Position:</label>
|
||||||
|
<select id="numberPosition">
|
||||||
|
<option value="end">At End</option>
|
||||||
|
<option value="start">At Start</option>
|
||||||
|
<option value="each">After Each Word</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Strength Indicator:</label>
|
||||||
|
<div id="strengthIndicator" style="color: #999;">Generate a password to see strength</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label>Word List Status:</label>
|
||||||
|
<div id="wordListStatus" style="color: #999; font-size: 12px;">Loading...</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<label for="enableHistory">Save Password History (7 days):</label>
|
||||||
|
<input type="checkbox" id="enableHistory">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="historySection" style="display: none; margin-top: 20px;">
|
||||||
|
<div class="control-group" style="max-width: 100%;">
|
||||||
|
<h3>📜 Password History</h3>
|
||||||
|
<div style="max-height: 300px; overflow-y: auto;">
|
||||||
|
<table id="historyTable" style="width: 100%; border-collapse: collapse;">
|
||||||
|
<thead>
|
||||||
|
<tr style="background: #2a2a2a;">
|
||||||
|
<th style="padding: 8px; border: 1px solid #444; text-align: left; color: #ccc;">Timestamp</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #444; text-align: left; color: #ccc;">Password</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #444; text-align: left; color: #ccc;">Type</th>
|
||||||
|
<th style="padding: 8px; border: 1px solid #444; text-align: left; color: #ccc;">Action</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="historyBody">
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentMode = 'passphrase'; // Default to passphrase
|
||||||
|
|
||||||
|
// Load settings from cookies and URL parameters
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
loadSettings();
|
||||||
|
updateShareUrl();
|
||||||
|
loadWordListInfo();
|
||||||
|
|
||||||
|
// Auto-generate if URL has parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.toString()) {
|
||||||
|
generatePassword();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadWordListInfo() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/password/info');
|
||||||
|
if (response.ok) {
|
||||||
|
const info = await response.json();
|
||||||
|
document.getElementById('wordListStatus').innerHTML =
|
||||||
|
`${info.wordCount} words from ${info.source}<br><small>Last updated: ${info.lastUpdate}</small>`;
|
||||||
|
} else {
|
||||||
|
document.getElementById('wordListStatus').textContent = 'Failed to load word list info';
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('wordListStatus').textContent = 'Error loading word list info';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchTab(mode) {
|
||||||
|
currentMode = mode;
|
||||||
|
|
||||||
|
document.getElementById('randomTab').classList.toggle('active', mode === 'random');
|
||||||
|
document.getElementById('passphraseTab').classList.toggle('active', mode === 'passphrase');
|
||||||
|
document.getElementById('passphraseControls').classList.toggle('active', mode === 'passphrase');
|
||||||
|
|
||||||
|
saveSettings();
|
||||||
|
updateShareUrl();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('randomTab').addEventListener('click', () => switchTab('random'));
|
||||||
|
document.getElementById('passphraseTab').addEventListener('click', () => switchTab('passphrase'));
|
||||||
|
|
||||||
|
function getConfig() {
|
||||||
|
return {
|
||||||
|
length: parseInt(document.getElementById('length').value),
|
||||||
|
includeUpper: document.getElementById('includeUpper').checked,
|
||||||
|
includeLower: document.getElementById('includeLower').checked,
|
||||||
|
numberCount: parseInt(document.getElementById('numberCount').value),
|
||||||
|
specialChars: document.getElementById('specialChars').value,
|
||||||
|
noConsecutive: document.getElementById('noConsecutive').checked,
|
||||||
|
usePassphrase: currentMode === 'passphrase',
|
||||||
|
wordCount: parseInt(document.getElementById('wordCount').value),
|
||||||
|
numberPosition: document.getElementById('numberPosition').value,
|
||||||
|
passphraseUseNumbers: document.getElementById('passphraseUseNumbers').checked,
|
||||||
|
passphraseUseSpecial: document.getElementById('passphraseUseSpecial').checked,
|
||||||
|
enableHistory: document.getElementById('enableHistory').checked
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setConfig(config) {
|
||||||
|
document.getElementById('length').value = config.length || 12;
|
||||||
|
document.getElementById('includeUpper').checked = config.includeUpper !== false;
|
||||||
|
document.getElementById('includeLower').checked = config.includeLower !== false;
|
||||||
|
document.getElementById('numberCount').value = config.numberCount || 1;
|
||||||
|
document.getElementById('specialChars').value = config.specialChars || '!@#$%^&*-_=+';
|
||||||
|
document.getElementById('noConsecutive').checked = config.noConsecutive || false;
|
||||||
|
document.getElementById('wordCount').value = config.wordCount || 3;
|
||||||
|
document.getElementById('numberPosition').value = config.numberPosition || 'end';
|
||||||
|
document.getElementById('passphraseUseNumbers').checked = config.passphraseUseNumbers !== false;
|
||||||
|
document.getElementById('passphraseUseSpecial').checked = config.passphraseUseSpecial !== false;
|
||||||
|
document.getElementById('enableHistory').checked = config.enableHistory || false;
|
||||||
|
|
||||||
|
if (config.usePassphrase !== false) { // Default to passphrase
|
||||||
|
switchTab('passphrase');
|
||||||
|
} else {
|
||||||
|
switchTab('random');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveSettings() {
|
||||||
|
const config = getConfig();
|
||||||
|
config.mode = currentMode;
|
||||||
|
document.cookie = 'passwordGenSettings=' + encodeURIComponent(JSON.stringify(config)) + '; max-age=31536000; path=/';
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSettings() {
|
||||||
|
// First try URL parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.toString()) {
|
||||||
|
const config = {};
|
||||||
|
for (const [key, value] of urlParams) {
|
||||||
|
if (key === 'includeUpper' || key === 'includeLower' || key === 'noConsecutive' || key === 'usePassphrase' || key === 'passphraseUseNumbers' || key === 'passphraseUseSpecial' || key === 'enableHistory') {
|
||||||
|
config[key] = value === 'true';
|
||||||
|
} else if (key === 'length' || key === 'numberCount' || key === 'wordCount') {
|
||||||
|
config[key] = parseInt(value);
|
||||||
|
} else {
|
||||||
|
config[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setConfig(config);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then try cookies
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
const [name, value] = cookie.trim().split('=');
|
||||||
|
if (name === 'passwordGenSettings') {
|
||||||
|
try {
|
||||||
|
const config = JSON.parse(decodeURIComponent(value));
|
||||||
|
setConfig(config);
|
||||||
|
currentMode = config.mode || 'passphrase'; // Default to passphrase
|
||||||
|
switchTab(currentMode);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse cookie settings:', e);
|
||||||
|
// Set defaults on error
|
||||||
|
switchTab('passphrase');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateShareUrl() {
|
||||||
|
const config = getConfig();
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(config)) {
|
||||||
|
params.set(key, value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
const newUrl = window.location.pathname + '?' + params.toString();
|
||||||
|
|
||||||
|
// Update browser URL without page reload
|
||||||
|
if (window.history && window.history.pushState) {
|
||||||
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners to update settings
|
||||||
|
document.querySelectorAll('input, select').forEach(element => {
|
||||||
|
element.addEventListener('change', function() {
|
||||||
|
saveSettings();
|
||||||
|
updateShareUrl();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function generatePassword() {
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/password', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(config)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to generate password');
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await response.text();
|
||||||
|
document.getElementById('passwordDisplay').textContent = result;
|
||||||
|
document.getElementById('copyBtn').style.display = 'inline-block';
|
||||||
|
|
||||||
|
// Save to history
|
||||||
|
const passwordType = config.usePassphrase ? 'Passphrase' : 'Random';
|
||||||
|
savePasswordToHistory(result, passwordType);
|
||||||
|
|
||||||
|
// Update strength indicator
|
||||||
|
updateStrengthIndicator(result, config);
|
||||||
|
|
||||||
|
// Refresh word list info if using passphrase
|
||||||
|
if (config.usePassphrase) {
|
||||||
|
loadWordListInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
document.getElementById('passwordDisplay').textContent = 'Error: ' + error.message;
|
||||||
|
document.getElementById('copyBtn').style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStrengthIndicator(password, config) {
|
||||||
|
let score = 0;
|
||||||
|
let feedback = [];
|
||||||
|
|
||||||
|
// Length score
|
||||||
|
if (password.length >= 12) score += 25;
|
||||||
|
else if (password.length >= 8) score += 15;
|
||||||
|
else score += 5;
|
||||||
|
|
||||||
|
// Character variety
|
||||||
|
if (/[a-z]/.test(password)) score += 10;
|
||||||
|
if (/[A-Z]/.test(password)) score += 10;
|
||||||
|
if (/[0-9]/.test(password)) score += 10;
|
||||||
|
if (/[^a-zA-Z0-9]/.test(password)) score += 15;
|
||||||
|
|
||||||
|
// Length bonus
|
||||||
|
if (password.length >= 16) score += 10;
|
||||||
|
if (password.length >= 20) score += 10;
|
||||||
|
|
||||||
|
// Passphrase bonus
|
||||||
|
if (config.usePassphrase) score += 10;
|
||||||
|
|
||||||
|
// No consecutive bonus
|
||||||
|
if (config.noConsecutive) score += 5;
|
||||||
|
|
||||||
|
let strength, color;
|
||||||
|
if (score >= 80) {
|
||||||
|
strength = "Very Strong 💪";
|
||||||
|
color = "#00aa44";
|
||||||
|
} else if (score >= 60) {
|
||||||
|
strength = "Strong 🔒";
|
||||||
|
color = "#007acc";
|
||||||
|
} else if (score >= 40) {
|
||||||
|
strength = "Medium ⚠️";
|
||||||
|
color = "#ff8800";
|
||||||
|
} else {
|
||||||
|
strength = "Weak ❌";
|
||||||
|
color = "#cc0000";
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('strengthIndicator').innerHTML =
|
||||||
|
`<span style="color: ${color}">${strength}</span> (Score: ${score}/100)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPassword() {
|
||||||
|
const passwordText = document.getElementById('passwordDisplay').textContent;
|
||||||
|
navigator.clipboard.writeText(passwordText).then(function() {
|
||||||
|
const btn = document.getElementById('copyBtn');
|
||||||
|
btn.textContent = 'Copied! ✓';
|
||||||
|
btn.classList.add('copied');
|
||||||
|
|
||||||
|
setTimeout(function() {
|
||||||
|
btn.textContent = 'Copy to Clipboard';
|
||||||
|
btn.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Failed to copy: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password History Management
|
||||||
|
function savePasswordToHistory(password, type) {
|
||||||
|
if (!document.getElementById('enableHistory').checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = getPasswordHistory();
|
||||||
|
const timestamp = new Date().toLocaleString();
|
||||||
|
|
||||||
|
history.unshift({
|
||||||
|
timestamp: timestamp,
|
||||||
|
password: password,
|
||||||
|
type: type,
|
||||||
|
date: new Date().getTime()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep only last 50 passwords and remove entries older than 7 days
|
||||||
|
const sevenDaysAgo = new Date().getTime() - (7 * 24 * 60 * 60 * 1000);
|
||||||
|
const filteredHistory = history
|
||||||
|
.filter(item => item.date > sevenDaysAgo)
|
||||||
|
.slice(0, 50);
|
||||||
|
|
||||||
|
localStorage.setItem('passwordHistory', JSON.stringify(filteredHistory));
|
||||||
|
updateHistoryDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPasswordHistory() {
|
||||||
|
try {
|
||||||
|
const history = localStorage.getItem('passwordHistory');
|
||||||
|
return history ? JSON.parse(history) : [];
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse password history:', e);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearPasswordHistory() {
|
||||||
|
localStorage.removeItem('passwordHistory');
|
||||||
|
updateHistoryDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateHistoryDisplay() {
|
||||||
|
const historyEnabled = document.getElementById('enableHistory').checked;
|
||||||
|
const historySection = document.getElementById('historySection');
|
||||||
|
|
||||||
|
if (!historyEnabled) {
|
||||||
|
historySection.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const history = getPasswordHistory();
|
||||||
|
const tbody = document.getElementById('historyBody');
|
||||||
|
|
||||||
|
if (history.length === 0) {
|
||||||
|
historySection.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
historySection.style.display = 'block';
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
|
||||||
|
history.forEach(item => {
|
||||||
|
const row = document.createElement('tr');
|
||||||
|
row.innerHTML = `
|
||||||
|
<td style="padding: 8px; border: 1px solid #444; font-size: 12px; color: #ccc;">${item.timestamp}</td>
|
||||||
|
<td style="padding: 8px; border: 1px solid #444; font-family: monospace; word-break: break-all; color: #00ff88;">${item.password}</td>
|
||||||
|
<td style="padding: 8px; border: 1px solid #444; font-size: 12px; color: #ccc;">${item.type}</td>
|
||||||
|
<td style="padding: 8px; border: 1px solid #444;">
|
||||||
|
<button onclick="copyPasswordFromHistory('${item.password}')" style="background: #007acc; color: white; border: none; padding: 4px 8px; border-radius: 4px; cursor: pointer; font-size: 12px;">Copy</button>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(row);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyPasswordFromHistory(password) {
|
||||||
|
navigator.clipboard.writeText(password).then(function() {
|
||||||
|
// Visual feedback could be added here
|
||||||
|
console.log('Password copied from history');
|
||||||
|
}).catch(function(err) {
|
||||||
|
console.error('Failed to copy password from history: ', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// History toggle event listener
|
||||||
|
document.getElementById('enableHistory').addEventListener('change', function() {
|
||||||
|
if (!this.checked) {
|
||||||
|
clearPasswordHistory();
|
||||||
|
}
|
||||||
|
updateHistoryDisplay();
|
||||||
|
saveSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load settings from cookies and URL parameters
|
||||||
|
window.addEventListener('load', function() {
|
||||||
|
loadSettings();
|
||||||
|
updateShareUrl();
|
||||||
|
loadWordListInfo();
|
||||||
|
updateHistoryDisplay();
|
||||||
|
|
||||||
|
// Auto-generate if URL has parameters
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
if (urlParams.toString()) {
|
||||||
|
generatePassword();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
307
web/style.css
Normal file
307
web/style.css
Normal file
@@ -0,0 +1,307 @@
|
|||||||
|
:root {
|
||||||
|
--bg-color: #1e1e1e;
|
||||||
|
--text-color: #e0e0e0;
|
||||||
|
--section-bg: #2d2d2d;
|
||||||
|
--border-color: #404040;
|
||||||
|
--highlight: #3c5c7c;
|
||||||
|
--success: #4caf50;
|
||||||
|
--warning: #ff9800;
|
||||||
|
--error: #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, h2, h3 {
|
||||||
|
color: var(--text-color);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
padding-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
background-color: var(--section-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: monospace;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
background-color: #4CAF50;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 10px 20px;
|
||||||
|
text-align: center;
|
||||||
|
text-decoration: none;
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 4px 2px;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
button:hover {
|
||||||
|
background-color: #45a049;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section {
|
||||||
|
background-color: var(--section-bg);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px 15px;
|
||||||
|
margin: 10px 0;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-container {
|
||||||
|
text-align: center;
|
||||||
|
padding: 20px;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-flex {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 60px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-left {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-indicators {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-container h2 {
|
||||||
|
font-size: 1.3em;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-circle {
|
||||||
|
width: 110px;
|
||||||
|
height: 110px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
background: conic-gradient(var(--success) 0%, var(--success) var(--score), #444 var(--score), #444 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.score-inner {
|
||||||
|
position: absolute;
|
||||||
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
|
background: var(--section-bg);
|
||||||
|
border-radius: 50%;
|
||||||
|
top: 10px;
|
||||||
|
left: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.7em;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 5px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.good {
|
||||||
|
background-color: #2d5a2d;
|
||||||
|
color: #90EE90;
|
||||||
|
border-left: 4px solid #4CAF50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.warning {
|
||||||
|
background-color: #5a4d2d;
|
||||||
|
color: #FFD700;
|
||||||
|
border-left: 4px solid #FF9800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status.error {
|
||||||
|
background-color: #5a2d2d;
|
||||||
|
color: #FFB6C1;
|
||||||
|
border-left: 4px solid #f44336;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: start;
|
||||||
|
margin: 10px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre, code {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
font-size: 0.97em;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details pre {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-flow {
|
||||||
|
position: relative;
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-flow li {
|
||||||
|
position: relative;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-flow li::before {
|
||||||
|
content: "↓";
|
||||||
|
position: absolute;
|
||||||
|
left: -20px;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mail-flow li:first-child::before {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.compact-score {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin: 10px 0 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Security Analysis vertical layout */
|
||||||
|
.security-analysis-vertical {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.security-analysis-vertical .section {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
border-left: 3px solid #555;
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
width: 100%;
|
||||||
|
background: #181818;
|
||||||
|
padding: 0.5em 0 0.5em 1.5em;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.12);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
nav a {
|
||||||
|
color: #fff;
|
||||||
|
text-decoration: none;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1.1em;
|
||||||
|
margin-right: 2em;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
nav a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.container, .section, .grid {
|
||||||
|
word-break: break-word;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
border-left: 4px solid #555;
|
||||||
|
padding: 10px;
|
||||||
|
margin: 10px 0;
|
||||||
|
border-radius: 0 4px 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.explanation small {
|
||||||
|
color: #ccc;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
.section {
|
||||||
|
background-color: white;
|
||||||
|
color: black;
|
||||||
|
box-shadow: none;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
}
|
||||||
|
.score-circle, .score-inner {
|
||||||
|
background: white;
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
pre, code {
|
||||||
|
background: #f5f5f5;
|
||||||
|
color: black;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 700px) {
|
||||||
|
.score-flex {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
.score-indicators {
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user