// Copyright 2013 Google Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package imageproxy provides an image proxy server. For typical use of // creating and using a Proxy, see cmd/imageproxy/main.go. package imageproxy // import "willnorris.com/go/imageproxy" import ( "bufio" "bytes" "crypto/hmac" "crypto/sha256" "encoding/base64" "fmt" "io" "io/ioutil" "net/http" "net/url" "strings" "time" "github.com/golang/glog" "github.com/gregjones/httpcache" ) // Proxy serves image requests. // // Note that a Proxy should not be run behind a http.ServeMux, since the // ServeMux aggressively cleans URLs and removes the double slash in the // embedded request URL. type Proxy struct { Client *http.Client // client used to fetch remote URLs Cache Cache // cache used to cache responses // Whitelist specifies a list of remote hosts that images can be // proxied from. An empty list means all hosts are allowed. Whitelist []string // Referrers, when given, requires that requests to the image // proxy come from a referring host. An empty list means all // hosts are allowed. Referrers []string // DefaultBaseURL is the URL that relative remote URLs are resolved in // reference to. If nil, all remote URLs specified in requests must be // absolute. DefaultBaseURL *url.URL // SignatureKey is the HMAC key used to verify signed requests. SignatureKey []byte // Allow images to scale beyond their original dimensions. ScaleUp bool } // NewProxy constructs a new proxy. The provided http RoundTripper will be // used to fetch remote URLs. If nil is provided, http.DefaultTransport will // be used. func NewProxy(transport http.RoundTripper, cache Cache) *Proxy { if transport == nil { transport = http.DefaultTransport } if cache == nil { cache = NopCache } proxy := Proxy{ Cache: cache, } client := new(http.Client) client.Transport = &httpcache.Transport{ Transport: &TransformingTransport{transport, client, &proxy}, Cache: cache, MarkCachedResponses: true, } proxy.Client = client return &proxy } // ServeHTTP handles image requests. func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.URL.Path == "/favicon.ico" { return // ignore favicon requests } req, err := NewRequest(r, p.DefaultBaseURL) if err != nil { msg := fmt.Sprintf("invalid request URL: %v", err) glog.Error(msg) http.Error(w, msg, http.StatusBadRequest) return } if !p.allowed(req) { msg := fmt.Sprintf("request does not contain an allowed host or valid signature") glog.Error(msg) http.Error(w, msg, http.StatusForbidden) return } resp, err := p.Client.Get(req.String()) if err != nil { msg := fmt.Sprintf("error fetching remote image: %v", err) glog.Error(msg) http.Error(w, msg, http.StatusInternalServerError) return } defer resp.Body.Close() cached := resp.Header.Get(httpcache.XFromCache) glog.Infof("request: %v (served from cache: %v)", *req, cached == "1") copyHeader(w, resp, "Last-Modified") copyHeader(w, resp, "Expires") copyHeader(w, resp, "Etag") if is304 := check304(r, resp); is304 { w.WriteHeader(http.StatusNotModified) return } copyHeader(w, resp, "Content-Length") copyHeader(w, resp, "Content-Type") w.WriteHeader(resp.StatusCode) io.Copy(w, resp.Body) } func copyHeader(w http.ResponseWriter, r *http.Response, header string) { key := http.CanonicalHeaderKey(header) if value, ok := r.Header[key]; ok { w.Header()[key] = value } } // allowed returns whether the specified request is allowed because it matches // a host in the proxy whitelist or it has a valid signature. func (p *Proxy) allowed(r *Request) bool { if len(p.Referrers) > 0 && !validReferrer(p.Referrers, r.Original) { glog.Infof("request not coming from allowed referrer: %v", r) return false } if len(p.Whitelist) == 0 && len(p.SignatureKey) == 0 { return true // no whitelist or signature key, all requests accepted } if len(p.Whitelist) > 0 { if validHost(p.Whitelist, r.URL) { return true } glog.Infof("request is not for an allowed host: %v", r) } if len(p.SignatureKey) > 0 { if validSignature(p.SignatureKey, r) { return true } glog.Infof("request contains invalid signature: %v", r) } return false } // validHost returns whether the host in u matches one of hosts. func validHost(hosts []string, u *url.URL) bool { for _, host := range hosts { if u.Host == host { return true } if strings.HasPrefix(host, "*.") && strings.HasSuffix(u.Host, host[2:]) { return true } } return false } // returns whether the referrer from the request is in the host list. func validReferrer(hosts []string, r *http.Request) bool { parsed, err := url.Parse(r.Header.Get("Referer")) if err != nil { // malformed or blank header, just deny return false } return validHost(hosts, parsed) } // validSignature returns whether the request signature is valid. func validSignature(key []byte, r *Request) bool { sig := r.Options.Signature if m := len(sig) % 4; m != 0 { // add padding if missing sig += strings.Repeat("=", 4-m) } got, err := base64.URLEncoding.DecodeString(sig) if err != nil { glog.Errorf("error base64 decoding signature %q", r.Options.Signature) return false } mac := hmac.New(sha256.New, key) mac.Write([]byte(r.URL.String())) want := mac.Sum(nil) return hmac.Equal(got, want) } // check304 checks whether we should send a 304 Not Modified in response to // req, based on the response resp. This is determined using the last modified // time and the entity tag of resp. func check304(req *http.Request, resp *http.Response) bool { // TODO(willnorris): if-none-match header can be a comma separated list // of multiple tags to be matched, or the special value "*" which // matches all etags etag := resp.Header.Get("Etag") if etag != "" && etag == req.Header.Get("If-None-Match") { return true } lastModified, err := time.Parse(time.RFC1123, resp.Header.Get("Last-Modified")) if err != nil { return false } ifModSince, err := time.Parse(time.RFC1123, req.Header.Get("If-Modified-Since")) if err != nil { return false } if lastModified.Before(ifModSince) { return true } return false } // TransformingTransport is an implementation of http.RoundTripper that // optionally transforms images using the options specified in the request URL // fragment. type TransformingTransport struct { // Transport is the underlying http.RoundTripper used to satisfy // non-transform requests (those that do not include a URL fragment). Transport http.RoundTripper // CachingClient is used to fetch images to be resized. This client is // used rather than Transport directly in order to ensure that // responses are properly cached. CachingClient *http.Client // Proxy is used to access command line flag settings during roundtripping. Proxy *Proxy } // RoundTrip implements the http.RoundTripper interface. func (t *TransformingTransport) RoundTrip(req *http.Request) (*http.Response, error) { if req.URL.Fragment == "" { // normal requests pass through glog.Infof("fetching remote URL: %v", req.URL) return t.Transport.RoundTrip(req) } u := *req.URL u.Fragment = "" resp, err := t.CachingClient.Get(u.String()) if err != nil { return nil, err } defer resp.Body.Close() b, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } opt := ParseOptions(req.URL.Fragment) // assign static settings from proxy to options if t.Proxy != nil { opt.ScaleUp = t.Proxy.ScaleUp } img, err := Transform(b, opt) if err != nil { glog.Errorf("error transforming image: %v", err) img = b } // replay response with transformed image and updated content length buf := new(bytes.Buffer) fmt.Fprintf(buf, "%s %s\n", resp.Proto, resp.Status) resp.Header.WriteSubset(buf, map[string]bool{"Content-Length": true}) fmt.Fprintf(buf, "Content-Length: %d\n\n", len(img)) buf.Write(img) return http.ReadResponse(bufio.NewReader(buf), req) }