feat(geoip): support maxmind

This change adds maxmind support for translating client IP addresses
to their country of origin. We decorate our metrics with this country
code to improve our understanding of users.

Resolves https://github.com/mjpitz/pages/issues/3
This commit is contained in:
Mya 2022-06-09 10:39:44 -05:00
parent 5b1ee690f9
commit b07bbea0aa
No known key found for this signature in database
GPG Key ID: C3ECFA648DAD27FA
7 changed files with 102 additions and 25 deletions

1
go.mod

@ -8,6 +8,7 @@ require (
) )
require ( require (
github.com/IncSW/geoip2 v0.1.2
github.com/go-git/go-billy/v5 v5.3.1 github.com/go-git/go-billy/v5 v5.3.1
github.com/go-git/go-git/v5 v5.4.2 github.com/go-git/go-git/v5 v5.4.2
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0

2
go.sum

@ -35,6 +35,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/IncSW/geoip2 v0.1.2 h1:v7iAyDiNZjHES45P1JPM3SMvkw0VNeJtz0XSVxkRwOY=
github.com/IncSW/geoip2 v0.1.2/go.mod h1:adcasR40vXiUBjtzdaTTKL/6wSf+fgO4M8Gve/XzPUk=
github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA= github.com/Microsoft/go-winio v0.4.14/go.mod h1:qXqCSQ3Xa7+6tgxaGTIe4Kpcdsi+P8jBhyzoq1bpyYA=
github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk= github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=

@ -31,13 +31,13 @@ import (
) )
type HostConfig struct { type HostConfig struct {
Server internal.ServerConfig `json:"server"` internal.ServerConfig
Git git.Config `json:"git"` Git git.Config `json:"git"`
} }
var ( var (
hostConfig = &HostConfig{ hostConfig = &HostConfig{
Server: internal.ServerConfig{ ServerConfig: internal.ServerConfig{
Public: internal.BindConfig{Address: "0.0.0.0:8080"}, Public: internal.BindConfig{Address: "0.0.0.0:8080"},
Private: internal.BindConfig{Address: "0.0.0.0:8081"}, Private: internal.BindConfig{Address: "0.0.0.0:8081"},
}, },
@ -57,7 +57,7 @@ var (
return err return err
} }
server, err := internal.NewServer(ctx.Context, hostConfig.Server) server, err := internal.NewServer(ctx.Context, hostConfig.ServerConfig)
if err != nil { if err != nil {
return err return err
} }
@ -78,8 +78,8 @@ var (
server.PublicMux.PathPrefix("/").Handler(http.FileServer(git.HTTP(gitService.FS))).Methods(http.MethodGet) server.PublicMux.PathPrefix("/").Handler(http.FileServer(git.HTTP(gitService.FS))).Methods(http.MethodGet)
log.Info("serving", log.Info("serving",
zap.String("public", hostConfig.Server.Public.Address), zap.String("public", hostConfig.Public.Address),
zap.String("private", hostConfig.Server.Private.Address)) zap.String("private", hostConfig.Private.Address))
group, c := errgroup.WithContext(ctx.Context) group, c := errgroup.WithContext(ctx.Context)
group.Go(server.ListenAndServe) group.Go(server.ListenAndServe)

76
internal/geoip/config.go Normal file

@ -0,0 +1,76 @@
// Copyright (C) 2022 The pages authors
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
package geoip
import (
"net"
"github.com/IncSW/geoip2"
"github.com/pkg/errors"
)
// Config contains the set of configuration options needed to configure looking up a users location.
type Config struct {
DB string `json:"db" usage:"path of the Maxmind GeoIP2Country database"`
}
// Open uses information from the Config to determine which type of lookup to use.
func (c Config) Open() (Interface, error) {
if c.DB != "" {
return CountryLite(c.DB)
}
return Empty{}, nil
}
type Info struct {
CountryCode string
}
type Interface interface {
Lookup(ip string) Info
}
type Empty struct{}
func (e Empty) Lookup(ip string) Info {
return Info{}
}
func CountryLite(file string) (*Maxmind, error) {
reader, err := geoip2.NewCountryReaderFromFile(file)
if err != nil {
return nil, errors.Wrap(err, "failed to open file")
}
return &Maxmind{reader}, nil
}
type Maxmind struct {
reader *geoip2.CountryReader
}
func (m *Maxmind) Lookup(ip string) Info {
record, err := m.reader.Lookup(net.ParseIP(ip))
if err != nil {
return Info{}
}
return Info{
CountryCode: record.Country.ISOCode,
}
}

@ -28,10 +28,6 @@ import (
var key = myago.ContextKey("geoip.info") var key = myago.ContextKey("geoip.info")
type Info struct {
CountryCode string
}
func Extract(ctx context.Context) Info { func Extract(ctx context.Context) Info {
val := ctx.Value(key) val := ctx.Value(key)
v, ok := val.(Info) v, ok := val.(Info)
@ -43,16 +39,6 @@ func Extract(ctx context.Context) Info {
return v return v
} }
type Interface interface {
Lookup(ip string) Info
}
type Empty struct{}
func (e Empty) Lookup(ip string) Info {
return Info{}
}
func Middleware(geoip Interface) mux.MiddlewareFunc { func Middleware(geoip Interface) mux.MiddlewareFunc {
return func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

@ -54,6 +54,7 @@ type BindConfig struct {
// ServerConfig defines configuration for a public and private interface. // ServerConfig defines configuration for a public and private interface.
type ServerConfig struct { type ServerConfig struct {
Admin AdminConfig `json:"admin"` Admin AdminConfig `json:"admin"`
GeoIP geoip.Config `json:"geoip"`
Session session.Config `json:"session"` Session session.Config `json:"session"`
TLS livetls.Config `json:"tls"` TLS livetls.Config `json:"tls"`
Public BindConfig `json:"public"` Public BindConfig `json:"public"`
@ -67,6 +68,11 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) {
return nil, err return nil, err
} }
ipdb, err := config.GeoIP.Open()
if err != nil {
return nil, err
}
private := mux.NewRouter() private := mux.NewRouter()
private.Handle("/metrics", promhttp.Handler()) private.Handle("/metrics", promhttp.Handler())
@ -79,15 +85,20 @@ func NewServer(ctx context.Context, config ServerConfig) (*Server, error) {
public := mux.NewRouter() public := mux.NewRouter()
public.Use( public.Use(
func(next http.Handler) http.Handler { return headers.HTTP(next) }, func(next http.Handler) http.Handler { return headers.HTTP(next) },
geoip.Middleware(geoip.Empty{}), geoip.Middleware(ipdb),
pageviews.Middleware( pageviews.Middleware(
pageviews.Exclusions(exclusions...), pageviews.Exclusions(exclusions...),
), ),
)
if config.Session.Enable {
public.Use(
session.Middleware( session.Middleware(
session.Exclusions(exclusions...), session.Exclusions(exclusions...),
session.JavaScriptPath(path.Join(config.Session.Prefix, "pages.js")), session.JavaScriptPath(path.Join(config.Session.Prefix, "pages.js")),
), ),
) )
}
admin := public.PathPrefix(config.Admin.Prefix).Subrouter() admin := public.PathPrefix(config.Admin.Prefix).Subrouter()

@ -18,5 +18,6 @@ package session
// Config encapsulates configuration for the session endpoints. // Config encapsulates configuration for the session endpoints.
type Config struct { type Config struct {
Enable bool `json:"enable" usage:"enables session tracking and script injection" default:"true"`
Prefix string `json:"prefix" usage:"configure the prefix to use for recording sessions" default:"/_session" hidden:"true"` Prefix string `json:"prefix" usage:"configure the prefix to use for recording sessions" default:"/_session" hidden:"true"`
} }