forked from TrueCloudLab/distribution
Extract request utilities into its own package
The RemoteAddr and RemoteIP functions operate on *http.Request values, not contexts. They have very low cohesion with the rest of the package. Signed-off-by: Cory Snider <csnider@mirantis.com>
This commit is contained in:
parent
d0f5aa670b
commit
9157226e7b
6 changed files with 134 additions and 119 deletions
|
@ -3,15 +3,14 @@ package dcontext
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/distribution/distribution/v3/internal/requestutil"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
log "github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Common errors used with this package.
|
// Common errors used with this package.
|
||||||
|
@ -20,48 +19,6 @@ var (
|
||||||
ErrNoResponseWriterContext = errors.New("no http response in context")
|
ErrNoResponseWriterContext = errors.New("no http response in context")
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseIP(ipStr string) net.IP {
|
|
||||||
ip := net.ParseIP(ipStr)
|
|
||||||
if ip == nil {
|
|
||||||
log.Warnf("invalid remote IP address: %q", ipStr)
|
|
||||||
}
|
|
||||||
return ip
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteAddr extracts the remote address of the request, taking into
|
|
||||||
// account proxy headers.
|
|
||||||
func RemoteAddr(r *http.Request) string {
|
|
||||||
if prior := r.Header.Get("X-Forwarded-For"); prior != "" {
|
|
||||||
remoteAddr, _, _ := strings.Cut(prior, ",")
|
|
||||||
remoteAddr = strings.Trim(remoteAddr, " ")
|
|
||||||
if parseIP(remoteAddr) != nil {
|
|
||||||
return remoteAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// X-Real-Ip is less supported, but worth checking in the
|
|
||||||
// absence of X-Forwarded-For
|
|
||||||
if realIP := r.Header.Get("X-Real-Ip"); realIP != "" {
|
|
||||||
if parseIP(realIP) != nil {
|
|
||||||
return realIP
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.RemoteAddr
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoteIP extracts the remote IP of the request, taking into
|
|
||||||
// account proxy headers.
|
|
||||||
func RemoteIP(r *http.Request) string {
|
|
||||||
addr := RemoteAddr(r)
|
|
||||||
|
|
||||||
// Try parsing it as "IP:port"
|
|
||||||
if ip, _, err := net.SplitHostPort(addr); err == nil {
|
|
||||||
return ip
|
|
||||||
}
|
|
||||||
|
|
||||||
return addr
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRequest places the request on the context. The context of the request
|
// WithRequest places the request on the context. The context of the request
|
||||||
// is assigned a unique id, available at "http.request.id". The request itself
|
// is assigned a unique id, available at "http.request.id". The request itself
|
||||||
// is available at "http.request". Other common attributes are available under
|
// is available at "http.request". Other common attributes are available under
|
||||||
|
@ -193,7 +150,7 @@ func (ctx *httpRequestContext) Value(key interface{}) interface{} {
|
||||||
case "http.request.uri":
|
case "http.request.uri":
|
||||||
return ctx.r.RequestURI
|
return ctx.r.RequestURI
|
||||||
case "http.request.remoteaddr":
|
case "http.request.remoteaddr":
|
||||||
return RemoteAddr(ctx.r)
|
return requestutil.RemoteAddr(ctx.r)
|
||||||
case "http.request.method":
|
case "http.request.method":
|
||||||
return ctx.r.Method
|
return ctx.r.Method
|
||||||
case "http.request.host":
|
case "http.request.host":
|
||||||
|
|
|
@ -2,9 +2,6 @@ package dcontext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
@ -219,70 +216,3 @@ func TestWithVars(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SingleHostReverseProxy will insert an X-Forwarded-For header, and can be used to test
|
|
||||||
// RemoteAddr(). A fake RemoteAddr cannot be set on the HTTP request - it is overwritten
|
|
||||||
// at the transport layer to 127.0.0.1:<port> . However, as the X-Forwarded-For header
|
|
||||||
// just contains the IP address, it is different enough for testing.
|
|
||||||
func TestRemoteAddr(t *testing.T) {
|
|
||||||
var expectedRemote string
|
|
||||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
defer r.Body.Close()
|
|
||||||
|
|
||||||
if r.RemoteAddr == expectedRemote {
|
|
||||||
t.Errorf("Unexpected matching remote addresses")
|
|
||||||
}
|
|
||||||
|
|
||||||
actualRemote := RemoteAddr(r)
|
|
||||||
if expectedRemote != actualRemote {
|
|
||||||
t.Errorf("Mismatching remote hosts: %v != %v", expectedRemote, actualRemote)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(200)
|
|
||||||
}))
|
|
||||||
|
|
||||||
defer backend.Close()
|
|
||||||
backendURL, err := url.Parse(backend.URL)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := httputil.NewSingleHostReverseProxy(backendURL)
|
|
||||||
frontend := httptest.NewServer(proxy)
|
|
||||||
defer frontend.Close()
|
|
||||||
|
|
||||||
// X-Forwarded-For set by proxy
|
|
||||||
expectedRemote = "127.0.0.1"
|
|
||||||
proxyReq, err := http.NewRequest(http.MethodGet, frontend.URL, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := http.DefaultClient.Do(proxyReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// RemoteAddr in X-Real-Ip
|
|
||||||
getReq, err := http.NewRequest(http.MethodGet, backend.URL, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedRemote = "1.2.3.4"
|
|
||||||
getReq.Header["X-Real-ip"] = []string{expectedRemote}
|
|
||||||
resp, err = http.DefaultClient.Do(getReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
// Valid X-Real-Ip and invalid X-Forwarded-For
|
|
||||||
getReq.Header["X-forwarded-for"] = []string{"1.2.3"}
|
|
||||||
resp, err = http.DefaultClient.Do(getReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
51
internal/requestutil/util.go
Normal file
51
internal/requestutil/util.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package requestutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func parseIP(ipStr string) net.IP {
|
||||||
|
ip := net.ParseIP(ipStr)
|
||||||
|
if ip == nil {
|
||||||
|
log.Warnf("invalid remote IP address: %q", ipStr)
|
||||||
|
}
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteAddr extracts the remote address of the request, taking into
|
||||||
|
// account proxy headers.
|
||||||
|
func RemoteAddr(r *http.Request) string {
|
||||||
|
if prior := r.Header.Get("X-Forwarded-For"); prior != "" {
|
||||||
|
remoteAddr, _, _ := strings.Cut(prior, ",")
|
||||||
|
remoteAddr = strings.Trim(remoteAddr, " ")
|
||||||
|
if parseIP(remoteAddr) != nil {
|
||||||
|
return remoteAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// X-Real-Ip is less supported, but worth checking in the
|
||||||
|
// absence of X-Forwarded-For
|
||||||
|
if realIP := r.Header.Get("X-Real-Ip"); realIP != "" {
|
||||||
|
if parseIP(realIP) != nil {
|
||||||
|
return realIP
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.RemoteAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoteIP extracts the remote IP of the request, taking into
|
||||||
|
// account proxy headers.
|
||||||
|
func RemoteIP(r *http.Request) string {
|
||||||
|
addr := RemoteAddr(r)
|
||||||
|
|
||||||
|
// Try parsing it as "IP:port"
|
||||||
|
if ip, _, err := net.SplitHostPort(addr); err == nil {
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr
|
||||||
|
}
|
76
internal/requestutil/util_test.go
Normal file
76
internal/requestutil/util_test.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package requestutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SingleHostReverseProxy will insert an X-Forwarded-For header, and can be used to test
|
||||||
|
// RemoteAddr(). A fake RemoteAddr cannot be set on the HTTP request - it is overwritten
|
||||||
|
// at the transport layer to 127.0.0.1:<port> . However, as the X-Forwarded-For header
|
||||||
|
// just contains the IP address, it is different enough for testing.
|
||||||
|
func TestRemoteAddr(t *testing.T) {
|
||||||
|
var expectedRemote string
|
||||||
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
if r.RemoteAddr == expectedRemote {
|
||||||
|
t.Errorf("Unexpected matching remote addresses")
|
||||||
|
}
|
||||||
|
|
||||||
|
actualRemote := RemoteAddr(r)
|
||||||
|
if expectedRemote != actualRemote {
|
||||||
|
t.Errorf("Mismatching remote hosts: %v != %v", expectedRemote, actualRemote)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}))
|
||||||
|
|
||||||
|
defer backend.Close()
|
||||||
|
backendURL, err := url.Parse(backend.URL)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(backendURL)
|
||||||
|
frontend := httptest.NewServer(proxy)
|
||||||
|
defer frontend.Close()
|
||||||
|
|
||||||
|
// X-Forwarded-For set by proxy
|
||||||
|
expectedRemote = "127.0.0.1"
|
||||||
|
proxyReq, err := http.NewRequest(http.MethodGet, frontend.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := http.DefaultClient.Do(proxyReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// RemoteAddr in X-Real-Ip
|
||||||
|
getReq, err := http.NewRequest(http.MethodGet, backend.URL, nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedRemote = "1.2.3.4"
|
||||||
|
getReq.Header["X-Real-ip"] = []string{expectedRemote}
|
||||||
|
resp, err = http.DefaultClient.Do(getReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// Valid X-Real-Ip and invalid X-Forwarded-For
|
||||||
|
getReq.Header["X-forwarded-for"] = []string{"1.2.3"}
|
||||||
|
resp, err = http.DefaultClient.Do(getReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3"
|
"github.com/distribution/distribution/v3"
|
||||||
"github.com/distribution/distribution/v3/internal/dcontext"
|
"github.com/distribution/distribution/v3/internal/requestutil"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
events "github.com/docker/go-events"
|
events "github.com/docker/go-events"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
|
@ -49,7 +49,7 @@ func NewBridge(ub URLBuilder, source SourceRecord, actor ActorRecord, request Re
|
||||||
func NewRequestRecord(id string, r *http.Request) RequestRecord {
|
func NewRequestRecord(id string, r *http.Request) RequestRecord {
|
||||||
return RequestRecord{
|
return RequestRecord{
|
||||||
ID: id,
|
ID: id,
|
||||||
Addr: dcontext.RemoteAddr(r),
|
Addr: requestutil.RemoteAddr(r),
|
||||||
Host: r.Host,
|
Host: r.Host,
|
||||||
Method: r.Method,
|
Method: r.Method,
|
||||||
UserAgent: r.UserAgent(),
|
UserAgent: r.UserAgent(),
|
||||||
|
|
|
@ -12,6 +12,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/distribution/distribution/v3/internal/dcontext"
|
"github.com/distribution/distribution/v3/internal/dcontext"
|
||||||
|
"github.com/distribution/distribution/v3/internal/requestutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -188,7 +189,7 @@ func parseIPFromRequest(ctx context.Context) (net.IP, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
ipStr := dcontext.RemoteIP(request)
|
ipStr := requestutil.RemoteIP(request)
|
||||||
ip := net.ParseIP(ipStr)
|
ip := net.ParseIP(ipStr)
|
||||||
if ip == nil {
|
if ip == nil {
|
||||||
return nil, fmt.Errorf("invalid ip address from requester: %s", ipStr)
|
return nil, fmt.Errorf("invalid ip address from requester: %s", ipStr)
|
||||||
|
@ -208,7 +209,7 @@ func eligibleForS3(ctx context.Context, awsIPs *awsIPs) bool {
|
||||||
} else {
|
} else {
|
||||||
loggerField := map[interface{}]interface{}{
|
loggerField := map[interface{}]interface{}{
|
||||||
"user-client": request.UserAgent(),
|
"user-client": request.UserAgent(),
|
||||||
"ip": dcontext.RemoteIP(request),
|
"ip": requestutil.RemoteIP(request),
|
||||||
}
|
}
|
||||||
if awsIPs.contains(addr) {
|
if awsIPs.contains(addr) {
|
||||||
dcontext.GetLoggerWithFields(ctx, loggerField).Info("request from the allowed AWS region, skipping CloudFront")
|
dcontext.GetLoggerWithFields(ctx, loggerField).Info("request from the allowed AWS region, skipping CloudFront")
|
||||||
|
|
Loading…
Reference in a new issue