diff --git a/main.go b/main.go index 3445b03..3ee8dfa 100644 --- a/main.go +++ b/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. 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 ._domainkey. 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) diff --git a/parser/handler.go b/parser/handler.go new file mode 100644 index 0000000..ac7868a --- /dev/null +++ b/parser/handler.go @@ -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) +} diff --git a/passwordgenerator/api.go b/passwordgenerator/api.go new file mode 100644 index 0000000..40b9fb3 --- /dev/null +++ b/passwordgenerator/api.go @@ -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) +} diff --git a/passwordgenerator/generator.go b/passwordgenerator/generator.go index f8b3394..cb4dfde 100644 --- a/passwordgenerator/generator.go +++ b/passwordgenerator/generator.go @@ -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 { diff --git a/passwordgenerator/handler.go b/passwordgenerator/handler.go new file mode 100644 index 0000000..fcae9ca --- /dev/null +++ b/passwordgenerator/handler.go @@ -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 +} diff --git a/resolver/dns_api.go b/resolver/dns_api.go new file mode 100644 index 0000000..6748847 --- /dev/null +++ b/resolver/dns_api.go @@ -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 +} diff --git a/resolver/handler.go b/resolver/handler.go new file mode 100644 index 0000000..66b2aae --- /dev/null +++ b/resolver/handler.go @@ -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) +} diff --git a/web/base.html b/web/base.html new file mode 100644 index 0000000..5b1f1c9 --- /dev/null +++ b/web/base.html @@ -0,0 +1,29 @@ + + + + + + {{block "title" .}}HeaderAnalyzer{{end}} + + + {{block "head" .}}{{end}} + + + + +
+ {{block "content" .}} +
+

HeaderAnalyzer

+

Welcome to HeaderAnalyzer - your tool for email header analysis, DNS tools, and password generation.

+
+ {{end}} +
+ + {{block "scripts" .}}{{end}} + + diff --git a/web/dns.html b/web/dns.html index 3a9ff52..6a405ce 100644 --- a/web/dns.html +++ b/web/dns.html @@ -1,26 +1,21 @@ - - - - DNS Tools - - - - - - +{{template "base.html" .}} + +{{define "title"}}DNS Tools - HeaderAnalyzer{{end}} + +{{define "head"}} + +{{end}} + +{{define "content"}}

DNS Tools

@@ -48,45 +43,102 @@
+{{end}} + +{{define "scripts"}} - - +{{end}} diff --git a/web/headeranalyzer.html b/web/headeranalyzer.html new file mode 100644 index 0000000..4863b68 --- /dev/null +++ b/web/headeranalyzer.html @@ -0,0 +1,710 @@ +{{template "base.html" .}} + +{{define "title"}}Email Header Analyzer{{end}} + +{{define "head"}} + + + +{{end}} + +{{define "content"}} +
+

Email Header Analyzer

+ {{if not .From}} + + +
+ + + {{end}} + + {{if .From}} +
+
+
+

Sender Identification

+
+
+

Envelope Sender (Return-Path): {{.EnvelopeSender}}

+

From Domain: {{.FromDomain}}

+

Sending Server: {{.SendingServer}}

+
+
+ SPF {{if .SPFPass}}✓{{else}}✗{{end}} + DMARC {{if .DMARCPass}}✓{{else}}✗{{end}} + DKIM {{if .DKIMPass}}✓{{else}}✗{{end}} + Encrypted {{if .Encrypted}}✓{{else}}✗{{end}} + {{if .Blacklists}} + Blacklisted {{len .Blacklists}} times + {{else}} + Not listed on major blacklists + {{end}} +
+
+ {{if .SenderRep}} +
+ Sender Reputation:
+ {{.SenderRep}} +
+
+ {{end}} +
+ + Envelope Sender is the real sender used for delivery (can differ from From).
+ From Domain is the domain shown to the recipient.
+ Sending Server is the host or IP that actually sent the message (from first Received header).
+ If these differ, the message may be sent on behalf of another user or via a third-party service. +
+
+
+
+
+ All Email Headers Table +
+ +
+
+ + + + + + + + + {{range $k, $v := .AllHeaders}} + + + + + {{end}} + +
Header NameValue
{{$k}}{{$v}}
+
+
+ +
+

