first commit

This commit is contained in:
2021-11-10 20:06:12 +01:00
commit 2fcfa91545
29 changed files with 2724 additions and 0 deletions

101
router/generic.go Normal file
View File

@ -0,0 +1,101 @@
package router
import (
"net"
"net/http"
"path/filepath"
"regexp"
"strings"
"github.com/dcarrillo/whatismyip/internal/httputils"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/dcarrillo/whatismyip/service"
"github.com/gin-gonic/gin"
)
const userAgentPattern = `curl|wget|libwww-perl|python|ansible-httpget|HTTPie|WindowsPowerShell|http_request|Go-http-client|^$`
type JSONResponse struct {
IP string `json:"ip"`
IPVersion byte `json:"ip_version"`
ClientPort string `json:"client_port"`
Country string `json:"country"`
CountryCode string `json:"country_code"`
City string `json:"city"`
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
PostalCode string `json:"postal_code"`
TimeZone string `json:"time_zone"`
ASN uint `json:"asn"`
ASNOrganization string `json:"asn_organization"`
Host string `json:"host"`
Headers http.Header `json:"headers"`
}
func getRoot(ctx *gin.Context) {
reg := regexp.MustCompile(userAgentPattern)
if reg.Match([]byte(ctx.Request.UserAgent())) {
ctx.String(http.StatusOK, ctx.ClientIP())
} else {
name := "home"
if setting.App.TemplatePath != "" {
name = filepath.Base(setting.App.TemplatePath)
}
ctx.HTML(http.StatusOK, name, jsonOutput(ctx))
}
}
func getClientPortAsString(ctx *gin.Context) {
ctx.String(http.StatusOK, strings.Split(ctx.Request.RemoteAddr, ":")[1]+"\n")
}
func getAllAsString(ctx *gin.Context) {
output := "IP: " + ctx.ClientIP() + "\n"
output += "Client Port: " + strings.Split(ctx.Request.RemoteAddr, ":")[1] + "\n"
r := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
if record := r.LookUpCity(); record != nil {
output += geoCityRecordToString(record) + "\n"
}
if record := r.LookUpASN(); record != nil {
output += geoASNRecordToString(record) + "\n"
}
h := ctx.Request.Header
h["Host"] = []string{ctx.Request.Host}
output += httputils.HeadersToSortedString(h)
ctx.String(http.StatusOK, output)
}
func getJSON(ctx *gin.Context) {
ctx.JSON(http.StatusOK, jsonOutput(ctx))
}
func jsonOutput(ctx *gin.Context) JSONResponse {
ip := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
asnRecord := ip.LookUpASN()
cityRecord := ip.LookUpCity()
var version byte = 4
if p := net.ParseIP(ctx.ClientIP()).To4(); p == nil {
version = 6
}
return JSONResponse{
IP: ctx.ClientIP(),
IPVersion: version,
ClientPort: strings.Split(ctx.Request.RemoteAddr, ":")[1],
Country: cityRecord.Country.Names["en"],
CountryCode: cityRecord.Country.ISOCode,
City: cityRecord.City.Names["en"],
Latitude: cityRecord.Location.Latitude,
Longitude: cityRecord.Location.Longitude,
PostalCode: cityRecord.Postal.Code,
TimeZone: cityRecord.Location.TimeZone,
ASN: asnRecord.AutonomousSystemNumber,
ASNOrganization: asnRecord.AutonomousSystemOrganization,
Host: ctx.Request.Host,
Headers: ctx.Request.Header,
}
}

120
router/generic_test.go Normal file
View File

