Day 16 — io.Reader & io.Writer
Learning Objectives
- Compose readers and writers to inspect traffic without buffering it all
- Use
io.TeeReaderto mirror traffic — the foundation of your TCP traffic inspector (Week 5) - Use
io.LimitReaderto enforce body size limits — protection against request body attacks
Key Concepts
io.Reader interface:
Read(p []byte) (n int, err error)
io.Writer interface:
Write(p []byte) (n int, err error)
Implementations: net.Conn, os.File, bytes.Buffer, strings.Reader,
http.Request.Body, gzip.Reader, tls.Conn
Composing:
io.TeeReader(r, w) — reads from r, copies every byte to w
io.LimitReader(r, n) — reads at most n bytes, then returns io.EOF
io.MultiWriter(w1,w2) — writes to all writers simultaneously
io.Copy(dst, src) — copy all bytes; returns (n, err)
bufio.NewReader(r) — adds buffering; enables ReadLine, ReadString
Exercise: Request Body Inspector (WAF Foundation)
This is the core of your WAF body scanner (Day 72):
package main
import (
"bytes"
"fmt"
"io"
"strings"
)
const maxBodySize = 10 * 1024 // 10KB
// InspectBody reads a request body, enforces size limit, logs it,
// and returns a new reader that can be read again by the downstream handler.
// This solves the "you can only read a body once" problem.
func InspectBody(body io.ReadCloser, log io.Writer) (io.ReadCloser, int64, error) {
if body == nil {
return http.NoBody, 0, nil
}
// LimitReader: stop reading after maxBodySize (attacker can't force OOM)
limited := io.LimitReader(body, maxBodySize+1)
// TeeReader: every byte read from 'limited' is also written to 'log'
var buf bytes.Buffer
tee := io.TeeReader(limited, &buf)
// Read all — this drains the original body
n, err := io.Copy(io.Discard, tee)
body.Close()
if n > maxBodySize {
return nil, n, fmt.Errorf("request body exceeds %d bytes", maxBodySize)
}
// Log the body content (buf was written to by TeeReader)
fmt.Fprintf(log, "BODY[%d bytes]: %s\n", buf.Len(), buf.String())
// Wrap the buffer as a new ReadCloser for downstream handler
return io.NopCloser(bytes.NewReader(buf.Bytes())), n, err
}
func main() {
// Simulate an HTTP request body
originalBody := io.NopCloser(strings.NewReader(`{"username":"admin","password":"secret"}`))
var auditLog strings.Builder
newBody, size, err := InspectBody(originalBody, &auditLog)
if err != nil {
fmt.Println("ERROR:", err)
return
}
fmt.Printf("Inspected %d bytes\n", size)
fmt.Printf("Audit log: %s\n", auditLog.String())
// Downstream handler reads the body again — works because we restored it
data, _ := io.ReadAll(newBody)
fmt.Printf("Handler got: %s\n", data)
}
Real-World Context
io.TeeReader is used by AWS WAF, Cloudflare, and ModSecurity to inspect request bodies. The critical requirement: the body must be readable by the WAF AND by the application handler. Without io.NopCloser(bytes.NewReader(...)), the body would be empty when it reaches your route handler.