Skip to content

Commit d32b626

Browse files
authored
Feature/extra info debug (#28)
Summary: - tls not set for Basic server eventhough info passed - tls variables are no longer mandatory - clean all non signed headers when checking signature that covers specific headers - make sure to send error back when there is an upstream error - tests: allow self-signed certificates for the tests - misc change: more debug logging More detailed * tracing: extra info on header manipulations * special handling content length * test: add dummy test for local debugging. * bugfix: Basic server was never enabling TLS * bugfix: no conservatism when cleaning for validating signature When we have to verify a signature and we are cleaning then we should clean everything that is not considered signed. This change allows the cleaner to take in options to specify the type of cleaning and e make sure that when checking signature it always cleans. * docker: example docker compose to run locally. * logging: more detailed debug Add information when doign debug logging. Including logging related to the aws signing. * bugfix: allow self-signe certificates in tests also for presigned urls * config: tls config parameters are no longer mandatory, they can be left empty for non-tls proxying * bugfix: fail explicitly when upstream error bugfix: fail explicitly when using unsupported chunked signing
1 parent d70adb7 commit d32b626

15 files changed

+188
-32
lines changed

aws/service/s3/handler-builder.go

+18-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/VITObelgium/fakes3pp/constants"
1515
"github.com/VITObelgium/fakes3pp/presign"
1616
"github.com/VITObelgium/fakes3pp/requestctx"
17+
"github.com/VITObelgium/fakes3pp/usererror"
1718
)
1819

