浏览代码

Merge pull request #1 from kodnaplakal/feature/placeholders

Feature/placeholders
Andrey Blinov 7 年之前
父节点
当前提交
3378766322
共有 7 个文件被更改,包括 191 次插入260 次删除
  1. 23 24
      README.md
  2. 7 75
      config.go
  3. 2 18
      config_test.go
  4. 0 82
      handler.go
  5. 0 60
      handler_test.go
  6. 87 1
      setup.go
  7. 72 0
      setup_test.go

+ 23 - 24
README.md

@@ -3,44 +3,43 @@
 
 `geoip` is a Caddy plugin that allow to determine user Geolocation by IP address using MaxMind database.
 
-## Headers
+## Placeholders
 
-`geoip` set this headers:
+The following placeholders are available:
 
 ```
-  X-Geoip-Country-Code - Country ISO code, example CY for Cyprus
-  X-Geoip-Location-Lat - Latitude, example 34.684100
-  X-Geoip-Location-Lon - Longitude, example 33.037900
-  X-Geoip-Location-Tz - Time zone, example Asia/Nicosia
-  X-Geoip-Country-Eu - Return 'true' if country in Europen Union
-  X-Geoip-Country-Name - Full country name
-  X-Geoip-City-Name - City name
+  geoip_country_code - Country ISO code, example CY for Cyprus
+  geoip_latitude - Latitude, example 34.684100
+  geoip_longitude - Longitude, example 33.037900
+  geoip_time_zone - Time zone, example Asia/Nicosia
+  geoip_country_eu - Return 'true' if country in Europen Union
+  geoip_country_name - Full country name
+  geoip_city_name - City name
 ```
 
 
 ## Examples
 
-(1) Set database path:
+(1) Set database path and return country code header:
 
 ```
-geoip {
-  database /path/to/db/GeoLite2-City.mmdb
-}
+geoip /path/to/db/GeoLite2-City.mmdb
+header Country-Code {geoip_country_code}
 ```
 
-
-(2) Set custom header names.
+(2) Proxy pass headers to backend:
 
 ```
-geoip {
-  database path/to/maxmind/db
-  set_header_country_code Code
-  set_header_country_name CountryName
-  set_header_country_eu Eu
-  set_header_city_name CityName
-  set_header_location_lat Lat
-  set_header_location_lon Lon
-  set_header_location_tz TZ
+localhost
+geoip /path/to/db/GeoLite2-City.mmdb
+proxy / localhost:3000 {
+  header_upstream Country-Name {geoip_country_name}
+  header_upstream Country-Code {geoip_country_code}
+  header_upstream Country-Eu {geoip_country_eu}
+  header_upstream City-Name {geoip_city_name}
+  header_upstream Latitude {geoip_latitude}
+  header_upstream Longitude {geoip_longitude}
+  header_upstream Time-Zone {geoip_time_zone}
 }
 ```
 

+ 7 - 75
config.go