Basic Information

+
+
+

From: {{.From}}

+

To: {{.To}}

+

Subject: {{.Subject}}

+

Date: {{.Date}}

+ {{if .ReplyTo}}

Reply-To: {{.ReplyTo}}

{{end}} +
+
+

Message-ID: {{.MessageID}}

+

Priority: {{.Priority}}

+

Content Type: {{.ContentType}}

+

Encoding: {{.Encoding}}

+
+
+
+ +
+

Mail Flow

+
    + {{range .Received}}
  • {{.}}
  • {{end}} +
+
+ + {{if ne .DeliveryDelay "Insufficient data for delay analysis"}} +
+

Delivery Analysis

+

Delivery Timing: {{.DeliveryDelay}}

+ {{if .GeoLocation}}

Geographic Info: {{.GeoLocation}}

{{end}} +
+ {{end}} + +
+

Security Analysis

+
+
+

SPF Authentication

+
+ {{if .SPFPass}}✓ Passed{{else}}✗ Failed{{end}} +
+

{{.SPFDetails}}

+ {{if .SPFRecord}}
{{.SPFRecord}}
{{end}} + {{if .SPFHeader}} +
Show SPF Header
{{.SPFHeader}}
+ {{end}} +
+ +
+

DMARC Policy

+
+ {{if .DMARCPass}}✓ Passed{{else}}✗ Failed{{end}} +
+

{{.DMARCDetails}}

+ {{if .DMARCRecord}}
{{.DMARCRecord}}
{{end}} + {{if .DMARCHeader}} +
Show DMARC Header
{{.DMARCHeader}}
+ {{end}} +
+ +
+

DKIM Signature

+
+ {{if .DKIMPass}}✓ Present{{else}}✗ Missing{{end}} +
+

{{.DKIMDetails}}

+ {{if .DKIM}} +
Show DKIM Header
{{.DKIM}}
+ {{else if .DKIMHeader}} +
Show DKIM Header
{{.DKIMHeader}}
+ {{end}} +
+
+
+ +
+

Encryption

+
+ {{if .Encrypted}}Encrypted (TLS){{else}}Not Encrypted{{end}} +
+
Show Encryption Details
{{.EncryptionDetail}}
+
+ + {{if .Warnings}} +
+

Warnings

+
    + {{range .Warnings}}
  • ⚠️ {{.}}
  • {{end}} +
+
+ {{end}} + + {{if .SecurityFlags}} +
+

Security Flags

+
    + {{range .SecurityFlags}}
  • 🔒 {{.}}
  • {{end}} +
+
+ {{end}} + + {{if .Blacklists}} +
+

Blacklist Status

+
+ Checked: + {{if .SendingServer}}IP {{.SendingServer}}{{else if .FromDomain}}Domain {{.FromDomain}}{{end}} +
+
⚠️ Listed on the following blacklists:
+
    + {{range .Blacklists}}
  • {{.}}
  • {{end}} +
+
+ {{end}} + + + {{if .SpamFlags}} +
+

Spam Analysis

+
+ {{if gt (len .SpamFlags) 0}}⚠️ Spam Indicators Found{{else}}✓ No Spam Indicators{{end}} +
+ {{if .SpamScore}}

Spam Score: {{.SpamScore}}

{{end}} + {{if .SpamFlags}} +
    + {{range .SpamFlags}}
  • {{.}}
  • {{end}} +
+ {{end}} +
+ {{end}} + + {{if ne .VirusInfo "No virus scanning information found"}} +
+

Virus Scanning

+
🛡️ Virus Scanning Information
+

{{.VirusInfo}}

+
+ {{end}} + + {{if .PhishingRisk}} +
+

Security Risk Assessment

+
+
+

Phishing Risk

+
+ {{.PhishingRisk}} +
+
+
+

Spoofing Risk

+
+ {{.SpoofingRisk}} +
+
+
+
+ {{end}} + + {{if .ListInfo}} +
+

Mailing List Information

+
    + {{range .ListInfo}}
  • {{.}}
  • {{end}} +
+ {{if .AutoReply}}

📧 Auto-reply message detected

{{end}} + {{if .BulkEmail}}

📬 Bulk/marketing email detected

{{end}} +
+ {{end}} + + {{if .Compliance}} +
+

