Skip to content

Commit ce0ca7f

Browse files
committed
Introduce core.ErrOnLimitReader and DefaultMaxRead
1 parent 265e841 commit ce0ca7f

12 files changed

Lines changed: 130 additions & 20 deletions

File tree

bdns/dns.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"github.com/prometheus/client_golang/prometheus"
1818
"github.com/prometheus/client_golang/prometheus/promauto"
1919

20+
"github.com/letsencrypt/boulder/core"
2021
blog "github.com/letsencrypt/boulder/log"
2122
"github.com/letsencrypt/boulder/metrics"
2223
)
@@ -375,7 +376,9 @@ func (d *dohExchanger) ExchangeContext(ctx context.Context, query *dns.Msg, serv
375376
return nil, d.clk.Since(start), fmt.Errorf("doh: http status %d", resp.StatusCode)
376377
}
377378

378-
b, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 300_000})
379+
// DNS response over 65535 is malformed https://datatracker.ietf.org/doc/html/rfc8484#section-6
380+
lmr := core.ErrOnLimitReader(resp.Body, 65_535)
381+
b, err := io.ReadAll(lmr)
379382
if err != nil {
380383
return nil, d.clk.Since(start), fmt.Errorf("doh: reading response body: %w", err)
381384
}

cmd/crl-checker/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ func downloadShard(url string) (*x509.RevocationList, error) {
2828
return nil, fmt.Errorf("downloading crl: http status %d", resp.StatusCode)
2929
}
3030

31-
crlBytes, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 1_000_000_000})
31+
lmr := core.ErrOnLimitReader(resp.Body, 1_000_000_000)
32+
crlBytes, err := io.ReadAll(lmr)
3233
if err != nil {
3334
return nil, fmt.Errorf("reading CRL bytes: %w", err)
3435
}

