1 // Copyright 2009 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
15 // transferWriter inspects the fields of a user-supplied Request or Response,
16 // sanitizes them without changing the user object and provides methods for
17 // writing the respective header, body and trailer in wire format.
18 type transferWriter
struct {
23 TransferEncoding
[]string
24 Trailer
map[string]string
27 func newTransferWriter(r
interface{}) (t
*transferWriter
, err os
.Error
) {
30 // Extract relevant fields
31 atLeastHTTP11
:= false
32 switch rr
:= r
.(type) {
35 t
.ContentLength
= rr
.ContentLength
37 t
.TransferEncoding
= rr
.TransferEncoding
38 t
.Trailer
= rr
.Trailer
39 atLeastHTTP11
= rr
.ProtoAtLeast(1, 1)
42 t
.ContentLength
= rr
.ContentLength
44 t
.TransferEncoding
= rr
.TransferEncoding
45 t
.Trailer
= rr
.Trailer
46 atLeastHTTP11
= rr
.ProtoAtLeast(1, 1)
47 t
.ResponseToHEAD
= noBodyExpected(rr
.RequestMethod
)
50 // Sanitize Body,ContentLength,TransferEncoding
53 t
.TransferEncoding
= nil
54 // ContentLength is expected to hold Content-Length
55 if t
.ContentLength
< 0 {
56 return nil, ErrMissingContentLength
59 if !atLeastHTTP11 || t
.Body
== nil {
60 t
.TransferEncoding
= nil
62 if chunked(t
.TransferEncoding
) {
64 } else if t
.Body
== nil { // no chunking, no body
70 if !chunked(t
.TransferEncoding
) {
77 func noBodyExpected(requestMethod
string) bool {
78 return requestMethod
== "HEAD"
81 func (t
*transferWriter
) WriteHeader(w io
.Writer
) (err os
.Error
) {
83 _
, err
= io
.WriteString(w
, "Connection: close\r\n")
89 // Write Content-Length and/or Transfer-Encoding whose values are a
90 // function of the sanitized field triple (Body, ContentLength,
92 if chunked(t
.TransferEncoding
) {
93 _
, err
= io
.WriteString(w
, "Transfer-Encoding: chunked\r\n")
97 } else if t
.ContentLength
> 0 || t
.ResponseToHEAD
{
98 io
.WriteString(w
, "Content-Length: ")
99 _
, err
= io
.WriteString(w
, strconv
.Itoa64(t
.ContentLength
)+"\r\n")
105 // Write Trailer header
106 if t
.Trailer
!= nil {
107 // TODO: At some point, there should be a generic mechanism for
108 // writing long headers, using HTTP line splitting
109 io
.WriteString(w
, "Trailer: ")
111 for k
, _
:= range t
.Trailer
{
112 k
= CanonicalHeaderKey(k
)
114 case "Transfer-Encoding", "Trailer", "Content-Length":
115 return &badStringError
{"invalid Trailer key", k
}
118 io
.WriteString(w
, ",")
123 _
, err
= io
.WriteString(w
, "\r\n")
129 func (t
*transferWriter
) WriteBody(w io
.Writer
) (err os
.Error
) {
132 if chunked(t
.TransferEncoding
) {
133 cw
:= NewChunkedWriter(w
)
134 _
, err
= io
.Copy(cw
, t
.Body
)
138 } else if t
.ContentLength
== -1 {
139 _
, err
= io
.Copy(w
, t
.Body
)
141 _
, err
= io
.Copy(w
, io
.LimitReader(t
.Body
, t
.ContentLength
))
146 if err
= t
.Body
.Close(); err
!= nil {
151 // TODO(petar): Place trailer writer code here.
152 if chunked(t
.TransferEncoding
) {
153 // Last chunk, empty trailer
154 _
, err
= io
.WriteString(w
, "\r\n")
160 type transferReader
struct {
162 Header
map[string]string
170 TransferEncoding
[]string
172 Trailer
map[string]string
175 // msg is *Request or *Response.
176 func readTransfer(msg
interface{}, r
*bufio
.Reader
) (err os
.Error
) {
177 t
:= &transferReader
{}
180 switch rr
:= msg
.(type) {
183 t
.StatusCode
= rr
.StatusCode
184 t
.RequestMethod
= rr
.RequestMethod
185 t
.ProtoMajor
= rr
.ProtoMajor
186 t
.ProtoMinor
= rr
.ProtoMinor
187 t
.Close
= shouldClose(t
.ProtoMajor
, t
.ProtoMinor
, t
.Header
)
190 t
.ProtoMajor
= rr
.ProtoMajor
191 t
.ProtoMinor
= rr
.ProtoMinor
192 // Transfer semantics for Requests are exactly like those for
193 // Responses with status code 200, responding to a GET method
195 t
.RequestMethod
= "GET"
198 // Default to HTTP/1.1
199 if t
.ProtoMajor
== 0 && t
.ProtoMinor
== 0 {
200 t
.ProtoMajor
, t
.ProtoMinor
= 1, 1
203 // Transfer encoding, content length
204 t
.TransferEncoding
, err
= fixTransferEncoding(t
.Header
)
209 t
.ContentLength
, err
= fixLength(t
.StatusCode
, t
.RequestMethod
, t
.Header
, t
.TransferEncoding
)
215 t
.Trailer
, err
= fixTrailer(t
.Header
, t
.TransferEncoding
)
220 // Prepare body reader. ContentLength < 0 means chunked encoding
221 // or close connection when finished, since multipart is not supported yet
223 case chunked(t
.TransferEncoding
):
224 t
.Body
= &body
{Reader
: newChunkedReader(r
), hdr
: msg
, r
: r
, closing
: t
.Close
}
225 case t
.ContentLength
>= 0:
226 // TODO: limit the Content-Length. This is an easy DoS vector.
227 t
.Body
= &body
{Reader
: io
.LimitReader(r
, t
.ContentLength
), closing
: t
.Close
}
229 // t.ContentLength < 0, i.e. "Content-Length" not mentioned in header
231 // Close semantics (i.e. HTTP/1.0)
232 t
.Body
= &body
{Reader
: r
, closing
: t
.Close
}
234 // Persistent connection (i.e. HTTP/1.1)
235 t
.Body
= &body
{Reader
: io
.LimitReader(r
, 0), closing
: t
.Close
}
237 // TODO(petar): It may be a good idea, for extra robustness, to
238 // assume ContentLength=0 for GET requests (and other special
239 // cases?). This logic should be in fixLength().
243 switch rr
:= msg
.(type) {
246 rr
.ContentLength
= t
.ContentLength
247 rr
.TransferEncoding
= t
.TransferEncoding
249 rr
.Trailer
= t
.Trailer
252 rr
.ContentLength
= t
.ContentLength
253 rr
.TransferEncoding
= t
.TransferEncoding
255 rr
.Trailer
= t
.Trailer
261 // Checks whether chunked is part of the encodings stack
262 func chunked(te
[]string) bool { return len(te
) > 0 && te
[0] == "chunked" }
264 // Sanitize transfer encoding
265 func fixTransferEncoding(header
map[string]string) ([]string, os
.Error
) {
266 raw
, present
:= header
["Transfer-Encoding"]
271 header
["Transfer-Encoding"] = "", false
272 encodings
:= strings
.Split(raw
, ",", -1)
273 te
:= make([]string, 0, len(encodings
))
274 // TODO: Even though we only support "identity" and "chunked"
275 // encodings, the loop below is designed with foresight. One
276 // invariant that must be maintained is that, if present,
277 // chunked encoding must always come first.
278 for _
, encoding
:= range encodings
{
279 encoding
= strings
.ToLower(strings
.TrimSpace(encoding
))
280 // "identity" encoding is not recored
281 if encoding
== "identity" {
284 if encoding
!= "chunked" {
285 return nil, &badStringError
{"unsupported transfer encoding", encoding
}
287 te
= te
[0 : len(te
)+1]
288 te
[len(te
)-1] = encoding
291 return nil, &badStringError
{"too many transfer encodings", strings
.Join(te
, ",")}
294 // Chunked encoding trumps Content-Length. See RFC 2616
295 // Section 4.4. Currently len(te) > 0 implies chunked
297 header
["Content-Length"] = "", false
304 // Determine the expected body length, using RFC 2616 Section 4.4. This
305 // function is not a method, because ultimately it should be shared by
306 // ReadResponse and ReadRequest.
307 func fixLength(status
int, requestMethod
string, header
map[string]string, te
[]string) (int64, os
.Error
) {
309 // Logic based on response type or status
310 if noBodyExpected(requestMethod
) {
321 // Logic based on Transfer-Encoding
326 // Logic based on Content-Length
327 if cl
, present
:= header
["Content-Length"]; present
{
328 cl
= strings
.TrimSpace(cl
)
330 n
, err
:= strconv
.Atoi64(cl
)
331 if err
!= nil || n
< 0 {
332 return -1, &badStringError
{"bad Content-Length", cl
}
336 header
["Content-Length"] = "", false
340 // Logic based on media type. The purpose of the following code is just
341 // to detect whether the unsupported "multipart/byteranges" is being
342 // used. A proper Content-Type parser is needed in the future.
343 if strings
.Contains(strings
.ToLower(header
["Content-Type"]), "multipart/byteranges") {
344 return -1, ErrNotSupported
347 // Body-EOF logic based on other methods (like closing, or chunked coding)
351 // Determine whether to hang up after sending a request and body, or
352 // receiving a response and body
353 // 'header' is the request headers
354 func shouldClose(major
, minor
int, header
map[string]string) bool {
357 } else if major
== 1 && minor
== 0 {
358 v
, present
:= header
["Connection"]
362 v
= strings
.ToLower(v
)
363 if !strings
.Contains(v
, "keep-alive") {
367 } else if v
, present
:= header
["Connection"]; present
{
368 // TODO: Should split on commas, toss surrounding white space,
369 // and check each field.
371 header
["Connection"] = "", false
378 // Parse the trailer header
379 func fixTrailer(header
map[string]string, te
[]string) (map[string]string, os
.Error
) {
380 raw
, present
:= header
["Trailer"]
385 header
["Trailer"] = "", false
386 trailer
:= make(map[string]string)
387 keys
:= strings
.Split(raw
, ",", -1)
388 for _
, key
:= range keys
{
389 key
= CanonicalHeaderKey(strings
.TrimSpace(key
))
391 case "Transfer-Encoding", "Trailer", "Content-Length":
392 return nil, &badStringError
{"bad trailer key", key
}
396 if len(trailer
) == 0 {
400 // Trailer and no chunking
401 return nil, ErrUnexpectedTrailer
406 // body turns a Reader into a ReadCloser.
407 // Close ensures that the body has been fully read
408 // and then reads the trailer if necessary.
411 hdr
interface{} // non-nil (Response or Request) value means read trailer
412 r
*bufio
.Reader
// underlying wire-format reader for the trailer
413 closing
bool // is the connection to be closed after reading body?
416 func (b
*body
) Close() os
.Error
{
417 if b
.hdr
== nil && b
.closing
{
418 // no trailer and closing the connection next.
419 // no point in reading to EOF.
423 trashBuf
:= make([]byte, 1024) // local for thread safety
425 _
, err
:= b
.Read(trashBuf
)
434 if b
.hdr
== nil { // not reading trailer
438 // TODO(petar): Put trailer reader code here