Compliance Information

+
    + {{range .Compliance}}
  • ✓ {{.}}
  • {{end}} +
+
+ {{end}} + + {{if .ARC}} +
+

ARC (Authenticated Received Chain)

+
Show ARC Headers +
    + {{range .ARC}}
  • {{.}}
  • {{end}} +
+
+
+ {{end}} + + {{if ne .BIMI "No BIMI record found"}} +
+

Brand Indicators (BIMI)

+

{{.BIMI}}

+
+ {{end}} + + {{if .Attachments}} +
+

Attachment Information

+
    + {{range .Attachments}}
  • {{.}}
  • {{end}} +
+
+ {{end}} + + {{if .URLs}} +
+

URL Information

+
    + {{range .URLs}}
  • {{.}}
  • {{end}} +
+
+ {{end}} + + {{if ne .ThreadInfo "No threading information available"}} +
+

Message Threading

+
Show Threading Information +
{{.ThreadInfo}}
+
+
+ {{end}} + + +
+ + + +
+
+ {{end}} +
+{{end}} + +{{define "scripts"}} + +{{end}} diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 930fadb..0000000 --- a/web/index.html +++ /dev/null @@ -1,374 +0,0 @@ - - - - Email Header Analyzer - - - - - - - -
-

Email Header Analyzer

- {{if not .From}} -
- -
- -
- {{end}} - - {{if .From}} -
-
-
-

Sender Identification

-
-
-

Envelope Sender (Return-Path): {{.EnvelopeSender}}

-

From Domain: {{.FromDomain}}

-

Sending Server: {{.SendingServer}}

-
-
- SPF {{if .SPFPass}}✓{{else}}✗{{end}} - DMARC {{if .DMARCPass}}✓{{else}}✗{{end}} - DKIM {{if .DKIMPass}}✓{{else}}✗{{end}} - Encrypted {{if .Encrypted}}✓{{else}}✗{{end}} - {{if .Blacklists}} - Blacklisted {{len .Blacklists}} times - {{else}} - Not listed on major blacklists - {{end}} -
-
- {{if .SenderRep}} -
- Sender Reputation:
- {{.SenderRep}} -
-
- {{end}} -
- - Envelope Sender is the real sender used for delivery (can differ from From).
- From Domain is the domain shown to the recipient.
- Sending Server is the host or IP that actually sent the message (from first Received header).
- If these differ, the message may be sent on behalf of another user or via a third-party service. -
-
-
-
-
- All Email Headers Table -
- -
-
- - - - - - - - - {{range $k, $v := .AllHeaders}} - - - - - {{end}} - -
Header NameValue
{{$k}}{{$v}}
-
-
- -
-

Basic Information

-
-
-

From: {{.From}}

-

To: {{.To}}

-

Subject: {{.Subject}}

-

Date: {{.Date}}

- {{if .ReplyTo}}

Reply-To: {{.ReplyTo}}

{{end}} -
-
-

Message-ID: {{.MessageID}}

-

Priority: {{.Priority}}

-

Content Type: {{.ContentType}}

-

Encoding: {{.Encoding}}

-
-
-
- -
-

Mail Flow

-
    - {{range .Received}}
  • {{.}}
  • {{end}} -
-
- - {{if ne .DeliveryDelay "Insufficient data for delay analysis"}} -
-

Delivery Analysis

-

Delivery Timing: {{.DeliveryDelay}}

- {{if .GeoLocation}}

Geographic Info: {{.GeoLocation}}

{{end}} -
- {{end}} - -
-

Security Analysis

-
-
-

SPF Authentication

-
- {{if .SPFPass}}✓ Passed{{else}}✗ Failed{{end}} -
-

{{.SPFDetails}}

- {{if .SPFRecord}}
{{.SPFRecord}}
{{end}} - {{if .SPFHeader}} -
Show SPF Header
{{.SPFHeader}}
- {{end}} -
- -
-

DMARC Policy

-
- {{if .DMARCPass}}✓ Passed{{else}}✗ Failed{{end}} -
-

{{.DMARCDetails}}

- {{if .DMARCRecord}}
{{.DMARCRecord}}
{{end}} - {{if .DMARCHeader}} -
Show DMARC Header
{{.DMARCHeader}}
- {{end}} -
- -
-