@@ -7,86 +7,18 @@ import (
 // Config specifies configuration parsed for Caddyfile
 type Config struct {
 	DatabasePath string
-
-	// Yout can set returned header names in config
-	// Country
-	HeaderNameCountryCode string
-	HeaderNameCountryIsEU string
-	HeaderNameCountryName string
-
-	// City
-	HeaderNameCityName string
-
-	// Location
-	HeaderNameLocationLat      string
-	HeaderNameLocationLon      string
-	HeaderNameLocationTimeZone string
-}
-
-// NewConfig initialize new Config with default values
-func NewConfig() Config {
-	c := Config{}
-
-	c.HeaderNameCountryCode = "X-Geoip-Country-Code"
-	c.HeaderNameCountryIsEU = "X-Geoip-Country-Eu"
-	c.HeaderNameCountryName = "X-Geoip-Country-Name"
-
-	c.HeaderNameCityName = "X-Geoip-City-Name"
-
-	c.HeaderNameLocationLat = "X-Geoip-Location-Lat"
-	c.HeaderNameLocationLon = "X-Geoip-Location-Lon"
-	c.HeaderNameLocationTimeZone = "X-Geoip-Location-Tz"
-	return c
 }
 
 func parseConfig(c *caddy.Controller) (Config, error) {
-	var config = NewConfig()
+	var config = Config{}
 	for c.Next() {
-		for c.NextBlock() {
-			value := c.Val()
-
-			switch value {
-			case "database":
-				if !c.NextArg() {
-					continue
-				}
-				config.DatabasePath = c.Val()
-			case "set_header_country_code":
-				if !c.NextArg() {
-					continue
-				}
-				config.HeaderNameCountryCode = c.Val()
-			case "set_header_country_name":
-				if !c.NextArg() {
-					continue
-				}
-				config.HeaderNameCountryName = c.Val()
-			case "set_header_country_eu":
-				if !c.NextArg() {
-					continue
-				}
-				config.HeaderNameCountryIsEU = c.Val()
-			case "set_header_city_name":
-				if !c.NextArg() {
-					continue
-				}
-				config.HeaderNameCityName = c.Val()
-			case "set_header_location_lat":
-				if !c.NextArg() {
-					continue
-				}
-				config.HeaderNameLocationLat = c.Val()
-			case "set_header_location_lon":
-				if !c.NextArg() {
-					continue
-				}
-				config.HeaderNameLocationLon = c.Val()
-			case "set_header_location_tz":
-				if !c.NextArg() {
-					continue
-				}
-				config.HeaderNameLocationTimeZone = c.Val()
+		value := c.Val()
+		switch value {
+		case "geoip":
+			if !c.NextArg() {
+				continue
 			}
+			config.DatabasePath = c.Val()
 		}
 	}
 	return config, nil

+ 2 - 18
config_test.go

@@ -10,30 +10,14 @@ import (
 func TestParseConfig(t *testing.T) {
 	controller := caddy.NewTestController("http", `
 		localhost:8080
-		geoip {
-			database path/to/maxmind/db
-			set_header_country_code Code
-			set_header_country_name CountryName
-			set_header_country_eu Eu
-			set_header_city_name CityName
-			set_header_location_lat Lat
-			set_header_location_lon Lon
-			set_header_location_tz TZ
-		}
+		geoip path/to/maxmind/db
 	`)
 	actual, err := parseConfig(controller)
 	if err != nil {
 		t.Errorf("parseConfig return err: %v", err)
 	}
 	expected := Config{
-		DatabasePath:               "path/to/maxmind/db",
-		HeaderNameCountryCode:      "Code",
-		HeaderNameCountryName:      "CountryName",
-		HeaderNameCountryIsEU:      "Eu",
-		HeaderNameCityName:         "CityName",
-		HeaderNameLocationLat:      "Lat",
-		HeaderNameLocationLon:      "Lon",
-		HeaderNameLocationTimeZone: "TZ",
+		DatabasePath: "path/to/maxmind/db",
 	}
 	if !reflect.DeepEqual(expected, actual) {
 		t.Errorf("Expected %v actual %v", expected, actual)

+ 0 - 82
handler.go

@@ -1,82 +0,0 @@
-package geoip
-
-import (
-	"errors"
-	"log"
-	"net"
-	"net/http"
-	"strconv"
-	"strings"
-)
-
-func (gip GeoIP) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
-	gip.addGeoIPHeaders(w, r)
-	return gip.Next.ServeHTTP(w, r)
-}
-
-var record struct {
-	Country struct {
-		ISOCode           string            `maxminddb:"iso_code"`
-		IsInEuropeanUnion bool              `maxminddb:"is_in_european_union"`
-		Names             map[string]string `maxminddb:"names"`
-	} `maxminddb:"country"`
-
-	City struct {
-		Names map[string]string `maxminddb:"names"`
-	} `maxminddb:"city"`
-
-	Location struct {
-		Latitude  float64 `maxminddb:"latitude"`
-		Longitude float64 `maxminddb:"longitude"`
-		TimeZone  string  `maxminddb:"time_zone"`
-	} `maxminddb:"location"`
-}
-
-func (gip GeoIP) addGeoIPHeaders(w http.ResponseWriter, r *http.Request) {
-	clientIP, _ := getClientIP(r, true)
-
-	err := gip.DBHandler.Lookup(clientIP, &record)
-	if err != nil {
-		log.Println(err)
-	}
-
-	r.Header.Set(gip.Config.HeaderNameCountryCode, record.Country.ISOCode)
-	r.Header.Set(gip.Config.HeaderNameCountryIsEU, strconv.FormatBool(record.Country.IsInEuropeanUnion))
-	r.Header.Set(gip.Config.HeaderNameCountryName, record.Country.Names["en"])
-
-	r.Header.Set(gip.Config.HeaderNameCityName, record.City.Names["en"])
-
-	r.Header.Set(gip.Config.HeaderNameLocationLat, strconv.FormatFloat(record.Location.Latitude, 'f', 6, 64))
-	r.Header.Set(gip.Config.HeaderNameLocationLon, strconv.FormatFloat(record.Location.Longitude, 'f', 6, 64))
-	r.Header.Set(gip.Config.HeaderNameLocationTimeZone, record.Location.TimeZone)
-}
-
-func getClientIP(r *http.Request, strict bool) (net.IP, error) {
-	var ip string
-
-	// Use the client ip from the 'X-Forwarded-For' header, if available.
-	if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" && !strict {
-		ips := strings.Split(fwdFor, ", ")
-		ip = ips[0]
-	} else {
-		// Otherwise, get the client ip from the request remote address.
-		var err error
-		ip, _, err = net.SplitHostPort(r.RemoteAddr)
-		if err != nil {
-			if serr, ok := err.(*net.AddrError); ok && serr.Err == "missing port in address" { // It's not critical try parse
-				ip = r.RemoteAddr
-			} else {
-				log.Printf("Error when SplitHostPort: %v", serr.Err)
-				return nil, err
-			}
-		}
-	}
-
-	// Parse the ip address string into a net.IP.
-	parsedIP := net.ParseIP(ip)
-	if parsedIP == nil {
-		return nil, errors.New("unable to parse address")
-	}
-
-	return parsedIP, nil
-}

+ 0 - 60
handler_test.go

@@ -1,60 +0,0 @@
-package geoip
-
-import (
-	"net/http"
-	"net/http/httptest"
-	"reflect"
-	"strings"
-	"testing"
-
-	"github.com/mholt/caddy/caddyhttp/httpserver"
-	maxminddb "github.com/oschwald/maxminddb-golang"
-)
-
-func TestToResolveGeoip(t *testing.T) {
-	dbhandler, err := maxminddb.Open("./test-data/GeoLite2-City.mmdb")
-	if err != nil {
-		t.Errorf("geoip: Can't open database: GeoLite2-City.mmdb")
-	}
-
-	config := Config{}
-
-	config.HeaderNameCountryCode = "X-Geoip-Country-Code"
-	config.HeaderNameCountryIsEU = "X-Geoip-Country-Eu"
-	config.HeaderNameCountryName = "X-Geoip-Country-Name"
-
-	config.HeaderNameCityName = "X-Geoip-City-Name"
-
-	config.HeaderNameLocationLat = "X-Geoip-Location-Lat"
-	config.HeaderNameLocationLon = "X-Geoip-Location-Lon"
-	config.HeaderNameLocationTimeZone = "X-Geoip-Location-Tz"
-
-	var (
-		gotHeaders      http.Header
-		expectedHeaders = http.Header{
-			"X-Geoip-Country-Code": []string{"CY"},
-			"X-Geoip-Location-Lat": []string{"34.684100"},
-			"X-Geoip-Location-Lon": []string{"33.037900"},
-			"X-Geoip-Location-Tz":  []string{"Asia/Nicosia"},
-			"X-Geoip-Country-Eu":   []string{"false"},
-			"X-Geoip-Country-Name": []string{"Cyprus"},
-			"X-Geoip-City-Name":    []string{"Limassol"},
-		}
-	)
-	l := GeoIP{
-		Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
-			gotHeaders = r.Header
-			return 0, nil
-		}),
-		DBHandler: dbhandler,
-		Config:    config,
-	}
-
-	r := httptest.NewRequest("GET", "/", strings.NewReader(""))
-	r.RemoteAddr = "212.50.99.193"
-	l.ServeHTTP(httptest.NewRecorder(), r)
-
-	if !reflect.DeepEqual(expectedHeaders, gotHeaders) {
-		t.Errorf("Expected %v actual %v", expectedHeaders, gotHeaders)
-	}
-}

+ 87 - 1
setup.go

@@ -1,18 +1,43 @@
 package geoip
 
 import (
+	"errors"
+	"log"
+	"net"
+	"net/http"
+	"strconv"
+	"strings"
+
 	"github.com/mholt/caddy"
 	"github.com/mholt/caddy/caddyhttp/httpserver"
 	maxminddb "github.com/oschwald/maxminddb-golang"
 )
 
-// GeoIP Comments me
+// GeoIP represents a middleware instance
 type GeoIP struct {
 	Next      httpserver.Handler
 	DBHandler *maxminddb.Reader
 	Config    Config
 }
 
+var record struct {
+	Country struct {
+		ISOCode           string            `maxminddb:"iso_code"`
+		IsInEuropeanUnion bool              `maxminddb:"is_in_european_union"`
+		Names             map[string]string `maxminddb:"names"`
+	} `maxminddb:"country"`
+
+	City struct {
+		Names map[string]string `maxminddb:"names"`
+	} `maxminddb:"city"`
+
+	Location struct {
+		Latitude  float64 `maxminddb:"latitude"`
+		Longitude float64 `maxminddb:"longitude"`
+		TimeZone  string  `maxminddb:"time_zone"`
+	} `maxminddb:"location"`
+}
+
 // Init initializes the plugin
 func init() {
 	caddy.RegisterPlugin("geoip", caddy.Plugin{
@@ -45,3 +70,64 @@ func setup(c *caddy.Controller) error {
 
 	return nil
 }
+
+func (gip GeoIP) ServeHTTP(w http.ResponseWriter, r *http.Request) (int, error) {
+	gip.lookupLocation(w, r)
+	return gip.Next.ServeHTTP(w, r)
+}
+
+func (gip GeoIP) lookupLocation(w http.ResponseWriter, r *http.Request) {
+	clientIP, _ := getClientIP(r, true)
+	replacer := newReplacer(r)
+
+	err := gip.DBHandler.Lookup(clientIP, &record)
+	if err != nil {
+		log.Println(err)
+	}
+
+	replacer.Set("geoip_country_code", record.Country.ISOCode)
+	replacer.Set("geoip_country_name", record.Country.Names["en"])
+	replacer.Set("geoip_country_eu", strconv.FormatBool(record.Country.IsInEuropeanUnion))
+	replacer.Set("geoip_city_name", record.City.Names["en"])
+	replacer.Set("geoip_latitude", strconv.FormatFloat(record.Location.Latitude, 'f', 6, 64))
+	replacer.Set("geoip_longitude", strconv.FormatFloat(record.Location.Longitude, 'f', 6, 64))
+	replacer.Set("geoip_time_zone", record.Location.TimeZone)
+
+	if rr, ok := w.(*httpserver.ResponseRecorder); ok {
+		rr.Replacer = replacer
+	}
+}
+
+func getClientIP(r *http.Request, strict bool) (net.IP, error) {
+	var ip string
+
+	// Use the client ip from the 'X-Forwarded-For' header, if available.
+	if fwdFor := r.Header.Get("X-Forwarded-For"); fwdFor != "" && !strict {
+		ips := strings.Split(fwdFor, ", ")
+		ip = ips[0]
+	} else {
+		// Otherwise, get the client ip from the request remote address.
+		var err error
+		ip, _, err = net.SplitHostPort(r.RemoteAddr)
+		if err != nil {
+			if serr, ok := err.(*net.AddrError); ok && serr.Err == "missing port in address" { // It's not critical try parse
+				ip = r.RemoteAddr
+			} else {
+				log.Printf("Error when SplitHostPort: %v", serr.Err)
+				return nil, err
+			}
+		}
+	}
+
+	// Parse the ip address string into a net.IP.
+	parsedIP := net.ParseIP(ip)
+	if parsedIP == nil {
+		return nil, errors.New("unable to parse address")
+	}
+
+	return parsedIP, nil
+}
+
+func newReplacer(r *http.Request) httpserver.Replacer {
+	return httpserver.NewReplacer(r, nil, "")
+}

+ 72 - 0
setup_test.go

@@ -0,0 +1,72 @@
+package geoip
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"strings"
+	"testing"
+
+	"github.com/mholt/caddy/caddyhttp/httpserver"
+	maxminddb "github.com/oschwald/maxminddb-golang"
+)
+
+type testResponseRecorder struct {
+	*httpserver.ResponseWriterWrapper
+}
+
+func (testResponseRecorder) CloseNotify() <-chan bool { return nil }
+
+func TestReplacers(t *testing.T) {
+	dbhandler, err := maxminddb.Open("./test-data/GeoLite2-City.mmdb")
+	if err != nil {
+		t.Errorf("geoip: Can't open database: GeoLite2-City.mmdb")
+	}
+
+	config := Config{}
+
+	l := GeoIP{
+		Next: httpserver.HandlerFunc(func(w http.ResponseWriter, r *http.Request) (int, error) {
+			return 0, nil
+		}),
+		DBHandler: dbhandler,
+		Config:    config,
+	}
+
+	r := httptest.NewRequest("GET", "/", strings.NewReader(""))
+	r.RemoteAddr = "212.50.99.193"
+	rr := httpserver.NewResponseRecorder(testResponseRecorder{
+		ResponseWriterWrapper: &httpserver.ResponseWriterWrapper{ResponseWriter: httptest.NewRecorder()},
+	})
+
+	rr.Replacer = httpserver.NewReplacer(r, rr, "-")
+
+	l.ServeHTTP(rr, r)
+
+	if got, want := rr.Replacer.Replace("{geoip_country_code}"), "CY"; got != want {
+		t.Errorf("Expected custom placeholder {geoip_country_code} to be set (%s), but it wasn't; got: %s", want, got)
+	}
+
+	if got, want := rr.Replacer.Replace("{geoip_country_name}"), "Cyprus"; got != want {
+		t.Errorf("Expected custom placeholder {geoip_country_name} to be set (%s), but it wasn't; got: %s", want, got)
+	}
+
+	if got, want := rr.Replacer.Replace("{geoip_country_eu}"), "false"; got != want {
+		t.Errorf("Expected custom placeholder {geoip_country_eu} to be set (%s), but it wasn't; got: %s", want, got)
+	}
+
+	if got, want := rr.Replacer.Replace("{geoip_city_name}"), "Limassol"; got != want {
+		t.Errorf("Expected custom placeholder {geoip_city_name} to be set (%s), but it wasn't; got: %s", want, got)
+	}
+
+	if got, want := rr.Replacer.Replace("{geoip_latitude}"), "34.684100"; got != want {
+		t.Errorf("Expected custom placeholder {geoip_latitude} to be set (%s), but it wasn't; got: %s", want, got)
+	}
+
+	if got, want := rr.Replacer.Replace("{geoip_longitude}"), "33.037900"; got != want {
+		t.Errorf("Expected custom placeholder {geoip_longitude} to be set (%s), but it wasn't; got: %s", want, got)
+	}
+
+	if got, want := rr.Replacer.Replace("{geoip_time_zone}"), "Asia/Nicosia"; got != want {
+		t.Errorf("Expected custom placeholder {geoip_time_zone} to be set (%s), but it wasn't; got: %s", want, got)
+	}
+}