-
-
Notifications
You must be signed in to change notification settings - Fork 11
feat(endpoints): Added new endpoints #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
f8aa1fa
4b93794
0e99e95
a234574
b921621
04161fd
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "math" | ||
| "net/http" | ||
| "strconv" | ||
| "strings" | ||
| "time" | ||
| ) | ||
|
|
||
| const archiveAPIURL = "https://web.archive.org/cdx/search/cdx" | ||
|
|
||
| func convertTimestampToDate(timestamp string) (time.Time, error) { | ||
| year, err := strconv.Atoi(timestamp[0:4]) | ||
| if err != nil { | ||
| return time.Time{}, err | ||
| } | ||
| month, err := strconv.Atoi(timestamp[4:6]) | ||
| if err != nil { | ||
| return time.Time{}, err | ||
| } | ||
| day, err := strconv.Atoi(timestamp[6:8]) | ||
| if err != nil { | ||
| return time.Time{}, err | ||
| } | ||
| hour, err := strconv.Atoi(timestamp[8:10]) | ||
| if err != nil { | ||
| return time.Time{}, err | ||
| } | ||
| minute, err := strconv.Atoi(timestamp[10:12]) | ||
| if err != nil { | ||
| return time.Time{}, err | ||
| } | ||
| second, err := strconv.Atoi(timestamp[12:14]) | ||
| if err != nil { | ||
| return time.Time{}, err | ||
| } | ||
| return time.Date(year, time.Month(month), day, hour, minute, second, 0, time.UTC), nil | ||
| } | ||
|
|
||
| func countPageChanges(results [][]string) int { | ||
| prevDigest := "" | ||
| changeCount := -1 | ||
| for _, curr := range results { | ||
| if curr[2] != prevDigest { | ||
| prevDigest = curr[2] | ||
| changeCount++ | ||
| } | ||
| } | ||
| return changeCount | ||
| } | ||
|
|
||
| func getAveragePageSize(scans [][]string) int { | ||
| totalSize := 0 | ||
| for _, scan := range scans { | ||
| size, err := strconv.Atoi(scan[3]) | ||
| if err != nil { | ||
| continue | ||
| } | ||
| totalSize += size | ||
| } | ||
| return totalSize / len(scans) | ||
| } | ||
|
|
||
| func getScanFrequency(firstScan, lastScan time.Time, totalScans, changeCount int) map[string]float64 { | ||
| formatToTwoDecimal := func(num float64) float64 { | ||
|
||
| return math.Round(num*100) / 100 | ||
| } | ||
|
|
||
| dayFactor := lastScan.Sub(firstScan).Hours() / 24 | ||
| daysBetweenScans := formatToTwoDecimal(dayFactor / float64(totalScans)) | ||
| daysBetweenChanges := formatToTwoDecimal(dayFactor / float64(changeCount)) | ||
| scansPerDay := formatToTwoDecimal(float64(totalScans-1) / dayFactor) | ||
| changesPerDay := formatToTwoDecimal(float64(changeCount) / dayFactor) | ||
|
|
||
| // Handle NaN values | ||
| if math.IsNaN(daysBetweenScans) { | ||
| daysBetweenScans = 0 | ||
| } | ||
| if math.IsNaN(daysBetweenChanges) { | ||
| daysBetweenChanges = 0 | ||
| } | ||
| if math.IsNaN(scansPerDay) { | ||
| scansPerDay = 0 | ||
| } | ||
| if math.IsNaN(changesPerDay) { | ||
| changesPerDay = 0 | ||
| } | ||
|
|
||
| return map[string]float64{ | ||
| "daysBetweenScans": daysBetweenScans, | ||
| "daysBetweenChanges": daysBetweenChanges, | ||
| "scansPerDay": scansPerDay, | ||
| "changesPerDay": changesPerDay, | ||
| } | ||
| } | ||
|
|
||
| func getWaybackData(url string) (map[string]interface{}, error) { | ||
| cdxUrl := fmt.Sprintf("%s?url=%s&output=json&fl=timestamp,statuscode,digest,length,offset", archiveAPIURL, url) | ||
|
|
||
| resp, err := http.Get(cdxUrl) | ||
|
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer resp.Body.Close() | ||
|
|
||
| var data [][]string | ||
| err = json.NewDecoder(resp.Body).Decode(&data) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| if len(data) <= 1 { | ||
| return map[string]interface{}{ | ||
| "skipped": "Site has never before been archived via the Wayback Machine", | ||
| }, nil | ||
| } | ||
|
|
||
| // Remove the header row | ||
| data = data[1:] | ||
|
|
||
| firstScan, err := convertTimestampToDate(data[0][0]) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we need to do a length check, before accessing |
||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| lastScan, err := convertTimestampToDate(data[len(data)-1][0]) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
| totalScans := len(data) | ||
| changeCount := countPageChanges(data) | ||
|
|
||
| return map[string]interface{}{ | ||
| "firstScan": firstScan.Format(time.RFC3339), | ||
| "lastScan": lastScan.Format(time.RFC3339), | ||
| "totalScans": totalScans, | ||
| "changeCount": changeCount, | ||
| "averagePageSize": getAveragePageSize(data), | ||
| "scanFrequency": getScanFrequency(firstScan, lastScan, totalScans, changeCount), | ||
| "scans": data, | ||
| "scanUrl": url, | ||
| }, nil | ||
| } | ||
|
|
||
| func HandleArchives() http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| urlParam := r.URL.Query().Get("url") | ||
| if urlParam == "" { | ||
| http.Error(w, "missing 'url' parameter", http.StatusBadRequest) | ||
|
||
| return | ||
| } | ||
|
|
||
| if !strings.HasPrefix(urlParam, "http://") && !strings.HasPrefix(urlParam, "https://") { | ||
| urlParam = "http://" + urlParam | ||
| } | ||
|
|
||
| data, err := getWaybackData(urlParam) | ||
| if err != nil { | ||
| http.Error(w, fmt.Sprintf("Error fetching Wayback data: %v", err), http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| w.Header().Set("Content-Type", "application/json") | ||
| err = json.NewEncoder(w).Encode(data) | ||
| if err != nil { | ||
| http.Error(w, fmt.Sprintf("Error encoding response: %v", err), http.StatusInternalServerError) | ||
| } | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,158 @@ | ||
| package handlers | ||
|
|
||
| import ( | ||
| "errors" | ||
| "net/http" | ||
| "net/url" | ||
| "strings" | ||
|
|
||
| "github.com/miekg/dns" | ||
| ) | ||
|
|
||
| func ResolveMx(domain string) ([]*dns.MX, int, error) { | ||
| c := new(dns.Client) | ||
| m := new(dns.Msg) | ||
| m.SetQuestion(dns.Fqdn(domain), dns.TypeMX) | ||
| r, _, err := c.Exchange(m, "8.8.8.8:53") | ||
| if err != nil { | ||
| return nil, dns.RcodeServerFailure, err | ||
| } | ||
| if r.Rcode != dns.RcodeSuccess { | ||
| return nil, r.Rcode, &dns.Error{} | ||
| } | ||
| var mxRecords []*dns.MX | ||
| for _, ans := range r.Answer { | ||
| if mx, ok := ans.(*dns.MX); ok { | ||
| mxRecords = append(mxRecords, mx) | ||
| } | ||
| } | ||
| if len(mxRecords) == 0 { | ||
| return nil, dns.RcodeNameError, nil | ||
| } | ||
| return mxRecords, dns.RcodeSuccess, nil | ||
| } | ||
|
|
||
| func ResolveTxt(domain string) ([]string, int, error) { | ||
| c := new(dns.Client) | ||
| m := new(dns.Msg) | ||
| m.SetQuestion(dns.Fqdn(domain), dns.TypeTXT) | ||
| r, _, err := c.Exchange(m, "8.8.8.8:53") | ||
| if err != nil { | ||
| return nil, dns.RcodeServerFailure, err | ||
| } | ||
| if r.Rcode != dns.RcodeSuccess { | ||
| return nil, r.Rcode, &dns.Error{} | ||
| } | ||
| var txtRecords []string | ||
| for _, ans := range r.Answer { | ||
| if txt, ok := ans.(*dns.TXT); ok { | ||
| txtRecords = append(txtRecords, txt.Txt...) | ||
| } | ||
| } | ||
| if len(txtRecords) == 0 { | ||
| return nil, dns.RcodeNameError, nil | ||
| } | ||
| return txtRecords, dns.RcodeSuccess, nil | ||
| } | ||
|
|
||
| func HandleMailConfig() http.Handler { | ||
| return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| urlParam := r.URL.Query().Get("url") | ||
| if urlParam == "" { | ||
| JSONError(w, errors.New("URL parameter is required"), http.StatusBadRequest) | ||
| return | ||
| } | ||
|
|
||
| if !strings.HasPrefix(urlParam, "http://") && !strings.HasPrefix(urlParam, "https://") { | ||
| urlParam = "http://" + urlParam | ||
| } | ||
|
|
||
| parsedURL, err := url.Parse(urlParam) | ||
| if err != nil { | ||
| JSONError(w, errors.New("Invalid URL"), http.StatusBadRequest) | ||
| return | ||
| } | ||
| domain := parsedURL.Hostname() | ||
| if domain == "" { | ||
| domain = parsedURL.Path | ||
| } | ||
|
|
||
| mxRecords, rcode, err := ResolveMx(domain) | ||
| if err != nil { | ||
| JSONError(w, err, http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| if rcode == dns.RcodeNameError || rcode == dns.RcodeServerFailure { | ||
| JSON(w, map[string]string{"skipped": "No mail server in use on this domain"}, http.StatusOK) | ||
| return | ||
| } | ||
|
|
||
| txtRecords, rcode, err := ResolveTxt(domain) | ||
| if err != nil { | ||
| JSONError(w, err, http.StatusInternalServerError) | ||
| return | ||
| } | ||
|
|
||
| if rcode == dns.RcodeNameError || rcode == dns.RcodeServerFailure { | ||
| JSON(w, map[string]string{"skipped": "No mail server in use on this domain"}, http.StatusOK) | ||
| return | ||
| } | ||
|
|
||
| emailTxtRecords := filterEmailTxtRecords(txtRecords) | ||
| mailServices := identifyMailServices(emailTxtRecords, mxRecords) | ||
|
|
||
| JSON(w, map[string]interface{}{ | ||
| "mxRecords": mxRecords, | ||
| "txtRecords": emailTxtRecords, | ||
| "mailServices": mailServices, | ||
| }, http.StatusOK) | ||
| }) | ||
| } | ||
|
|
||
| func filterEmailTxtRecords(records []string) []string { | ||
| var emailTxtRecords []string | ||
| for _, record := range records { | ||
| if strings.HasPrefix(record, "v=spf1") || | ||
| strings.HasPrefix(record, "v=DKIM1") || | ||
| strings.HasPrefix(record, "v=DMARC1") || | ||
| strings.HasPrefix(record, "protonmail-verification=") || | ||
| strings.HasPrefix(record, "google-site-verification=") || | ||
| strings.HasPrefix(record, "MS=") || | ||
| strings.HasPrefix(record, "zoho-verification=") || | ||
| strings.HasPrefix(record, "titan-verification=") || | ||
| strings.Contains(record, "bluehost.com") { | ||
| emailTxtRecords = append(emailTxtRecords, record) | ||
| } | ||
| } | ||
| return emailTxtRecords | ||
| } | ||
|
|
||
| func identifyMailServices(emailTxtRecords []string, mxRecords []*dns.MX) []map[string]string { | ||
| var mailServices []map[string]string | ||
| for _, record := range emailTxtRecords { | ||
| if strings.HasPrefix(record, "protonmail-verification=") { | ||
| mailServices = append(mailServices, map[string]string{"provider": "ProtonMail", "value": strings.Split(record, "=")[1]}) | ||
| } else if strings.HasPrefix(record, "google-site-verification=") { | ||
| mailServices = append(mailServices, map[string]string{"provider": "Google Workspace", "value": strings.Split(record, "=")[1]}) | ||
| } else if strings.HasPrefix(record, "MS=") { | ||
| mailServices = append(mailServices, map[string]string{"provider": "Microsoft 365", "value": strings.Split(record, "=")[1]}) | ||
| } else if strings.HasPrefix(record, "zoho-verification=") { | ||
| mailServices = append(mailServices, map[string]string{"provider": "Zoho", "value": strings.Split(record, "=")[1]}) | ||
| } else if strings.HasPrefix(record, "titan-verification=") { | ||
| mailServices = append(mailServices, map[string]string{"provider": "Titan", "value": strings.Split(record, "=")[1]}) | ||
| } else if strings.Contains(record, "bluehost.com") { | ||
| mailServices = append(mailServices, map[string]string{"provider": "BlueHost", "value": record}) | ||
| } | ||
| } | ||
|
|
||
| for _, mx := range mxRecords { | ||
| if strings.Contains(mx.Mx, "yahoodns.net") { | ||
| mailServices = append(mailServices, map[string]string{"provider": "Yahoo", "value": mx.Mx}) | ||
| } else if strings.Contains(mx.Mx, "mimecast.com") { | ||
| mailServices = append(mailServices, map[string]string{"provider": "Mimecast", "value": mx.Mx}) | ||
| } | ||
| } | ||
|
|
||
| return mailServices | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's a small thing, but I'd probably put the minuteIndex, hourIndex, dayIndex, etc as variables, to avoid magic numbers. Because, for example
hour, err := strconv.Atoi(timestamp[8:10])is a bit hard to read.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The golang way :)