@ -0,0 +1,120 @@
package router
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestIP4RootFromCli(t *testing.T) {
uas := []string{"", "curl", "wget", "libwww-perl", "python", "ansible-httpget", "HTTPie", "WindowsPowerShell", "http_request", "Go-http-client"}
req, _ := http.NewRequest("GET", "/", nil)
req.Header.Set("X-Real-IP", testIP.ipv4)
for _, ua := range uas {
req.Header.Set("User-Agent", ua)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, testIP.ipv4, w.Body.String())
}
}
func TestHost(t *testing.T) {
req, _ := http.NewRequest("GET", "/host", nil)
req.Host = "test"
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, "test", w.Body.String())
}
func TestClientPort(t *testing.T) {
req, _ := http.NewRequest("GET", "/client-port", nil)
req.RemoteAddr = testIP.ipv4 + ":" + "1000"
req.Header.Set("X-Real-IP", testIP.ipv4)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, "1000\n", w.Body.String())
}
func TestNotFound(t *testing.T) {
req, _ := http.NewRequest("GET", "/not-found", nil)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 404, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, "Not Found", w.Body.String())
}
func TestJSON(t *testing.T) {
expectedIPv4 := `{"client_port":"1000","ip":"81.2.69.192","ip_version":4,"country":"United Kingdom","country_code":"GB","city":"London","latitude":51.5142,"longitude":-0.0931,"postal_code":"","time_zone":"Europe/London","asn":0,"asn_organization":"","host":"test","headers":{"X-Real-Ip":["81.2.69.192"]}}`
expectedIPv6 := `{"asn":3352, "asn_organization":"TELEFONICA DE ESPANA", "city":"", "client_port":"9000", "country":"", "country_code":"", "headers":{"X-Real-Ip":["2a02:9000::1"]}, "host":"test", "ip":"2a02:9000::1", "ip_version":6, "latitude":0, "longitude":0, "postal_code":"", "time_zone":""}`
req, _ := http.NewRequest("GET", "/json", nil)
req.RemoteAddr = testIP.ipv4 + ":" + "1000"
req.Host = "test"
req.Header.Set("X-Real-IP", testIP.ipv4)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.json, w.Header().Get("Content-Type"))
assert.JSONEq(t, expectedIPv4, w.Body.String())
req.RemoteAddr = testIP.ipv6 + ":" + "1000"
req.Host = "test"
req.Header.Set("X-Real-IP", testIP.ipv6)
w = httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.json, w.Header().Get("Content-Type"))
assert.JSONEq(t, expectedIPv6, w.Body.String())
}
func TestAll(t *testing.T) {
expected := `IP: 81.2.69.192
Client Port: 1000
City: London
Country: United Kingdom
Country Code: GB
Latitude: 51.514200
Longitude: -0.093100
Postal Code:
Time Zone: Europe/London
ASN Number: 0
ASN Organization:
Header1: one
Host: test
X-Real-Ip: 81.2.69.192
`
req, _ := http.NewRequest("GET", "/all", nil)
req.RemoteAddr = testIP.ipv4 + ":" + "1000"
req.Host = "test"
req.Header.Set("X-Real-IP", testIP.ipv4)
req.Header.Set("Header1", "one")
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, expected, w.Body.String())
}

143
router/geo.go Normal file
View File

