pages/internal/middleware.go

129 lines
3.2 KiB
Go

// 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 internal
import (
"net"
"net/http"
"path"
"regexp"
"strings"
"github.com/gorilla/mux"
"github.com/mjpitz/pages/internal/metrics"
)
// GeoIP provides an abstraction for looking up a country code from an IP address.
type GeoIP interface {
Lookup(ip string) (countryCode string)
}
type emptyGeoIP struct{}
func (e emptyGeoIP) Lookup(ip string) (countryCode string) {
return ""
}
// Matcher defines an abstraction for matching paths.
type Matcher func(s string) bool
// AnyMatcher returns a matcher who returns true if any of the provided matchers match the string.
func AnyMatcher(matchers ...Matcher) Matcher {
return func(s string) bool {
for _, matcher := range matchers {
if matcher(s) {
return true
}
}
return false
}
}
// AssetMatcher returns a matcher who returns true when an asset file is requested.
func AssetMatcher() Matcher {
return func(s string) bool {
return path.Ext(s) != ""
}
}
// PrefixMatcher returns a matcher who returns true if the string matches the provided prefix.
func PrefixMatcher(prefix string) Matcher {
return func(s string) bool {
return strings.HasPrefix(s, prefix)
}
}
// RegexMatcher returns a matcher who returns true if the string matches the provided regular expression pattern.
func RegexMatcher(pattern string) Matcher {
exp := regexp.MustCompile(pattern)
return func(s string) bool {
return exp.MatchString(s)
}
}
// Middleware produces an HTTP middleware function that reports page views.
func Middleware(geoIP GeoIP, excludes ...Matcher) mux.MiddlewareFunc {
exclude := AnyMatcher(excludes...)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if exclude(r.URL.Path) {
next.ServeHTTP(w, r)
return
}
clientIP, _, err := net.SplitHostPort(r.RemoteAddr)
if err != nil {
http.Error(w, "", http.StatusBadRequest)
return
}
countryCode := geoIP.Lookup(clientIP)
d := &writer{w, http.StatusOK}
defer func() {
if d.statusCode != http.StatusNotFound {
metrics.PageViewCount.WithLabelValues(r.URL.Path, r.Referer(), countryCode).Inc()
}
}()
next.ServeHTTP(d, r)
})
}
}
type writer struct {
writer http.ResponseWriter
statusCode int
}
func (w *writer) Header() http.Header {
return w.writer.Header()
}
func (w *writer) Write(bytes []byte) (int, error) {
return w.writer.Write(bytes)
}
func (w *writer) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.writer.WriteHeader(statusCode)
}