echoip/http/http.go

320 lines
7.6 KiB
Go

package http
import (
"encoding/json"
"fmt"
"html/template"
"path/filepath"
"strings"
"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
gr geo.Reader
}
type Response struct {
IP net.IP `json:"ip"`
IPDecimal *big.Int `json:"ip_decimal"`
Country string `json:"country,omitempty"`
CountryEU bool `json:"country_eu,omitempty"`
CountryISO string `json:"country_iso,omitempty"`
City string `json:"city,omitempty"`
Hostname string `json:"hostname,omitempty"`
Latitude float64 `json:"latitude,omitempty"`
Longitude float64 `json:"longitude,omitempty"`
}
type PortResponse struct {
IP net.IP `json:"ip"`
Port uint64 `json:"port"`
Reachable bool `json:"reachable"`
}
func New(db geo.Reader) *Server {
return &Server{gr: db}
}
func ipFromForwardedForHeader(v string) string {
sep := strings.Index(v, ",")
if sep == -1 {
return v
}
return v[:sep]
}
func ipFromRequest(headers []string, r *http.Request) (net.IP, error) {
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 (s *Server) newResponse(r *http.Request) (Response, error) {
ip, err := ipFromRequest(s.IPHeaders, r)
if err != nil {
return Response{}, err
}
ipDecimal := iputil.ToDecimal(ip)
country, _ := s.gr.Country(ip)
city, _ := s.gr.City(ip)
var hostname string
if s.LookupAddr != nil {
hostname, _ = s.LookupAddr(ip)
}
return Response{
IP: ip,
IPDecimal: ipDecimal,
Country: country.Name,
CountryISO: country.ISO,
CountryEU: country.IsEU,
City: city.Name,
Hostname: hostname,
Latitude: city.Latitude,
Longitude: city.Longitude,
}, 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 > 65355 {
return PortResponse{Port: port}, fmt.Errorf("invalid port: %d", port)
}
ip, err := ipFromRequest(s.IPHeaders, r)
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)
if err != nil {
return internalServerError(err)
}
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 internalServerError(err)
}
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 internalServerError(err)
}
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 internalServerError(err)
}
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 internalServerError(err)
}
fmt.Fprintf(w, "%s,%s\n", formatCoordinate(response.Latitude), formatCoordinate(response.Longitude))
return nil
}
func (s *Server) JSONHandler(w http.ResponseWriter, r *http.Request) *appError {
response, err := s.newResponse(r)
if err != nil {
return internalServerError(err).AsJSON()
}
b, err := json.Marshal(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(fmt.Sprintf("Invalid port: %d", response.Port)).AsJSON()
}
b, err := json.Marshal(response)
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 internalServerError(err)
}
t, err := template.ParseFiles(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
JSON string
Port bool
}{
response,
r.Host,
string(json),
s.LookupPort != nil,
}
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", "Wget", "fetch libfetch", "Go", "Go-http-client", "ddclient":
return true
}
return false
}
type appHandler func(http.ResponseWriter, *http.Request) *appError
func (fn appHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if e := fn(w, r); e != nil { // e is *appError
// When Content-Type for error is JSON, we need to marshal the response into JSON
if e.IsJSON() {
var data = struct {
Error string `json:"error"`
}{e.Message}
b, err := json.Marshal(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)
}
// Browser
r.Route("GET", "/", s.DefaultHandler)
// Port testing
if s.LookupPort != nil {
r.RoutePrefix("GET", "/port/", s.PortHandler)
}
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)
}