-
-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(endpoints): Added new endpoints
- Loading branch information
1 parent
4a8721d
commit f8aa1fa
Showing
15 changed files
with
1,485 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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]) | ||
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) | ||
} | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
Oops, something went wrong.