github.com/coreos/goproxy@v0.0.0-20190513173959-f8dc2d7ba04e/examples/goproxy-httpdump/httpdump.go (about) 1 package main 2 3 import ( 4 "errors" 5 "flag" 6 "fmt" 7 "io" 8 "log" 9 "net" 10 "net/http" 11 "net/http/httputil" 12 "os" 13 "os/signal" 14 "path" 15 "sync" 16 "time" 17 18 "github.com/elazarl/goproxy" 19 "github.com/elazarl/goproxy/transport" 20 ) 21 22 type FileStream struct { 23 path string 24 f *os.File 25 } 26 27 func NewFileStream(path string) *FileStream { 28 return &FileStream{path, nil} 29 } 30 31 func (fs *FileStream) Write(b []byte) (nr int, err error) { 32 if fs.f == nil { 33 fs.f, err = os.Create(fs.path) 34 if err != nil { 35 return 0, err 36 } 37 } 38 return fs.f.Write(b) 39 } 40 41 func (fs *FileStream) Close() error { 42 fmt.Println("Close", fs.path) 43 if fs.f == nil { 44 return errors.New("FileStream was never written into") 45 } 46 return fs.f.Close() 47 } 48 49 type Meta struct { 50 req *http.Request 51 resp *http.Response 52 err error 53 t time.Time 54 sess int64 55 bodyPath string 56 from string 57 } 58 59 func fprintf(nr *int64, err *error, w io.Writer, pat string, a ...interface{}) { 60 if *err != nil { 61 return 62 } 63 var n int 64 n, *err = fmt.Fprintf(w, pat, a...) 65 *nr += int64(n) 66 } 67 68 func write(nr *int64, err *error, w io.Writer, b []byte) { 69 if *err != nil { 70 return 71 } 72 var n int 73 n, *err = w.Write(b) 74 *nr += int64(n) 75 } 76 77 func (m *Meta) WriteTo(w io.Writer) (nr int64, err error) { 78 if m.req != nil { 79 fprintf(&nr, &err, w, "Type: request\r\n") 80 } else if m.resp != nil { 81 fprintf(&nr, &err, w, "Type: response\r\n") 82 } 83 fprintf(&nr, &err, w, "ReceivedAt: %v\r\n", m.t) 84 fprintf(&nr, &err, w, "Session: %d\r\n", m.sess) 85 fprintf(&nr, &err, w, "From: %v\r\n", m.from) 86 if m.err != nil { 87 // note the empty response 88 fprintf(&nr, &err, w, "Error: %v\r\n\r\n\r\n\r\n", m.err) 89 } else if m.req != nil { 90 fprintf(&nr, &err, w, "\r\n") 91 buf, err2 := httputil.DumpRequest(m.req, false) 92 if err2 != nil { 93 return nr, err2 94 } 95 write(&nr, &err, w, buf) 96 } else if m.resp != nil { 97 fprintf(&nr, &err, w, "\r\n") 98 buf, err2 := httputil.DumpResponse(m.resp, false) 99 if err2 != nil { 100 return nr, err2 101 } 102 write(&nr, &err, w, buf) 103 } 104 return 105 } 106 107 // HttpLogger is an asynchronous HTTP request/response logger. It traces 108 // requests and responses headers in a "log" file in logger directory and dumps 109 // their bodies in files prefixed with the session identifiers. 110 // Close it to ensure pending items are correctly logged. 111 type HttpLogger struct { 112 path string 113 c chan *Meta 114 errch chan error 115 } 116 117 func NewLogger(basepath string) (*HttpLogger, error) { 118 f, err := os.Create(path.Join(basepath, "log")) 119 if err != nil { 120 return nil, err 121 } 122 logger := &HttpLogger{basepath, make(chan *Meta), make(chan error)} 123 go func() { 124 for m := range logger.c { 125 if _, err := m.WriteTo(f); err != nil { 126 log.Println("Can't write meta", err) 127 } 128 } 129 logger.errch <- f.Close() 130 }() 131 return logger, nil 132 } 133 134 func (logger *HttpLogger) LogResp(resp *http.Response, ctx *goproxy.ProxyCtx) { 135 body := path.Join(logger.path, fmt.Sprintf("%d_resp", ctx.Session)) 136 from := "" 137 if ctx.UserData != nil { 138 from = ctx.UserData.(*transport.RoundTripDetails).TCPAddr.String() 139 } 140 if resp == nil { 141 resp = emptyResp 142 } else { 143 resp.Body = NewTeeReadCloser(resp.Body, NewFileStream(body)) 144 } 145 logger.LogMeta(&Meta{ 146 resp: resp, 147 err: ctx.Error, 148 t: time.Now(), 149 sess: ctx.Session, 150 from: from}) 151 } 152 153 var emptyResp = &http.Response{} 154 var emptyReq = &http.Request{} 155 156 func (logger *HttpLogger) LogReq(req *http.Request, ctx *goproxy.ProxyCtx) { 157 body := path.Join(logger.path, fmt.Sprintf("%d_req", ctx.Session)) 158 if req == nil { 159 req = emptyReq 160 } else { 161 req.Body = NewTeeReadCloser(req.Body, NewFileStream(body)) 162 } 163 logger.LogMeta(&Meta{ 164 req: req, 165 err: ctx.Error, 166 t: time.Now(), 167 sess: ctx.Session, 168 from: req.RemoteAddr}) 169 } 170 171 func (logger *HttpLogger) LogMeta(m *Meta) { 172 logger.c <- m 173 } 174 175 func (logger *HttpLogger) Close() error { 176 close(logger.c) 177 return <-logger.errch 178 } 179 180 // TeeReadCloser extends io.TeeReader by allowing reader and writer to be 181 // closed. 182 type TeeReadCloser struct { 183 r io.Reader 184 w io.WriteCloser 185 c io.Closer 186 } 187 188 func NewTeeReadCloser(r io.ReadCloser, w io.WriteCloser) io.ReadCloser { 189 return &TeeReadCloser{io.TeeReader(r, w), w, r} 190 } 191 192 func (t *TeeReadCloser) Read(b []byte) (int, error) { 193 return t.r.Read(b) 194 } 195 196 // Close attempts to close the reader and write. It returns an error if both 197 // failed to Close. 198 func (t *TeeReadCloser) Close() error { 199 err1 := t.c.Close() 200 err2 := t.w.Close() 201 if err1 != nil { 202 return err1 203 } 204 return err2 205 } 206 207 // stoppableListener serves stoppableConn and tracks their lifetime to notify 208 // when it is safe to terminate the application. 209 type stoppableListener struct { 210 net.Listener 211 sync.WaitGroup 212 } 213 214 type stoppableConn struct { 215 net.Conn 216 wg *sync.WaitGroup 217 } 218 219 func newStoppableListener(l net.Listener) *stoppableListener { 220 return &stoppableListener{l, sync.WaitGroup{}} 221 } 222 223 func (sl *stoppableListener) Accept() (net.Conn, error) { 224 c, err := sl.Listener.Accept() 225 if err != nil { 226 return c, err 227 } 228 sl.Add(1) 229 return &stoppableConn{c, &sl.WaitGroup}, nil 230 } 231 232 func (sc *stoppableConn) Close() error { 233 sc.wg.Done() 234 return sc.Conn.Close() 235 } 236 237 func main() { 238 verbose := flag.Bool("v", false, "should every proxy request be logged to stdout") 239 addr := flag.String("l", ":8080", "on which address should the proxy listen") 240 flag.Parse() 241 proxy := goproxy.NewProxyHttpServer() 242 proxy.Verbose = *verbose 243 if err := os.MkdirAll("db", 0755); err != nil { 244 log.Fatal("Can't create dir", err) 245 } 246 logger, err := NewLogger("db") 247 if err != nil { 248 log.Fatal("can't open log file", err) 249 } 250 tr := transport.Transport{Proxy: transport.ProxyFromEnvironment} 251 // For every incoming request, override the RoundTripper to extract 252 // connection information. Store it is session context log it after 253 // handling the response. 254 proxy.OnRequest().DoFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) { 255 ctx.RoundTripper = goproxy.RoundTripperFunc(func(req *http.Request, ctx *goproxy.ProxyCtx) (resp *http.Response, err error) { 256 ctx.UserData, resp, err = tr.DetailedRoundTrip(req) 257 return 258 }) 259 logger.LogReq(req, ctx) 260 return req, nil 261 }) 262 proxy.OnResponse().DoFunc(func(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response { 263 logger.LogResp(resp, ctx) 264 return resp 265 }) 266 l, err := net.Listen("tcp", *addr) 267 if err != nil { 268 log.Fatal("listen:", err) 269 } 270 sl := newStoppableListener(l) 271 ch := make(chan os.Signal) 272 signal.Notify(ch, os.Interrupt) 273 go func() { 274 <-ch 275 log.Println("Got SIGINT exiting") 276 sl.Add(1) 277 sl.Close() 278 logger.Close() 279 sl.Done() 280 }() 281 log.Println("Starting Proxy") 282 http.Serve(sl, proxy) 283 sl.Wait() 284 log.Println("All connections closed - exit") 285 }