Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,11 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/miekg/dns v1.1.61 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
golang.org/x/mod v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/tools v0.22.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)

Expand Down
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL
github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
Expand All @@ -45,6 +47,10 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0=
golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
Expand All @@ -55,6 +61,8 @@ golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -79,6 +87,10 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA=
golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
Expand Down
156 changes: 156 additions & 0 deletions handlers/archives.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package handlers

import (
"encoding/json"
"fmt"
"math"
"net/http"
"net/url"
"strconv"
"time"
)

const archiveAPIURL = "https://web.archive.org/cdx/search/cdx"

func convertTimestampToDate(timestamp string) (time.Time, error) {
mask := "20060102150405"
return time.Parse(mask, timestamp)
}

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]string {
formatToTwoDecimal := func(num float64) string {
return fmt.Sprintf("%.2f", num)
}

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)

if math.IsNaN(dayFactor / float64(totalScans)) {
daysBetweenScans = "0.00"
}
if math.IsNaN(dayFactor / float64(changeCount)) {
daysBetweenChanges = "0.00"
}
if math.IsNaN(float64(totalScans-1) / dayFactor) {
scansPerDay = "0.00"
}
if math.IsNaN(float64(changeCount) / dayFactor) {
changesPerDay = "0.00"
}

return map[string]string{
"daysBetweenScans": daysBetweenScans,
"daysBetweenChanges": daysBetweenChanges,
"scansPerDay": scansPerDay,
"changesPerDay": changesPerDay,
}
}

func getWaybackData(url *url.URL) (map[string]interface{}, error) {
cdxUrl := fmt.Sprintf("%s?url=%s&output=json&fl=timestamp,statuscode,digest,length,offset", archiveAPIURL, url)

client := http.Client{
Timeout: 60 * time.Second,
}

resp, err := client.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
}

if len(data) < 1 {
return nil, fmt.Errorf("data slice is empty")
}

// Remove the header row
data = data[1:]

if len(data) < 1 {
return nil, fmt.Errorf("data slice became empty after removing the first element")
}

// Access the first element of the remaining data
firstScan, err := convertTimestampToDate(data[0][0])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to do a length check, before accessing data[0] and data[len(data)-1] to avoid potential panics?

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) {
rawURL, err := extractURL(r)
if err != nil {
JSONError(w, ErrMissingURLParameter, http.StatusBadRequest)
return
}

data, err := getWaybackData(rawURL)
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)
}
})
}
142 changes: 142 additions & 0 deletions handlers/mail-config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package handlers

import (
"net/http"
"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) {
rawURL, err := extractURL(r)
if err != nil {
JSONError(w, ErrMissingURLParameter, http.StatusBadRequest)
return
}

mxRecords, rcode, err := ResolveMx(rawURL.Hostname())
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(rawURL.Hostname())
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
}
Loading