-
-
Notifications
You must be signed in to change notification settings - Fork 5
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
feat(endpoints): Added new endpoints #16
base: main
Are you sure you want to change the base?
Changes from 2 commits
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 { | ||
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. can use fmt.Sprintf("%.2f", num) |
||
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) | ||
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. Sometimes requests just hang, so we should include a timeout to prevent that |
||
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]) | ||
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) | ||
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. Don't forget, if there's an error anywhere, we need to still respond with JSON, like |
||
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 :)