DKIM Signature

-
- {{if .DKIMPass}}✓ Present{{else}}✗ Missing{{end}} -
-

{{.DKIMDetails}}

- {{if .DKIM}} -
Show DKIM Header
{{.DKIM}}
- {{else if .DKIMHeader}} -
Show DKIM Header
{{.DKIMHeader}}
- {{end}} -
-
-
- -
-

Encryption

-
- {{if .Encrypted}}Encrypted (TLS){{else}}Not Encrypted{{end}} -
-
Show Encryption Details
{{.EncryptionDetail}}
-
- - {{if .Warnings}} -
-

Warnings

-
    - {{range .Warnings}}
  • ⚠️ {{.}}
  • {{end}} -
-
- {{end}} - - {{if .SecurityFlags}} -
-

Security Flags

-
    - {{range .SecurityFlags}}
  • 🔒 {{.}}
  • {{end}} -
-
- {{end}} - - {{if .Blacklists}} -
-

Blacklist Status

-
- Checked: - {{if .SendingServer}}IP {{.SendingServer}}{{else if .FromDomain}}Domain {{.FromDomain}}{{end}} -
-
⚠️ Listed on the following blacklists:
-
    - {{range .Blacklists}}
  • {{.}}
  • {{end}} -
-
- {{end}} - - - {{if .SpamFlags}} -
-

Spam Analysis

-
- {{if gt (len .SpamFlags) 0}}⚠️ Spam Indicators Found{{else}}✓ No Spam Indicators{{end}} -
- {{if .SpamScore}}

Spam Score: {{.SpamScore}}

{{end}} - {{if .SpamFlags}} -
    - {{range .SpamFlags}}
  • {{.}}
  • {{end}} -
- {{end}} -
- {{end}} - - {{if ne .VirusInfo "No virus scanning information found"}} -
-

Virus Scanning

-
🛡️ Virus Scanning Information
-

{{.VirusInfo}}

-
- {{end}} - - {{if .PhishingRisk}} -
-

Security Risk Assessment

-
-
-

Phishing Risk

-
- {{.PhishingRisk}} -
-
-
-

Spoofing Risk

-
- {{.SpoofingRisk}} -
-
-
-
- {{end}} - - {{if .ListInfo}} -
-

Mailing List Information

-
    - {{range .ListInfo}}
  • {{.}}
  • {{end}} -
- {{if .AutoReply}}

📧 Auto-reply message detected

{{end}} - {{if .BulkEmail}}

📬 Bulk/marketing email detected

{{end}} -
- {{end}} - - {{if .Compliance}} -
-

Compliance Information

-
    - {{range .Compliance}}
  • ✓ {{.}}
  • {{end}} -
-
- {{end}} - - {{if .ARC}} -
-

ARC (Authenticated Received Chain)

-
Show ARC Headers -
    - {{range .ARC}}
  • {{.}}
  • {{end}} -
-
-
- {{end}} - - {{if ne .BIMI "No BIMI record found"}} -
-

Brand Indicators (BIMI)

-

{{.BIMI}}

-
- {{end}} - - {{if .Attachments}} -
-

Attachment Information

-
    - {{range .Attachments}}
  • {{.}}
  • {{end}} -
-
- {{end}} - - {{if .URLs}} -
-

URL Information

-
    - {{range .URLs}}
  • {{.}}
  • {{end}} -
-
- {{end}} - - {{if ne .ThreadInfo "No threading information available"}} -
-

Message Threading

-
Show Threading Information -
{{.ThreadInfo}}
-
-
- {{end}} - - -
- - -
-
- {{end}} - - - - diff --git a/web/password.html b/web/password.html index 04a17fc..7117d2a 100644 --- a/web/password.html +++ b/web/password.html @@ -1,664 +1,571 @@ - - - - - - Password Generator - HeaderAnalyzer - - - - - - -
-

🔐 Password Generator

- -
- - -
- -
-
Click "Generate Password" to create a secure password
- -
- - - -
-
-

🔧 Basic Settings

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
+ +
+ +
- -
-

🎯 Advanced Settings

- -
-
- - -
- -
- - -
- -
- - -
- -
- - -
-
- -
- -
Generate a password to see strength
-
- -
- -
Loading...
-
- -
- - -
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
-