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
View File

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

2
go.sum
View File

@ -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/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/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.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/cFDk=
github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0=

View File

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

76
internal/geoip/config.go Normal file
View 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,
}
}

View File

@ -28,10 +28,6 @@ import (
var key = myago.ContextKey("geoip.info")
type Info struct {
CountryCode string
}
func Extract(ctx context.Context) Info {
val := ctx.Value(key)
v, ok := val.(Info)
@ -43,16 +39,6 @@ func Extract(ctx context.Context) Info {
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 {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

View File

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

View File

@ -18,5 +18,6 @@ package session
// Config encapsulates configuration for the session endpoints.
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"`
}