cmd/shell.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,8 @@ func ValidateYAMLConfig(cv *ConfigValidator, in io.Reader) error {
504504
// Register custom types for use with existing validation tags.
505505
validate.RegisterCustomTypeFunc(config.DurationCustomTypeFunc, config.Duration{})
506506

507-
inBytes, err := io.ReadAll(&io.LimitedReader{R: in, N: 300_000})
507+
lmr := core.ErrOnLimitReader(in, core.DefaultMaxRead)
508+
inBytes, err := io.ReadAll(lmr)
508509
if err != nil {
509510
return err
510511
}

core/core_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package core
33
import (
44
"encoding/base64"
55
"encoding/json"
6+
"io"
7+
"strings"
68
"testing"
79

810
"github.com/go-jose/go-jose/v4"
@@ -80,3 +82,38 @@ func TestFingerprint(t *testing.T) {
8082
t.Errorf("Incorrect SHA-256 fingerprint: %v", digest)
8183
}
8284
}
85+
86+
func TestErrOnLimitReader(t *testing.T) {
87+
// test a read where the limit is larger than the source
88+
strReader := strings.NewReader("foo bar baz bot")
89+
lmr := ErrOnLimitReader(strReader, 21)
90+
91+
strOut01, err := io.ReadAll(lmr)
92+
93+
// should return no err, and the full source
94+
test.AssertNotError(t, err, "unexpectedly errored")
95+
test.AssertEquals(t, string(strOut01), "foo bar baz bot")
96+
97+
// test a read where the limit exactly matches source size
98+
strReader = strings.NewReader("foo bar baz bot")
99+
lmr = ErrOnLimitReader(strReader, 15)
100+
101+
strOut02, err := io.ReadAll(lmr)
102+
103+
// should return an error, and the full source
104+
test.AssertError(t, err, "unexpectedly succeeded")
105+
test.AssertEquals(t, err, ErrReaderLimitReached)
106+
test.AssertEquals(t, string(strOut02), "foo bar baz bot")
107+
108+
// test a read where the limit is lower than the source
109+
strReader = strings.NewReader("foo bar baz bot")
110+
lmr = ErrOnLimitReader(strReader, 9)
111+
112+
strOut03, err := io.ReadAll(lmr)
113+
114+
// should return an error, and a partial read result
115+
test.AssertError(t, err, "unexpectedly succeeded")
116+
test.AssertEquals(t, err, ErrReaderLimitReached)
117+
test.AssertEquals(t, string(strOut03), "foo bar b")
118+
119+
}

core/util.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,14 @@ var BuildHost string
5151
// BuildTime is set by the compiler and is used by GetBuildTime
5252
var BuildTime string
5353

54+
// DefaultMaxRead is for use by ErrOnLimitReader when to conveniently limit
55+
// reads to less than half a MB, which should be most of the time
56+
var DefaultMaxRead int64 = 300_000
57+
58+
// ErrReaderLimitReached as an exported error type allows callers to check for
59+
// this error type after Read
60+
var ErrReaderLimitReached error = errors.New("reader size limit reached")
61+
5462
func init() {
5563
expvar.NewString("BuildID").Set(BuildID)
5664
expvar.NewString("BuildTime").Set(BuildTime)
@@ -431,3 +439,39 @@ func NormalizeIssuerDomainName(name string) (string, error) {
431439
}
432440
return name, nil
433441
}
442+
443+
// An ErrOnLimitedReader reads from R but limits the amount of data returned to
444+
// just N bytes. Each call to Read updates N to reflect the new amount
445+
// remaining.
446+
// Read returns ErrReaderLimitExceeded when N <= 0, or EOF when the underlying R
447+
// returns EOF.
448+
type ErrOnLimitedReader struct {
449+
R io.Reader
450+
N int64
451+
}
452+
453+
// ErrOnLimitReader returns a Reader that reads from r but stops with
454+
// ErrReaderLimitExceeded after n bytes.
455+
// The underlying implementation is a *ErrOnLimitedReader.
456+
//
457+
// If LimitedReader gets an Err field, we can switch to use that
458+
// https://github.com/golang/go/issues/51115
459+
func ErrOnLimitReader(r io.Reader, n int64) io.Reader {
460+
return &ErrOnLimitedReader{r, n}
461+
}
462+
463+
func (ltdR *ErrOnLimitedReader) Read(b []byte) (n int, err error) {
464+
// Important: when an ErrOnLimitedReader reads _exactly_ the limit amount,
465+
// it results in the LimitReached Error
466+
if ltdR.N <= 0 {
467+
return 0, ErrReaderLimitReached
468+
}
469+
470+
if int64(len(b)) > ltdR.N {
471+
b = b[0:ltdR.N]
472+
}
473+
474+
n, err = ltdR.R.Read(b)
475+
ltdR.N -= int64(n)
476+
return
477+
}

linter/pkimetal/client.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import (
1515
"time"
1616

1717
"github.com/zmap/zlint/v3/lint"
18+
19+
"github.com/letsencrypt/boulder/core"
1820
)
1921

2022
// Config holds configuration for linting both certs and CRLs using PKIMetal.
@@ -80,7 +82,8 @@ func (pkim *Client) Execute(endpoint string, der []byte) (*lint.LintResult, erro
8082
return nil, fmt.Errorf("got status %d (%s) from pkimetal API", resp.StatusCode, resp.Status)
8183
}
8284

83-
resJSON, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 300_000})
85+
lmr := core.ErrOnLimitReader(resp.Body, core.DefaultMaxRead)
86+
resJSON, err := io.ReadAll(lmr)
8487
if err != nil {
8588
return nil, fmt.Errorf("reading response from pkimetal API: %s", err)
8689
}

observer/probers/aia/aia.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import (
88
"net/http"
99

1010
"github.com/prometheus/client_golang/prometheus"
11+
12+
"github.com/letsencrypt/boulder/core"
1113
)
1214

