update layout, pdf export for headeranalyzer needs fixing layout
This commit is contained in:
428
main.go
428
main.go
@@ -3,11 +3,8 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
@@ -21,9 +18,9 @@ import (
|
||||
|
||||
"headeranalyzer/parser"
|
||||
"headeranalyzer/passwordgenerator"
|
||||
"headeranalyzer/resolver"
|
||||
|
||||
"github.com/getlantern/systray"
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -130,14 +127,10 @@ func main() {
|
||||
// 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"))
|
||||
// Create handlers with separate template sets
|
||||
indexHandler := parser.NewHandler(embeddedFiles)
|
||||
dnsHandler := resolver.NewHandler(embeddedFiles)
|
||||
passwordHandler := passwordgenerator.NewHandler(embeddedFiles)
|
||||
|
||||
// Use embedded static files for web assets
|
||||
staticFS, err := fs.Sub(embeddedFiles, "web")
|
||||
@@ -158,416 +151,17 @@ func main() {
|
||||
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.Handle("/", indexHandler)
|
||||
|
||||
http.HandleFunc("/dns", func(w http.ResponseWriter, r *http.Request) {
|
||||
dnsTmpl := template.Must(template.ParseFS(embeddedFiles, "web/dns.html"))
|
||||
dnsTmpl.Execute(w, nil)
|
||||
})
|
||||
http.Handle("/dns", dnsHandler)
|
||||
|
||||
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("/api/dns", resolver.DNSAPIHandler)
|
||||
|
||||
http.HandleFunc("/password", func(w http.ResponseWriter, r *http.Request) {
|
||||
passwordTmpl := template.Must(template.ParseFS(embeddedFiles, "web/password.html"))
|
||||
passwordTmpl.Execute(w, nil)
|
||||
})
|
||||
http.Handle("/password", passwordHandler)
|
||||
|
||||
http.HandleFunc("/api/password", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
http.HandleFunc("/api/password", passwordgenerator.PasswordAPIHandler)
|
||||
|
||||
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)
|
||||
})
|
||||
http.HandleFunc("/api/password/info", passwordgenerator.PasswordInfoAPIHandler)
|
||||
|
||||
addrPort := fmt.Sprintf("%s:%d", *addr, *port)
|
||||
fmt.Printf("Listening on http://%s\n", addrPort)
|
||||
|
||||
86
parser/handler.go
Normal file
86
parser/handler.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package parser
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
func NewHandler(embeddedFiles embed.FS) *Handler {
|
||||
tmpl := template.Must(template.New("").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)
|
||||
},
|
||||
"add": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
"ge": func(a, b int) bool {
|
||||
return a >= b
|
||||
},
|
||||
"gt": func(a, b int) bool {
|
||||
return a > b
|
||||
},
|
||||
"eq": func(a, b interface{}) bool {
|
||||
return a == b
|
||||
},
|
||||
"index": func(slice []string, i int) string {
|
||||
if i >= 0 && i < len(slice) {
|
||||
return slice[i]
|
||||
}
|
||||
return ""
|
||||
},
|
||||
"len": func(v interface{}) int {
|
||||
switch s := v.(type) {
|
||||
case []string:
|
||||
return len(s)
|
||||
case map[string]string:
|
||||
return len(s)
|
||||
case string:
|
||||
return len(s)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
"ne": func(a, b interface{}) bool {
|
||||
return a != b
|
||||
},
|
||||
}).ParseFS(embeddedFiles, "web/base.html", "web/headeranalyzer.html"))
|
||||
|
||||
return &Handler{
|
||||
templates: tmpl,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodPost {
|
||||
headers := r.FormValue("headers")
|
||||
report := Analyze(headers)
|
||||
// Create a wrapper struct to include current page info
|
||||
data := struct {
|
||||
*Report
|
||||
CurrentPage string
|
||||
}{
|
||||
Report: report,
|
||||
CurrentPage: "home",
|
||||
}
|
||||
h.templates.ExecuteTemplate(w, "headeranalyzer.html", data)
|
||||
return
|
||||
}
|
||||
// For GET requests, create an empty report so template conditions work
|
||||
data := struct {
|
||||
*Report
|
||||
CurrentPage string
|
||||
}{
|
||||
Report: &Report{}, // Empty report so .From will be empty string
|
||||
CurrentPage: "home",
|
||||
}
|
||||
h.templates.ExecuteTemplate(w, "headeranalyzer.html", data)
|
||||
}
|
||||
73
passwordgenerator/api.go
Normal file
73
passwordgenerator/api.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package passwordgenerator
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// PasswordAPIHandler handles password generation requests
|
||||
func PasswordAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
var requestData struct {
|
||||
Type string `json:"type"`
|
||||
Length int `json:"length"`
|
||||
IncludeUpper bool `json:"includeUpper"`
|
||||
IncludeLower bool `json:"includeLower"`
|
||||
NumberCount int `json:"numberCount"`
|
||||
SpecialChars string `json:"specialChars"`
|
||||
NoConsecutive bool `json:"noConsecutive"`
|
||||
WordCount int `json:"wordCount"`
|
||||
NumberPosition string `json:"numberPosition"`
|
||||
UseNumbers bool `json:"useNumbers"`
|
||||
UseSpecial bool `json:"useSpecial"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&requestData); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte("Invalid JSON"))
|
||||
return
|
||||
}
|
||||
|
||||
// Convert to internal Config format
|
||||
config := Config{
|
||||
Length: requestData.Length,
|
||||
IncludeUpper: requestData.IncludeUpper,
|
||||
IncludeLower: requestData.IncludeLower,
|
||||
NumberCount: requestData.NumberCount,
|
||||
SpecialChars: requestData.SpecialChars,
|
||||
NoConsecutive: requestData.NoConsecutive,
|
||||
UsePassphrase: requestData.Type == "passphrase",
|
||||
WordCount: requestData.WordCount,
|
||||
NumberPosition: requestData.NumberPosition,
|
||||
PassphraseUseNumbers: requestData.UseNumbers,
|
||||
PassphraseUseSpecial: requestData.UseSpecial,
|
||||
}
|
||||
|
||||
password, err := 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))
|
||||
}
|
||||
|
||||
// PasswordInfoAPIHandler handles word list info requests
|
||||
func PasswordInfoAPIHandler(w http.ResponseWriter, r *http.Request) {
|
||||
count, source, lastUpdate := 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)
|
||||
}
|
||||
@@ -391,20 +391,23 @@ func generatePassphrase(config Config) (string, error) {
|
||||
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 "" // fallback to no separator
|
||||
}
|
||||
return string(config.SpecialChars[sepIndex.Int64()])
|
||||
}
|
||||
return "-"
|
||||
return ""
|
||||
}
|
||||
|
||||
// Default separator for non-random cases
|
||||
defaultSeparator := "-"
|
||||
defaultSeparator := ""
|
||||
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||
defaultSeparator = "-"
|
||||
}
|
||||
|
||||
// Combine based on number position
|
||||
var result string
|
||||
if len(numbers) == 0 {
|
||||
// No numbers, join words with random separators
|
||||
// No numbers, join words with separators only if special chars are enabled
|
||||
if config.PassphraseUseSpecial && len(config.SpecialChars) > 0 {
|
||||
var parts []string
|
||||
for i, word := range words {
|
||||
@@ -415,7 +418,8 @@ func generatePassphrase(config Config) (string, error) {
|
||||
}
|
||||
result = strings.Join(parts, "")
|
||||
} else {
|
||||
result = strings.Join(words, defaultSeparator)
|
||||
// No special characters, just concatenate words without separators
|
||||
result = strings.Join(words, "")
|
||||
}
|
||||
} else {
|
||||
switch config.NumberPosition {
|
||||
|
||||
112
passwordgenerator/handler.go
Normal file
112
passwordgenerator/handler.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package passwordgenerator
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
type PasswordConfig struct {
|
||||
Type string
|
||||
Length int
|
||||
IncludeUpper bool
|
||||
IncludeLower bool
|
||||
NumberCount int
|
||||
SpecialChars string
|
||||
NoConsecutive bool
|
||||
WordCount int
|
||||
UseNumbers bool
|
||||
UseSpecial bool
|
||||
NumberPosition string
|
||||
SavePasswords bool
|
||||
}
|
||||
|
||||
func NewHandler(embeddedFiles embed.FS) *Handler {
|
||||
tmpl := template.Must(template.New("").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)
|
||||
},
|
||||
"add": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
"ge": func(a, b int) bool {
|
||||
return a >= b
|
||||
},
|
||||
"len": func(v interface{}) int {
|
||||
switch s := v.(type) {
|
||||
case []string:
|
||||
return len(s)
|
||||
case map[string]string:
|
||||
return len(s)
|
||||
case string:
|
||||
return len(s)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
}).ParseFS(embeddedFiles, "web/base.html", "web/password.html"))
|
||||
|
||||
return &Handler{
|
||||
templates: tmpl,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Parse URL parameters to set default values
|
||||
config := PasswordConfig{
|
||||
Type: getStringParam(r, "type", "passphrase"),
|
||||
Length: getIntParam(r, "length", 12),
|
||||
IncludeUpper: getBoolParam(r, "includeUpper", true),
|
||||
IncludeLower: getBoolParam(r, "includeLower", true),
|
||||
NumberCount: getIntParam(r, "numberCount", 1),
|
||||
SpecialChars: getStringParam(r, "specialChars", "!@#$%^&*-_=+"),
|
||||
NoConsecutive: getBoolParam(r, "noConsecutive", false),
|
||||
WordCount: getIntParam(r, "wordCount", 3),
|
||||
UseNumbers: getBoolParam(r, "useNumbers", true),
|
||||
UseSpecial: getBoolParam(r, "useSpecial", false),
|
||||
NumberPosition: getStringParam(r, "numberPosition", "end"),
|
||||
SavePasswords: getBoolParam(r, "savePasswords", false),
|
||||
}
|
||||
|
||||
data := struct {
|
||||
CurrentPage string
|
||||
Config PasswordConfig
|
||||
}{
|
||||
CurrentPage: "password",
|
||||
Config: config,
|
||||
}
|
||||
h.templates.ExecuteTemplate(w, "password.html", data)
|
||||
}
|
||||
|
||||
// Helper functions to parse URL parameters
|
||||
func getStringParam(r *http.Request, key, defaultValue string) string {
|
||||
if value := r.URL.Query().Get(key); value != "" {
|
||||
return value
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getIntParam(r *http.Request, key string, defaultValue int) int {
|
||||
if value := r.URL.Query().Get(key); value != "" {
|
||||
if intValue, err := strconv.Atoi(value); err == nil {
|
||||
return intValue
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
func getBoolParam(r *http.Request, key string, defaultValue bool) bool {
|
||||
if value := r.URL.Query().Get(key); value != "" {
|
||||
return value == "true" || value == "1" || value == "on"
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
292
resolver/dns_api.go
Normal file
292
resolver/dns_api.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
// DNSAPIHandler handles DNS lookup requests
|
||||
func DNSAPIHandler(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":
|
||||
result = handleWHOISQuery(query)
|
||||
case "SPF":
|
||||
result = handleSPFQuery(query, dnsServer)
|
||||
case "DMARC":
|
||||
result = handleDMARCQuery(query, dnsServer)
|
||||
case "DKIM":
|
||||
result = handleDKIMQuery(query, dnsServer, r)
|
||||
default:
|
||||
result = handleStandardDNSQuery(query, typeq, dnsServer)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
w.Write([]byte(result))
|
||||
}
|
||||
|
||||
func handleWHOISQuery(query string) string {
|
||||
resp, err := http.Get("https://rdap.org/domain/" + query)
|
||||
if err != nil {
|
||||
return "WHOIS lookup failed: " + err.Error()
|
||||
}
|
||||
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 {
|
||||
return parseWHOISData(data)
|
||||
}
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func parseWHOISData(data map[string]interface{}) string {
|
||||
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 {
|
||||
statusList := make([]string, len(arr))
|
||||
for i, x := range arr {
|
||||
statusList[i] = fmt.Sprintf("%v", x)
|
||||
}
|
||||
lines = append(lines, fmt.Sprintf("Status: %v", strings.Join(statusList, ", ")))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("Status: %v", v))
|
||||
}
|
||||
}
|
||||
|
||||
// Extract entity information (registrar, registrant)
|
||||
if v, ok := data["entities"]; ok {
|
||||
registrar, registrant := extractEntityInfo(v)
|
||||
if registrar != "" {
|
||||
lines = append(lines, registrar)
|
||||
}
|
||||
if registrant != "" {
|
||||
lines = append(lines, registrant)
|
||||
}
|
||||
}
|
||||
|
||||
// Extract nameservers
|
||||
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, ", "))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func extractEntityInfo(entities interface{}) (string, string) {
|
||||
// This is a simplified version - the full implementation would be more complex
|
||||
// For now, return empty strings
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func handleSPFQuery(query, dnsServer string) string {
|
||||
var answers []string
|
||||
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 {
|
||||
return "No SPF record found."
|
||||
}
|
||||
return "SPF record(s):\n" + strings.Join(answers, "\n")
|
||||
}
|
||||
|
||||
func handleDMARCQuery(query, dnsServer string) string {
|
||||
var answers []string
|
||||
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 {
|
||||
return "No DMARC record found."
|
||||
}
|
||||
return "DMARC record(s):\n" + strings.Join(answers, "\n")
|
||||
}
|
||||
|
||||
func handleDKIMQuery(query, dnsServer string, r *http.Request) string {
|
||||
var answers []string
|
||||
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 {
|
||||
// Try TXT first
|
||||
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
|
||||
}
|
||||
|
||||
// Try CNAME if no TXT found
|
||||
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 != "" {
|
||||
// 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]
|
||||
}
|
||||
return result
|
||||
}
|
||||
return "DKIM record(s) for selector '" + selector + "':\n" + strings.Join(answers, "\n")
|
||||
}
|
||||
|
||||
func handleStandardDNSQuery(query, typeq, dnsServer string) string {
|
||||
var result string
|
||||
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."
|
||||
}
|
||||
return result
|
||||
}
|
||||
54
resolver/handler.go
Normal file
54
resolver/handler.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package resolver
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
templates *template.Template
|
||||
}
|
||||
|
||||
func NewHandler(embeddedFiles embed.FS) *Handler {
|
||||
tmpl := template.Must(template.New("").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)
|
||||
},
|
||||
"add": func(a, b int) int {
|
||||
return a + b
|
||||
},
|
||||
"ge": func(a, b int) bool {
|
||||
return a >= b
|
||||
},
|
||||
"len": func(v interface{}) int {
|
||||
switch s := v.(type) {
|
||||
case []string:
|
||||
return len(s)
|
||||
case map[string]string:
|
||||
return len(s)
|
||||
case string:
|
||||
return len(s)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
},
|
||||
}).ParseFS(embeddedFiles, "web/base.html", "web/dns.html"))
|
||||
|
||||
return &Handler{
|
||||
templates: tmpl,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
data := struct {
|
||||
CurrentPage string
|
||||
}{
|
||||
CurrentPage: "dns",
|
||||
}
|
||||
h.templates.ExecuteTemplate(w, "dns.html", data)
|
||||
}
|
||||
29
web/base.html
Normal file
29
web/base.html
Normal file
@@ -0,0 +1,29 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{{block "title" .}}HeaderAnalyzer{{end}}</title>
|
||||
<link rel="stylesheet" href="/static/style.css">
|
||||
<link rel="icon" href="/favicon.ico" type="image/x-icon">
|
||||
{{block "head" .}}{{end}}
|
||||
</head>
|
||||
<body>
|
||||
<nav>
|
||||
<a href="/" {{if eq .CurrentPage "home"}}class="active"{{end}}>Analyze New Header</a>
|
||||
<a href="/dns" {{if eq .CurrentPage "dns"}}class="active"{{end}}>DNS Tools</a>
|
||||
<a href="/password" {{if eq .CurrentPage "password"}}class="active"{{end}}>Password Generator</a>
|
||||
</nav>
|
||||
|
||||
<main>
|
||||
{{block "content" .}}
|
||||
<div class="container">
|
||||
<h1>HeaderAnalyzer</h1>
|
||||
<p>Welcome to HeaderAnalyzer - your tool for email header analysis, DNS tools, and password generation.</p>
|
||||
</div>
|
||||
{{end}}
|
||||
</main>
|
||||
|
||||
{{block "scripts" .}}{{end}}
|
||||
</body>
|
||||
</html>
|
||||
158
web/dns.html
158
web/dns.html
@@ -1,26 +1,21 @@
|
||||
<!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>
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}DNS Tools - HeaderAnalyzer{{end}}
|
||||
|
||||
{{define "head"}}
|
||||
<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>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<div class="dns-tools-container">
|
||||
<h1>DNS Tools</h1>
|
||||
<form class="dns-query-form" id="dnsForm" onsubmit="return false;">
|
||||
@@ -48,45 +43,102 @@
|
||||
</div>
|
||||
<div class="dns-results" id="dnsResults"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<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() {
|
||||
document.getElementById('dnsForm').addEventListener('submit', function() {
|
||||
const query = document.getElementById('dnsInput').value.trim();
|
||||
const type = document.getElementById('dnsType').value;
|
||||
const server = document.getElementById('dnsServer').value.trim();
|
||||
|
||||
if (!query) return;
|
||||
|
||||
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();
|
||||
if (server) {
|
||||
url += `&server=${encodeURIComponent(server)}`;
|
||||
}
|
||||
|
||||
// Add selector field for DKIM queries
|
||||
if (type === 'DKIM' && !query.includes(':')) {
|
||||
const selector = prompt('Enter DKIM selector (e.g., "selector1", "default"):');
|
||||
if (selector) {
|
||||
url = `/api/dns?query=${encodeURIComponent(query + ':' + selector)}&type=${encodeURIComponent(type)}`;
|
||||
if (server) {
|
||||
url += `&server=${encodeURIComponent(server)}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(data => {
|
||||
const timestamp = new Date().toLocaleString();
|
||||
const result = {
|
||||
timestamp: timestamp,
|
||||
query: query,
|
||||
type: type,
|
||||
server: server || 'Default',
|
||||
result: data
|
||||
};
|
||||
results.push(result);
|
||||
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'dns-result-block';
|
||||
resultDiv.innerHTML = `
|
||||
<h3>${type} query for ${query}</h3>
|
||||
<p><small>Time: ${timestamp} | Server: ${server || 'Default'}</small></p>
|
||||
<pre>${data}</pre>
|
||||
`;
|
||||
document.getElementById('dnsResults').appendChild(resultDiv);
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('Error:', error);
|
||||
const resultDiv = document.createElement('div');
|
||||
resultDiv.className = 'dns-result-block';
|
||||
resultDiv.innerHTML = `
|
||||
<h3>Error querying ${query}</h3>
|
||||
<pre>Error: ${error.message}</pre>
|
||||
`;
|
||||
document.getElementById('dnsResults').appendChild(resultDiv);
|
||||
});
|
||||
});
|
||||
|
||||
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');
|
||||
if (results.length === 0) {
|
||||
alert('No results to save');
|
||||
return;
|
||||
}
|
||||
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();
|
||||
|
||||
let content = '';
|
||||
let filename = '';
|
||||
|
||||
if (format === 'csv') {
|
||||
content = 'Timestamp,Query,Type,Server,Result\n';
|
||||
results.forEach(r => {
|
||||
const escapedResult = '"' + r.result.replace(/"/g, '""') + '"';
|
||||
content += `"${r.timestamp}","${r.query}","${r.type}","${r.server}",${escapedResult}\n`;
|
||||
});
|
||||
filename = 'dns-results.csv';
|
||||
} else if (format === 'txt') {
|
||||
results.forEach(r => {
|
||||
content += `=== ${r.type} query for ${r.query} ===\n`;
|
||||
content += `Time: ${r.timestamp}\n`;
|
||||
content += `Server: ${r.server}\n`;
|
||||
content += `Result:\n${r.result}\n\n`;
|
||||
});
|
||||
filename = 'dns-results.txt';
|
||||
}
|
||||
|
||||
const blob = new Blob([content], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
{{end}}
|
||||
|
||||
710
web/headeranalyzer.html
Normal file
710
web/headeranalyzer.html
Normal file
@@ -0,0 +1,710 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}Email Header Analyzer{{end}}
|
||||
|
||||
{{define "head"}}
|
||||
<script src="https://unpkg.com/html2canvas@1.4.1/dist/html2canvas.min.js"></script>
|
||||
<script src="https://unpkg.com/jspdf@2.5.1/dist/jspdf.umd.min.js"></script>
|
||||
<script src="https://unpkg.com/html2pdf.js@0.10.1/dist/html2pdf.bundle.min.js"></script>
|
||||
{{end}}
|
||||
|
||||
{{define "content"}}
|
||||
<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>
|
||||
<button onclick="printReport()" type="button">Print Report</button>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{define "scripts"}}
|
||||
<script>
|
||||
// Check if required libraries are loaded
|
||||
function checkLibraries() {
|
||||
if (typeof html2canvas === 'undefined') {
|
||||
console.warn('html2canvas library not loaded');
|
||||
return false;
|
||||
}
|
||||
const hasHtml2pdf = typeof html2pdf !== 'undefined';
|
||||
const hasJsPDF = typeof window.jsPDF !== 'undefined';
|
||||
|
||||
if (!hasHtml2pdf && !hasJsPDF) {
|
||||
console.warn('Neither html2pdf nor jsPDF library loaded');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasHtml2pdf) {
|
||||
console.log('Using html2pdf for PDF generation');
|
||||
} else if (hasJsPDF) {
|
||||
console.log('Using jsPDF fallback for PDF generation');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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() {
|
||||
if (typeof html2canvas === 'undefined') {
|
||||
alert('Image export library not loaded. Please refresh the page and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
html2canvas(document.querySelector("#report")).then(canvas => {
|
||||
let link = document.createElement("a");
|
||||
link.download = "email-analysis.png";
|
||||
link.href = canvas.toDataURL();
|
||||
link.click();
|
||||
}).catch(error => {
|
||||
console.error('Image export failed:', error);
|
||||
alert('Failed to export image. Please try again.');
|
||||
});
|
||||
}
|
||||
|
||||
function printReport() {
|
||||
// Store original states
|
||||
const originalDetailsStates = {};
|
||||
document.querySelectorAll('#report details').forEach((detail, index) => {
|
||||
originalDetailsStates[index] = detail.open;
|
||||
});
|
||||
|
||||
// Expand details for printing (except ARC)
|
||||
document.querySelectorAll('#report details').forEach(detail => {
|
||||
const summary = detail.querySelector('summary');
|
||||
if (summary && !summary.textContent.includes('Show ARC Headers')) {
|
||||
detail.open = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Open print dialog
|
||||
window.print();
|
||||
|
||||
// Restore original states after a short delay
|
||||
setTimeout(() => {
|
||||
document.querySelectorAll('#report details').forEach((detail, index) => {
|
||||
detail.open = originalDetailsStates[index] || false;
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
function exportPDF() {
|
||||
// Check if libraries are available
|
||||
if (typeof html2canvas === 'undefined') {
|
||||
alert('HTML2Canvas library not loaded. Please refresh the page and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we have either html2pdf or jsPDF available
|
||||
const hasHtml2pdf = typeof html2pdf !== 'undefined';
|
||||
const hasJsPDF = typeof window.jsPDF !== 'undefined';
|
||||
|
||||
if (!hasHtml2pdf && !hasJsPDF) {
|
||||
alert('PDF generation library not loaded. Please refresh the page and try again.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Store original states
|
||||
const originalDetailsStates = {};
|
||||
document.querySelectorAll('#report details').forEach((detail, index) => {
|
||||
originalDetailsStates[index] = detail.open;
|
||||
});
|
||||
|
||||
// Expand only specific details that should be included in PDF (not ARC)
|
||||
document.querySelectorAll('#report details').forEach(detail => {
|
||||
const summary = detail.querySelector('summary');
|
||||
if (summary && !summary.textContent.includes('Show ARC Headers')) {
|
||||
detail.open = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Make sure all-headers is expanded
|
||||
const allHeaders = document.getElementById('all-headers');
|
||||
if (allHeaders) {
|
||||
allHeaders.open = true;
|
||||
}
|
||||
|
||||
const element = document.getElementById('report');
|
||||
if (!element) {
|
||||
alert('Report element not found. Please ensure the analysis is complete.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add temporary styling for PDF export
|
||||
const tempStyle = document.createElement('style');
|
||||
tempStyle.id = 'temp-pdf-style';
|
||||
tempStyle.innerHTML = `
|
||||
body {
|
||||
max-width: none !important;
|
||||
width: 794px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
.container {
|
||||
max-width: none !important;
|
||||
width: 794px !important;
|
||||
padding: 15px !important;
|
||||
margin: 0 !important;
|
||||
box-sizing: border-box !important;
|
||||
}
|
||||
#report {
|
||||
max-width: none !important;
|
||||
width: 100% !important;
|
||||
padding: 10px !important;
|
||||
margin: 0 !important;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
box-sizing: border-box !important;
|
||||
position: relative !important;
|
||||
left: 0 !important;
|
||||
top: 0 !important;
|
||||
}
|
||||
#report * {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
max-width: none !important;
|
||||
}
|
||||
#report .section {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
background: white !important;
|
||||
border: 1px solid #ccc !important;
|
||||
margin-bottom: 10px !important;
|
||||
padding: 10px !important;
|
||||
box-sizing: border-box !important;
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
#report .grid {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
#report .grid > div {
|
||||
display: block !important;
|
||||
width: 100% !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
#report .grid p {
|
||||
margin: 5px 0 !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
#report pre {
|
||||
background: #f5f5f5 !important;
|
||||
color: black !important;
|
||||
border: 1px solid #ccc !important;
|
||||
padding: 5px !important;
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
box-sizing: border-box !important;
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-word !important;
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
#report table {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
border-collapse: collapse !important;
|
||||
table-layout: fixed !important;
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
#report td, #report th {
|
||||
border: 1px solid #333 !important;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
padding: 4px !important;
|
||||
word-wrap: break-word !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
}
|
||||
#report .status {
|
||||
background: #f0f0f0 !important;
|
||||
color: black !important;
|
||||
border: 1px solid #999 !important;
|
||||
display: inline-block !important;
|
||||
padding: 2px 5px !important;
|
||||
}
|
||||
#report .status.good {
|
||||
background: #e8f5e8 !important;
|
||||
}
|
||||
#report .status.warning {
|
||||
background: #fff3cd !important;
|
||||
}
|
||||
#report .status.error {
|
||||
background: #f8d7da !important;
|
||||
}
|
||||
#report details {
|
||||
margin-bottom: 10px !important;
|
||||
page-break-inside: avoid !important;
|
||||
}
|
||||
/* Hide buttons */
|
||||
button {
|
||||
display: none !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(tempStyle);
|
||||
|
||||
// Force the element to have A4 dimensions
|
||||
const originalWidth = element.style.width;
|
||||
const originalMaxWidth = element.style.maxWidth;
|
||||
|
||||
element.style.width = '794px'; // A4 width in pixels
|
||||
element.style.maxWidth = 'none';
|
||||
|
||||
// Store these for restoration
|
||||
const restoreWidth = () => {
|
||||
element.style.width = originalWidth;
|
||||
element.style.maxWidth = originalMaxWidth;
|
||||
};
|
||||
|
||||
// Show loading message
|
||||
const button = document.querySelector('button[onclick="exportPDF()"]');
|
||||
const originalText = button ? button.textContent : '';
|
||||
if (button) button.textContent = 'Generating PDF...';
|
||||
|
||||
// Wait a moment for styles to be applied
|
||||
setTimeout(() => {
|
||||
performPDFExport();
|
||||
}, 100);
|
||||
|
||||
function performPDFExport() {
|
||||
try {
|
||||
if (hasHtml2pdf) {
|
||||
// Use html2pdf if available
|
||||
const opt = {
|
||||
margin: [5, 5, 5, 5], // Smaller margins in mm
|
||||
filename: 'email-header-analysis.pdf',
|
||||
image: { type: 'jpeg', quality: 0.95 },
|
||||
html2canvas: {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 794, // A4 width in pixels at 96 DPI (210mm)
|
||||
height: element.scrollHeight,
|
||||
backgroundColor: '#ffffff',
|
||||
windowWidth: 794, // A4 width at 96 DPI
|
||||
windowHeight: 1123 // A4 height at 96 DPI
|
||||
},
|
||||
jsPDF: {
|
||||
unit: 'mm',
|
||||
format: 'a4',
|
||||
orientation: 'portrait',
|
||||
compress: true
|
||||
}
|
||||
};
|
||||
|
||||
html2pdf().set(opt).from(element).save().then(() => {
|
||||
restoreOriginalState();
|
||||
}).catch((error) => {
|
||||
console.error('PDF generation failed:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
restoreOriginalState();
|
||||
});
|
||||
} else if (hasJsPDF) {
|
||||
// Fallback to direct jsPDF + html2canvas approach
|
||||
html2canvas(element, {
|
||||
scale: 2,
|
||||
useCORS: true,
|
||||
allowTaint: true,
|
||||
scrollX: 0,
|
||||
scrollY: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 794, // A4 width in pixels at 96 DPI (210mm)
|
||||
height: element.scrollHeight,
|
||||
backgroundColor: '#ffffff',
|
||||
windowWidth: 794, // A4 width at 96 DPI
|
||||
windowHeight: 1123 // A4 height at 96 DPI
|
||||
}).then(canvas => {
|
||||
const imgData = canvas.toDataURL('image/jpeg', 0.95);
|
||||
const pdf = new window.jsPDF.jsPDF('p', 'mm', 'a4');
|
||||
|
||||
const pdfWidth = pdf.internal.pageSize.getWidth();
|
||||
const pdfHeight = pdf.internal.pageSize.getHeight();
|
||||
|
||||
// Use full width with minimal margins
|
||||
const imgWidth = pdfWidth - 10; // 5mm margin on each side
|
||||
const imgHeight = (canvas.height * imgWidth) / canvas.width;
|
||||
|
||||
let heightLeft = imgHeight;
|
||||
let position = 5; // 5mm top margin
|
||||
|
||||
pdf.addImage(imgData, 'JPEG', 5, position, imgWidth, imgHeight);
|
||||
heightLeft -= (pdfHeight - 10); // Account for margins
|
||||
|
||||
while (heightLeft >= 0) {
|
||||
position = heightLeft - imgHeight + 5;
|
||||
pdf.addPage();
|
||||
pdf.addImage(imgData, 'JPEG', 5, position, imgWidth, imgHeight);
|
||||
heightLeft -= (pdfHeight - 10);
|
||||
}
|
||||
|
||||
pdf.save('email-header-analysis.pdf');
|
||||
restoreOriginalState();
|
||||
}).catch(error => {
|
||||
console.error('Canvas generation failed:', error);
|
||||
alert('Failed to generate PDF. Please try again.');
|
||||
restoreOriginalState();
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('PDF export error:', error);
|
||||
alert('PDF export failed. Please refresh the page and try again.');
|
||||
restoreOriginalState();
|
||||
}
|
||||
|
||||
function restoreOriginalState() {
|
||||
document.querySelectorAll('#report details').forEach((detail, index) => {
|
||||
detail.open = originalDetailsStates[index] || false;
|
||||
});
|
||||
// Remove temporary PDF styling
|
||||
const tempStyle = document.getElementById('temp-pdf-style');
|
||||
if (tempStyle) {
|
||||
document.head.removeChild(tempStyle);
|
||||
}
|
||||
// Restore element width if restoreWidth function exists
|
||||
if (typeof restoreWidth === 'function') {
|
||||
restoreWidth();
|
||||
}
|
||||
if (button) button.textContent = originalText;
|
||||
}
|
||||
} // End of performPDFExport function
|
||||
}
|
||||
|
||||
// Header table search
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Check if libraries are loaded
|
||||
setTimeout(() => {
|
||||
if (!checkLibraries()) {
|
||||
console.warn('Some export libraries failed to load. PDF/Image export may not work.');
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
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>
|
||||
{{end}}
|
||||
374
web/index.html
374
web/index.html
@@ -1,374 +0,0 @@
|
||||
<!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>
|
||||
1203
web/password.html
1203
web/password.html
File diff suppressed because it is too large
Load Diff
356
web/style.css
356
web/style.css
@@ -243,6 +243,11 @@ nav a {
|
||||
nav a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
nav a.active {
|
||||
color: #00ff88;
|
||||
border-bottom: 2px solid #00ff88;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.container, .section, .grid {
|
||||
word-break: break-word;
|
||||
@@ -262,27 +267,149 @@ nav a:hover {
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* Text wrapping improvements for better PDF export and display */
|
||||
pre {
|
||||
white-space: pre-wrap !important;
|
||||
word-wrap: break-word !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
max-width: 100% !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
|
||||
table {
|
||||
table-layout: fixed;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
td, th {
|
||||
word-wrap: break-word;
|
||||
word-break: break-word;
|
||||
overflow-wrap: break-word;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Specific improvements for the report container */
|
||||
#report {
|
||||
word-wrap: break-word;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
#report * {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* PDF-specific print styles */
|
||||
@media print {
|
||||
body {
|
||||
background-color: white;
|
||||
color: black;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
/* Override all colors for print */
|
||||
* {
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
border-color: #000 !important;
|
||||
}
|
||||
|
||||
/* Make sure containers use full width */
|
||||
.container {
|
||||
max-width: 100% !important;
|
||||
padding: 10px !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
#report {
|
||||
max-width: 100% !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
#report * {
|
||||
word-wrap: break-word !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
max-width: 100% !important;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
#report pre {
|
||||
font-size: 10px !important;
|
||||
white-space: pre-wrap !important;
|
||||
word-break: break-all !important;
|
||||
background: #f5f5f5 !important;
|
||||
border: 1px solid #ccc !important;
|
||||
padding: 5px !important;
|
||||
}
|
||||
|
||||
#report table {
|
||||
table-layout: fixed !important;
|
||||
width: 100% !important;
|
||||
border-collapse: collapse !important;
|
||||
}
|
||||
|
||||
#report td, #report th {
|
||||
word-wrap: break-word !important;
|
||||
word-break: break-word !important;
|
||||
overflow-wrap: break-word !important;
|
||||
padding: 4px !important;
|
||||
font-size: 10px !important;
|
||||
border: 1px solid #000 !important;
|
||||
background: white !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.section {
|
||||
background-color: white;
|
||||
color: black;
|
||||
box-shadow: none;
|
||||
border: 1px solid #ccc;
|
||||
page-break-inside: avoid;
|
||||
margin-bottom: 15px !important;
|
||||
padding: 10px !important;
|
||||
border: 1px solid #ccc !important;
|
||||
background: white !important;
|
||||
}
|
||||
.score-circle, .score-inner {
|
||||
background: white;
|
||||
color: black;
|
||||
|
||||
.status {
|
||||
background: #f0f0f0 !important;
|
||||
color: black !important;
|
||||
border: 1px solid #999 !important;
|
||||
padding: 2px 5px !important;
|
||||
}
|
||||
pre, code {
|
||||
background: #f5f5f5;
|
||||
color: black;
|
||||
border: 1px solid #ccc;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
|
||||
.status.good {
|
||||
background: #e8f5e8 !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.status.warning {
|
||||
background: #fff3cd !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: #f8d7da !important;
|
||||
color: black !important;
|
||||
}
|
||||
|
||||
/* Hide buttons in print */
|
||||
button {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Navigation should be hidden */
|
||||
nav {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
h1, h2, h3 {
|
||||
color: black !important;
|
||||
border-bottom: 2px solid black !important;
|
||||
background: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -305,3 +432,202 @@ nav a:hover {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
.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;
|
||||
}
|
||||
|
||||
/* Notification styles */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 6px;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
||||
z-index: 1000;
|
||||
transform: translateX(100%);
|
||||
transition: transform 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.notification.show {
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.notification.success {
|
||||
background: #28a745;
|
||||
}
|
||||
|
||||
.notification.info {
|
||||
background: #17a2b8;
|
||||
}
|
||||
|
||||
.notification.warning {
|
||||
background: #ffc107;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.notification.error {
|
||||
background: #dc3545;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.notification {
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
left: 10px;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.notification.show {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user