@ -0,0 +1,143 @@
package router
import (
"fmt"
"net"
"net/http"
"sort"
"strings"
"github.com/dcarrillo/whatismyip/models"
"github.com/dcarrillo/whatismyip/service"
"github.com/gin-gonic/gin"
)
type geoDataFormatter struct {
title string
format func(*models.GeoRecord) string
}
type asnDataFormatter struct {
title string
format func(*models.ASNRecord) string
}
var geoOutput = map[string]geoDataFormatter{
"country": {
title: "Country",
format: func(record *models.GeoRecord) string {
return record.Country.Names["en"]
},
},
"country_code": {
title: "Country Code",
format: func(record *models.GeoRecord) string {
return record.Country.ISOCode
},
},
"city": {
title: "City",
format: func(record *models.GeoRecord) string {
return record.City.Names["en"]
},
},
"latitude": {
title: "Latitude",
format: func(record *models.GeoRecord) string {
return fmt.Sprintf("%f", record.Location.Latitude)
},
},
"longitude": {
title: "Longitude",
format: func(record *models.GeoRecord) string {
return fmt.Sprintf("%f", record.Location.Longitude)
},
},
"postal_code": {
title: "Postal Code",
format: func(record *models.GeoRecord) string {
return record.Postal.Code
},
},
"time_zone": {
title: "Time Zone",
format: func(record *models.GeoRecord) string {
return record.Location.TimeZone
},
},
}
var asnOutput = map[string]asnDataFormatter{
"number": {
title: "ASN Number",
format: func(record *models.ASNRecord) string {
return fmt.Sprintf("%d", record.AutonomousSystemNumber)
},
},
"organization": {
title: "ASN Organization",
format: func(record *models.ASNRecord) string {
return record.AutonomousSystemOrganization
},
},
}
func getGeoAsString(ctx *gin.Context) {
field := strings.ToLower(ctx.Params.ByName("field"))
ip := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
record := ip.LookUpCity()
if field == "" {
ctx.String(http.StatusOK, geoCityRecordToString(record))
} else if g, ok := geoOutput[field]; ok {
ctx.String(http.StatusOK, g.format(record))
} else {
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
}
func getASNAsString(ctx *gin.Context) {
field := strings.ToLower(ctx.Params.ByName("field"))
ip := service.Geo{IP: net.ParseIP(ctx.ClientIP())}
record := ip.LookUpASN()
if field == "" {
ctx.String(http.StatusOK, geoASNRecordToString(record))
} else if g, ok := asnOutput[field]; ok {
ctx.String(http.StatusOK, g.format(record))
} else {
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
}
func geoCityRecordToString(record *models.GeoRecord) string {
var output string
keys := make([]string, 0, len(geoOutput))
for k := range geoOutput {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
output += fmt.Sprintf("%s: %v\n", geoOutput[k].title, geoOutput[k].format(record))
}
return output
}
func geoASNRecordToString(record *models.ASNRecord) string {
var output string
keys := make([]string, 0, len(asnOutput))
for k := range asnOutput {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
output += fmt.Sprintf("%s: %v\n", asnOutput[k].title, asnOutput[k].format(record))
}
return output
}

106
router/geo_test.go Normal file
View File

@ -0,0 +1,106 @@
package router
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestGeo(t *testing.T) {
expected := `City: London
Country: United Kingdom
Country Code: GB
Latitude: 51.514200
Longitude: -0.093100
Postal Code:
Time Zone: Europe/London
`
req, _ := http.NewRequest("GET", "/geo", nil)
req.Header.Set("X-Real-IP", testIP.ipv4)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, expected, w.Body.String())
}
func TestGeoField(t *testing.T) {
req, _ := http.NewRequest("GET", "/geo/latitude", nil)
req.Header.Set("X-Real-IP", testIP.ipv4)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, "51.514200", w.Body.String())
}
func TestGeoField404(t *testing.T) {
req, _ := http.NewRequest("GET", "/geo/not-found", nil)
req.Header.Set("X-Real-IP", testIP.ipv4)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 404, w.Code)
}
func TestASN(t *testing.T) {
expected := `ASN Number: 12552
ASN Organization: IP-Only
`
req, _ := http.NewRequest("GET", "/asn", nil)
req.Header.Set("X-Real-IP", testIP.ipv4ASN)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, expected, w.Body.String())
}
func TestASNField(t *testing.T) {
req, _ := http.NewRequest("GET", "/asn/organization", nil)
req.Header.Set("X-Real-IP", testIP.ipv4ASN)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, "IP-Only", w.Body.String())
}
func TestASNField404(t *testing.T) {
req, _ := http.NewRequest("GET", "/asn/not-found", nil)
req.Header.Set("X-Real-IP", testIP.ipv4ASN)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 404, w.Code)
}
func TestASN_IPv6(t *testing.T) {
expected := `ASN Number: 6739
ASN Organization: Cableuropa - ONO
`
req, _ := http.NewRequest("GET", "/asn", nil)
req.Header.Set("X-Real-IP", testIP.ipv6ASN)
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, expected, w.Body.String())
}

28
router/headers.go Normal file
View File

