From 067c19622abd0d7bd1dc46710f2122056c574eef Mon Sep 17 00:00:00 2001 From: Alexandre Fiori Date: Mon, 14 Mar 2016 20:00:30 -0400 Subject: [PATCH] Update base package and http server --- .travis.yml | 4 +- LICENSE | 2 +- apiserver/api.go | 298 ++++++++++++++++++++++++++++++++++++ apiserver/api_test.go | 115 ++++++++++++++ apiserver/cmd.go | 136 ---------------- apiserver/cmd_test.go | 26 ---- apiserver/config.go | 109 +++++++++++++ apiserver/config_test.go | 15 ++ apiserver/cors.go | 44 ------ apiserver/cors_test.go | 89 ----------- apiserver/db.go | 40 ----- apiserver/doc.go | 5 +- apiserver/http.go | 125 --------------- apiserver/http_test.go | 66 -------- apiserver/main.go | 100 ++++++++++++ apiserver/metrics.go | 2 +- apiserver/ratelimit.go | 97 ------------ apiserver/ratelimit_test.go | 68 -------- cmd/freegeoip/main.go | 12 +- db.go | 34 +++- db_test.go | 14 +- doc.go | 7 +- encoder.go | 263 ------------------------------- encoder_test.go | 236 ---------------------------- example_test.go | 58 +------ freegeoip.go | 109 ------------- freegeoip_test.go | 162 -------------------- 27 files changed, 684 insertions(+), 1552 deletions(-) create mode 100644 apiserver/api.go create mode 100644 apiserver/api_test.go delete mode 100644 apiserver/cmd.go delete mode 100644 apiserver/cmd_test.go create mode 100644 apiserver/config.go create mode 100644 apiserver/config_test.go delete mode 100644 apiserver/cors.go delete mode 100644 apiserver/cors_test.go delete mode 100644 apiserver/db.go delete mode 100644 apiserver/http.go delete mode 100644 apiserver/http_test.go create mode 100644 apiserver/main.go delete mode 100644 apiserver/ratelimit.go delete mode 100644 apiserver/ratelimit_test.go delete mode 100644 encoder.go delete mode 100644 encoder_test.go delete mode 100644 freegeoip.go delete mode 100644 freegeoip_test.go diff --git a/.travis.yml b/.travis.yml index c9a33cc..7a18358 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,11 +9,9 @@ go: install: - go get -t -d -v ./... - - go get golang.org/x/tools/cmd/cover services: - redis-server script: - - go test -v -cover . - - go test -v -cover ./apiserver + - go test -v ./ ./apiserver diff --git a/LICENSE b/LICENSE index 27dafae..12d6a5c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2009-2013 The freegeoip authors. All rights reserved. +Copyright (c) 2009 The freegeoip authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff --git a/apiserver/api.go b/apiserver/api.go new file mode 100644 index 0000000..78c600d --- /dev/null +++ b/apiserver/api.go @@ -0,0 +1,298 @@ +// Copyright 2009 The freegeoip authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package apiserver + +import ( + "bytes" + "encoding/csv" + "encoding/json" + "encoding/xml" + "fmt" + "io" + "log" + "math/rand" + "net" + "net/http" + "net/url" + "strconv" + "strings" + + "github.com/bradfitz/gomemcache/memcache" + "github.com/fiorix/go-redis/redis" + "github.com/go-web/httplog" + "github.com/go-web/httpmux" + "github.com/go-web/httprl" + "github.com/go-web/httprl/memcacherl" + "github.com/go-web/httprl/redisrl" + "github.com/prometheus/client_golang/prometheus" + "github.com/rs/cors" + + "github.com/fiorix/freegeoip" +) + +type apiHandler struct { + db *freegeoip.DB + conf *Config + cors *cors.Cors +} + +// NewHandler creates an http handler for the freegeoip server that +// can be embedded in other servers. +func NewHandler(c *Config) (http.Handler, error) { + db, err := openDB(c) + if err != nil { + return nil, fmt.Errorf("failed to open database: %v", err) + } + cf := cors.New(cors.Options{ + AllowedOrigins: []string{c.CORSOrigin}, + AllowedMethods: []string{"GET"}, + AllowCredentials: true, + }) + f := &apiHandler{db: db, conf: c, cors: cf} + mc := httpmux.DefaultConfig + if err := f.config(&mc); err != nil { + return nil, err + } + mux := httpmux.NewHandler(&mc) + mux.GET("/csv/*host", f.register("csv", csvWriter)) + mux.GET("/xml/*host", f.register("xml", xmlWriter)) + mux.GET("/json/*host", f.register("json", jsonWriter)) + go watchEvents(db) + return mux, nil +} + +func (f *apiHandler) config(mc *httpmux.Config) error { + mc.Prefix = f.conf.APIPrefix + if f.conf.PublicDir != "" { + mc.NotFound = f.publicDir() + } + if f.conf.UseXForwardedFor { + mc.Use(httplog.UseXForwardedFor) + } + if !f.conf.Silent { + mc.Use(httplog.ApacheCombinedFormat(f.conf.accessLogger())) + } + mc.Use(f.metrics) + if f.conf.RateLimitLimit > 0 { + rl, err := newRateLimiter(f.conf) + if err != nil { + return fmt.Errorf("failed to create rate limiter: %v", err) + } + mc.Use(rl.HandleFunc) + } + return nil +} + +func (f *apiHandler) publicDir() http.HandlerFunc { + fs := http.FileServer(http.Dir(f.conf.PublicDir)) + return prometheus.InstrumentHandler("frontend", fs) +} + +func (f *apiHandler) metrics(next http.HandlerFunc) http.HandlerFunc { + type query struct { + Country struct { + ISOCode string `maxminddb:"iso_code"` + } `maxminddb:"country"` + } + return func(w http.ResponseWriter, r *http.Request) { + next(w, r) + // Collect metrics after serving the request. + host, _, err := net.SplitHostPort(r.RemoteAddr) + if err != nil { + return + } + ip := net.ParseIP(host) + if ip == nil { + return + } + if ip.To4() != nil { + clientIPProtoCounter.WithLabelValues("4").Inc() + } else { + clientIPProtoCounter.WithLabelValues("6").Inc() + } + var q query + err = f.db.Lookup(ip, &q) + if err != nil || q.Country.ISOCode == "" { + clientCountryCounter.WithLabelValues("unknown").Inc() + return + } + clientCountryCounter.WithLabelValues(q.Country.ISOCode).Inc() + } +} + +type writerFunc func(w http.ResponseWriter, r *http.Request, d *responseRecord) + +func (f *apiHandler) register(name string, writer writerFunc) http.HandlerFunc { + h := prometheus.InstrumentHandler(name, f.iplookup(writer)) + return f.cors.Handler(h).ServeHTTP +} + +func (f *apiHandler) iplookup(writer writerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + host := httpmux.Params(r).ByName("host") + if len(host) > 0 && host[0] == '/' { + host = host[1:] + } + if host == "" { + host, _, _ = net.SplitHostPort(r.RemoteAddr) + if host == "" { + host = r.RemoteAddr + } + } + ips, err := net.LookupIP(host) + if err != nil || len(ips) == 0 { + http.NotFound(w, r) + return + } + ip, q := ips[rand.Intn(len(ips))], &geoipQuery{} + err = f.db.Lookup(ip, &q.DefaultQuery) + if err != nil { + http.Error(w, "Try again later.", http.StatusServiceUnavailable) + return + } + w.Header().Set("X-Database-Date", f.db.Date().Format(http.TimeFormat)) + resp := q.Record(ip, r.Header.Get("Accept-Language")) + writer(w, r, resp) + } +} + +func csvWriter(w http.ResponseWriter, r *http.Request, d *responseRecord) { + w.Header().Set("Content-Type", "text/csv") + io.WriteString(w, d.String()) +} + +func xmlWriter(w http.ResponseWriter, r *http.Request, d *responseRecord) { + w.Header().Set("Content-Type", "application/xml") + x := xml.NewEncoder(w) + x.Indent("", "\t") + x.Encode(d) + w.Write([]byte{'\n'}) +} + +func jsonWriter(w http.ResponseWriter, r *http.Request, d *responseRecord) { + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(d) +} + +type geoipQuery struct { + freegeoip.DefaultQuery +} + +func (q *geoipQuery) Record(ip net.IP, lang string) *responseRecord { + // TODO: parse accept-language value from lang. + if q.Country.Names[lang] == "" { + lang = "en" + } + r := &responseRecord{ + IP: ip.String(), + CountryCode: q.Country.ISOCode, + CountryName: q.Country.Names[lang], + City: q.City.Names[lang], + ZipCode: q.Postal.Code, + TimeZone: q.Location.TimeZone, + Latitude: q.Location.Latitude, + Longitude: q.Location.Longitude, + MetroCode: q.Location.MetroCode, + } + if len(q.Region) > 0 { + r.RegionCode = q.Region[0].ISOCode + r.RegionName = q.Region[0].Names[lang] + } + return r +} + +type responseRecord struct { + XMLName xml.Name `xml:"Response" json:"-"` + IP string `json:"ip"` + CountryCode string `json:"country_code"` + CountryName string `json:"country_name"` + RegionCode string `json:"region_code"` + RegionName string `json:"region_name"` + City string `json:"city"` + ZipCode string `json:"zip_code"` + TimeZone string `json:"time_zone"` + Latitude float64 `json:"latitude"` + Longitude float64 `json:"longitude"` + MetroCode uint `json:"metro_code"` +} + +func (rr *responseRecord) String() string { + b := &bytes.Buffer{} + w := csv.NewWriter(b) + w.UseCRLF = true + w.Write([]string{ + rr.IP, + rr.CountryCode, + rr.CountryName, + rr.RegionCode, + rr.RegionName, + rr.City, + rr.ZipCode, + rr.TimeZone, + strconv.FormatFloat(rr.Latitude, 'f', 2, 64), + strconv.FormatFloat(rr.Longitude, 'f', 2, 64), + strconv.Itoa(int(rr.MetroCode)), + }) + w.Flush() + return b.String() +} + +// openDB opens and returns the IP database file or URL. +func openDB(c *Config) (*freegeoip.DB, error) { + u, err := url.Parse(c.DB) + if err != nil || len(u.Scheme) == 0 { + return freegeoip.Open(c.DB) + } + return freegeoip.OpenURL(c.DB, c.UpdateInterval, c.RetryInterval) +} + +// watchEvents logs and collect metrics of database events. +func watchEvents(db *freegeoip.DB) { + for { + select { + case file := <-db.NotifyOpen(): + log.Println("database loaded:", file) + dbEventCounter.WithLabelValues("loaded").Inc() + case err := <-db.NotifyError(): + log.Println("database error:", err) + dbEventCounter.WithLabelValues("failed").Inc() + case <-db.NotifyClose(): + return + } + } +} + +func newRateLimiter(c *Config) (*httprl.RateLimiter, error) { + var backend httprl.Backend + switch c.RateLimitBackend { + case "map": + m := httprl.NewMap(1) + m.Start() + backend = m + case "redis": + addrs := strings.Split(c.RedisAddr, ",") + rc, err := redis.NewClient(addrs...) + if err != nil { + return nil, err + } + rc.SetTimeout(c.RedisTimeout) + backend = redisrl.New(rc) + case "memcache": + addrs := strings.Split(c.MemcacheAddr, ",") + mc := memcache.New(addrs...) + mc.Timeout = c.MemcacheTimeout + backend = memcacherl.New(mc) + default: + return nil, fmt.Errorf("unsupported backend: %q" + c.RateLimitBackend) + } + rl := &httprl.RateLimiter{ + Backend: backend, + Limit: c.RateLimitLimit, + Interval: int32(c.RateLimitInterval.Seconds()), + ErrorLog: c.errorLogger(), + //Policy: httprl.AllowPolicy, + } + return rl, nil +} diff --git a/apiserver/api_test.go b/apiserver/api_test.go new file mode 100644 index 0000000..d9a7ad5 --- /dev/null +++ b/apiserver/api_test.go @@ -0,0 +1,115 @@ +// Copyright 2009 The freegeoip authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package apiserver + +import ( + "bytes" + "encoding/json" + "net/http" + "net/http/httptest" + "net/url" + "testing" +) + +func newTestHandler() (http.Handler, error) { + c := NewConfig() + c.APIPrefix = "/api" + c.PublicDir = "." + c.DB = "../testdata/db.gz" + c.RateLimitLimit = 5 + c.RateLimitBackend = "map" + c.Silent = true + return NewHandler(c) +} + +func TestHandler(t *testing.T) { + f, err := newTestHandler() + if err != nil { + t.Fatal(err) + } + w := &httptest.ResponseRecorder{Body: &bytes.Buffer{}} + r := &http.Request{ + Method: "GET", + URL: &url.URL{Path: "/api/json/200.1.2.3"}, + RemoteAddr: "[::1]:1905", + } + f.ServeHTTP(w, r) + if w.Code != http.StatusOK { + t.Fatalf("Unexpected response: %d %s", w.Code, w.Body.String()) + } + m := struct { + Country string `json:"country_name"` + City string `json:"city"` + }{} + if err = json.NewDecoder(w.Body).Decode(&m); err != nil { + t.Fatal(err) + } + if m.Country != "Venezuela" && m.City != "Caracas" { + t.Fatalf("Query data does not match: want Caracas,Venezuela, have %q,%q", + m.City, m.Country) + } +} + +func TestMetricsHandler(t *testing.T) { + f, err := newTestHandler() + if err != nil { + t.Fatal(err) + } + tp := []http.Request{ + http.Request{ + Method: "GET", + URL: &url.URL{Path: "/api/json/200.1.2.3"}, + RemoteAddr: "[::1]:1905", + }, + http.Request{ + Method: "GET", + URL: &url.URL{Path: "/api/json/200.1.2.3"}, + RemoteAddr: "127.0.0.1:1905", + }, + http.Request{ + Method: "GET", + URL: &url.URL{Path: "/api/json/200.1.2.3"}, + RemoteAddr: "200.1.2.3:1905", + }, + } + for i, r := range tp { + w := &httptest.ResponseRecorder{Body: &bytes.Buffer{}} + f.ServeHTTP(w, &r) + if w.Code != http.StatusOK { + t.Fatalf("Test %d: Unexpected response: %d %s", i, w.Code, w.Body.String()) + } + } +} + +func TestWriters(t *testing.T) { + f, err := newTestHandler() + if err != nil { + t.Fatal(err) + } + tp := []http.Request{ + http.Request{ + Method: "GET", + URL: &url.URL{Path: "/api/csv/"}, + RemoteAddr: "[::1]:1905", + }, + http.Request{ + Method: "GET", + URL: &url.URL{Path: "/api/xml/"}, + RemoteAddr: "[::1]:1905", + }, + http.Request{ + Method: "GET", + URL: &url.URL{Path: "/api/json/"}, + RemoteAddr: "[::1]:1905", + }, + } + for i, r := range tp { + w := &httptest.ResponseRecorder{Body: &bytes.Buffer{}} + f.ServeHTTP(w, &r) + if w.Code != http.StatusOK { + t.Fatalf("Test %d: Unexpected response: %d %s", i, w.Code, w.Body.String()) + } + } +} diff --git a/apiserver/cmd.go b/apiserver/cmd.go deleted file mode 100644 index 37490e4..0000000 --- a/apiserver/cmd.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package apiserver - -import ( - "flag" - "fmt" - "log" - "net/http" - "os" - "strings" - "time" - - // embed pprof server. - _ "net/http/pprof" - - "github.com/fiorix/freegeoip" - "github.com/fiorix/go-redis/redis" - gorilla "github.com/gorilla/handlers" - "github.com/prometheus/client_golang/prometheus" - "golang.org/x/net/http2" -) - -// Version tag. -var Version = "3.0.11" - -var maxmindDB = "http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz" - -var ( - flAPIPrefix = flag.String("api-prefix", "/", "Prefix for API endpoints") - flCORSOrigin = flag.String("cors-origin", "*", "CORS origin API endpoints") - flHTTPAddr = flag.String("http", ":8080", "Address in form of ip:port to listen on for HTTP") - flHTTPSAddr = flag.String("https", "", "Address in form of ip:port to listen on for HTTPS") - flCertFile = flag.String("cert", "cert.pem", "X.509 certificate file") - flKeyFile = flag.String("key", "key.pem", "X.509 key file") - flReadTimeout = flag.Duration("read-timeout", 30*time.Second, "Read timeout for HTTP and HTTPS client conns") - flWriteTimeout = flag.Duration("write-timeout", 15*time.Second, "Write timeout for HTTP and HTTPS client conns") - flPublicDir = flag.String("public", "", "Public directory to serve at the {prefix}/ endpoint") - flDB = flag.String("db", maxmindDB, "IP database file or URL") - flUpdateIntvl = flag.Duration("update", 24*time.Hour, "Database update check interval") - flRetryIntvl = flag.Duration("retry", time.Hour, "Max time to wait before retrying to download database") - flUseXFF = flag.Bool("use-x-forwarded-for", false, "Use the X-Forwarded-For header when available (e.g. when running behind proxies)") - flSilent = flag.Bool("silent", false, "Do not log HTTP or HTTPS requests to stderr") - flLogToStdout = flag.Bool("logtostdout", false, "Log to stdout instead of stderr") - flRedisAddr = flag.String("redis", "127.0.0.1:6379", "Redis address in form of ip:port[,ip:port] for quota") - flRedisTimeout = flag.Duration("redis-timeout", time.Second, "Redis read/write timeout") - flQuotaMax = flag.Int("quota-max", 0, "Max requests per source IP per interval; set 0 to turn off") - flQuotaIntvl = flag.Duration("quota-interval", time.Hour, "Quota expiration interval per source IP querying the API") - flVersion = flag.Bool("version", false, "Show version and exit") - flInternalServer = flag.String("internal-server", "", "Address in form of ip:port to listen on for /metrics and /debug/pprof") -) - -// Run is the entrypoint for the freegeoip daemon tool. -func Run() error { - flag.Parse() - - if *flVersion { - fmt.Printf("freegeoip %s\n", Version) - return nil - } - - if *flLogToStdout { - log.SetOutput(os.Stdout) - } - - log.SetPrefix("[freegeoip] ") - - addrs := strings.Split(*flRedisAddr, ",") - rc, err := redis.NewClient(addrs...) - if err != nil { - return err - } - rc.SetTimeout(*flRedisTimeout) - - db, err := openDB(*flDB, *flUpdateIntvl, *flRetryIntvl) - if err != nil { - return err - } - go watchEvents(db) - - ah := NewHandler(&HandlerConfig{ - Prefix: *flAPIPrefix, - Origin: *flCORSOrigin, - PublicDir: *flPublicDir, - DB: db, - RateLimiter: RateLimiter{ - Redis: rc, - Max: *flQuotaMax, - Interval: *flQuotaIntvl, - }, - UseXForwardedFor: *flUseXFF, - }) - - if !*flSilent { - ah = gorilla.CombinedLoggingHandler(os.Stderr, ah) - } - - if *flUseXFF { - ah = freegeoip.ProxyHandler(ah) - } - - if len(*flInternalServer) > 0 { - http.Handle("/metrics", prometheus.Handler()) - log.Println("freegeoip internal server starting on", *flInternalServer) - go func() { log.Fatal(http.ListenAndServe(*flInternalServer, nil)) }() - } - - if *flHTTPAddr != "" { - log.Println("freegeoip http server starting on", *flHTTPAddr) - srv := &http.Server{ - Addr: *flHTTPAddr, - Handler: ah, - ReadTimeout: *flReadTimeout, - WriteTimeout: *flWriteTimeout, - ConnState: ConnStateMetrics("http"), - } - go func() { log.Fatal(srv.ListenAndServe()) }() - } - - if *flHTTPSAddr != "" { - log.Println("freegeoip https server starting on", *flHTTPSAddr) - srv := &http.Server{ - Addr: *flHTTPSAddr, - Handler: ah, - ReadTimeout: *flReadTimeout, - WriteTimeout: *flWriteTimeout, - ConnState: ConnStateMetrics("https"), - } - http2.ConfigureServer(srv, nil) - go func() { log.Fatal(srv.ListenAndServeTLS(*flCertFile, *flKeyFile)) }() - } - - select {} -} diff --git a/apiserver/cmd_test.go b/apiserver/cmd_test.go deleted file mode 100644 index c5c43f0..0000000 --- a/apiserver/cmd_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package apiserver - -import ( - "flag" - "testing" - "time" -) - -func TestCmd(t *testing.T) { - flag.Set("http", ":0") - flag.Set("db", "../testdata/db.gz") - flag.Set("silent", "true") - errc := make(chan error) - go func() { - errc <- Run() - }() - select { - case err := <-errc: - t.Fatal(err) - case <-time.After(time.Second): - } -} diff --git a/apiserver/config.go b/apiserver/config.go new file mode 100644 index 0000000..6e1cf92 --- /dev/null +++ b/apiserver/config.go @@ -0,0 +1,109 @@ +// Copyright 2009 The freegeoip authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package apiserver + +import ( + "flag" + "io" + "log" + "os" + "time" + + "github.com/fiorix/freegeoip" +) + +// Config is the configuration of the freegeoip server. +type Config struct { + ServerAddr string + TLSServerAddr string + TLSCertFile string + TLSKeyFile string + APIPrefix string + CORSOrigin string + ReadTimeout time.Duration + WriteTimeout time.Duration + PublicDir string + DB string + UpdateInterval time.Duration + RetryInterval time.Duration + UseXForwardedFor bool + Silent bool + LogToStdout bool + RedisAddr string + RedisTimeout time.Duration + MemcacheAddr string + MemcacheTimeout time.Duration + RateLimitBackend string + RateLimitLimit uint64 + RateLimitInterval time.Duration + InternalServerAddr string + + errorLog *log.Logger + accessLog *log.Logger +} + +// NewConfig creates and initializes a new Config with default values. +func NewConfig() *Config { + return &Config{ + ServerAddr: "localhost:8080", + TLSCertFile: "cert.pem", + TLSKeyFile: "key.pem", + APIPrefix: "/", + CORSOrigin: "*", + ReadTimeout: 30 * time.Second, + WriteTimeout: 15 * time.Second, + DB: freegeoip.MaxMindDB, + UpdateInterval: 24 * time.Hour, + RetryInterval: 2 * time.Hour, + RedisAddr: "localhost:6379", + RedisTimeout: time.Second, + MemcacheAddr: "localhost:11211", + MemcacheTimeout: time.Second, + RateLimitBackend: "redis", + RateLimitInterval: time.Hour, + } +} + +// AddFlags adds configuration flags to the given FlagSet. +func (c *Config) AddFlags(fs *flag.FlagSet) { + fs.StringVar(&c.ServerAddr, "http", c.ServerAddr, "Address in form of ip:port to listen on for HTTP") + fs.StringVar(&c.TLSServerAddr, "https", c.TLSServerAddr, "Address in form of ip:port to listen on for HTTPS") + fs.StringVar(&c.TLSCertFile, "cert", c.TLSCertFile, "X.509 certificate file for HTTPS server") + fs.StringVar(&c.TLSKeyFile, "key", c.TLSKeyFile, "X.509 key file for HTTPS server") + fs.StringVar(&c.APIPrefix, "api-prefix", c.APIPrefix, "URL prefix for API endpoints") + fs.StringVar(&c.CORSOrigin, "cors-origin", c.CORSOrigin, "CORS origin API endpoints") + fs.DurationVar(&c.ReadTimeout, "read-timeout", c.ReadTimeout, "Read timeout for HTTP and HTTPS client conns") + fs.DurationVar(&c.WriteTimeout, "write-timeout", c.WriteTimeout, "Write timeout for HTTP and HTTPS client conns") + fs.StringVar(&c.PublicDir, "public", c.PublicDir, "Public directory to serve at the {prefix}/ endpoint") + fs.StringVar(&c.DB, "db", c.DB, "IP database file or URL") + fs.DurationVar(&c.UpdateInterval, "update", c.UpdateInterval, "Database update check interval") + fs.DurationVar(&c.RetryInterval, "retry", c.RetryInterval, "Max time to wait before retrying to download database") + fs.BoolVar(&c.UseXForwardedFor, "use-x-forwarded-for", c.UseXForwardedFor, "Use the X-Forwarded-For header when available (e.g. behind proxy)") + fs.BoolVar(&c.Silent, "silent", c.Silent, "Disable HTTP and HTTPS log request details") + fs.BoolVar(&c.LogToStdout, "logtostdout", c.LogToStdout, "Log to stdout instead of stderr") + fs.StringVar(&c.RedisAddr, "redis", c.RedisAddr, "Redis address in form of host:port[,host:port] for quota") + fs.DurationVar(&c.RedisTimeout, "redis-timeout", c.RedisTimeout, "Redis read/write timeout") + fs.StringVar(&c.MemcacheAddr, "memcache", c.MemcacheAddr, "Memcache address in form of host:port[,host:port] for quota") + fs.DurationVar(&c.MemcacheTimeout, "memcache-timeout", c.MemcacheTimeout, "Memcache read/write timeout") + fs.StringVar(&c.RateLimitBackend, "quota-backend", c.RateLimitBackend, "Backend for rate limiter: map, redis, or memcache") + fs.Uint64Var(&c.RateLimitLimit, "quota-max", c.RateLimitLimit, "Max requests per source IP per interval; set 0 to turn quotas off") + fs.DurationVar(&c.RateLimitInterval, "quota-interval", c.RateLimitInterval, "Quota expiration interval, per source IP querying the API") + fs.StringVar(&c.InternalServerAddr, "internal-server", c.InternalServerAddr, "Address in form of ip:port to listen on for metrics and pprof") +} + +func (c *Config) logWriter() io.Writer { + if c.LogToStdout { + return os.Stdout + } + return os.Stderr +} + +func (c *Config) errorLogger() *log.Logger { + return log.New(c.logWriter(), "[error] ", log.LstdFlags) +} + +func (c *Config) accessLogger() *log.Logger { + return log.New(c.logWriter(), "[access] ", 0) +} diff --git a/apiserver/config_test.go b/apiserver/config_test.go new file mode 100644 index 0000000..7cef385 --- /dev/null +++ b/apiserver/config_test.go @@ -0,0 +1,15 @@ +// Copyright 2009 The freegeoip authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package apiserver + +import ( + "flag" + "testing" +) + +func TestConfig(t *testing.T) { + c := NewConfig() + c.AddFlags(flag.NewFlagSet("freegeoip", flag.ContinueOnError)) +} diff --git a/apiserver/cors.go b/apiserver/cors.go deleted file mode 100644 index 0af9ace..0000000 --- a/apiserver/cors.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package apiserver - -import ( - "net/http" - "strings" -) - -// cors is an HTTP handler for managing cross-origin resource sharing. -// Ref: https://en.wikipedia.org/wiki/Cross-origin_resource_sharing. -func cors(f http.Handler, origin string, methods ...string) http.Handler { - ms := strings.Join(methods, ", ") + ", OPTIONS" - md := make(map[string]struct{}) - for _, method := range methods { - md[method] = struct{}{} - } - cf := func(w http.ResponseWriter, r *http.Request) { - orig := origin - if orig == "*" { - if ro := r.Header.Get("Origin"); ro != "" { - orig = ro - } - } - w.Header().Set("Access-Control-Allow-Origin", orig) - w.Header().Set("Access-Control-Allow-Methods", ms) - w.Header().Set("Access-Control-Allow-Credentials", "true") - if r.Method == "OPTIONS" { - w.WriteHeader(http.StatusOK) - return - } - if _, exists := md[r.Method]; exists { - f.ServeHTTP(w, r) - return - } - w.Header().Set("Allow", ms) - http.Error(w, - http.StatusText(http.StatusMethodNotAllowed), - http.StatusMethodNotAllowed) - } - return http.HandlerFunc(cf) -} diff --git a/apiserver/cors_test.go b/apiserver/cors_test.go deleted file mode 100644 index 5dcfe02..0000000 --- a/apiserver/cors_test.go +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package apiserver - -import ( - "bytes" - "io" - "io/ioutil" - "net/http" - "net/http/httptest" - "net/url" - "testing" -) - -func TestCORS(t *testing.T) { - // set up the test server - handler := func(w http.ResponseWriter, r *http.Request) { - io.WriteString(w, "hello world") - } - mux := http.NewServeMux() - mux.Handle("/", cors(http.HandlerFunc(handler), "*", "GET")) - ts := httptest.NewServer(mux) - defer ts.Close() - // create and issue an OPTIONS request and - // validate response status and headers. - req, err := http.NewRequest("OPTIONS", ts.URL, nil) - if err != nil { - t.Fatal(err) - } - req.Header.Add("Origin", ts.URL) - resp, err := http.DefaultClient.Do(req) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - t.Fatalf("Unexpected response status: %s", resp.Status) - } - if resp.ContentLength != 0 { - t.Fatalf("Unexpected Content-Length. Want 0, have %d", - resp.ContentLength) - } - want := []struct { - Name string - Value string - }{ - {"Access-Control-Allow-Origin", ts.URL}, - {"Access-Control-Allow-Methods", "GET, OPTIONS"}, - {"Access-Control-Allow-Credentials", "true"}, - } - for _, th := range want { - if v := resp.Header.Get(th.Name); v != th.Value { - t.Fatalf("Unexpected value for %q. Want %q, have %q", - th.Name, th.Value, v) - } - } - // issue a GET request and validate response headers and body - resp, err = http.Get(ts.URL) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - want[0].Value = "*" // Origin - for _, th := range want { - if v := resp.Header.Get(th.Name); v != th.Value { - t.Fatalf("Unexpected value for %q. Want %q, have %q", - th.Name, th.Value, v) - } - } - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - wb := []byte("hello world") - if !bytes.Equal(b, wb) { - t.Fatalf("Unexpected response body. Want %q, have %q", b, wb) - } - // issue a POST request and validate response status - resp, err = http.PostForm(ts.URL, url.Values{}) - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusMethodNotAllowed { - t.Fatalf("Unexpected response status: %s", resp.Status) - } -} diff --git a/apiserver/db.go b/apiserver/db.go deleted file mode 100644 index e815aa7..0000000 --- a/apiserver/db.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package apiserver - -import ( - "log" - "net/url" - "time" - - "github.com/fiorix/freegeoip" -) - -// openDB opens and returns the IP database. -func openDB(dsn string, updateIntvl, maxRetryIntvl time.Duration) (db *freegeoip.DB, err error) { - u, err := url.Parse(dsn) - if err != nil || len(u.Scheme) == 0 { - db, err = freegeoip.Open(dsn) - } else { - db, err = freegeoip.OpenURL(dsn, updateIntvl, maxRetryIntvl) - } - return -} - -// watchEvents logs and collect metrics of database events. -func watchEvents(db *freegeoip.DB) { - for { - select { - case file := <-db.NotifyOpen(): - log.Println("database loaded:", file) - dbEventCounter.WithLabelValues("loaded").Inc() - case err := <-db.NotifyError(): - log.Println("database error:", err) - dbEventCounter.WithLabelValues("failed").Inc() - case <-db.NotifyClose(): - return - } - } -} diff --git a/apiserver/doc.go b/apiserver/doc.go index 0434eea..8ad530c 100644 --- a/apiserver/doc.go +++ b/apiserver/doc.go @@ -1,7 +1,6 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. +// Copyright 2009 The freegeoip authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// Package apiserver provides the freegeoip web server API, used by -// the freegeoip daemon tool. +// Package apiserver provides the freegeoip web server. package apiserver diff --git a/apiserver/http.go b/apiserver/http.go deleted file mode 100644 index 87fdd93..0000000 --- a/apiserver/http.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package apiserver - -import ( - "net" - "net/http" - "path" - - "github.com/fiorix/freegeoip" - "github.com/prometheus/client_golang/prometheus" -) - -// HandlerConfig holds configuration for freegeoip http handlers. -type HandlerConfig struct { - Prefix string - Origin string - PublicDir string - DB *freegeoip.DB - RateLimiter RateLimiter - UseXForwardedFor bool -} - -// NewHandler creates a freegeoip http handler. -func NewHandler(conf *HandlerConfig) http.Handler { - ah := &apiHandler{conf} - mux := http.NewServeMux() - ah.RegisterPublicDir(mux) - ah.RegisterEncoder(mux, "csv", &freegeoip.CSVEncoder{UseCRLF: true}) - ah.RegisterEncoder(mux, "xml", &freegeoip.XMLEncoder{Indent: true}) - ah.RegisterEncoder(mux, "json", &freegeoip.JSONEncoder{}) - return ah.metricsCollector(mux) -} - -// ConnStateFunc is a function that can handle connection state. -type ConnStateFunc func(c net.Conn, s http.ConnState) - -// ConnStateMetrics collect metrics per connection state, per protocol. -// e.g. new http, closed http. -func ConnStateMetrics(proto string) ConnStateFunc { - return func(c net.Conn, s http.ConnState) { - switch s { - case http.StateNew: - clientConnsGauge.WithLabelValues(proto).Inc() - case http.StateClosed: - clientConnsGauge.WithLabelValues(proto).Dec() - } - } -} - -type apiHandler struct { - conf *HandlerConfig -} - -func (ah *apiHandler) prefix(p string) string { - p = path.Clean(path.Join("/", ah.conf.Prefix, p)) - if p[len(p)-1] != '/' { - p += "/" - } - return p -} - -func (ah *apiHandler) RegisterPublicDir(mux *http.ServeMux) { - if ah.conf.PublicDir == "" { - return - } - fs := http.FileServer(http.Dir(ah.conf.PublicDir)) - fs = prometheus.InstrumentHandler("frontend", fs) - prefix := ah.prefix("") - mux.Handle(prefix, http.StripPrefix(prefix, fs)) -} - -func (ah *apiHandler) RegisterEncoder(mux *http.ServeMux, path string, enc freegeoip.Encoder) { - f := http.Handler(freegeoip.NewHandler(ah.conf.DB, enc)) - if ah.conf.RateLimiter.Max > 0 { - rl := ah.conf.RateLimiter - rl.Handler = f - f = &rl - } - origin := ah.conf.Origin - if origin == "" { - origin = "*" - } - f = cors(f, origin, "GET", "HEAD") - f = prometheus.InstrumentHandler(path, f) - mux.Handle(ah.prefix(path), f) -} - -func (ah *apiHandler) metricsCollector(handler http.Handler) http.Handler { - type query struct { - Country struct { - ISOCode string `maxminddb:"iso_code"` - } `maxminddb:"country"` - } - f := func(w http.ResponseWriter, r *http.Request) { - handler.ServeHTTP(w, r) - // Collect metrics after serving the request. - var ip net.IP - if ah.conf.UseXForwardedFor { - ip = net.ParseIP(r.RemoteAddr) - } else { - addr, _, _ := net.SplitHostPort(r.RemoteAddr) - ip = net.ParseIP(addr) - } - if ip == nil { - // TODO: increment error count? - return - } - if ip.To4() != nil { - clientIPProtoCounter.WithLabelValues("4").Inc() - } else { - clientIPProtoCounter.WithLabelValues("6").Inc() - } - var q query - err := ah.conf.DB.Lookup(ip, &q) - if err != nil || q.Country.ISOCode == "" { - clientCountryCounter.WithLabelValues("unknown").Inc() - return - } - clientCountryCounter.WithLabelValues(q.Country.ISOCode).Inc() - } - return http.HandlerFunc(f) -} diff --git a/apiserver/http_test.go b/apiserver/http_test.go deleted file mode 100644 index 335464e..0000000 --- a/apiserver/http_test.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package apiserver - -import ( - "encoding/json" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/fiorix/freegeoip" - "github.com/fiorix/go-redis/redis" -) - -func newTestHandler(db *freegeoip.DB) http.Handler { - return NewHandler(&HandlerConfig{ - Prefix: "/api", - PublicDir: ".", - DB: db, - RateLimiter: RateLimiter{ - Redis: redis.New(), - Max: 5, - Interval: time.Second, - KeyMaker: KeyMakerFunc(func(r *http.Request) string { - return "handler-test" - }), - }, - }) -} - -func TestHandler(t *testing.T) { - db, err := freegeoip.Open("../testdata/db.gz") - if err != nil { - t.Fatal(err) - } - defer db.Close() - s := httptest.NewServer(newTestHandler(db)) - defer s.Close() - // query some known location... - resp, err := http.Get(s.URL + "/api/json/200.1.2.3") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - switch resp.StatusCode { - case http.StatusOK: - case http.StatusServiceUnavailable: - t.Skip("Redis available?") - default: - t.Fatal(resp.Status) - } - m := struct { - Country string `json:"country_name"` - City string `json:"city"` - }{} - if err = json.NewDecoder(resp.Body).Decode(&m); err != nil { - t.Fatal(err) - } - if m.Country != "Venezuela" && m.City != "Caracas" { - t.Fatalf("Query data does not match: want Caracas,Venezuela, have %q,%q", - m.City, m.Country) - } -} diff --git a/apiserver/main.go b/apiserver/main.go new file mode 100644 index 0000000..21ef98a --- /dev/null +++ b/apiserver/main.go @@ -0,0 +1,100 @@ +// Copyright 2009 The freegeoip authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package apiserver + +import ( + "flag" + "fmt" + "log" + "net" + "net/http" + "os" + + // embed pprof server. + _ "net/http/pprof" + + "github.com/prometheus/client_golang/prometheus" +) + +// Version tag. +var Version = "3.1.0" + +// Run is the entrypoint for the freegeoip server. +func Run() { + c := NewConfig() + c.AddFlags(flag.CommandLine) + sv := flag.Bool("version", false, "Show version and exit") + flag.Parse() + if *sv { + fmt.Printf("freegeoip %s\n", Version) + return + } + if c.LogToStdout { + log.SetOutput(os.Stdout) + } + log.SetPrefix("[freegeoip] ") + f, err := NewHandler(c) + if err != nil { + log.Fatal(err) + } + if c.ServerAddr != "" { + go runServer(c, f) + } + if c.TLSServerAddr != "" { + go runTLSServer(c, f) + } + if c.InternalServerAddr != "" { + go runInternalServer(c) + } + select {} +} + +// connStateFunc is a function that can handle connection state. +type connStateFunc func(c net.Conn, s http.ConnState) + +// connStateMetrics collect metrics per connection state, per protocol. +// e.g. new http, closed http. +func connStateMetrics(proto string) connStateFunc { + return func(c net.Conn, s http.ConnState) { + switch s { + case http.StateNew: + clientConnsGauge.WithLabelValues(proto).Inc() + case http.StateClosed: + clientConnsGauge.WithLabelValues(proto).Dec() + } + } +} + +func runServer(c *Config, f http.Handler) { + log.Println("freegeoip http server starting on", c.ServerAddr) + srv := &http.Server{ + Addr: c.ServerAddr, + Handler: f, + ReadTimeout: c.ReadTimeout, + WriteTimeout: c.WriteTimeout, + ErrorLog: c.errorLogger(), + ConnState: connStateMetrics("http"), + } + log.Fatal(srv.ListenAndServe()) +} + +func runTLSServer(c *Config, f http.Handler) { + log.Println("freegeoip https server starting on", c.TLSServerAddr) + srv := &http.Server{ + Addr: c.TLSServerAddr, + Handler: f, + ReadTimeout: c.ReadTimeout, + WriteTimeout: c.WriteTimeout, + ErrorLog: c.errorLogger(), + ConnState: connStateMetrics("https"), + } + log.Fatal(srv.ListenAndServeTLS(c.TLSCertFile, c.TLSKeyFile)) +} + +func runInternalServer(c *Config) { + http.Handle("/metrics", prometheus.Handler()) + log.Println("freegeoip internal server starting on", c.InternalServerAddr) + log.Fatal(http.ListenAndServe(c.InternalServerAddr, nil)) +} diff --git a/apiserver/metrics.go b/apiserver/metrics.go index f924435..8188bd1 100644 --- a/apiserver/metrics.go +++ b/apiserver/metrics.go @@ -1,4 +1,4 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. +// Copyright 2009 The freegeoip authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. diff --git a/apiserver/ratelimit.go b/apiserver/ratelimit.go deleted file mode 100644 index af05bdc..0000000 --- a/apiserver/ratelimit.go +++ /dev/null @@ -1,97 +0,0 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package apiserver - -import ( - "errors" - "net" - "net/http" - "strconv" - "sync" - "time" - - "github.com/fiorix/go-redis/redis" -) - -var ( - errQuotaExceeded = errors.New("Quota exceeded") - errRedisUnavailable = errors.New("Try again later") -) - -// A KeyMaker makes keys from the http.Request object to the RateLimiter. -type KeyMaker interface { - KeyFor(r *http.Request) string -} - -// KeyMakerFunc is an adapter function for KeyMaker. -type KeyMakerFunc func(r *http.Request) string - -// KeyFor implements the KeyMaker interface. -func (f KeyMakerFunc) KeyFor(r *http.Request) string { - return f(r) -} - -// DefaultKeyMaker is a KeyMaker that returns the client IP -// address from http.Request.RemoteAddr. -var DefaultKeyMaker = KeyMakerFunc(func(r *http.Request) string { - addr, _, _ := net.SplitHostPort(r.RemoteAddr) - return addr -}) - -// A RateLimiter is an http.Handler that wraps another handler, -// and calls it up to a certain limit, max per interval. -type RateLimiter struct { - Redis *redis.Client - Max int - Interval time.Duration - KeyMaker KeyMaker - Handler http.Handler - - secInterval int - once sync.Once -} - -// ServeHTTP implements the http.Handler interface. -func (rl *RateLimiter) ServeHTTP(w http.ResponseWriter, r *http.Request) { - rl.once.Do(func() { - rl.secInterval = int(rl.Interval.Seconds()) - if rl.KeyMaker == nil { - rl.KeyMaker = DefaultKeyMaker - } - }) - status, err := rl.do(w, r) - if err != nil { - http.Error(w, err.Error(), status) - return - } -} - -func (rl *RateLimiter) do(w http.ResponseWriter, r *http.Request) (int, error) { - k := rl.KeyMaker.KeyFor(r) - nreq, err := rl.Redis.Incr(k) - if err != nil { - return http.StatusServiceUnavailable, errRedisUnavailable - } - ttl, err := rl.Redis.TTL(k) - if err != nil { - return http.StatusServiceUnavailable, errRedisUnavailable - } - if ttl == -1 { - if _, err = rl.Redis.Expire(k, rl.secInterval); err != nil { - return http.StatusServiceUnavailable, errRedisUnavailable - } - ttl = rl.secInterval - } - rem := rl.Max - nreq - w.Header().Set("X-RateLimit-Limit", strconv.Itoa(rl.Max)) - w.Header().Set("X-RateLimit-Reset", strconv.Itoa(ttl)) - if rem < 0 { - w.Header().Set("X-RateLimit-Remaining", "0") - return http.StatusForbidden, errQuotaExceeded - } - w.Header().Set("X-RateLimit-Remaining", strconv.Itoa(rem)) - rl.Handler.ServeHTTP(w, r) - return http.StatusOK, nil -} diff --git a/apiserver/ratelimit_test.go b/apiserver/ratelimit_test.go deleted file mode 100644 index 05b6fbc..0000000 --- a/apiserver/ratelimit_test.go +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package apiserver - -import ( - "log" - "net/http" - "net/http/httptest" - "strconv" - "sync" - "testing" - "time" - - "github.com/fiorix/go-redis/redis" -) - -func TestRateLimiter(t *testing.T) { - counter := struct { - sync.Mutex - n int - }{} - hf := func(w http.ResponseWriter, r *http.Request) { - counter.Lock() - counter.n++ - counter.Unlock() - } - kmf := func(r *http.Request) string { - return "rate-limiter-test" - } - rl := &RateLimiter{ - Redis: redis.New(), - Max: 2, - Interval: time.Second, - KeyMaker: KeyMakerFunc(kmf), - Handler: http.HandlerFunc(hf), - } - mux := http.NewServeMux() - mux.Handle("/", rl) - s := httptest.NewServer(mux) - defer s.Close() - for i := 0; i < 3; i++ { - resp, err := http.Get(s.URL) - if err != nil { - t.Fatal(err) - } - resp.Body.Close() - if resp.StatusCode == http.StatusServiceUnavailable { - t.Skip("Redis unavailable, cannot proceed") - } - if resp.StatusCode != http.StatusOK { - if resp.StatusCode == http.StatusForbidden && i != 2 { - t.Fatal(resp.Status) - } - } - lim, _ := strconv.Atoi(resp.Header.Get("X-RateLimit-Limit")) - rem, _ := strconv.Atoi(resp.Header.Get("X-RateLimit-Remaining")) - res, _ := strconv.Atoi(resp.Header.Get("X-RateLimit-Reset")) - switch { - case i == 0 && lim == 2 && rem == 1 && res > 0: - case (i == 1 || i == 2) && lim == 2 && rem == 0 && res > 0: - default: - log.Fatalf("Unexpected values: limit=%d, remaining=%d, reset=%d", - lim, rem, res) - } - } -} diff --git a/cmd/freegeoip/main.go b/cmd/freegeoip/main.go index 5a65817..4894a03 100644 --- a/cmd/freegeoip/main.go +++ b/cmd/freegeoip/main.go @@ -1,17 +1,11 @@ -// Copyright 2009-2015 The freegeoip authors. All rights reserved. +// Copyright 2009 The freegeoip authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package main -import ( - "log" - - "github.com/fiorix/freegeoip/apiserver" -) +import "github.com/fiorix/freegeoip/apiserver" func main() { - if err := apiserver.Run(); err != nil { - log.Fatal(err) - } + apiserver.Run() } diff --git a/db.go b/db.go index 39a5ceb..ed5792c 100644 --- a/db.go +++ b/db.go @@ -1,4 +1,4 @@ -// Copyright 2009-2014 The freegeoip authors. All rights reserved. +// Copyright 2009 The freegeoip authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -31,6 +31,9 @@ var ( // Local cached copy of a database downloaded from a URL. defaultDB = filepath.Join(os.TempDir(), "freegeoip", "db.gz") + + // MaxMindDB is the URL of the free MaxMind GeoLite2 database. + MaxMindDB = "http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz" ) // DB is the IP geolocation database. @@ -72,7 +75,7 @@ func Open(dsn string) (db *DB, err error) { return db, nil } -// OpenURL creates and initializes a DB from a remote file. +// OpenURL creates and initializes a DB from a URL. // It automatically downloads and updates the file in background, and // keeps a local copy on $TMPDIR. func OpenURL(url string, updateInterval, maxRetryInterval time.Duration) (db *DB, err error) { @@ -331,6 +334,33 @@ func (db *DB) Lookup(addr net.IP, result interface{}) error { return ErrUnavailable } +// DefaultQuery is the default query used for database lookups. +type DefaultQuery struct { + Continent struct { + Names map[string]string `maxminddb:"names"` + } `maxminddb:"continent"` + Country struct { + ISOCode string `maxminddb:"iso_code"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"country"` + Region []struct { + ISOCode string `maxminddb:"iso_code"` + Names map[string]string `maxminddb:"names"` + } `maxminddb:"subdivisions"` + City struct { + Names map[string]string `maxminddb:"names"` + } `maxminddb:"city"` + Location struct { + Latitude float64 `maxminddb:"latitude"` + Longitude float64 `maxminddb:"longitude"` + MetroCode uint `maxminddb:"metro_code"` + TimeZone string `maxminddb:"time_zone"` + } `maxminddb:"location"` + Postal struct { + Code string `maxminddb:"code"` + } `maxminddb:"postal"` +} + // Close the database. func (db *DB) Close() { db.mu.Lock() diff --git a/db_test.go b/db_test.go index 0cc7a30..4d03730 100644 --- a/db_test.go +++ b/db_test.go @@ -1,4 +1,4 @@ -// Copyright 2009-2014 The freegeoip authors. All rights reserved. +// Copyright 2009 The freegeoip authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -22,7 +22,7 @@ func TestDownload(t *testing.T) { t.Skip("Test database already exists:", testFile) } db := &DB{} - dbfile, err := db.download(maxmindFile) + dbfile, err := db.download(MaxMindDB) if err != nil { t.Fatal(err) } @@ -198,7 +198,7 @@ func TestLookupOnFile(t *testing.T) { t.Fatal(err) } defer db.Close() - var record testRecord + var record DefaultQuery err = db.Lookup(net.ParseIP("8.8.8.8"), &record) if err != nil { t.Fatal(err) @@ -231,7 +231,7 @@ func TestLookupOnURL(t *testing.T) { case <-time.After(5 * time.Second): t.Fatal("Timed out") } - var record testRecord + var record DefaultQuery err = db.Lookup(net.ParseIP("8.8.8.8"), &record) if err != nil { t.Fatal(err) @@ -248,9 +248,3 @@ func TestLookuUnavailable(t *testing.T) { t.Fatal("Unexpected lookup worked") } } - -type testRecord struct { - Country struct { - ISOCode string `maxminddb:"iso_code"` - } `maxminddb:"country"` -} diff --git a/doc.go b/doc.go index 61663e7..6590347 100644 --- a/doc.go +++ b/doc.go @@ -1,4 +1,4 @@ -// Copyright 2009-2014 The freegeoip authors. All rights reserved. +// Copyright 2009 The freegeoip authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. @@ -11,9 +11,4 @@ // // Remote databases are automatically downloaded and updated in background // so you can focus on using the API and not managing the database. -// -// Also, the freegeoip package provides http handlers that any Go http -// server (net/http) can use. These handlers can process IP geolocation -// lookup requests and return data in multiple formats like CSV, XML, -// JSON and JSONP. It has also an API for supporting custom formats. package freegeoip diff --git a/encoder.go b/encoder.go deleted file mode 100644 index 88588c8..0000000 --- a/encoder.go +++ /dev/null @@ -1,263 +0,0 @@ -// Copyright 2009-2014 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package freegeoip - -import ( - "encoding/csv" - "encoding/json" - "encoding/xml" - "io" - "math" - "net" - "net/http" - "strconv" - "strings" -) - -// A Query object is used to query the IP database. -// -// Currently the only database supported is MaxMind, and the query is a -// data structure with tags that are used by the maxminddb.Lookup function. -type Query interface{} - -// An Encoder that can provide a query specification to be used for -// querying the IP database, and later encode the results of that -// query in a specific format. -type Encoder interface { - // NewQuery returns a query specification that is used to query - // the IP database. It should be a data structure with tags - // associated to its fields describing what fields to query in - // the IP database, such as country and city. - // - // See the maxminddb package documentation for details on - // fields available for the MaxMind database. - NewQuery() Query - - // Encode writes data to the response of an http request - // using the results of a query to the IP database. - // - // It encodes the query object into a specific format such - // as XML or JSON and writes to the response. - // - // The IP passed to the encoder may be the result of a DNS - // lookup, and if there are multiple IPs associated to the - // hostname this will be a random one from the list. - Encode(w http.ResponseWriter, r *http.Request, q Query, ip net.IP) error -} - -// JSONEncoder encodes the results of an IP lookup as JSON. -type JSONEncoder struct { - Indent bool -} - -// NewQuery implements the Encoder interface. -func (f *JSONEncoder) NewQuery() Query { - return &maxmindQuery{} -} - -// Encode implements the Encoder interface. -func (f *JSONEncoder) Encode(w http.ResponseWriter, r *http.Request, q Query, ip net.IP) error { - record := newResponse(q.(*maxmindQuery), ip, requestLang(r)) - callback := r.FormValue("callback") - if len(callback) > 0 { - return f.P(w, r, record, callback) - } - w.Header().Set("Content-Type", "application/json") - if f.Indent { - } - return json.NewEncoder(w).Encode(record) -} - -// P writes JSONP to an http response. -func (f *JSONEncoder) P(w http.ResponseWriter, r *http.Request, record *responseRecord, callback string) error { - w.Header().Set("Content-Type", "application/javascript") - _, err := io.WriteString(w, callback+"(") - if err != nil { - return err - } - err = json.NewEncoder(w).Encode(record) - if err != nil { - return err - } - _, err = io.WriteString(w, ");") - return err -} - -// XMLEncoder encodes the results of an IP lookup as XML. -type XMLEncoder struct { - Indent bool -} - -// NewQuery implements the Encoder interface. -func (f *XMLEncoder) NewQuery() Query { - return &maxmindQuery{} -} - -// Encode implements the Encoder interface. -func (f *XMLEncoder) Encode(w http.ResponseWriter, r *http.Request, q Query, ip net.IP) error { - record := newResponse(q.(*maxmindQuery), ip, requestLang(r)) - w.Header().Set("Content-Type", "application/xml") - _, err := io.WriteString(w, xml.Header) - if err != nil { - return err - } - if f.Indent { - enc := xml.NewEncoder(w) - enc.Indent("", "\t") - err := enc.Encode(record) - if err != nil { - return err - } - _, err = w.Write([]byte("\n")) - return err - } - return xml.NewEncoder(w).Encode(record) -} - -// CSVEncoder encodes the results of an IP lookup as CSV. -type CSVEncoder struct { - UseCRLF bool -} - -// NewQuery implements the Encoder interface. -func (f *CSVEncoder) NewQuery() Query { - return &maxmindQuery{} -} - -// Encode implements the Encoder interface. -func (f *CSVEncoder) Encode(w http.ResponseWriter, r *http.Request, q Query, ip net.IP) error { - record := newResponse(q.(*maxmindQuery), ip, requestLang(r)) - w.Header().Set("Content-Type", "text/csv") - cw := csv.NewWriter(w) - cw.UseCRLF = f.UseCRLF - err := cw.Write([]string{ - ip.String(), - record.CountryCode, - record.CountryName, - record.RegionCode, - record.RegionName, - record.City, - record.ZipCode, - record.TimeZone, - strconv.FormatFloat(record.Latitude, 'f', 2, 64), - strconv.FormatFloat(record.Longitude, 'f', 2, 64), - strconv.Itoa(int(record.MetroCode)), - }) - if err != nil { - return err - } - cw.Flush() - return nil -} - -// maxmindQuery is the object used to query the maxmind database. -// -// See the maxminddb package documentation for details. -type maxmindQuery struct { - Country struct { - ISOCode string `maxminddb:"iso_code"` - Names map[string]string `maxminddb:"names"` - } `maxminddb:"country"` - Region []struct { - ISOCode string `maxminddb:"iso_code"` - Names map[string]string `maxminddb:"names"` - } `maxminddb:"subdivisions"` - City struct { - Names map[string]string `maxminddb:"names"` - } `maxminddb:"city"` - Location struct { - Latitude float64 `maxminddb:"latitude"` - Longitude float64 `maxminddb:"longitude"` - MetroCode uint `maxminddb:"metro_code"` - TimeZone string `maxminddb:"time_zone"` - } `maxminddb:"location"` - Postal struct { - Code string `maxminddb:"code"` - } `maxminddb:"postal"` -} - -// responseRecord is the object that gets encoded as the response of an -// IP lookup request. It is encoded to formats such as xml and json. -type responseRecord struct { - XMLName xml.Name `xml:"Response" json:"-"` - IP string `json:"ip"` - CountryCode string `json:"country_code"` - CountryName string `json:"country_name"` - RegionCode string `json:"region_code"` - RegionName string `json:"region_name"` - City string `json:"city"` - ZipCode string `json:"zip_code"` - TimeZone string `json:"time_zone"` - Latitude float64 `json:"latitude"` - Longitude float64 `json:"longitude"` - MetroCode uint `json:"metro_code"` -} - -// newResponse translates a maxmindQuery into a responseRecord, setting -// the country, region and city names to their localized name according -// to the given lang. -// -// See the maxminddb documentation for supported languages. -func newResponse(query *maxmindQuery, ip net.IP, lang []string) *responseRecord { - record := &responseRecord{ - IP: ip.String(), - CountryCode: query.Country.ISOCode, - CountryName: localizedName(query.Country.Names, lang), - City: localizedName(query.City.Names, lang), - ZipCode: query.Postal.Code, - TimeZone: query.Location.TimeZone, - Latitude: roundFloat(query.Location.Latitude, .5, 4), - Longitude: roundFloat(query.Location.Longitude, .5, 4), - MetroCode: query.Location.MetroCode, - } - if len(query.Region) > 0 { - record.RegionCode = query.Region[0].ISOCode - record.RegionName = localizedName(query.Region[0].Names, lang) - } - return record -} - -func requestLang(r *http.Request) (list []string) { - // TODO: Check Accept-Charset, sort languages by qvalue. - l := r.Header.Get("Accept-Language") - if len(l) == 0 { - return nil - } - accpt := strings.Split(l, ",") - if len(accpt) == 0 { - return nil - } - for n, name := range accpt { - accpt[n] = strings.Trim(strings.SplitN(name, ";", 2)[0], " ") - } - return accpt -} - -func localizedName(field map[string]string, accept []string) (name string) { - if accept != nil { - var f string - var ok bool - for _, l := range accept { - f, ok = field[l] - if ok { - return f - } - } - } - return field["en"] -} - -func roundFloat(val float64, roundOn float64, places int) (newVal float64) { - var round float64 - pow := math.Pow(10, float64(places)) - digit := pow * val - _, div := math.Modf(digit) - if div >= roundOn { - round = math.Ceil(digit) - } else { - round = math.Floor(digit) - } - return round / pow -} diff --git a/encoder_test.go b/encoder_test.go deleted file mode 100644 index d51b77a..0000000 --- a/encoder_test.go +++ /dev/null @@ -1,236 +0,0 @@ -// Copyright 2009-2014 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package freegeoip - -import ( - "bytes" - "encoding/csv" - "encoding/json" - "encoding/xml" - "io/ioutil" - "net/http" - "testing" - - "github.com/robertkrimen/otto" -) - -func TestCSVEncoder(t *testing.T) { - _, srv, err := runServer("/csv/", &CSVEncoder{}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - resp, err := http.Get(srv.URL + "/csv/8.8.8.8") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - t.Fatal(resp.Status) - } - row, err := csv.NewReader(resp.Body).Read() - if err != nil { - t.Fatal(err) - } - if row[1] != "US" { - t.Fatalf("Unexpected country code in record: %#v", row) - } -} - -func TestXMLEncoder(t *testing.T) { - _, srv, err := runServer("/xml/", &XMLEncoder{}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - resp, err := http.Get(srv.URL + "/xml/8.8.8.8") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - t.Fatal(resp.Status) - } - var record responseRecord - err = xml.NewDecoder(resp.Body).Decode(&record) - if err != nil { - t.Fatal(err) - } - if record.CountryCode != "US" { - t.Fatalf("Unexpected country code in record: %#v", record.CountryCode) - } -} - -func TestXMLEncoderIndent(t *testing.T) { - // TODO: validate indentation? - _, srv, err := runServer("/xml/", &XMLEncoder{Indent: true}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - resp, err := http.Get(srv.URL + "/xml/8.8.8.8") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - t.Fatal(resp.Status) - } - var record responseRecord - err = xml.NewDecoder(resp.Body).Decode(&record) - if err != nil { - t.Fatal(err) - } - if record.CountryCode != "US" { - t.Fatalf("Unexpected country code in record: %#v", record.CountryCode) - } -} - -func TestJSONEncoder(t *testing.T) { - _, srv, err := runServer("/json/", &JSONEncoder{}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - resp, err := http.Get(srv.URL + "/json/8.8.8.8") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - t.Fatal(resp.Status) - } - var record responseRecord - err = json.NewDecoder(resp.Body).Decode(&record) - if err != nil { - t.Fatal(err) - } - if record.CountryCode != "US" { - t.Fatalf("Unexpected country code in record: %#v", record.CountryCode) - } -} - -func TestJSONEncoderIndent(t *testing.T) { - // TODO: validate indentation? - _, srv, err := runServer("/json/", &JSONEncoder{Indent: true}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - resp, err := http.Get(srv.URL + "/json/8.8.8.8") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - t.Fatal(resp.Status) - } - var record responseRecord - err = json.NewDecoder(resp.Body).Decode(&record) - if err != nil { - t.Fatal(err) - } - if record.CountryCode != "US" { - t.Fatalf("Unexpected country code in record: %#v", record.CountryCode) - } -} - -func TestJSONPEncoder(t *testing.T) { - _, srv, err := runServer("/json/", &JSONEncoder{}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - resp, err := http.Get(srv.URL + "/json/8.8.8.8?callback=f") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - t.Fatal(resp.Status) - } - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - code := bytes.NewBuffer([]byte(` - function f(record) { - set(record.country_code); - }; - `)) - code.Write(b) - vm := otto.New() - var countryCode string - vm.Set("set", func(call otto.FunctionCall) otto.Value { - if len(call.ArgumentList) > 0 { - countryCode = call.Argument(0).String() - } - return otto.UndefinedValue() - }) - _, err = vm.Run(code.Bytes()) - if err != nil { - t.Fatal(err) - } - if countryCode != "US" { - t.Fatalf("Unexpected country code in record: %#v", countryCode) - } -} - -func TestRequestLang(t *testing.T) { - r := http.Request{} - list := requestLang(&r) - if list != nil { - t.Fatal("Unexpected list is not nil") - } - r.Header = map[string][]string{ - "Accept-Language": {"en-us,en;q=0.5"}, - } - want := []string{"en-us", "en"} - list = requestLang(&r) - if len(list) != 2 { - t.Fatal("Unexpected list length:", len(list)) - } - for i, lang := range want { - if list[i] != lang { - t.Fatal("Unexpected item in list:", list[i]) - } - } -} - -func TestLocalizedName(t *testing.T) { - names := map[string]string{ - "de": "USA", - "en": "United States", - "es": "Estados Unidos", - "fr": "États-Unis", - "ja": "アメリカ合衆国", - "pt-BR": "Estados Unidos", - "ru": "Сша", - "zh-CN": "美国", - } - mkReq := func(lang string) *http.Request { - return &http.Request{ - Header: map[string][]string{ - "Accept-Language": {lang}, - }, - } - } - test := map[string]string{ - "pt-BR,en": "Estados Unidos", - "pt-br": "United States", - "es-ES,ru;q=0.8,q=0.2": "Сша", - "da, en-gb;q=0.8, en;q=0.7": "United States", - "da, fr;q=0.8, en;q=0.7": "États-Unis", - "da, de;q=0.5, zh-CN;q=0.8": "USA", // TODO: Use qvalue. - "da, es": "Estados Unidos", - "es-ES, ja": "アメリカ合衆国", - } - for k, v := range test { - name := localizedName(names, requestLang(mkReq(k))) - if name != v { - t.Fatalf("Unexpected name: want %q, have %q", v, name) - } - } -} diff --git a/example_test.go b/example_test.go index 38f3a66..105a2d6 100644 --- a/example_test.go +++ b/example_test.go @@ -1,19 +1,15 @@ -// Copyright 2009-2014 The freegeoip authors. All rights reserved. +// Copyright 2009 The freegeoip authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package freegeoip import ( - "encoding/json" "log" "net" - "net/http" "time" ) -var maxmindFile = "http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz" - func ExampleDatabaseQuery() { db, err := Open("./testdata.gz") if err != nil { @@ -31,7 +27,7 @@ func ExampleDatabaseQuery() { func ExampleRemoteDatabaseQuery() { updateInterval := 24 * time.Hour maxRetryInterval := time.Hour - db, err := OpenURL(maxmindFile, updateInterval, maxRetryInterval) + db, err := OpenURL(MaxMindDB, updateInterval, maxRetryInterval) if err != nil { log.Fatal(err) } @@ -50,26 +46,6 @@ func ExampleRemoteDatabaseQuery() { log.Printf("%#v", result) } -func ExampleServer() { - db, err := OpenURL(maxmindFile, 24*time.Hour, time.Hour) - if err != nil { - log.Fatal(err) - } - http.Handle("/csv/", NewHandler(db, &CSVEncoder{})) - http.Handle("/xml/", NewHandler(db, &XMLEncoder{})) - http.Handle("/json/", NewHandler(db, &JSONEncoder{})) - http.ListenAndServe(":8080", nil) -} - -func ExampleServerWithCustomEncoder() { - db, err := Open("./testdata/db.gz") - if err != nil { - log.Fatal(err) - } - http.Handle("/custom/json/", NewHandler(db, &customEncoder{})) - http.ListenAndServe(":8080", nil) -} - // A customEncoder writes a custom JSON object to an http response. type customEncoder struct{} @@ -86,33 +62,3 @@ type customQuery struct { TimeZone string `maxminddb:"time_zone"` } `maxminddb:"location"` } - -// A customResponse is what gets written to the http response as JSON. -type customResponse struct { - IP string - CountryCode string - CountryName string - Latitude float64 - Longitude float64 - TimeZone string -} - -// NewQuery implements the freegeoip.Encoder interface. -func (f *customEncoder) NewQuery() Query { - return &customQuery{} -} - -// Encode implements the freegeoip.Encoder interface. -func (f *customEncoder) Encode(w http.ResponseWriter, r *http.Request, q Query, ip net.IP) error { - record := q.(*customQuery) - out := &customResponse{ - IP: ip.String(), - CountryCode: record.Country.ISOCode, - CountryName: record.Country.Names["en"], // Set to client lang. - Latitude: record.Location.Latitude, - Longitude: record.Location.Longitude, - TimeZone: record.Location.TimeZone, - } - w.Header().Set("Content-Type", "application/json") - return json.NewEncoder(w).Encode(&out) -} diff --git a/freegeoip.go b/freegeoip.go deleted file mode 100644 index 962fb1e..0000000 --- a/freegeoip.go +++ /dev/null @@ -1,109 +0,0 @@ -// Copyright 2009-2014 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package freegeoip - -import ( - "fmt" - "math/rand" - "net" - "net/http" - "strings" - "time" -) - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -// A Handler provides http handlers that can process requests and return -// data in multiple formats. -// -// Usage: -// -// handle := NewHandler(db) -// http.Handle("/json/", handle.JSON()) -// -// Note that the url pattern must end with a trailing slash since the -// handler looks for IP addresses or hostnames as parameters, for -// example /json/8.8.8.8 or /json/domain.com. -// -// If no IP or hostname is provided, then the handler will query the -// IP address of the caller. See the ProxyHandler for more. -type Handler struct { - db *DB - enc Encoder -} - -// NewHandler creates and initializes a new Handler. -func NewHandler(db *DB, enc Encoder) *Handler { - return &Handler{db, enc} -} - -// ServeHTTP implements the http.Handler interface. -func (f *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - ip := f.queryIP(r) - if ip == nil { - http.NotFound(w, r) - return - } - q := f.enc.NewQuery() - err := f.db.Lookup(ip, q) - if err != nil { - http.Error(w, "Try again later.", - http.StatusServiceUnavailable) - return - } - w.Header().Set("X-Database-Date", f.db.Date().Format(http.TimeFormat)) - err = f.enc.Encode(w, r, q, ip) - if err != nil { - f.db.sendError(fmt.Errorf("Failed to encode %#v: %s", q, err)) - http.Error(w, "An unexpected error occurred.", - http.StatusInternalServerError) - return - } -} - -func (f *Handler) queryIP(r *http.Request) net.IP { - if r.URL.Path[len(r.URL.Path)-1] == '/' { - return f.remoteAddr(r) - } - q := r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:] - if ip := net.ParseIP(q); ip != nil { - return ip - } - ip, err := net.LookupIP(q) - if err != nil { - return nil // Not found. - } - if len(ip) == 0 { - return nil - } - return ip[rand.Intn(len(ip))] -} - -func (f *Handler) remoteAddr(r *http.Request) net.IP { - host, _, err := net.SplitHostPort(r.RemoteAddr) - if err != nil { - return net.ParseIP(r.RemoteAddr) - } - return net.ParseIP(host) -} - -// ProxyHandler is a wrapper for other http handlers that sets the -// client IP address in request.RemoteAddr to the first value of a -// comma separated list of IPs from the X-Forwarded-For request -// header. It resets the original RemoteAddr back after running the -// designated handler f. -func ProxyHandler(f http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - addr := r.Header.Get("X-Forwarded-For") - if len(addr) > 0 { - remoteAddr := r.RemoteAddr - r.RemoteAddr = strings.SplitN(addr, ",", 2)[0] - defer func() { r.RemoteAddr = remoteAddr }() - } - f.ServeHTTP(w, r) - }) -} diff --git a/freegeoip_test.go b/freegeoip_test.go deleted file mode 100644 index dbace08..0000000 --- a/freegeoip_test.go +++ /dev/null @@ -1,162 +0,0 @@ -// Copyright 2009-2014 The freegeoip authors. All rights reserved. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -package freegeoip - -import ( - "bytes" - "encoding/csv" - - "io/ioutil" - "net" - "net/http" - "net/http/httptest" - "net/url" - "testing" -) - -func TestQueryRemoteAddr(t *testing.T) { - want := net.ParseIP("8.8.8.8") - // No query argument, so we query the remote IP. - r := http.Request{ - URL: &url.URL{Path: "/"}, - RemoteAddr: "8.8.8.8:8888", - Header: http.Header{}, - } - f := &Handler{} - if ip := f.queryIP(&r); !bytes.Equal(ip, want) { - t.Errorf("Unexpected IP: %s", ip) - } -} - -func TestQueryDNS(t *testing.T) { - want4 := net.ParseIP("8.8.8.8") - want6 := net.ParseIP("2001:4860:4860::8888") - r := http.Request{ - URL: &url.URL{Path: "/google-public-dns-a.google.com"}, - RemoteAddr: "127.0.0.1:8080", - Header: make(map[string][]string), - } - f := &Handler{} - ip := f.queryIP(&r) - if ip == nil { - t.Fatal("Failed to resolve", r.URL.Path) - } - if !bytes.Equal(ip, want4) && !bytes.Equal(ip, want6) { - t.Errorf("Unexpected IP: %s", ip) - } -} - -// Test the server. - -func runServer(pattern string, f Encoder) (*Handler, *httptest.Server, error) { - db, err := Open(testFile) - if err != nil { - return nil, nil, err - } - select { - case <-db.NotifyOpen(): - case err := <-db.NotifyError(): - if err != nil { - return nil, nil, err - } - } - mux := http.NewServeMux() - handle := NewHandler(db, f) - mux.Handle(pattern, ProxyHandler(handle)) - return handle, httptest.NewServer(mux), nil -} - -func TestLookupUnavailable(t *testing.T) { - handle, srv, err := runServer("/csv/", &CSVEncoder{}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - handle.db.mu.Lock() - reader := handle.db.reader - handle.db.reader = nil - handle.db.mu.Unlock() - defer func() { - handle.db.mu.Lock() - handle.db.reader = reader - handle.db.mu.Unlock() - }() - resp, err := http.Get(srv.URL + "/csv/8.8.8.8") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusServiceUnavailable { - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - t.Fatalf("Unexpected query worked: %s\n%s", resp.Status, b) - } -} - -func TestLookupNotFound(t *testing.T) { - _, srv, err := runServer("/csv/", &CSVEncoder{}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - resp, err := http.Get(srv.URL + "/csv/fail-me") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusNotFound { - b, err := ioutil.ReadAll(resp.Body) - if err != nil { - t.Fatal(err) - } - t.Fatalf("Unexpected query worked: %s\n%s", resp.Status, b) - } -} - -func TestLookupXForwardedFor(t *testing.T) { - _, srv, err := runServer("/csv/", &CSVEncoder{}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - req, err := http.NewRequest("GET", srv.URL+"/csv/", nil) - if err != nil { - t.Fatal(err) - } - req.Header.Set("X-Forwarded-For", "8.8.8.8") - resp, err := http.DefaultClient.Do(req) - defer resp.Body.Close() - if resp.StatusCode != 200 { - t.Fatal(resp.Status) - } - row, err := csv.NewReader(resp.Body).Read() - if err != nil { - t.Fatal(err) - } - if row[1] != "US" { - t.Fatalf("Unexpected country code in record: %#v", row) - } -} - -func TestLookupDatabaseDate(t *testing.T) { - _, srv, err := runServer("/csv/", &CSVEncoder{}) - if err != nil { - t.Fatal(err) - } - defer srv.Close() - resp, err := http.Get(srv.URL + "/csv/") - if err != nil { - t.Fatal(err) - } - defer resp.Body.Close() - if resp.StatusCode != 200 { - t.Fatal(resp.Status) - } - if len(resp.Header.Get("X-Database-Date")) == 0 { - t.Fatal("Header X-Database-Date is missing") - } -}