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