github.com/google/martian/v3@v3.3.3/har/har.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 har collects HTTP requests and responses and stores them in HAR format. 16 // 17 // For more information on HAR, see: 18 // https://w3c.github.io/web-performance/specs/HAR/Overview.html 19 package har 20 21 import ( 22 "bytes" 23 "encoding/base64" 24 "encoding/json" 25 "fmt" 26 "io" 27 "io/ioutil" 28 "mime" 29 "mime/multipart" 30 "net/http" 31 "net/url" 32 "strings" 33 "sync" 34 "time" 35 "unicode/utf8" 36 37 "github.com/google/martian/v3" 38 "github.com/google/martian/v3/log" 39 "github.com/google/martian/v3/messageview" 40 "github.com/google/martian/v3/proxyutil" 41 ) 42 43 // Logger maintains request and response log entries. 44 type Logger struct { 45 bodyLogging func(*http.Response) bool 46 postDataLogging func(*http.Request) bool 47 48 creator *Creator 49 50 mu sync.Mutex 51 entries map[string]*Entry 52 tail *Entry 53 } 54 55 // HAR is the top level object of a HAR log. 56 type HAR struct { 57 Log *Log `json:"log"` 58 } 59 60 // Log is the HAR HTTP request and response log. 61 type Log struct { 62 // Version number of the HAR format. 63 Version string `json:"version"` 64 // Creator holds information about the log creator application. 65 Creator *Creator `json:"creator"` 66 // Entries is a list containing requests and responses. 67 Entries []*Entry `json:"entries"` 68 } 69 70 // Creator is the program responsible for generating the log. Martian, in this case. 71 type Creator struct { 72 // Name of the log creator application. 73 Name string `json:"name"` 74 // Version of the log creator application. 75 Version string `json:"version"` 76 } 77 78 // Entry is a individual log entry for a request or response. 79 type Entry struct { 80 // ID is the unique ID for the entry. 81 ID string `json:"_id"` 82 // StartedDateTime is the date and time stamp of the request start (ISO 8601). 83 StartedDateTime time.Time `json:"startedDateTime"` 84 // Time is the total elapsed time of the request in milliseconds. 85 Time int64 `json:"time"` 86 // Request contains the detailed information about the request. 87 Request *Request `json:"request"` 88 // Response contains the detailed information about the response. 89 Response *Response `json:"response,omitempty"` 90 // Cache contains information about a request coming from browser cache. 91 Cache *Cache `json:"cache"` 92 // Timings describes various phases within request-response round trip. All 93 // times are specified in milliseconds. 94 Timings *Timings `json:"timings"` 95 next *Entry 96 } 97 98 // Request holds data about an individual HTTP request. 99 type Request struct { 100 // Method is the request method (GET, POST, ...). 101 Method string `json:"method"` 102 // URL is the absolute URL of the request (fragments are not included). 103 URL string `json:"url"` 104 // HTTPVersion is the Request HTTP version (HTTP/1.1). 105 HTTPVersion string `json:"httpVersion"` 106 // Cookies is a list of cookies. 107 Cookies []Cookie `json:"cookies"` 108 // Headers is a list of headers. 109 Headers []Header `json:"headers"` 110 // QueryString is a list of query parameters. 111 QueryString []QueryString `json:"queryString"` 112 // PostData is the posted data information. 113 PostData *PostData `json:"postData,omitempty"` 114 // HeaderSize is the Total number of bytes from the start of the HTTP request 115 // message until (and including) the double CLRF before the body. Set to -1 116 // if the info is not available. 117 HeadersSize int64 `json:"headersSize"` 118 // BodySize is the size of the request body (POST data payload) in bytes. Set 119 // to -1 if the info is not available. 120 BodySize int64 `json:"bodySize"` 121 } 122 123 // Response holds data about an individual HTTP response. 124 type Response struct { 125 // Status is the response status code. 126 Status int `json:"status"` 127 // StatusText is the response status description. 128 StatusText string `json:"statusText"` 129 // HTTPVersion is the Response HTTP version (HTTP/1.1). 130 HTTPVersion string `json:"httpVersion"` 131 // Cookies is a list of cookies. 132 Cookies []Cookie `json:"cookies"` 133 // Headers is a list of headers. 134 Headers []Header `json:"headers"` 135 // Content contains the details of the response body. 136 Content *Content `json:"content"` 137 // RedirectURL is the target URL from the Location response header. 138 RedirectURL string `json:"redirectURL"` 139 // HeadersSize is the total number of bytes from the start of the HTTP 140 // request message until (and including) the double CLRF before the body. 141 // Set to -1 if the info is not available. 142 HeadersSize int64 `json:"headersSize"` 143 // BodySize is the size of the request body (POST data payload) in bytes. Set 144 // to -1 if the info is not available. 145 BodySize int64 `json:"bodySize"` 146 } 147 148 // Cache contains information about a request coming from browser cache. 149 type Cache struct { 150 // Has no fields as they are not supported, but HAR requires the "cache" 151 // object to exist. 152 } 153 154 // Timings describes various phases within request-response round trip. All 155 // times are specified in milliseconds 156 type Timings struct { 157 // Send is the time required to send HTTP request to the server. 158 Send int64 `json:"send"` 159 // Wait is the time spent waiting for a response from the server. 160 Wait int64 `json:"wait"` 161 // Receive is the time required to read entire response from server or cache. 162 Receive int64 `json:"receive"` 163 } 164 165 // Cookie is the data about a cookie on a request or response. 166 type Cookie struct { 167 // Name is the cookie name. 168 Name string `json:"name"` 169 // Value is the cookie value. 170 Value string `json:"value"` 171 // Path is the path pertaining to the cookie. 172 Path string `json:"path,omitempty"` 173 // Domain is the host of the cookie. 174 Domain string `json:"domain,omitempty"` 175 // Expires contains cookie expiration time. 176 Expires time.Time `json:"-"` 177 // Expires8601 contains cookie expiration time in ISO 8601 format. 178 Expires8601 string `json:"expires,omitempty"` 179 // HTTPOnly is set to true if the cookie is HTTP only, false otherwise. 180 HTTPOnly bool `json:"httpOnly,omitempty"` 181 // Secure is set to true if the cookie was transmitted over SSL, false 182 // otherwise. 183 Secure bool `json:"secure,omitempty"` 184 } 185 186 // Header is an HTTP request or response header. 187 type Header struct { 188 // Name is the header name. 189 Name string `json:"name"` 190 // Value is the header value. 191 Value string `json:"value"` 192 } 193 194 // QueryString is a query string parameter on a request. 195 type QueryString struct { 196 // Name is the query parameter name. 197 Name string `json:"name"` 198 // Value is the query parameter value. 199 Value string `json:"value"` 200 } 201 202 // PostData describes posted data on a request. 203 type PostData struct { 204 // MimeType is the MIME type of the posted data. 205 MimeType string `json:"mimeType"` 206 // Params is a list of posted parameters (in case of URL encoded parameters). 207 Params []Param `json:"params"` 208 // Text contains the posted data. Although its type is string, it may contain 209 // binary data. 210 Text string `json:"text"` 211 } 212 213 // pdBinary is the JSON representation of binary PostData. 214 type pdBinary struct { 215 MimeType string `json:"mimeType"` 216 // Params is a list of posted parameters (in case of URL encoded parameters). 217 Params []Param `json:"params"` 218 Text []byte `json:"text"` 219 Encoding string `json:"encoding"` 220 } 221 222 // MarshalJSON returns a JSON representation of binary PostData. 223 func (p *PostData) MarshalJSON() ([]byte, error) { 224 if utf8.ValidString(p.Text) { 225 type noMethod PostData // avoid infinite recursion 226 return json.Marshal((*noMethod)(p)) 227 } 228 return json.Marshal(pdBinary{ 229 MimeType: p.MimeType, 230 Params: p.Params, 231 Text: []byte(p.Text), 232 Encoding: "base64", 233 }) 234 } 235 236 // UnmarshalJSON populates PostData based on the []byte representation of 237 // the binary PostData. 238 func (p *PostData) UnmarshalJSON(data []byte) error { 239 if bytes.Equal(data, []byte("null")) { // conform to json.Unmarshaler spec 240 return nil 241 } 242 var enc struct { 243 Encoding string `json:"encoding"` 244 } 245 if err := json.Unmarshal(data, &enc); err != nil { 246 return err 247 } 248 if enc.Encoding != "base64" { 249 type noMethod PostData // avoid infinite recursion 250 return json.Unmarshal(data, (*noMethod)(p)) 251 } 252 var pb pdBinary 253 if err := json.Unmarshal(data, &pb); err != nil { 254 return err 255 } 256 p.MimeType = pb.MimeType 257 p.Params = pb.Params 258 p.Text = string(pb.Text) 259 return nil 260 } 261 262 // Param describes an individual posted parameter. 263 type Param struct { 264 // Name of the posted parameter. 265 Name string `json:"name"` 266 // Value of the posted parameter. 267 Value string `json:"value,omitempty"` 268 // Filename of a posted file. 269 Filename string `json:"fileName,omitempty"` 270 // ContentType is the content type of a posted file. 271 ContentType string `json:"contentType,omitempty"` 272 } 273 274 // Content describes details about response content. 275 type Content struct { 276 // Size is the length of the returned content in bytes. Should be equal to 277 // response.bodySize if there is no compression and bigger when the content 278 // has been compressed. 279 Size int64 `json:"size"` 280 // MimeType is the MIME type of the response text (value of the Content-Type 281 // response header). 282 MimeType string `json:"mimeType"` 283 // Text contains the response body sent from the server or loaded from the 284 // browser cache. This field is populated with fully decoded version of the 285 // respose body. 286 Text []byte `json:"text,omitempty"` 287 // The desired encoding to use for the text field when encoding to JSON. 288 Encoding string `json:"encoding,omitempty"` 289 } 290 291 // For marshaling Content to and from json. This works around the json library's 292 // default conversion of []byte to base64 encoded string. 293 type contentJSON struct { 294 Size int64 `json:"size"` 295 MimeType string `json:"mimeType"` 296 297 // Text contains the response body sent from the server or loaded from the 298 // browser cache. This field is populated with textual content only. The text 299 // field is either HTTP decoded text or a encoded (e.g. "base64") 300 // representation of the response body. Leave out this field if the 301 // information is not available. 302 Text string `json:"text,omitempty"` 303 304 // Encoding used for response text field e.g "base64". Leave out this field 305 // if the text field is HTTP decoded (decompressed & unchunked), than 306 // trans-coded from its original character set into UTF-8. 307 Encoding string `json:"encoding,omitempty"` 308 } 309 310 // MarshalJSON marshals the byte slice into json after encoding based on c.Encoding. 311 func (c Content) MarshalJSON() ([]byte, error) { 312 var txt string 313 switch c.Encoding { 314 case "base64": 315 txt = base64.StdEncoding.EncodeToString(c.Text) 316 case "": 317 txt = string(c.Text) 318 default: 319 return nil, fmt.Errorf("unsupported encoding for Content.Text: %s", c.Encoding) 320 } 321 322 cj := contentJSON{ 323 Size: c.Size, 324 MimeType: c.MimeType, 325 Text: txt, 326 Encoding: c.Encoding, 327 } 328 return json.Marshal(cj) 329 } 330 331 // UnmarshalJSON unmarshals the bytes slice into Content. 332 func (c *Content) UnmarshalJSON(data []byte) error { 333 var cj contentJSON 334 if err := json.Unmarshal(data, &cj); err != nil { 335 return err 336 } 337 338 var txt []byte 339 var err error 340 switch cj.Encoding { 341 case "base64": 342 txt, err = base64.StdEncoding.DecodeString(cj.Text) 343 if err != nil { 344 return fmt.Errorf("failed to decode base64-encoded Content.Text: %v", err) 345 } 346 case "": 347 txt = []byte(cj.Text) 348 default: 349 return fmt.Errorf("unsupported encoding for Content.Text: %s", cj.Encoding) 350 } 351 352 c.Size = cj.Size 353 c.MimeType = cj.MimeType 354 c.Text = txt 355 c.Encoding = cj.Encoding 356 return nil 357 } 358 359 // Option is a configurable setting for the logger. 360 type Option func(l *Logger) 361 362 // PostDataLogging returns an option that configures request post data logging. 363 func PostDataLogging(enabled bool) Option { 364 return func(l *Logger) { 365 l.postDataLogging = func(*http.Request) bool { 366 return enabled 367 } 368 } 369 } 370 371 // PostDataLoggingForContentTypes returns an option that logs request bodies based 372 // on opting in to the Content-Type of the request. 373 func PostDataLoggingForContentTypes(cts ...string) Option { 374 return func(l *Logger) { 375 l.postDataLogging = func(req *http.Request) bool { 376 rct := req.Header.Get("Content-Type") 377 378 for _, ct := range cts { 379 if strings.HasPrefix(strings.ToLower(rct), strings.ToLower(ct)) { 380 return true 381 } 382 } 383 384 return false 385 } 386 } 387 } 388 389 // SkipPostDataLoggingForContentTypes returns an option that logs request bodies based 390 // on opting out of the Content-Type of the request. 391 func SkipPostDataLoggingForContentTypes(cts ...string) Option { 392 return func(l *Logger) { 393 l.postDataLogging = func(req *http.Request) bool { 394 rct := req.Header.Get("Content-Type") 395 396 for _, ct := range cts { 397 if strings.HasPrefix(strings.ToLower(rct), strings.ToLower(ct)) { 398 return false 399 } 400 } 401 402 return true 403 } 404 } 405 } 406 407 // BodyLogging returns an option that configures response body logging. 408 func BodyLogging(enabled bool) Option { 409 return func(l *Logger) { 410 l.bodyLogging = func(*http.Response) bool { 411 return enabled 412 } 413 } 414 } 415 416 // BodyLoggingForContentTypes returns an option that logs response bodies based 417 // on opting in to the Content-Type of the response. 418 func BodyLoggingForContentTypes(cts ...string) Option { 419 return func(l *Logger) { 420 l.bodyLogging = func(res *http.Response) bool { 421 rct := res.Header.Get("Content-Type") 422 423 for _, ct := range cts { 424 if strings.HasPrefix(strings.ToLower(rct), strings.ToLower(ct)) { 425 return true 426 } 427 } 428 429 return false 430 } 431 } 432 } 433 434 // SkipBodyLoggingForContentTypes returns an option that logs response bodies based 435 // on opting out of the Content-Type of the response. 436 func SkipBodyLoggingForContentTypes(cts ...string) Option { 437 return func(l *Logger) { 438 l.bodyLogging = func(res *http.Response) bool { 439 rct := res.Header.Get("Content-Type") 440 441 for _, ct := range cts { 442 if strings.HasPrefix(strings.ToLower(rct), strings.ToLower(ct)) { 443 return false 444 } 445 } 446 447 return true 448 } 449 } 450 } 451 452 // NewLogger returns a HAR logger. The returned 453 // logger logs all request post data and response bodies by default. 454 func NewLogger() *Logger { 455 l := &Logger{ 456 creator: &Creator{ 457 Name: "martian proxy", 458 Version: "2.0.0", 459 }, 460 entries: make(map[string]*Entry), 461 } 462 l.SetOption(BodyLogging(true)) 463 l.SetOption(PostDataLogging(true)) 464 return l 465 } 466 467 // SetOption sets configurable options on the logger. 468 func (l *Logger) SetOption(opts ...Option) { 469 for _, opt := range opts { 470 opt(l) 471 } 472 } 473 474 // ModifyRequest logs requests. 475 func (l *Logger) ModifyRequest(req *http.Request) error { 476 ctx := martian.NewContext(req) 477 if ctx.SkippingLogging() { 478 return nil 479 } 480 481 id := ctx.ID() 482 483 return l.RecordRequest(id, req) 484 } 485 486 // RecordRequest logs the HTTP request with the given ID. The ID should be unique 487 // per request/response pair. 488 func (l *Logger) RecordRequest(id string, req *http.Request) error { 489 hreq, err := NewRequest(req, l.postDataLogging(req)) 490 if err != nil { 491 return err 492 } 493 494 entry := &Entry{ 495 ID: id, 496 StartedDateTime: time.Now().UTC(), 497 Request: hreq, 498 Cache: &Cache{}, 499 Timings: &Timings{}, 500 } 501 502 l.mu.Lock() 503 defer l.mu.Unlock() 504 505 if _, exists := l.entries[id]; exists { 506 return fmt.Errorf("Duplicate request ID: %s", id) 507 } 508 l.entries[id] = entry 509 if l.tail == nil { 510 l.tail = entry 511 } 512 entry.next = l.tail.next 513 l.tail.next = entry 514 l.tail = entry 515 516 return nil 517 } 518 519 // NewRequest constructs and returns a Request from req. If withBody is true, 520 // req.Body is read to EOF and replaced with a copy in a bytes.Buffer. An error 521 // is returned (and req.Body may be in an intermediate state) if an error is 522 // returned from req.Body.Read. 523 func NewRequest(req *http.Request, withBody bool) (*Request, error) { 524 r := &Request{ 525 Method: req.Method, 526 URL: req.URL.String(), 527 HTTPVersion: req.Proto, 528 HeadersSize: -1, 529 BodySize: req.ContentLength, 530 QueryString: []QueryString{}, 531 Headers: headers(proxyutil.RequestHeader(req).Map()), 532 Cookies: cookies(req.Cookies()), 533 } 534 535 for n, vs := range req.URL.Query() { 536 for _, v := range vs { 537 r.QueryString = append(r.QueryString, QueryString{ 538 Name: n, 539 Value: v, 540 }) 541 } 542 } 543 544 pd, err := postData(req, withBody) 545 if err != nil { 546 return nil, err 547 } 548 r.PostData = pd 549 550 return r, nil 551 } 552 553 // ModifyResponse logs responses. 554 func (l *Logger) ModifyResponse(res *http.Response) error { 555 ctx := martian.NewContext(res.Request) 556 if ctx.SkippingLogging() { 557 return nil 558 } 559 id := ctx.ID() 560 561 return l.RecordResponse(id, res) 562 } 563 564 // RecordResponse logs an HTTP response, associating it with the previously-logged 565 // HTTP request with the same ID. 566 func (l *Logger) RecordResponse(id string, res *http.Response) error { 567 hres, err := NewResponse(res, l.bodyLogging(res)) 568 if err != nil { 569 return err 570 } 571 572 l.mu.Lock() 573 defer l.mu.Unlock() 574 575 if e, ok := l.entries[id]; ok { 576 e.Response = hres 577 e.Time = time.Since(e.StartedDateTime).Nanoseconds() / 1000000 578 } 579 580 return nil 581 } 582 583 // NewResponse constructs and returns a Response from resp. If withBody is true, 584 // resp.Body is read to EOF and replaced with a copy in a bytes.Buffer. An error 585 // is returned (and resp.Body may be in an intermediate state) if an error is 586 // returned from resp.Body.Read. 587 func NewResponse(res *http.Response, withBody bool) (*Response, error) { 588 r := &Response{ 589 HTTPVersion: res.Proto, 590 Status: res.StatusCode, 591 StatusText: http.StatusText(res.StatusCode), 592 HeadersSize: -1, 593 BodySize: res.ContentLength, 594 Headers: headers(proxyutil.ResponseHeader(res).Map()), 595 Cookies: cookies(res.Cookies()), 596 } 597 598 if res.StatusCode >= 300 && res.StatusCode < 400 { 599 r.RedirectURL = res.Header.Get("Location") 600 } 601 602 r.Content = &Content{ 603 Encoding: "base64", 604 MimeType: res.Header.Get("Content-Type"), 605 } 606 607 if withBody { 608 mv := messageview.New() 609 if err := mv.SnapshotResponse(res); err != nil { 610 return nil, err 611 } 612 613 br, err := mv.BodyReader(messageview.Decode()) 614 if err != nil { 615 return nil, err 616 } 617 618 body, err := ioutil.ReadAll(br) 619 if err != nil { 620 return nil, err 621 } 622 623 r.Content.Text = body 624 r.Content.Size = int64(len(body)) 625 } 626 return r, nil 627 } 628 629 // Export returns the in-memory log. 630 func (l *Logger) Export() *HAR { 631 l.mu.Lock() 632 defer l.mu.Unlock() 633 634 es := make([]*Entry, 0, len(l.entries)) 635 curr := l.tail 636 for curr != nil { 637 curr = curr.next 638 es = append(es, curr) 639 if curr == l.tail { 640 break 641 } 642 } 643 644 return l.makeHAR(es) 645 } 646 647 // ExportAndReset returns the in-memory log for completed requests, clearing them. 648 func (l *Logger) ExportAndReset() *HAR { 649 l.mu.Lock() 650 defer l.mu.Unlock() 651 652 es := make([]*Entry, 0, len(l.entries)) 653 curr := l.tail 654 prev := l.tail 655 var first *Entry 656 for curr != nil { 657 curr = curr.next 658 if curr.Response != nil { 659 es = append(es, curr) 660 delete(l.entries, curr.ID) 661 } else { 662 if first == nil { 663 first = curr 664 } 665 prev.next = curr 666 prev = curr 667 } 668 if curr == l.tail { 669 break 670 } 671 } 672 if len(l.entries) == 0 { 673 l.tail = nil 674 } else { 675 l.tail = prev 676 l.tail.next = first 677 } 678 679 return l.makeHAR(es) 680 } 681 682 func (l *Logger) makeHAR(es []*Entry) *HAR { 683 return &HAR{ 684 Log: &Log{ 685 Version: "1.2", 686 Creator: l.creator, 687 Entries: es, 688 }, 689 } 690 } 691 692 // Reset clears the in-memory log of entries. 693 func (l *Logger) Reset() { 694 l.mu.Lock() 695 defer l.mu.Unlock() 696 697 l.entries = make(map[string]*Entry) 698 l.tail = nil 699 } 700 701 func cookies(cs []*http.Cookie) []Cookie { 702 hcs := make([]Cookie, 0, len(cs)) 703 704 for _, c := range cs { 705 var expires string 706 if !c.Expires.IsZero() { 707 expires = c.Expires.Format(time.RFC3339) 708 } 709 710 hcs = append(hcs, Cookie{ 711 Name: c.Name, 712 Value: c.Value, 713 Path: c.Path, 714 Domain: c.Domain, 715 HTTPOnly: c.HttpOnly, 716 Secure: c.Secure, 717 Expires: c.Expires, 718 Expires8601: expires, 719 }) 720 } 721 722 return hcs 723 } 724 725 func headers(hs http.Header) []Header { 726 hhs := make([]Header, 0, len(hs)) 727 728 for n, vs := range hs { 729 for _, v := range vs { 730 hhs = append(hhs, Header{ 731 Name: n, 732 Value: v, 733 }) 734 } 735 } 736 737 return hhs 738 } 739 740 func postData(req *http.Request, logBody bool) (*PostData, error) { 741 // If the request has no body (no Content-Length and Transfer-Encoding isn't 742 // chunked), skip the post data. 743 if req.ContentLength <= 0 && len(req.TransferEncoding) == 0 { 744 return nil, nil 745 } 746 747 ct := req.Header.Get("Content-Type") 748 mt, ps, err := mime.ParseMediaType(ct) 749 if err != nil { 750 log.Errorf("har: cannot parse Content-Type header %q: %v", ct, err) 751 mt = ct 752 } 753 754 pd := &PostData{ 755 MimeType: mt, 756 Params: []Param{}, 757 } 758 759 if !logBody { 760 return pd, nil 761 } 762 763 mv := messageview.New() 764 if err := mv.SnapshotRequest(req); err != nil { 765 return nil, err 766 } 767 768 br, err := mv.BodyReader() 769 if err != nil { 770 return nil, err 771 } 772 773 switch mt { 774 case "multipart/form-data": 775 mpr := multipart.NewReader(br, ps["boundary"]) 776 777 for { 778 p, err := mpr.NextPart() 779 if err == io.EOF { 780 break 781 } 782 if err != nil { 783 return nil, err 784 } 785 defer p.Close() 786 787 body, err := ioutil.ReadAll(p) 788 if err != nil { 789 return nil, err 790 } 791 792 pd.Params = append(pd.Params, Param{ 793 Name: p.FormName(), 794 Filename: p.FileName(), 795 ContentType: p.Header.Get("Content-Type"), 796 Value: string(body), 797 }) 798 } 799 case "application/x-www-form-urlencoded": 800 body, err := ioutil.ReadAll(br) 801 if err != nil { 802 return nil, err 803 } 804 805 vs, err := url.ParseQuery(string(body)) 806 if err != nil { 807 return nil, err 808 } 809 810 for n, vs := range vs { 811 for _, v := range vs { 812 pd.Params = append(pd.Params, Param{ 813 Name: n, 814 Value: v, 815 }) 816 } 817 } 818 default: 819 body, err := ioutil.ReadAll(br) 820 if err != nil { 821 return nil, err 822 } 823 824 pd.Text = string(body) 825 } 826 827 return pd, nil 828 }