urlfetch.go

  1// Copyright 2011 Google Inc. All rights reserved.
  2// Use of this source code is governed by the Apache 2.0
  3// license that can be found in the LICENSE file.
  4
  5// Package urlfetch provides an http.RoundTripper implementation
  6// for fetching URLs via App Engine's urlfetch service.
  7package urlfetch // import "google.golang.org/appengine/urlfetch"
  8
  9import (
 10	"errors"
 11	"fmt"
 12	"io"
 13	"io/ioutil"
 14	"net/http"
 15	"net/url"
 16	"strconv"
 17	"strings"
 18	"time"
 19
 20	"github.com/golang/protobuf/proto"
 21	"golang.org/x/net/context"
 22
 23	"google.golang.org/appengine/internal"
 24	pb "google.golang.org/appengine/internal/urlfetch"
 25)
 26
 27// Transport is an implementation of http.RoundTripper for
 28// App Engine. Users should generally create an http.Client using
 29// this transport and use the Client rather than using this transport
 30// directly.
 31type Transport struct {
 32	Context context.Context
 33
 34	// Controls whether the application checks the validity of SSL certificates
 35	// over HTTPS connections. A value of false (the default) instructs the
 36	// application to send a request to the server only if the certificate is
 37	// valid and signed by a trusted certificate authority (CA), and also
 38	// includes a hostname that matches the certificate. A value of true
 39	// instructs the application to perform no certificate validation.
 40	AllowInvalidServerCertificate bool
 41}
 42
 43// Verify statically that *Transport implements http.RoundTripper.
 44var _ http.RoundTripper = (*Transport)(nil)
 45
 46// Client returns an *http.Client using a default urlfetch Transport. This
 47// client will have the default deadline of 5 seconds, and will check the
 48// validity of SSL certificates.
 49//
 50// Any deadline of the provided context will be used for requests through this client;
 51// if the client does not have a deadline then a 5 second default is used.
 52func Client(ctx context.Context) *http.Client {
 53	return &http.Client{
 54		Transport: &Transport{
 55			Context: ctx,
 56		},
 57	}
 58}
 59
 60type bodyReader struct {
 61	content   []byte
 62	truncated bool
 63	closed    bool
 64}
 65
 66// ErrTruncatedBody is the error returned after the final Read() from a
 67// response's Body if the body has been truncated by App Engine's proxy.
 68var ErrTruncatedBody = errors.New("urlfetch: truncated body")
 69
 70func statusCodeToText(code int) string {
 71	if t := http.StatusText(code); t != "" {
 72		return t
 73	}
 74	return strconv.Itoa(code)
 75}
 76
 77func (br *bodyReader) Read(p []byte) (n int, err error) {
 78	if br.closed {
 79		if br.truncated {
 80			return 0, ErrTruncatedBody
 81		}
 82		return 0, io.EOF
 83	}
 84	n = copy(p, br.content)
 85	if n > 0 {
 86		br.content = br.content[n:]
 87		return
 88	}
 89	if br.truncated {
 90		br.closed = true
 91		return 0, ErrTruncatedBody
 92	}
 93	return 0, io.EOF
 94}
 95
 96func (br *bodyReader) Close() error {
 97	br.closed = true
 98	br.content = nil
 99	return nil
100}
101
102// A map of the URL Fetch-accepted methods that take a request body.
103var methodAcceptsRequestBody = map[string]bool{
104	"POST":  true,
105	"PUT":   true,
106	"PATCH": true,
107}
108
109// urlString returns a valid string given a URL. This function is necessary because
110// the String method of URL doesn't correctly handle URLs with non-empty Opaque values.
111// See http://code.google.com/p/go/issues/detail?id=4860.
112func urlString(u *url.URL) string {
113	if u.Opaque == "" || strings.HasPrefix(u.Opaque, "//") {
114		return u.String()
115	}
116	aux := *u
117	aux.Opaque = "//" + aux.Host + aux.Opaque
118	return aux.String()
119}
120
121// RoundTrip issues a single HTTP request and returns its response. Per the
122// http.RoundTripper interface, RoundTrip only returns an error if there
123// was an unsupported request or the URL Fetch proxy fails.
124// Note that HTTP response codes such as 5xx, 403, 404, etc are not
125// errors as far as the transport is concerned and will be returned
126// with err set to nil.
127func (t *Transport) RoundTrip(req *http.Request) (res *http.Response, err error) {
128	methNum, ok := pb.URLFetchRequest_RequestMethod_value[req.Method]
129	if !ok {
130		return nil, fmt.Errorf("urlfetch: unsupported HTTP method %q", req.Method)
131	}
132
133	method := pb.URLFetchRequest_RequestMethod(methNum)
134
135	freq := &pb.URLFetchRequest{
136		Method:                        &method,
137		Url:                           proto.String(urlString(req.URL)),
138		FollowRedirects:               proto.Bool(false), // http.Client's responsibility
139		MustValidateServerCertificate: proto.Bool(!t.AllowInvalidServerCertificate),
140	}
141	if deadline, ok := t.Context.Deadline(); ok {
142		freq.Deadline = proto.Float64(deadline.Sub(time.Now()).Seconds())
143	}
144
145	for k, vals := range req.Header {
146		for _, val := range vals {
147			freq.Header = append(freq.Header, &pb.URLFetchRequest_Header{
148				Key:   proto.String(k),
149				Value: proto.String(val),
150			})
151		}
152	}
153	if methodAcceptsRequestBody[req.Method] && req.Body != nil {
154		// Avoid a []byte copy if req.Body has a Bytes method.
155		switch b := req.Body.(type) {
156		case interface {
157			Bytes() []byte
158		}:
159			freq.Payload = b.Bytes()
160		default:
161			freq.Payload, err = ioutil.ReadAll(req.Body)
162			if err != nil {
163				return nil, err
164			}
165		}
166	}
167
168	fres := &pb.URLFetchResponse{}
169	if err := internal.Call(t.Context, "urlfetch", "Fetch", freq, fres); err != nil {
170		return nil, err
171	}
172
173	res = &http.Response{}
174	res.StatusCode = int(*fres.StatusCode)
175	res.Status = fmt.Sprintf("%d %s", res.StatusCode, statusCodeToText(res.StatusCode))
176	res.Header = make(http.Header)
177	res.Request = req
178
179	// Faked:
180	res.ProtoMajor = 1
181	res.ProtoMinor = 1
182	res.Proto = "HTTP/1.1"
183	res.Close = true
184
185	for _, h := range fres.Header {
186		hkey := http.CanonicalHeaderKey(*h.Key)
187		hval := *h.Value
188		if hkey == "Content-Length" {
189			// Will get filled in below for all but HEAD requests.
190			if req.Method == "HEAD" {
191				res.ContentLength, _ = strconv.ParseInt(hval, 10, 64)
192			}
193			continue
194		}
195		res.Header.Add(hkey, hval)
196	}
197
198	if req.Method != "HEAD" {
199		res.ContentLength = int64(len(fres.Content))
200	}
201
202	truncated := fres.GetContentWasTruncated()
203	res.Body = &bodyReader{content: fres.Content, truncated: truncated}
204	return
205}
206
207func init() {
208	internal.RegisterErrorCodeMap("urlfetch", pb.URLFetchServiceError_ErrorCode_name)
209	internal.RegisterTimeoutErrorCode("urlfetch", int32(pb.URLFetchServiceError_DEADLINE_EXCEEDED))
210}