github.com/google/martian/v3@v3.3.3/messageview/messageview.go (about)

     1  // Copyright 2015 Google Inc. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package messageview provides no-op snapshots for HTTP requests and
    16  // responses.
    17  package messageview
    18  
    19  import (
    20  	"bytes"
    21  	"compress/flate"
    22  	"compress/gzip"
    23  	"fmt"
    24  	"io"
    25  	"io/ioutil"
    26  	"net/http"
    27  	"net/http/httputil"
    28  	"strings"
    29  )
    30  
    31  // MessageView is a static view of an HTTP request or response.
    32  type MessageView struct {
    33  	message       []byte
    34  	cts           []string
    35  	chunked       bool
    36  	skipBody      bool
    37  	compress      string
    38  	bodyoffset    int64
    39  	traileroffset int64
    40  }
    41  
    42  type config struct {
    43  	decode bool
    44  }
    45  
    46  // Option is a configuration option for a MessageView.
    47  type Option func(*config)
    48  
    49  // Decode sets an option to decode the message body for logging purposes.
    50  func Decode() Option {
    51  	return func(c *config) {
    52  		c.decode = true
    53  	}
    54  }
    55  
    56  // New returns a new MessageView.
    57  func New() *MessageView {
    58  	return &MessageView{}
    59  }
    60  
    61  // SkipBody will skip reading the body when the view is loaded with a request
    62  // or response.
    63  func (mv *MessageView) SkipBody(skipBody bool) {
    64  	mv.skipBody = skipBody
    65  }
    66  
    67  // SkipBodyUnlessContentType will skip reading the body unless the
    68  // Content-Type matches one in cts.
    69  func (mv *MessageView) SkipBodyUnlessContentType(cts ...string) {
    70  	mv.skipBody = true
    71  	mv.cts = cts
    72  }
    73  
    74  // SnapshotRequest reads the request into the MessageView. If mv.skipBody is false
    75  // it will also read the body into memory and replace the existing body with
    76  // the in-memory copy. This method is semantically a no-op.
    77  func (mv *MessageView) SnapshotRequest(req *http.Request) error {
    78  	buf := new(bytes.Buffer)
    79  
    80  	fmt.Fprintf(buf, "%s %s HTTP/%d.%d\r\n", req.Method,
    81  		req.URL, req.ProtoMajor, req.ProtoMinor)
    82  
    83  	if req.Host != "" {
    84  		fmt.Fprintf(buf, "Host: %s\r\n", req.Host)
    85  	}
    86  
    87  	if tec := len(req.TransferEncoding); tec > 0 {
    88  		mv.chunked = req.TransferEncoding[tec-1] == "chunked"
    89  		fmt.Fprintf(buf, "Transfer-Encoding: %s\r\n", strings.Join(req.TransferEncoding, ", "))
    90  	}
    91  	if !mv.chunked && req.ContentLength >= 0 {
    92  		fmt.Fprintf(buf, "Content-Length: %d\r\n", req.ContentLength)
    93  	}
    94  
    95  	mv.compress = req.Header.Get("Content-Encoding")
    96  
    97  	req.Header.WriteSubset(buf, map[string]bool{
    98  		"Host":              true,
    99  		"Content-Length":    true,
   100  		"Transfer-Encoding": true,
   101  	})
   102  
   103  	fmt.Fprint(buf, "\r\n")
   104  
   105  	mv.bodyoffset = int64(buf.Len())
   106  	mv.traileroffset = int64(buf.Len())
   107  
   108  	ct := req.Header.Get("Content-Type")
   109  	if mv.skipBody && !mv.matchContentType(ct) || req.Body == nil {
   110  		mv.message = buf.Bytes()
   111  		return nil
   112  	}
   113  
   114  	data, err := ioutil.ReadAll(req.Body)
   115  	if err != nil {
   116  		return err
   117  	}
   118  	req.Body.Close()
   119  
   120  	if mv.chunked {
   121  		cw := httputil.NewChunkedWriter(buf)
   122  		cw.Write(data)
   123  		cw.Close()
   124  	} else {
   125  		buf.Write(data)
   126  	}
   127  
   128  	mv.traileroffset = int64(buf.Len())
   129  
   130  	req.Body = ioutil.NopCloser(bytes.NewReader(data))
   131  
   132  	if req.Trailer != nil {
   133  		req.Trailer.Write(buf)
   134  	} else if mv.chunked {
   135  		fmt.Fprint(buf, "\r\n")
   136  	}
   137  
   138  	mv.message = buf.Bytes()
   139  
   140  	return nil
   141  }
   142  
   143  // SnapshotResponse reads the response into the MessageView. If mv.headersOnly
   144  // is false it will also read the body into memory and replace the existing
   145  // body with the in-memory copy. This method is semantically a no-op.
   146  func (mv *MessageView) SnapshotResponse(res *http.Response) error {
   147  	buf := new(bytes.Buffer)
   148  
   149  	fmt.Fprintf(buf, "HTTP/%d.%d %s\r\n", res.ProtoMajor, res.ProtoMinor, res.Status)
   150  
   151  	if tec := len(res.TransferEncoding); tec > 0 {
   152  		mv.chunked = res.TransferEncoding[tec-1] == "chunked"
   153  		fmt.Fprintf(buf, "Transfer-Encoding: %s\r\n", strings.Join(res.TransferEncoding, ", "))
   154  	}
   155  	if !mv.chunked && res.ContentLength >= 0 {
   156  		fmt.Fprintf(buf, "Content-Length: %d\r\n", res.ContentLength)
   157  	}
   158  
   159  	mv.compress = res.Header.Get("Content-Encoding")
   160  	// Do not uncompress if we have don't have the full contents.
   161  	if res.StatusCode == http.StatusNoContent || res.StatusCode == http.StatusPartialContent {
   162  		mv.compress = ""
   163  	}
   164  
   165  	res.Header.WriteSubset(buf, map[string]bool{
   166  		"Content-Length":    true,
   167  		"Transfer-Encoding": true,
   168  	})
   169  
   170  	fmt.Fprint(buf, "\r\n")
   171  
   172  	mv.bodyoffset = int64(buf.Len())
   173  	mv.traileroffset = int64(buf.Len())
   174  
   175  	ct := res.Header.Get("Content-Type")
   176  	if mv.skipBody && !mv.matchContentType(ct) || res.Body == nil {
   177  		mv.message = buf.Bytes()
   178  		return nil
   179  	}
   180  
   181  	data, err := ioutil.ReadAll(res.Body)
   182  	if err != nil {
   183  		return err
   184  	}
   185  	res.Body.Close()
   186  
   187  	if mv.chunked {
   188  		cw := httputil.NewChunkedWriter(buf)
   189  		cw.Write(data)
   190  		cw.Close()
   191  	} else {
   192  		buf.Write(data)
   193  	}
   194  
   195  	mv.traileroffset = int64(buf.Len())
   196  
   197  	res.Body = ioutil.NopCloser(bytes.NewReader(data))
   198  
   199  	if res.Trailer != nil {
   200  		res.Trailer.Write(buf)
   201  	} else if mv.chunked {
   202  		fmt.Fprint(buf, "\r\n")
   203  	}
   204  
   205  	mv.message = buf.Bytes()
   206  
   207  	return nil
   208  }
   209  
   210  // Reader returns the an io.ReadCloser that reads the full HTTP message.
   211  func (mv *MessageView) Reader(opts ...Option) (io.ReadCloser, error) {
   212  	hr := mv.HeaderReader()
   213  	br, err := mv.BodyReader(opts...)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	tr := mv.TrailerReader()
   218  
   219  	return struct {
   220  		io.Reader
   221  		io.Closer
   222  	}{
   223  		Reader: io.MultiReader(hr, br, tr),
   224  		Closer: br,
   225  	}, nil
   226  }
   227  
   228  // HeaderReader returns an io.Reader that reads the HTTP Status-Line or
   229  // HTTP Request-Line and headers.
   230  func (mv *MessageView) HeaderReader() io.Reader {
   231  	r := bytes.NewReader(mv.message)
   232  	return io.NewSectionReader(r, 0, mv.bodyoffset)
   233  }
   234  
   235  // BodyReader returns an io.ReadCloser that reads the HTTP request or response
   236  // body. If mv.skipBody was set the reader will immediately return io.EOF.
   237  //
   238  // If the Decode option is passed the body will be unchunked if
   239  // Transfer-Encoding is set to "chunked", and will decode the following
   240  // Content-Encodings: gzip, deflate.
   241  func (mv *MessageView) BodyReader(opts ...Option) (io.ReadCloser, error) {
   242  	var r io.Reader
   243  
   244  	conf := &config{}
   245  	for _, o := range opts {
   246  		o(conf)
   247  	}
   248  
   249  	br := bytes.NewReader(mv.message)
   250  	r = io.NewSectionReader(br, mv.bodyoffset, mv.traileroffset-mv.bodyoffset)
   251  
   252  	if !conf.decode {
   253  		return ioutil.NopCloser(r), nil
   254  	}
   255  
   256  	if mv.chunked {
   257  		r = httputil.NewChunkedReader(r)
   258  	}
   259  	switch mv.compress {
   260  	case "gzip":
   261  		gr, err := gzip.NewReader(r)
   262  		if err != nil {
   263  			return nil, err
   264  		}
   265  		return gr, nil
   266  	case "deflate":
   267  		return flate.NewReader(r), nil
   268  	default:
   269  		return ioutil.NopCloser(r), nil
   270  	}
   271  }
   272  
   273  // TrailerReader returns an io.Reader that reads the HTTP request or response
   274  // trailers, if present.
   275  func (mv *MessageView) TrailerReader() io.Reader {
   276  	r := bytes.NewReader(mv.message)
   277  	end := int64(len(mv.message)) - mv.traileroffset
   278  
   279  	return io.NewSectionReader(r, mv.traileroffset, end)
   280  }
   281  
   282  func (mv *MessageView) matchContentType(mct string) bool {
   283  	for _, ct := range mv.cts {
   284  		if strings.HasPrefix(mct, ct) {
   285  			return true
   286  		}
   287  	}
   288  
   289  	return false
   290  }