@ -0,0 +1,28 @@
package router
import (
"html/template"
"net/http"
"strings"
"github.com/dcarrillo/whatismyip/internal/httputils"
"github.com/gin-gonic/gin"
)
func getHeadersAsSortedString(ctx *gin.Context) {
h := ctx.Request.Header
h["Host"] = []string{ctx.Request.Host}
ctx.String(http.StatusOK, httputils.HeadersToSortedString(h))
}
func getHeaderAsString(ctx *gin.Context) {
h := ctx.Params.ByName("header")
if v := ctx.GetHeader(h); v != "" {
ctx.String(http.StatusOK, template.HTMLEscapeString(v))
} else if strings.ToLower(h) == "host" {
ctx.String(http.StatusOK, template.HTMLEscapeString(ctx.Request.Host))
} else {
ctx.String(http.StatusNotFound, http.StatusText(http.StatusNotFound))
}
}

41
router/headers_test.go Normal file
View File

@ -0,0 +1,41 @@
package router
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHeader(t *testing.T) {
req, _ := http.NewRequest("GET", "/user-agent", nil)
req.Header.Set("User-Agent", "test")
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, "test", w.Body.String())
}
func TestHeaders(t *testing.T) {
expected := `Header1: value1
Header2: value21
Header2: value22
Header3: value3
Host:
`
req, _ := http.NewRequest("GET", "/headers", nil)
req.Header["Header1"] = []string{"value1"}
req.Header["Header2"] = []string{"value21", "value22"}
req.Header["Header3"] = []string{"value3"}
w := httptest.NewRecorder()
app.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
assert.Equal(t, contentType.text, w.Header().Get("Content-Type"))
assert.Equal(t, expected, w.Body.String())
}

32
router/setup.go Normal file
View File

@ -0,0 +1,32 @@
package router
import (
"html/template"
"log"
"github.com/dcarrillo/whatismyip/internal/setting"
"github.com/gin-gonic/gin"
)
func SetupTemplate(r *gin.Engine) {
if setting.App.TemplatePath == "" {
t, _ := template.New("home").Parse(home)
r.SetHTMLTemplate(t)
} else {
log.Printf("Template %s has been loaded", setting.App.TemplatePath)
r.LoadHTMLFiles(setting.App.TemplatePath)
}
}
func Setup(r *gin.Engine) {
r.GET("/", getRoot)
r.GET("/client-port", getClientPortAsString)
r.GET("/geo", getGeoAsString)
r.GET("/geo/:field", getGeoAsString)
r.GET("/asn", getASNAsString)
r.GET("/asn/:field", getASNAsString)
r.GET("/headers", getHeadersAsSortedString)
r.GET("/all", getAllAsString)
r.GET("/json", getJSON)
r.GET("/:header", getHeaderAsString)
}

47
router/setup_test.go Normal file
View File

@ -0,0 +1,47 @@
package router
import (
"os"
"testing"
"github.com/dcarrillo/whatismyip/models"
"github.com/gin-gonic/gin"
)
type testIPs struct {
ipv4 string
ipv4ASN string
ipv6 string
ipv6ASN string
}
type contentTypes struct {
text string
json string
}
var (
app *gin.Engine
testIP = testIPs{
ipv4: "81.2.69.192",
ipv4ASN: "82.99.17.64",
ipv6: "2a02:9000::1",
ipv6ASN: "2a02:a800::1",
}
contentType = contentTypes{
text: "text/plain; charset=utf-8",
json: "application/json; charset=utf-8",
}
)
const trustedHeader = "X-Real-IP"
func TestMain(m *testing.M) {
app = gin.Default()
app.TrustedPlatform = trustedHeader
models.Setup("../test/GeoIP2-City-Test.mmdb", "../test/GeoLite2-ASN-Test.mmdb")
Setup(app)
defer models.CloseDBs()
os.Exit(m.Run())
}

45
router/templates.go Normal file
View File

