mirror of
https://github.com/casjaysdevdocker/ifconfig
synced 2025-09-18 03:57:52 -04:00
🦈🏠🐜❗ Initial Commit ❗🐜🦈🏠
This commit is contained in:
99
http/cache.go
Normal file
99
http/cache.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"container/list"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"net"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type Cache struct {
|
||||
capacity int
|
||||
mu sync.RWMutex
|
||||
entries map[uint64]*list.Element
|
||||
values *list.List
|
||||
evictions uint64
|
||||
}
|
||||
|
||||
type CacheStats struct {
|
||||
Capacity int
|
||||
Size int
|
||||
Evictions uint64
|
||||
}
|
||||
|
||||
func NewCache(capacity int) *Cache {
|
||||
if capacity < 0 {
|
||||
capacity = 0
|
||||
}
|
||||
return &Cache{
|
||||
capacity: capacity,
|
||||
entries: make(map[uint64]*list.Element),
|
||||
values: list.New(),
|
||||
}
|
||||
}
|
||||
|
||||
func key(ip net.IP) uint64 {
|
||||
h := fnv.New64a()
|
||||
h.Write(ip)
|
||||
return h.Sum64()
|
||||
}
|
||||
|
||||
func (c *Cache) Set(ip net.IP, resp Response) {
|
||||
if c.capacity == 0 {
|
||||
return
|
||||
}
|
||||
k := key(ip)
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
minEvictions := len(c.entries) - c.capacity + 1
|
||||
if minEvictions > 0 { // At or above capacity. Shrink the cache
|
||||
evicted := 0
|
||||
for el := c.values.Front(); el != nil && evicted < minEvictions; {
|
||||
value := el.Value.(Response)
|
||||
delete(c.entries, key(value.IP))
|
||||
next := el.Next()
|
||||
c.values.Remove(el)
|
||||
el = next
|
||||
evicted++
|
||||
}
|
||||
c.evictions += uint64(evicted)
|
||||
}
|
||||
current, ok := c.entries[k]
|
||||
if ok {
|
||||
c.values.Remove(current)
|
||||
}
|
||||
c.entries[k] = c.values.PushBack(resp)
|
||||
}
|
||||
|
||||
func (c *Cache) Get(ip net.IP) (Response, bool) {
|
||||
k := key(ip)
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
r, ok := c.entries[k]
|
||||
if !ok {
|
||||
return Response{}, false
|
||||
}
|
||||
return r.Value.(Response), true
|
||||
}
|
||||
|
||||
func (c *Cache) Resize(capacity int) error {
|
||||
if capacity < 0 {
|
||||
return fmt.Errorf("invalid capacity: %d\n", capacity)
|
||||
}
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
c.capacity = capacity
|
||||
c.evictions = 0
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Cache) Stats() CacheStats {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return CacheStats{
|
||||
Size: len(c.entries),
|
||||
Capacity: c.capacity,
|
||||
Evictions: c.evictions,
|
||||
}
|
||||
}
|
87
http/cache_test.go
Normal file
87
http/cache_test.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCacheCapacity(t *testing.T) {
|
||||
var tests = []struct {
|
||||
addCount, capacity, size int
|
||||
evictions uint64
|
||||
}{
|
||||
{1, 0, 0, 0},
|
||||
{1, 2, 1, 0},
|
||||
{2, 2, 2, 0},
|
||||
{3, 2, 2, 1},
|
||||
{10, 5, 5, 5},
|
||||
}
|
||||
for i, tt := range tests {
|
||||
c := NewCache(tt.capacity)
|
||||
var responses []Response
|
||||
for i := 0; i < tt.addCount; i++ {
|
||||
ip := net.ParseIP(fmt.Sprintf("192.0.2.%d", i))
|
||||
r := Response{IP: ip}
|
||||
responses = append(responses, r)
|
||||
c.Set(ip, r)
|
||||
}
|
||||
if got := len(c.entries); got != tt.size {
|
||||
t.Errorf("#%d: len(entries) = %d, want %d", i, got, tt.size)
|
||||
}
|
||||
if got := c.evictions; got != tt.evictions {
|
||||
t.Errorf("#%d: evictions = %d, want %d", i, got, tt.evictions)
|
||||
}
|
||||
if tt.capacity > 0 && tt.addCount > tt.capacity && tt.capacity == tt.size {
|
||||
lastAdded := responses[tt.addCount-1]
|
||||
if _, ok := c.Get(lastAdded.IP); !ok {
|
||||
t.Errorf("#%d: Get(%s) = (_, %t), want (_, %t)", i, lastAdded.IP.String(), ok, !ok)
|
||||
}
|
||||
firstAdded := responses[0]
|
||||
if _, ok := c.Get(firstAdded.IP); ok {
|
||||
t.Errorf("#%d: Get(%s) = (_, %t), want (_, %t)", i, firstAdded.IP.String(), ok, !ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheDuplicate(t *testing.T) {
|
||||
c := NewCache(10)
|
||||
ip := net.ParseIP("192.0.2.1")
|
||||
response := Response{IP: ip}
|
||||
c.Set(ip, response)
|
||||
c.Set(ip, response)
|
||||
want := 1
|
||||
if got := len(c.entries); got != want {
|
||||
t.Errorf("want %d entries, got %d", want, got)
|
||||
}
|
||||
if got := c.values.Len(); got != want {
|
||||
t.Errorf("want %d values, got %d", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheResize(t *testing.T) {
|
||||
c := NewCache(10)
|
||||
for i := 1; i <= 20; i++ {
|
||||
ip := net.ParseIP(fmt.Sprintf("192.0.2.%d", i))
|
||||
r := Response{IP: ip}
|
||||
c.Set(ip, r)
|
||||
}
|
||||
if got, want := len(c.entries), 10; got != want {
|
||||
t.Errorf("want %d entries, got %d", want, got)
|
||||
}
|
||||
if got, want := c.evictions, uint64(10); got != want {
|
||||
t.Errorf("want %d evictions, got %d", want, got)
|
||||
}
|
||||
if err := c.Resize(5); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, want := c.evictions, uint64(0); got != want {
|
||||
t.Errorf("want %d evictions, got %d", want, got)
|
||||
}
|
||||
r := Response{IP: net.ParseIP("192.0.2.42")}
|
||||
c.Set(r.IP, r)
|
||||
if got, want := len(c.entries), 5; got != want {
|
||||
t.Errorf("want %d entries, got %d", want, got)
|
||||
}
|
||||
}
|
40
http/error.go
Normal file
40
http/error.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
type appError struct {
|
||||
Error error
|
||||
Message string
|
||||
Code int
|
||||
ContentType string
|
||||
}
|
||||
|
||||
func internalServerError(err error) *appError {
|
||||
return &appError{
|
||||
Error: err,
|
||||
Message: "Internal server error",
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
func notFound(err error) *appError {
|
||||
return &appError{Error: err, Code: http.StatusNotFound}
|
||||
}
|
||||
|
||||
func badRequest(err error) *appError {
|
||||
return &appError{Error: err, Code: http.StatusBadRequest}
|
||||
}
|
||||
|
||||
func (e *appError) AsJSON() *appError {
|
||||
e.ContentType = jsonMediaType
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *appError) WithMessage(message string) *appError {
|
||||
e.Message = message
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *appError) IsJSON() bool {
|
||||
return e.ContentType == jsonMediaType
|
||||
}
|
466
http/http.go
Normal file
466
http/http.go
Normal file
@@ -0,0 +1,466 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"net/http/pprof"
|
||||
|
||||
"github.com/mpolden/echoip/iputil"
|
||||
"github.com/mpolden/echoip/iputil/geo"
|
||||
"github.com/mpolden/echoip/useragent"
|
||||
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
jsonMediaType = "application/json"
|
||||
textMediaType = "text/plain"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
Template string
|
||||
IPHeaders []string
|
||||
LookupAddr func(net.IP) (string, error)
|
||||
LookupPort func(net.IP, uint64) error
|
||||
cache *Cache
|
||||
gr geo.Reader
|
||||
profile bool
|
||||
Sponsor bool
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
IP net.IP `json:"ip"`
|
||||
IPDecimal *big.Int `json:"ip_decimal"`
|
||||
Country string `json:"country,omitempty"`
|
||||
CountryISO string `json:"country_iso,omitempty"`
|
||||
CountryEU *bool `json:"country_eu,omitempty"`
|
||||
RegionName string `json:"region_name,omitempty"`
|
||||
RegionCode string `json:"region_code,omitempty"`
|
||||
MetroCode uint `json:"metro_code,omitempty"`
|
||||
PostalCode string `json:"zip_code,omitempty"`
|
||||
City string `json:"city,omitempty"`
|
||||
Latitude float64 `json:"latitude,omitempty"`
|
||||
Longitude float64 `json:"longitude,omitempty"`
|
||||
Timezone string `json:"time_zone,omitempty"`
|
||||
ASN string `json:"asn,omitempty"`
|
||||
ASNOrg string `json:"asn_org,omitempty"`
|
||||
Hostname string `json:"hostname,omitempty"`
|
||||
UserAgent *useragent.UserAgent `json:"user_agent,omitempty"`
|
||||
}
|
||||
|
||||
type PortResponse struct {
|
||||
IP net.IP `json:"ip"`
|
||||
Port uint64 `json:"port"`
|
||||
Reachable bool `json:"reachable"`
|
||||
}
|
||||
|
||||
func New(db geo.Reader, cache *Cache, profile bool) *Server {
|
||||
return &Server{cache: cache, gr: db, profile: profile}
|
||||
}
|
||||
|
||||
func ipFromForwardedForHeader(v string) string {
|
||||
sep := strings.Index(v, ",")
|
||||
if sep == -1 {
|
||||
return v
|
||||
}
|
||||
return v[:sep]
|
||||
}
|
||||
|
||||
// ipFromRequest detects the IP address for this transaction.
|
||||
//
|
||||
// * `headers` - the specific HTTP headers to trust
|
||||
// * `r` - the incoming HTTP request
|
||||
// * `customIP` - whether to allow the IP to be pulled from query parameters
|
||||
func ipFromRequest(headers []string, r *http.Request, customIP bool) (net.IP, error) {
|
||||
remoteIP := ""
|
||||
if customIP && r.URL != nil {
|
||||
if v, ok := r.URL.Query()["ip"]; ok {
|
||||
remoteIP = v[0]
|
||||
}
|
||||
}
|
||||
if remoteIP == "" {
|
||||
for _, header := range headers {
|
||||
remoteIP = r.Header.Get(header)
|
||||
if http.CanonicalHeaderKey(header) == "X-Forwarded-For" {
|
||||
remoteIP = ipFromForwardedForHeader(remoteIP)
|
||||
}
|
||||
if remoteIP != "" {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if remoteIP == "" {
|
||||
host, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remoteIP = host
|
||||
}
|
||||
ip := net.ParseIP(remoteIP)
|
||||
if ip == nil {
|
||||
return nil, fmt.Errorf("could not parse IP: %s", remoteIP)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func userAgentFromRequest(r *http.Request) *useragent.UserAgent {
|
||||
var userAgent *useragent.UserAgent
|
||||
userAgentRaw := r.UserAgent()
|
||||
if userAgentRaw != "" {
|
||||
parsed := useragent.Parse(userAgentRaw)
|
||||
userAgent = &parsed
|
||||
}
|
||||
return userAgent
|
||||
}
|
||||
|
||||
func (s *Server) newResponse(r *http.Request) (Response, error) {
|
||||
ip, err := ipFromRequest(s.IPHeaders, r, true)
|
||||
if err != nil {
|
||||
return Response{}, err
|
||||
}
|
||||
response, ok := s.cache.Get(ip)
|
||||
if ok {
|
||||
// Do not cache user agent
|
||||
response.UserAgent = userAgentFromRequest(r)
|
||||
return response, nil
|
||||
}
|
||||
ipDecimal := iputil.ToDecimal(ip)
|
||||
country, _ := s.gr.Country(ip)
|
||||
city, _ := s.gr.City(ip)
|
||||
asn, _ := s.gr.ASN(ip)
|
||||
var hostname string
|
||||
if s.LookupAddr != nil {
|
||||
hostname, _ = s.LookupAddr(ip)
|
||||
}
|
||||
var autonomousSystemNumber string
|
||||
if asn.AutonomousSystemNumber > 0 {
|
||||
autonomousSystemNumber = fmt.Sprintf("AS%d", asn.AutonomousSystemNumber)
|
||||
}
|
||||
response = Response{
|
||||
IP: ip,
|
||||
IPDecimal: ipDecimal,
|
||||
Country: country.Name,
|
||||
CountryISO: country.ISO,
|
||||
CountryEU: country.IsEU,
|
||||
RegionName: city.RegionName,
|
||||
RegionCode: city.RegionCode,
|
||||
MetroCode: city.MetroCode,
|
||||
PostalCode: city.PostalCode,
|
||||
City: city.Name,
|
||||
Latitude: city.Latitude,
|
||||
Longitude: city.Longitude,
|
||||
Timezone: city.Timezone,
|
||||
ASN: autonomousSystemNumber,
|
||||
ASNOrg: asn.AutonomousSystemOrganization,
|
||||
Hostname: hostname,
|
||||
}
|
||||
s.cache.Set(ip, response)
|
||||
response.UserAgent = userAgentFromRequest(r)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *Server) newPortResponse(r *http.Request) (PortResponse, error) {
|
||||
lastElement := filepath.Base(r.URL.Path)
|
||||
port, err := strconv.ParseUint(lastElement, 10, 16)
|
||||
if err != nil || port < 1 || port > 65535 {
|
||||
return PortResponse{Port: port}, fmt.Errorf("invalid port: %s", lastElement)
|
||||
}
|
||||
ip, err := ipFromRequest(s.IPHeaders, r, false)
|
||||
if err != nil {
|
||||
return PortResponse{Port: port}, err
|
||||
}
|
||||
err = s.LookupPort(ip, port)
|
||||
return PortResponse{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Reachable: err == nil,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Server) CLIHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
ip, err := ipFromRequest(s.IPHeaders, r, true)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintln(w, ip.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICountryHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintln(w, response.Country)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICountryISOHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintln(w, response.CountryISO)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICityHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintln(w, response.City)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLICoordinatesHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintf(w, "%s,%s\n", formatCoordinate(response.Latitude), formatCoordinate(response.Longitude))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) CLIASNHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
fmt.Fprintf(w, "%s\n", response.ASN)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) JSONHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
b, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) HealthHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write([]byte(`{"status":"OK"}`))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) PortHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newPortResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
b, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) cacheResizeHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
capacity, err := strconv.Atoi(string(body))
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
if err := s.cache.Resize(capacity); err != nil {
|
||||
return badRequest(err).WithMessage(err.Error()).AsJSON()
|
||||
}
|
||||
data := struct {
|
||||
Message string `json:"message"`
|
||||
}{fmt.Sprintf("Changed cache capacity to %d.", capacity)}
|
||||
b, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) cacheHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
cacheStats := s.cache.Stats()
|
||||
var data = struct {
|
||||
Size int `json:"size"`
|
||||
Capacity int `json:"capacity"`
|
||||
Evictions uint64 `json:"evictions"`
|
||||
}{
|
||||
cacheStats.Size,
|
||||
cacheStats.Capacity,
|
||||
cacheStats.Evictions,
|
||||
}
|
||||
b, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err).AsJSON()
|
||||
}
|
||||
w.Header().Set("Content-Type", jsonMediaType)
|
||||
w.Write(b)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Server) DefaultHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
response, err := s.newResponse(r)
|
||||
if err != nil {
|
||||
return badRequest(err).WithMessage(err.Error())
|
||||
}
|
||||
t, err := template.ParseGlob(s.Template + "/*")
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
json, err := json.MarshalIndent(response, "", " ")
|
||||
if err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
|
||||
var data = struct {
|
||||
Response
|
||||
Host string
|
||||
BoxLatTop float64
|
||||
BoxLatBottom float64
|
||||
BoxLonLeft float64
|
||||
BoxLonRight float64
|
||||
JSON string
|
||||
Port bool
|
||||
Sponsor bool
|
||||
}{
|
||||
response,
|
||||
r.Host,
|
||||
response.Latitude + 0.05,
|
||||
response.Latitude - 0.05,
|
||||
response.Longitude - 0.05,
|
||||
response.Longitude + 0.05,
|
||||
string(json),
|
||||
s.LookupPort != nil,
|
||||
s.Sponsor,
|
||||
}
|
||||
if err := t.Execute(w, &data); err != nil {
|
||||
return internalServerError(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NotFoundHandler(w http.ResponseWriter, r *http.Request) *appError {
|
||||
err := notFound(nil).WithMessage("404 page not found")
|
||||
if r.Header.Get("accept") == jsonMediaType {
|
||||
err = err.AsJSON()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func cliMatcher(r *http.Request) bool {
|
||||
ua := useragent.Parse(r.UserAgent())
|
||||
switch ua.Product {
|
||||
case "curl", "HTTPie", "httpie-go", "Wget", "fetch libfetch", "Go", "Go-http-client", "ddclient", "Mikrotik", "xh":
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type appHandler func(http.ResponseWriter, *http.Request) *appError
|
||||
|
||||
func wrapHandlerFunc(f http.HandlerFunc) appHandler {
|
||||
return func(w http.ResponseWriter, r *http.Request) *appError {
|
||||
f.ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if e := fn(w, r); e != nil { // e is *appError
|
||||
if e.Code/100 == 5 {
|
||||
log.Println(e.Error)
|
||||
}
|
||||
// When Content-Type for error is JSON, we need to marshal the response into JSON
|
||||
if e.IsJSON() {
|
||||
var data = struct {
|
||||
Code int `json:"status"`
|
||||
Error string `json:"error"`
|
||||
}{e.Code, e.Message}
|
||||
b, err := json.MarshalIndent(data, "", " ")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
e.Message = string(b)
|
||||
}
|
||||
// Set Content-Type of response if set in error
|
||||
if e.ContentType != "" {
|
||||
w.Header().Set("Content-Type", e.ContentType)
|
||||
}
|
||||
w.WriteHeader(e.Code)
|
||||
fmt.Fprint(w, e.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Handler() http.Handler {
|
||||
r := NewRouter()
|
||||
|
||||
// Health
|
||||
r.Route("GET", "/health", s.HealthHandler)
|
||||
|
||||
// JSON
|
||||
r.Route("GET", "/", s.JSONHandler).Header("Accept", jsonMediaType)
|
||||
r.Route("GET", "/json", s.JSONHandler)
|
||||
|
||||
// CLI
|
||||
r.Route("GET", "/", s.CLIHandler).MatcherFunc(cliMatcher)
|
||||
r.Route("GET", "/", s.CLIHandler).Header("Accept", textMediaType)
|
||||
r.Route("GET", "/ip", s.CLIHandler)
|
||||
if !s.gr.IsEmpty() {
|
||||
r.Route("GET", "/country", s.CLICountryHandler)
|
||||
r.Route("GET", "/country-iso", s.CLICountryISOHandler)
|
||||
r.Route("GET", "/city", s.CLICityHandler)
|
||||
r.Route("GET", "/coordinates", s.CLICoordinatesHandler)
|
||||
r.Route("GET", "/asn", s.CLIASNHandler)
|
||||
}
|
||||
|
||||
// Browser
|
||||
if s.Template != "" {
|
||||
r.Route("GET", "/", s.DefaultHandler)
|
||||
}
|
||||
|
||||
// Port testing
|
||||
if s.LookupPort != nil {
|
||||
r.RoutePrefix("GET", "/port/", s.PortHandler)
|
||||
}
|
||||
|
||||
// Profiling
|
||||
if s.profile {
|
||||
r.Route("POST", "/debug/cache/resize", s.cacheResizeHandler)
|
||||
r.Route("GET", "/debug/cache/", s.cacheHandler)
|
||||
r.Route("GET", "/debug/pprof/cmdline", wrapHandlerFunc(pprof.Cmdline))
|
||||
r.Route("GET", "/debug/pprof/profile", wrapHandlerFunc(pprof.Profile))
|
||||
r.Route("GET", "/debug/pprof/symbol", wrapHandlerFunc(pprof.Symbol))
|
||||
r.Route("GET", "/debug/pprof/trace", wrapHandlerFunc(pprof.Trace))
|
||||
r.RoutePrefix("GET", "/debug/pprof/", wrapHandlerFunc(pprof.Index))
|
||||
}
|
||||
|
||||
return r.Handler()
|
||||
}
|
||||
|
||||
func (s *Server) ListenAndServe(addr string) error {
|
||||
return http.ListenAndServe(addr, s.Handler())
|
||||
}
|
||||
|
||||
func formatCoordinate(c float64) string {
|
||||
return strconv.FormatFloat(c, 'f', 6, 64)
|
||||
}
|
279
http/http_test.go
Normal file
279
http/http_test.go
Normal file
@@ -0,0 +1,279 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/mpolden/echoip/iputil/geo"
|
||||
)
|
||||
|
||||
func lookupAddr(net.IP) (string, error) { return "localhost", nil }
|
||||
func lookupPort(net.IP, uint64) error { return nil }
|
||||
|
||||
type testDb struct{}
|
||||
|
||||
func (t *testDb) Country(net.IP) (geo.Country, error) {
|
||||
return geo.Country{Name: "Elbonia", ISO: "EB", IsEU: new(bool)}, nil
|
||||
}
|
||||
|
||||
func (t *testDb) City(net.IP) (geo.City, error) {
|
||||
return geo.City{Name: "Bornyasherk", RegionName: "North Elbonia", RegionCode: "1234", MetroCode: 1234, PostalCode: "1234", Latitude: 63.416667, Longitude: 10.416667, Timezone: "Europe/Bornyasherk"}, nil
|
||||
}
|
||||
|
||||
func (t *testDb) ASN(net.IP) (geo.ASN, error) {
|
||||
return geo.ASN{AutonomousSystemNumber: 59795, AutonomousSystemOrganization: "Hosting4Real"}, nil
|
||||
}
|
||||
|
||||
func (t *testDb) IsEmpty() bool { return false }
|
||||
|
||||
func testServer() *Server {
|
||||
return &Server{cache: NewCache(100), gr: &testDb{}, LookupAddr: lookupAddr, LookupPort: lookupPort}
|
||||
}
|
||||
|
||||
func httpGet(url string, acceptMediaType string, userAgent string) (string, int, error) {
|
||||
r, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
if acceptMediaType != "" {
|
||||
r.Header.Set("Accept", acceptMediaType)
|
||||
}
|
||||
r.Header.Set("User-Agent", userAgent)
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return "", 0, err
|
||||
}
|
||||
return string(data), res.StatusCode, nil
|
||||
}
|
||||
|
||||
func httpPost(url, body string) (*http.Response, string, error) {
|
||||
r, err := http.NewRequest(http.MethodPost, url, strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
res, err := http.DefaultClient.Do(r)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return res, string(data), nil
|
||||
}
|
||||
|
||||
func TestCLIHandlers(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
s := httptest.NewServer(testServer().Handler())
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
out string
|
||||
status int
|
||||
userAgent string
|
||||
acceptMediaType string
|
||||
}{
|
||||
{s.URL, "127.0.0.1\n", 200, "curl/7.43.0", ""},
|
||||
{s.URL, "127.0.0.1\n", 200, "foo/bar", textMediaType},
|
||||
{s.URL + "/ip", "127.0.0.1\n", 200, "", ""},
|
||||
{s.URL + "/country", "Elbonia\n", 200, "", ""},
|
||||
{s.URL + "/country-iso", "EB\n", 200, "", ""},
|
||||
{s.URL + "/coordinates", "63.416667,10.416667\n", 200, "", ""},
|
||||
{s.URL + "/city", "Bornyasherk\n", 200, "", ""},
|
||||
{s.URL + "/foo", "404 page not found", 404, "", ""},
|
||||
{s.URL + "/asn", "AS59795\n", 200, "", ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
out, status, err := httpGet(tt.url, tt.acceptMediaType, tt.userAgent)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != tt.status {
|
||||
t.Errorf("Expected %d, got %d", tt.status, status)
|
||||
}
|
||||
if out != tt.out {
|
||||
t.Errorf("Expected %q, got %q", tt.out, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisabledHandlers(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
server := testServer()
|
||||
server.LookupPort = nil
|
||||
server.LookupAddr = nil
|
||||
server.gr, _ = geo.Open("", "", "")
|
||||
s := httptest.NewServer(server.Handler())
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
out string
|
||||
status int
|
||||
}{
|
||||
{s.URL + "/port/1337", "404 page not found", 404},
|
||||
{s.URL + "/country", "404 page not found", 404},
|
||||
{s.URL + "/country-iso", "404 page not found", 404},
|
||||
{s.URL + "/city", "404 page not found", 404},
|
||||
{s.URL + "/json", "{\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433\n}", 200},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
out, status, err := httpGet(tt.url, "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != tt.status {
|
||||
t.Errorf("Expected %d, got %d", tt.status, status)
|
||||
}
|
||||
if out != tt.out {
|
||||
t.Errorf("Expected %q, got %q", tt.out, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestJSONHandlers(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
s := httptest.NewServer(testServer().Handler())
|
||||
|
||||
var tests = []struct {
|
||||
url string
|
||||
out string
|
||||
status int
|
||||
}{
|
||||
{s.URL, "{\n \"ip\": \"127.0.0.1\",\n \"ip_decimal\": 2130706433,\n \"country\": \"Elbonia\",\n \"country_iso\": \"EB\",\n \"country_eu\": false,\n \"region_name\": \"North Elbonia\",\n \"region_code\": \"1234\",\n \"metro_code\": 1234,\n \"zip_code\": \"1234\",\n \"city\": \"Bornyasherk\",\n \"latitude\": 63.416667,\n \"longitude\": 10.416667,\n \"time_zone\": \"Europe/Bornyasherk\",\n \"asn\": \"AS59795\",\n \"asn_org\": \"Hosting4Real\",\n \"hostname\": \"localhost\",\n \"user_agent\": {\n \"product\": \"curl\",\n \"version\": \"7.2.6.0\",\n \"raw_value\": \"curl/7.2.6.0\"\n }\n}", 200},
|
||||
{s.URL + "/port/foo", "{\n \"status\": 400,\n \"error\": \"invalid port: foo\"\n}", 400},
|
||||
{s.URL + "/port/0", "{\n \"status\": 400,\n \"error\": \"invalid port: 0\"\n}", 400},
|
||||
{s.URL + "/port/65537", "{\n \"status\": 400,\n \"error\": \"invalid port: 65537\"\n}", 400},
|
||||
{s.URL + "/port/31337", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 31337,\n \"reachable\": true\n}", 200},
|
||||
{s.URL + "/port/80", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 80,\n \"reachable\": true\n}", 200}, // checking that our test server is reachable on port 80
|
||||
{s.URL + "/port/80?ip=1.3.3.7", "{\n \"ip\": \"127.0.0.1\",\n \"port\": 80,\n \"reachable\": true\n}", 200}, // ensuring that the "ip" parameter is not usable to check remote host ports
|
||||
{s.URL + "/foo", "{\n \"status\": 404,\n \"error\": \"404 page not found\"\n}", 404},
|
||||
{s.URL + "/health", `{"status":"OK"}`, 200},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
out, status, err := httpGet(tt.url, jsonMediaType, "curl/7.2.6.0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status != tt.status {
|
||||
t.Errorf("Expected %d for %s, got %d", tt.status, tt.url, status)
|
||||
}
|
||||
if out != tt.out {
|
||||
t.Errorf("Expected %q for %s, got %q", tt.out, tt.url, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheHandler(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
srv := testServer()
|
||||
srv.profile = true
|
||||
s := httptest.NewServer(srv.Handler())
|
||||
got, _, err := httpGet(s.URL+"/debug/cache/", jsonMediaType, "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "{\n \"size\": 0,\n \"capacity\": 100,\n \"evictions\": 0\n}"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCacheResizeHandler(t *testing.T) {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
srv := testServer()
|
||||
srv.profile = true
|
||||
s := httptest.NewServer(srv.Handler())
|
||||
_, got, err := httpPost(s.URL+"/debug/cache/resize", "10")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := "{\n \"message\": \"Changed cache capacity to 10.\"\n}"
|
||||
if got != want {
|
||||
t.Errorf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIPFromRequest(t *testing.T) {
|
||||
var tests = []struct {
|
||||
remoteAddr string
|
||||
headerKey string
|
||||
headerValue string
|
||||
trustedHeaders []string
|
||||
out string
|
||||
}{
|
||||
{"127.0.0.1:9999", "", "", nil, "127.0.0.1"}, // No header given
|
||||
{"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", nil, "127.0.0.1"}, // Trusted header is empty
|
||||
{"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", []string{"X-Foo-Bar"}, "127.0.0.1"}, // Trusted header does not match
|
||||
{"127.0.0.1:9999", "X-Real-IP", "1.3.3.7", []string{"X-Real-IP", "X-Forwarded-For"}, "1.3.3.7"}, // Trusted header matches
|
||||
{"127.0.0.1:9999", "X-Forwarded-For", "1.3.3.7", []string{"X-Real-IP", "X-Forwarded-For"}, "1.3.3.7"}, // Second trusted header matches
|
||||
{"127.0.0.1:9999", "X-Forwarded-For", "1.3.3.7,4.2.4.2", []string{"X-Forwarded-For"}, "1.3.3.7"}, // X-Forwarded-For with multiple entries (commas separator)
|
||||
{"127.0.0.1:9999", "X-Forwarded-For", "1.3.3.7, 4.2.4.2", []string{"X-Forwarded-For"}, "1.3.3.7"}, // X-Forwarded-For with multiple entries (space+comma separator)
|
||||
{"127.0.0.1:9999", "X-Forwarded-For", "", []string{"X-Forwarded-For"}, "127.0.0.1"}, // Empty header
|
||||
{"127.0.0.1:9999?ip=1.2.3.4", "", "", nil, "1.2.3.4"}, // passed in "ip" parameter
|
||||
{"127.0.0.1:9999?ip=1.2.3.4", "X-Forwarded-For", "1.3.3.7,4.2.4.2", []string{"X-Forwarded-For"}, "1.2.3.4"}, // ip parameter wins over X-Forwarded-For with multiple entries
|
||||
}
|
||||
for _, tt := range tests {
|
||||
u, err := url.Parse("http://" + tt.remoteAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := &http.Request{
|
||||
RemoteAddr: u.Host,
|
||||
Header: http.Header{},
|
||||
URL: u,
|
||||
}
|
||||
r.Header.Add(tt.headerKey, tt.headerValue)
|
||||
ip, err := ipFromRequest(tt.trustedHeaders, r, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := net.ParseIP(tt.out)
|
||||
if !ip.Equal(out) {
|
||||
t.Errorf("Expected %s, got %s", out, ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCLIMatcher(t *testing.T) {
|
||||
browserUserAgent := "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_4) " +
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.28 " +
|
||||
"Safari/537.36"
|
||||
var tests = []struct {
|
||||
in string
|
||||
out bool
|
||||
}{
|
||||
{"curl/7.26.0", true},
|
||||
{"Wget/1.13.4 (linux-gnu)", true},
|
||||
{"Wget", true},
|
||||
{"fetch libfetch/2.0", true},
|
||||
{"HTTPie/0.9.3", true},
|
||||
{"httpie-go/0.6.0", true},
|
||||
{"Go 1.1 package http", true},
|
||||
{"Go-http-client/1.1", true},
|
||||
{"Go-http-client/2.0", true},
|
||||
{"ddclient/3.8.3", true},
|
||||
{"Mikrotik/6.x Fetch", true},
|
||||
{browserUserAgent, false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
r := &http.Request{Header: http.Header{"User-Agent": []string{tt.in}}}
|
||||
if got := cliMatcher(r); got != tt.out {
|
||||
t.Errorf("Expected %t, got %t for %q", tt.out, got, tt.in)
|
||||
}
|
||||
}
|
||||
}
|
73
http/router.go
Normal file
73
http/router.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type router struct {
|
||||
routes []*route
|
||||
}
|
||||
|
||||
type route struct {
|
||||
method string
|
||||
path string
|
||||
prefix bool
|
||||
handler appHandler
|
||||
matcherFunc func(*http.Request) bool
|
||||
}
|
||||
|
||||
func NewRouter() *router {
|
||||
return &router{}
|
||||
}
|
||||
|
||||
func (r *router) Route(method, path string, handler appHandler) *route {
|
||||
route := route{
|
||||
method: method,
|
||||
path: path,
|
||||
handler: handler,
|
||||
}
|
||||
r.routes = append(r.routes, &route)
|
||||
return &route
|
||||
}
|
||||
|
||||
func (r *router) RoutePrefix(method, path string, handler appHandler) *route {
|
||||
route := r.Route(method, path, handler)
|
||||
route.prefix = true
|
||||
return route
|
||||
}
|
||||
|
||||
func (r *router) Handler() http.Handler {
|
||||
return appHandler(func(w http.ResponseWriter, req *http.Request) *appError {
|
||||
for _, route := range r.routes {
|
||||
if route.match(req) {
|
||||
return route.handler(w, req)
|
||||
}
|
||||
}
|
||||
return NotFoundHandler(w, req)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *route) Header(header, value string) {
|
||||
r.MatcherFunc(func(req *http.Request) bool {
|
||||
return req.Header.Get(header) == value
|
||||
})
|
||||
}
|
||||
|
||||
func (r *route) MatcherFunc(f func(*http.Request) bool) {
|
||||
r.matcherFunc = f
|
||||
}
|
||||
|
||||
func (r *route) match(req *http.Request) bool {
|
||||
if req.Method != r.method {
|
||||
return false
|
||||
}
|
||||
if r.prefix {
|
||||
if !strings.HasPrefix(req.URL.Path, r.path) {
|
||||
return false
|
||||
}
|
||||
} else if r.path != req.URL.Path {
|
||||
return false
|
||||
}
|
||||
return r.matcherFunc == nil || r.matcherFunc(req)
|
||||
}
|
Reference in New Issue
Block a user