1920
// A handler builder builds http handlers
@@ -66,6 +67,21 @@ func justProxy(ctx context.Context, w http.ResponseWriter, r *http.Request, targ
6667
writeS3ErrorResponse(ctx, w, ErrS3InternalError, nil)
6768
return
6869
}
70+
payloadHash := r.Header.Get(constants.AmzContentSHAKey)
71+
if payloadHash == "STREAMING-UNSIGNED-PAYLOAD-TRAILER" {
72+
writeS3ErrorResponse(
73+
ctx,
74+
w,
75+
ErrS3InternalError,
76+
usererror.New(
77+
errors.New("unsupported encryption to be implemented so giving internal error to user"),
78+
`We do not support STREAMING-UNSIGNED-PAYLOAD-TRAILER yet.
79+
For details or to upvote see https://github.com/VITObelgium/fakes3pp/issues/27
80+
`),
81+
)
82+
return
83+
}
84+
slog.DebugContext(ctx, "Headers before signing", "headers", r.Header)
6985
err = presign.SignWithCreds(ctx, r, creds, targetBackendId)
7086
if err != nil {
7187
slog.ErrorContext(ctx, "Could not sign request with permanent credentials", "error", err, "backendId", targetBackendId)
@@ -78,6 +94,7 @@ func justProxy(ctx context.Context, w http.ResponseWriter, r *http.Request, targ
7894
resp, err := client.Do(r)
7995
if err != nil {
8096
slog.ErrorContext(ctx, "Error making request", "error", err)
97+
writeS3ErrorResponse(ctx, w, ErrS3UpstreamError, nil)
8198
return
8299
}
83100
defer resp.Body.Close()
@@ -93,7 +110,7 @@ func justProxy(ctx context.Context, w http.ResponseWriter, r *http.Request, targ
93110
if err != nil {
94111
slog.ErrorContext(ctx, "Context had error", "error", err, "context_error", ctx.Err())
95112
} else {
96-
slog.InfoContext(ctx, "End of proxying", "bytes", i, "error", err, "status", resp.Status, "headers", resp.Header)
113+
slog.InfoContext(ctx, "End of proxying", "bytes", i, "error", err, "status", resp.Status, "headers", resp.Header, "trailer", r.Trailer)
97114
}
98115
}
99116

aws/service/s3/proxys3_test.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,8 @@ func TestWithValidPresignedUrlOtherRegion(t *testing.T) {
328328
if err != nil {
329329
t.Errorf("client: error making http request: %s\n", err)
330330
}
331-
res, err := http.DefaultClient.Do(httpReq)
331+
httpClient := testutils.BuildUnsafeHttpClientThatTrustsAnyCert(t)
332+
res, err := httpClient.Do(httpReq)
332333
if err != nil {
333334
t.Errorf("client: error making http request: %s\n", err)
334335
}
@@ -442,7 +443,7 @@ func performValidListObjectTestRequest(t testing.TB, s *S3Server, headerModifier
442443
}
443444
headerModifierPostSign(req.Header)
444445

445-
client := &http.Client{}
446+
client := testutils.BuildUnsafeHttpClientThatTrustsAnyCert(t)
446447
resp, err := client.Do(req)
447448
if err != nil {
448449
t.Errorf("Could not perform request: %s", err)

cmd/almost-e2e_test.go

+10-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88
"log/slog"
99
"net/http"
10+
"os"
1011
"testing"
1112
"time"
1213

@@ -78,10 +79,12 @@ var testProviderFakeTesting string = fmt.Sprintf(`
7879
var testConfigFakeTesting string = fmt.Sprintf("providers:%s", testProviderFakeTesting)
7980

8081
func TestMain(m *testing.M) {
81-
envFiles = "../etc/.env"
82-
loadEnvVarsFromDotEnv()
83-
initConfig()
84-
initializeTestLogging()
82+
if os.Getenv("DEBUG_LOCAL_TEST") == "" {
83+
envFiles = "../etc/.env"
84+
loadEnvVarsFromDotEnv()
85+
initConfig()
86+
initializeTestLogging()
87+
}
8588
m.Run()
8689
}
8790

@@ -394,7 +397,7 @@ func TestSigv4PresignedUrlsWork(t *testing.T) {
394397
if err != nil {
395398
t.Errorf("Did not expect error when signing url for %s. Got %s", backendRegion, err)
396399
}
397-
resp, err := http.Get(signedUri)
400+
resp, err := testutils.BuildUnsafeHttpClientThatTrustsAnyCert(t).Get(signedUri)
398401
if err != nil {
399402
t.Errorf("Did not expect error when using signing url for %s. Got %s", backendRegion, err)
400403
}
@@ -720,7 +723,7 @@ func TestSigv4PresignedUrlsFailWithOldSigningStrategy(t *testing.T) {
720723
t.Errorf("Did not expect error when signing url for %s. Got %s", backendRegion, err)
721724
t.FailNow()
722725
}
723-
resp, err := http.Get(signedUri)
726+
resp, err := testutils.BuildUnsafeHttpClientThatTrustsAnyCert(t).Get(signedUri)
724727
if err != nil {
725728
t.Errorf("The get should have gone through but got an error: %s", err)
726729
}
@@ -757,7 +760,7 @@ func TestSigv4PresignedUrlsSucceedWithOldSigningStrategyWhenBackwardsCompatibili
757760
if err != nil {
758761
t.Errorf("Did not expect error when signing url for %s. Got %s", backendRegion, err)
759762
}
760-
resp, err := http.Get(signedUri)
763+
resp, err := testutils.BuildUnsafeHttpClientThatTrustsAnyCert(t).Get(signedUri)
761764
if err != nil {
762765
t.Errorf("Did not expect error when using signing url for %s. Got %s", backendRegion, err)
763766
}

cmd/config.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,14 @@ var envVarDefs = []envVarDef{
9898
{
9999
s3ProxyCertFile,
100100
FAKES3PP_S3_PROXY_CERT_FILE,
101-
true,
101+
false,
102102
"The certificate file used for tls server-side",
103103
[]string{proxys3},
104104
},
105105
{
106106
s3ProxyKeyFile,
107107
FAKES3PP_S3_PROXY_KEY_FILE,
108-
true,
108+
false,
109109
"The key file used for tls server-side",
110110
[]string{proxys3},
111111
},
@@ -140,14 +140,14 @@ var envVarDefs = []envVarDef{
140140
{
141141
stsProxyCertFile,
142142
FAKES3PP_STS_PROXY_CERT_FILE,
143-
true,
143+
false,
144144
"The certificate file used for tls server-side",
145145
[]string{proxysts},
146146
},
147147
{
148148
stsProxyKeyFile,
149149
FAKES3PP_STS_PROXY_KEY_FILE,
150-
true,
150+
false,
151151
"The key file used for tls server-side",
152152
[]string{proxysts},
153153
},

cmd/debug-local_test.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package cmd
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"os"
7+
"testing"
8+
9+
"github.com/VITObelgium/fakes3pp/logging"
10+
"github.com/VITObelgium/fakes3pp/server"
11+
)
12+
13+
14+
//This is a dummy test that can be used to spawn a server with a local config
15+
//Just set DEBUG_LOCAL_TEST to a directory that has a fakes3pp.env file and all the other
16+
//required config that you reference in that env file. Then if you debug this test you have a server
17+
//running with the debugger attached.
18+
func TestRunLocalDebugEndpoint(t *testing.T) {
19+
if os.Getenv("DEBUG_LOCAL_TEST") == "" {
20+
t.Skip("Skipping this test because DEBUG_LOCAL_TEST is empty.")
21+
}
22+
23+
testDir := os.Getenv("DEBUG_LOCAL_TEST")
24+
envFiles = fmt.Sprintf("%s/fakes3pp.env", testDir)
25+
loadEnvVarsFromDotEnv()
26+
initConfig()
27+
logging.InitializeLogging(slog.LevelDebug, nil, nil)
28+
29+
server.CreateAndStartSync(buildS3Server(), getServerOptsFromViper())
30+
31+
}

constants/aws-constants.go

+3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ const (
4040
// ContentSHAKey is the SHA256 of request body
4141
AmzContentSHAKey = "X-Amz-Content-Sha256"
4242

43+
//as per https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
44+
AmzDecodedContentLength = "x-amz-decoded-content-length"
45+
4346
// TimeFormat is the time format to be used in the X-Amz-Date header or query parameter
4447
TimeFormat = "20060102T150405Z"
4548
)

etc/docker-compose.yaml

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
services:
2+
s3proxy:
3+
image: localhost/fakes3pp
4+
networks:
5+
- frontend
6+
ports:
7+
- "8443:8443"
8+
- "8000:5555"
9+
command: proxys3 --dot-env /etc/fakes3pp/.env.docker
10+
volumes:
11+
- ../etc.private:/etc/fakes3pp:Z
12+
13+
environment:
14+
HOME: /root
15+
LOG_LEVEL: DEBUG
16+
17+
18+
stsproxy:
19+
image: localhost/fakes3pp
20+
21+
networks:
22+
- frontend
23+
ports:
24+
- "8444:8444"
25+
- "8001:5556"
26+
command: proxysts --dot-env /etc/fakes3pp/.env.docker
27+
volumes:
28+
- ../etc.private:/etc/fakes3pp:Z
29+
environment:
30+
HOME: /root
31+
FAKES3PP_METRICS_PORT: "8001"
32+
33+
networks:
34+
frontend:
35+
# Specify driver options
36+
driver: bridge
37+

middleware/authentication.go

+7-3
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ func handleAuthNPresigned(w http.ResponseWriter, r *http.Request, keyStorage uti
7575

7676
err = makeSureSessionTokenIsForAccessKey(creds.SessionToken, creds.AccessKeyID)
7777
if err != nil {
78-
err := fmt.Errorf("Error when making sure session token corresponds to used credential pair: %w", err)
78+
err := fmt.Errorf("error when making sure session token corresponds to used credential pair: %w", err)
7979
e.WriteErrorResponse(r.Context(), w, service.ErrAuthorizationHeaderMalformed, err)
8080
return false
8181
}
@@ -142,7 +142,7 @@ func handleAuthNNormal(w http.ResponseWriter, r *http.Request, keyStorage utils.
142142

143143
err = makeSureSessionTokenIsForAccessKey(sessionToken, accessKeyId)
144144
if err != nil {
145-
err := fmt.Errorf("Error when making sure session token corresponds to used credential pair: %w", err)
145+
err := fmt.Errorf("error when making sure session token corresponds to used credential pair: %w", err)
146146
e.WriteErrorResponse(r.Context(), w, service.ErrAuthorizationHeaderMalformed, err)
147147
return false
148148
}
@@ -174,6 +174,8 @@ func handleAuthNNormal(w http.ResponseWriter, r *http.Request, keyStorage utils.
174174
passedSignature := r.Header.Get(constants.AuthorizationHeader)
175175
//Cleaning could have removed content length
176176
r.ContentLength = backupContentLength
177+
slog.DebugContext(r.Context(), "ContentLength after manipualation", "ContentLength", r.ContentLength)
178+
177179

178180
if calculatedSignature != passedSignature {
179181
slog.DebugContext(
@@ -215,9 +217,11 @@ func getCredentialsFromRequest(r *http.Request) (accessKeyId, sessionToken strin
215217
//cleanHeadersThatAreNotSignedInAuthHeader removes headers which are potentially added along the way
216218
//
217219
func cleanHeadersThatAreNotSignedInAuthHeader(req *http.Request) {
220+
slog.DebugContext(req.Context(), "Headers before manipualation", "Headers", req.Header)
221+
218222
signedHeaders := getSignedHeadersFromRequest(req)
219223

220-
presign.CleanHeadersTo(req.Context(), req, signedHeaders)
224+
presign.CleanHeadersTo(req.Context(), req, signedHeaders, presign.CleanerOptions{})
221225
}
222226

223227
const signedHeadersPrefix = "SignedHeaders="

middleware/observability.go

+5
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,11 @@ func LogMiddleware(requestLogLvl slog.Level, hc HealthChecker, promReg prometheu
102102
"Request start",
103103
getRequestCtxLogAttrs(rCtx)...
104104
)
105+
slog.DebugContext(
106+
ctx, "Extra debug details",
107+
"contentLength", r.ContentLength,
108+
"TransferEncoding", r.TransferEncoding,
109+
)
105110
defer logFinalRequestDetails(ctx, logLvl, startTime, rCtx)
106111

107112
if !wasHealthCheck{

presign/header-operations.go

+24-2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,15 @@ var cleanableHeaders = map[string]bool{
1919
"amz-sdk-invocation-id": true, //Added by AWS SDKs after signing
2020
"amz-sdk-request": true, //Added by AWS SDKs after signing
2121
"content-length": true,
22+
"Sec-Fetch-User": true,
23+
"Priority": true,
24+
"Te": true,
25+
"Accept": true,
26+
"Upgrade-Insecure-Requests": true,
27+
"Sec-Fetch-Dest": true,
28+
"Sec-Fetch-Mode": true,
29+
"Sec-Fetch-Site": true,
30+
"Accept-Language": true,
2231
}
2332

2433
//It is not always clear which headers are OK to skip cleaning. These headers
@@ -37,7 +46,14 @@ func isCleanable(headerName string) bool {
3746
return false
3847
}
3948

40-
func CleanHeadersTo(ctx context.Context, req *http.Request, toKeep map[string]string) {
49+
type CleanerOptions struct{
50+
//If set we do not check against a list of headers that is safe to clean
51+
//We just clean all that are not explicitly set to keep
52+
AlwaysClean bool
53+
}
54+
55+
//
56+
func CleanHeadersTo(ctx context.Context, req *http.Request, toKeep map[string]string, opts CleanerOptions) {
4157
var cleaned = []string{}
4258
var skipped = []string{}
4359
var signed = []string{}
@@ -55,11 +71,17 @@ func CleanHeadersTo(ctx context.Context, req *http.Request, toKeep map[string]st
5571
signed = append(signed, header)
5672
continue
5773
}
58-
if isCleanable(header) {
74+
if isCleanable(header) || opts.AlwaysClean {
5975
//If content-length is to be cleaned it should
6076
//also be <=0 otherwise it is taken in the signature
6177
//-1 means unknown so let's fall back to that
6278
if headerLC == "content-length" {
79+
slog.DebugContext(
80+
req.Context(),
81+
"Cleaning Content-Length",
82+
"header", req.Header.Get(header),
83+
"reqValue", req.ContentLength,
84+
)
6385
req.ContentLength = -1
6486
}
6587
req.Header.Del(header)

presign/s3v4.go

+21-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package presign
33
import (
44
"context"
55
"errors"
6+
"fmt"
67
"log/slog"
78
"net/http"
89
"strconv"
@@ -12,6 +13,7 @@ import (
1213
"github.com/VITObelgium/fakes3pp/requestutils"
1314
"github.com/aws/aws-sdk-go-v2/aws"
1415
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
16+
"github.com/aws/smithy-go/logging"
1517
)
1618

1719
//This file just contains helpers to presign for S3 with sigv4
@@ -20,14 +22,31 @@ func PreSignRequestWithCreds(ctx context.Context, req *http.Request, expiryInSec
2022
if expiryInSeconds <= 0 {
2123
return "", nil, errors.New("expiryInSeconds must be bigger than 0 for presigned requests")
2224
}
23-
signer := v4.NewSigner()
25+
signer := getSigner(ctx)
2426

2527
ctx, creds, req, payloadHash, service, region, signingTime := GetS3SignRequestParams(ctx, req, expiryInSeconds, signingTime, creds, defaultRegion)
2628
return signer.PresignHTTP(ctx, creds, req, payloadHash, service, region, signingTime)
2729
}
2830

31+
func getLogger(ctx context.Context, l *slog.Logger) logging.LoggerFunc {
32+
var f logging.LoggerFunc = func(classification logging.Classification, format string, v ...interface{}) {
33+
if len(classification) != 0 {
34+
format = string(classification) + " " + format
35+
}
36+
37+
l.DebugContext(ctx, "SigninLogging", "msg", fmt.Sprintf(format, v...))
38+
}
39+
40+
return f
41+
}
42+
43+
func getSigner(ctx context.Context) *v4.Signer {
44+
return v4.NewSigner(func(signer *v4.SignerOptions){signer.LogSigning = true; signer.Logger = getLogger(ctx, slog.Default())})
45+
}
46+
47+
2948
func SignRequestWithCreds(ctx context.Context, req *http.Request, expiryInSeconds int, signingTime time.Time, creds aws.Credentials, defaultRegion string) (err error){
30-
signer := v4.NewSigner()
49+
signer := getSigner(ctx)
3150

3251
ctx, creds, req, payloadHash, service, region, signingTime := GetS3SignRequestParams(ctx, req, expiryInSeconds, signingTime, creds, defaultRegion)
3352
return signer.SignHTTP(ctx, creds, req, payloadHash, service, region, signingTime)

presign/s3v4query.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ func (u presignedUrlS3V4Query) GetPresignedUrlDetails(ctx context.Context, deriv
8989
if c.Header.Get("Host") == "" {
9090
c.Header.Add("Host", c.Host)
9191
}
92-
CleanHeadersTo(ctx, c, u.getSignedHeaders())
92+
CleanHeadersTo(ctx, c, u.getSignedHeaders(), CleanerOptions{AlwaysClean: true})
9393
defaultRegion := "" // A Sigv4 always has a region specified as part of the X-amz-credentials parameter so no fallback needed.
9494
signedUri, _, err := PreSignRequestWithCreds(ctx, c, expirySeconds, signDate, creds, defaultRegion)
9595
if err != nil {

0 commit comments

Comments
 (0)