@ -0,0 +1,45 @@
package router
const home = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>What is my IP Address ?</title>
</head>
<body>
<h1>What is my IP ?</h1>
<hr />
<h2> Your IPv{{ .IPVersion }} address is: {{ .IP }}</h2>
<table>
<tr> <td> Client Port </td> <td> {{ .ClientPort }} </td> </tr>
<tr> <td> Host </td> <td> {{ .Host }} </td> </tr>
</table>
<h3> Geolocation </h3>
<table>
<tr> <td> Country </td> <td> {{ .Country }} </td> </tr>
<tr> <td> Country Code </td> <td> {{ .CountryCode }} </td> </tr>
<tr> <td> City </td> <td> {{ .City }} </td> </tr>
<tr> <td> Latitude </td> <td> {{ .Latitude }} </td> </tr>
<tr> <td> Longitude </td> <td> {{ .Longitude }} </td> </tr>
<tr> <td> Postal Code </td> <td> {{ .PostalCode }} </td> </tr>
<tr> <td> Time Zone </td> <td> {{ .TimeZone }} </td> </tr>
</table>
<h3> Autonomous System </h3>
<table>
<tr> <td> ASN </td> <td> {{ .ASN }} </td> </tr>
<tr> <td> ASN Organization </td> <td> {{ .ASNOrganization }} </td> </tr>
</table>
<h3> Headers </h3>
<table>
{{- range $key, $value := .Headers }}
{{- range $content := $value }}
<tr> <td> {{ $key }} </td> <td> {{ $content }} </td> </tr>
{{- end}}
{{- end }}
</table>
</body>
</html>
`

85
router/templates_test.go Normal file
View File

@ -0,0 +1,85 @@
package router
import (
"bytes"
"html/template"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
)
const expectedHome = `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>What is my IP Address ?</title>
</head>
<body>
<h1>What is my IP ?</h1>
<hr />
<h2> Your IPv4 address is: 127.0.0.1</h2>
<table>
<tr> <td> Client Port </td> <td> 1000 </td> </tr>
<tr> <td> Host </td> <td> localhost </td> </tr>
</table>
<h3> Geolocation </h3>
<table>
<tr> <td> Country </td> <td> A Country </td> </tr>
<tr> <td> Country Code </td> <td> XX </td> </tr>
<tr> <td> City </td> <td> A City </td> </tr>
<tr> <td> Latitude </td> <td> 100 </td> </tr>
<tr> <td> Longitude </td> <td> -100 </td> </tr>
<tr> <td> Postal Code </td> <td> 00000 </td> </tr>
<tr> <td> Time Zone </td> <td> My/Timezone </td> </tr>
</table>
<h3> Autonomous System </h3>
<table>
<tr> <td> ASN </td> <td> 0 </td> </tr>
<tr> <td> ASN Organization </td> <td> My ISP </td> </tr>
</table>
<h3> Headers </h3>
<table>
<tr> <td> Header1 </td> <td> value1 </td> </tr>
<tr> <td> Header2 </td> <td> value21 </td> </tr>
<tr> <td> Header2 </td> <td> value22 </td> </tr>
<tr> <td> Header3 </td> <td> value3 </td> </tr>
</table>
</body>
</html>
`
func TestDefaultTemplate(t *testing.T) {
req, _ := http.NewRequest("GET", "/", nil)
req.Header["Header1"] = []string{"value1"}
req.Header["Header2"] = []string{"value21", "value22"}
req.Header["Header3"] = []string{"value3"}
tmpl, _ := template.New("home").Parse(home)
response := JSONResponse{
IP: "127.0.0.1",
IPVersion: 4,
ClientPort: "1000",
Country: "A Country",
CountryCode: "XX",
City: "A City",
Latitude: 100,
Longitude: -100,
PostalCode: "00000",
TimeZone: "My/Timezone",
ASN: 0,
ASNOrganization: "My ISP",
Host: "localhost",
Headers: req.Header,
}
buf := &bytes.Buffer{}
err := tmpl.Execute(buf, response)
assert.Nil(t, err)
assert.Equal(t, expectedHome, buf.String())
}