1315
// AIAProbe is the exported 'Prober' object for monitors configured to
@@ -48,7 +50,8 @@ func (p AIAProbe) Probe(ctx context.Context) error {
4850
return fmt.Errorf("certificate Content-Type is %q but want application/pkix-cert", contentType)
4951
}
5052

51-
body, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 300_000})
53+
lmr := core.ErrOnLimitReader(resp.Body, core.DefaultMaxRead)
54+
body, err := io.ReadAll(lmr)
5255
if err != nil {
5356
return err
5457
}

observer/probers/ccadb/retryhttp.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"io"
77
"net/http"
88
"time"
9+
10+
"github.com/letsencrypt/boulder/core"
911
)
1012

1113
func getBody(ctx context.Context, url string) ([]byte, error) {
@@ -21,19 +23,24 @@ func getBody(ctx context.Context, url string) ([]byte, error) {
2123
}
2224
defer resp.Body.Close()
2325

24-
body, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 1_000_000_000})
25-
if err != nil {
26-
return nil, err
27-
}
28-
2926
if resp.StatusCode != http.StatusOK {
30-
// Truncate the response body in case it's too big to be useful in logs.
31-
if len(body) > 400 {
32-
body = body[:400]
27+
// Read up to 400 bytes of response body to include in error logs
28+
lmr := core.ErrOnLimitReader(resp.Body, 400)
29+
body, err := io.ReadAll(lmr)
30+
if err != nil && err != core.ErrReaderLimitReached {
31+
return nil, err
3332
}
33+
3434
return nil, fmt.Errorf("http status %d for %q: %s", resp.StatusCode, url, string(body))
3535
}
3636

37+
// Read up to ~1G of response body to return to caller
38+
lmr := core.ErrOnLimitReader(resp.Body, 1_000_000_000)
39+
body, err := io.ReadAll(lmr)
40+
if err != nil {
41+
return nil, err
42+
}
43+
3744
return body, nil
3845
}
3946

observer/probers/crl/crl.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010

1111
"github.com/prometheus/client_golang/prometheus"
1212

13+
"github.com/letsencrypt/boulder/core"
1314
"github.com/letsencrypt/boulder/crl/idp"
1415
"github.com/letsencrypt/boulder/observer/obsclient"
1516
)
@@ -47,7 +48,8 @@ func (p CRLProbe) Probe(ctx context.Context) error {
4748
}
4849
defer resp.Body.Close()
4950

50-
body, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 1_000_000_000})
51+
lmr := core.ErrOnLimitReader(resp.Body, 1_000_000_000)
52+
body, err := io.ReadAll(lmr)
5153
if err != nil {
5254
return err
5355
}

observer/probers/tls/tls.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/prometheus/client_golang/prometheus"
1717
"golang.org/x/crypto/ocsp"
1818

19+
"github.com/letsencrypt/boulder/core"
1920
"github.com/letsencrypt/boulder/observer/obsclient"
2021
)
2122

@@ -86,7 +87,8 @@ func checkOCSP(ctx context.Context, cert, issuer *x509.Certificate, want int) (b
8687
}
8788
defer res.Body.Close()
8889

89-
output, err := io.ReadAll(&io.LimitedReader{R: res.Body, N: 300_000})
90+
lmr := core.ErrOnLimitReader(res.Body, core.DefaultMaxRead)
91+
output, err := io.ReadAll(lmr)
9092
if err != nil {
9193
return false, err
9294
}
@@ -115,7 +117,8 @@ func checkCRL(ctx context.Context, cert, issuer *x509.Certificate, want int) (bo
115117
}
116118
defer resp.Body.Close()
117119

118-
der, err := io.ReadAll(&io.LimitedReader{R: resp.Body, N: 1_000_000_000})
120+
lmr := core.ErrOnLimitReader(resp.Body, 1_000_000_000)
121+
der, err := io.ReadAll(lmr)
119122
if err != nil {
120123
return false, fmt.Errorf("reading CRL: %w", err)
121124
}

0 commit comments

Comments
 (0)