Skip to content

Commit

Permalink
feat(endpoints): Added new endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
hammad-afzall committed Jun 8, 2024
1 parent 4a8721d commit f8aa1fa
Show file tree
Hide file tree
Showing 15 changed files with 1,485 additions and 0 deletions.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,16 @@ require (
github.com/kr/pretty v0.3.1 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
golang.org/x/mod v0.16.0 // indirect
golang.org/x/tools v0.19.0 // indirect
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
)

require (
github.com/PuerkitoBio/goquery v1.9.2
github.com/aeden/traceroute v0.0.0-20210211061815-03f5f7cb7908
github.com/jarcoal/httpmock v1.3.1
github.com/miekg/dns v1.1.59
github.com/stretchr/testify v1.9.0
golang.org/x/net v0.25.0
golang.org/x/sys v0.20.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g=
github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM=
github.com/miekg/dns v1.1.59 h1:C9EXc/UToRwKLhK5wKU/I4QVsBUc8kE6MkHBkeypWZs=
github.com/miekg/dns v1.1.59/go.mod h1:nZpewl5p6IvctfgrckopVx2OlSEHPRO/U4SYkRklrEk=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
Expand All @@ -53,6 +55,8 @@ 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/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 Down Expand Up @@ -87,6 +91,8 @@ 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/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
171 changes: 171 additions & 0 deletions handlers/archives.go
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)
}
})
}
158 changes: 158 additions & 0 deletions handlers/mail-config.go
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
}
Loading

0 comments on commit f8aa1fa

Please sign in to comment.