├── Procfile ├── signify.pub ├── .travis.yml ├── .gitignore ├── .github ├── pull_request_template.md └── issue_template.md ├── .circleci └── config.yml ├── go.mod ├── examples ├── go-camo.service ├── ruby-hex.rb ├── ruby-base64.rb ├── python-hex.py ├── python-base64.py ├── go-hex.go ├── go-base64.go └── python3-base64-filtering.py ├── pkg ├── camo │ ├── example_proxymetrics_test.go │ ├── misc.go │ ├── vars.go │ ├── encoding │ │ ├── url.go │ │ └── url_test.go │ ├── proxy_test.go │ └── proxy.go ├── stats │ ├── stats_test.go │ └── stats.go └── router │ ├── httpdate.go │ ├── httpdate_test.go │ └── router.go ├── cmd ├── go-camo │ ├── main_vers_gen.go │ └── main.go └── url-tool │ └── main.go ├── LICENSE.md ├── go.sum ├── man ├── url-tool.1.mdoc └── go-camo.1.mdoc ├── tools └── genversion.go ├── Makefile ├── CHANGELOG.md └── README.md /Procfile: -------------------------------------------------------------------------------- 1 | web: go-camo --listen=0.0.0.0:$PORT -k $HMAC_KEY 2 | -------------------------------------------------------------------------------- /signify.pub: -------------------------------------------------------------------------------- 1 | untrusted comment: signify public key 2 | RWRq5QOIGZJNa8f1xHPoLi01qEgT85SL6U/DmBcxECpKg+lRvupunbKx 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | before_script: 3 | - go get golang.org/x/vgo 4 | script: make test 5 | sudo: false 6 | go: 7 | - "1.10.x" 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | /config.json 3 | /diagrams 4 | /prox.exe 5 | /server.pem 6 | /server.key 7 | /server.crt 8 | /server.csr 9 | *.py[co] 10 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please explain the changes you made here. 4 | 5 | ### Checklist 6 | 7 | - [ ] Code compiles correctly 8 | - [ ] Created tests (if appropriate) 9 | - [ ] All tests passing 10 | - [ ] Extended the README / documentation (if necessary) 11 | 12 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | jobs: 3 | build: 4 | #working_directory: /go/src/github.com/cactus/go-camo 5 | docker: 6 | - image: circleci/golang:1 7 | steps: 8 | - checkout 9 | - run: env CGO_ENABLED=0 go get golang.org/x/vgo 10 | - run: env CGO_ENABLED=0 make test 11 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Specifications 2 | 3 | Please list the go-camo version, as well as the Operation System (and version) 4 | that go-camo is running on. The go-camo version can be found by `go-camo -V`. 5 | 6 | Version: 7 | Platform: 8 | 9 | ### Expected Behavior 10 | 11 | ### Actual Behavior 12 | 13 | ### Steps to reproduce 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cactus/go-camo 2 | 3 | require ( 4 | github.com/cactus/mlog v1.0.1 5 | github.com/cactus/tai64 v1.0.0 6 | github.com/davecgh/go-spew v1.1.0 7 | github.com/jessevdk/go-flags v1.3.0 8 | github.com/pelletier/go-toml v1.1.0 9 | github.com/pmezard/go-difflib v1.0.0 10 | github.com/stretchr/testify v1.2.1 11 | ) 12 | -------------------------------------------------------------------------------- /examples/go-camo.service: -------------------------------------------------------------------------------- 1 | # go-camo systemd unit file 2 | 3 | [Unit] 4 | Description=Go-Camo 5 | After=network-online.target 6 | Wants=network-online.target 7 | 8 | [Service] 9 | Environment="GOCAMO_HMAC=replace-me!!" 10 | ExecStart=/usr/local/bin/go-camo 11 | Restart=on-failure 12 | RestartSec=10 13 | User=nobody 14 | Group=nobody 15 | WorkingDirectory=/var/empty/ 16 | 17 | [Install] 18 | WantedBy=multi-user.target 19 | -------------------------------------------------------------------------------- /pkg/camo/example_proxymetrics_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package camo_test 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | 11 | "github.com/cactus/go-camo/pkg/camo" 12 | "github.com/cactus/go-camo/pkg/stats" 13 | ) 14 | 15 | func ExampleProxyMetrics() { 16 | config := camo.Config{} 17 | proxy, err := camo.New(config) 18 | if err != nil { 19 | fmt.Println("Error: ", err) 20 | os.Exit(1) 21 | } 22 | 23 | ps := &stats.ProxyStats{} 24 | proxy.SetMetricsCollector(ps) 25 | } 26 | -------------------------------------------------------------------------------- /examples/ruby-hex.rb: -------------------------------------------------------------------------------- 1 | require "openssl" 2 | 3 | CAMO_HOST = "https://img.example.com" 4 | 5 | def camo_url(hmac_key, image_url) 6 | if image_url.start_with?("https:") 7 | return image_url 8 | end 9 | hexdigest = OpenSSL::HMAC.hexdigest("sha1", hmac_key, image_url) 10 | hexurl = image_url.unpack("U*").collect{|x| x.to_s(16)}.join 11 | return "#{CAMO_HOST}/#{hexdigest}/#{hexurl}" 12 | end 13 | 14 | puts camo_url("test", "http://golang.org/doc/gopher/frontpage.png") 15 | # 'https://img.example.org/0f6def1cb147b0e84f39cbddc5ea10c80253a6f3/687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67' 16 | -------------------------------------------------------------------------------- /examples/ruby-base64.rb: -------------------------------------------------------------------------------- 1 | require "base64" 2 | require "openssl" 3 | 4 | CAMO_HOST = "https://img.example.com" 5 | 6 | def camo_url(hmac_key, image_url) 7 | if image_url.start_with?("https:") 8 | return image_url 9 | end 10 | b64digest = Base64.urlsafe_encode64(OpenSSL::HMAC.digest("sha1", hmac_key, image_url)).delete("=") 11 | b64url = Base64.urlsafe_encode64(image_url).delete("=") 12 | return "#{CAMO_HOST}/#{b64digest}/#{b64url}" 13 | end 14 | 15 | puts camo_url("test", "http://golang.org/doc/gopher/frontpage.png") 16 | # https://img.example.com/D23vHLFHsOhPOcvdxeoQyAJTpvM/aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n 17 | -------------------------------------------------------------------------------- /examples/python-hex.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | 4 | 5 | CAMO_HOST = 'https://img.example.com' 6 | 7 | 8 | def camo_url(hmac_key, image_url): 9 | if image_url.startswith("https:"): 10 | return image_url 11 | hexdigest = hmac.new(hmac_key, image_url, hashlib.sha1).hexdigest() 12 | hexurl = image_url.encode('hex') 13 | requrl = '%s/%s/%s' % (CAMO_HOST, hexdigest, hexurl) 14 | return requrl 15 | 16 | 17 | print camo_url("test", "http://golang.org/doc/gopher/frontpage.png") 18 | # 'https://img.example.org/0f6def1cb147b0e84f39cbddc5ea10c80253a6f3/687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67' 19 | -------------------------------------------------------------------------------- /examples/python-base64.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import hmac 3 | import base64 4 | 5 | 6 | CAMO_HOST = 'https://img.example.com' 7 | 8 | 9 | def camo_url(hmac_key, image_url): 10 | if image_url.startswith("https:"): 11 | return image_url 12 | b64digest = base64.urlsafe_b64encode( 13 | hmac.new(hmac_key, image_url, hashlib.sha1).digest()).strip('=') 14 | b64url = base64.urlsafe_b64encode(image_url).strip('=') 15 | requrl = '%s/%s/%s' % (CAMO_HOST, b64digest, b64url) 16 | return requrl 17 | 18 | 19 | print camo_url("test", "http://golang.org/doc/gopher/frontpage.png") 20 | # 'https://img.example.org/D23vHLFHsOhPOcvdxeoQyAJTpvM/aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n' 21 | 22 | -------------------------------------------------------------------------------- /cmd/go-camo/main_vers_gen.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // THIS FILE IS AUTOGENERATED. DO NOT EDIT! 6 | 7 | package main 8 | 9 | const licenseText = ` 10 | This software is available under the MIT License at: 11 | https://github.com/cactus/go-camo 12 | 13 | Portions of this software utilize third party libraries: 14 | * Runtime dependencies: 15 | ├── github.com/cactus/mlog (MIT license) 16 | └── github.com/jessevdk/go-flags (BSD license) 17 | 18 | * Test/Build only dependencies: 19 | ├── github.com/stretchr/testify (MIT license) 20 | └── github.com/pelletier/go-toml (MIT license) 21 | 22 | ` -------------------------------------------------------------------------------- /examples/go-hex.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "crypto/hmac" 7 | "crypto/sha1" 8 | "encoding/hex" 9 | "fmt" 10 | "strings" 11 | ) 12 | 13 | var CAMO_HOST = "https://img.example.com" 14 | 15 | func GenCamoUrl(hmacKey []byte, srcUrl string) string { 16 | if strings.HasPrefix(srcUrl, "https:") { 17 | return srcUrl 18 | } 19 | oBytes := []byte(srcUrl) 20 | mac := hmac.New(sha1.New, hmacKey) 21 | mac.Write(oBytes) 22 | macSum := hex.EncodeToString(mac.Sum(nil)) 23 | encodedUrl := hex.EncodeToString(oBytes) 24 | hexurl := CAMO_HOST + "/" + macSum + "/" + encodedUrl 25 | return hexurl 26 | } 27 | 28 | func main() { 29 | fmt.Println(GenCamoUrl([]byte("test"), "http://golang.org/doc/gopher/frontpage.png")) 30 | } 31 | -------------------------------------------------------------------------------- /pkg/stats/stats_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package stats 6 | 7 | import ( 8 | "runtime" 9 | "sync" 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestConcurrentUpdate(t *testing.T) { 16 | t.Parallel() 17 | ps := &ProxyStats{} 18 | var wg sync.WaitGroup 19 | for i := 0; i < 100; i++ { 20 | wg.Add(1) 21 | go func() { 22 | defer wg.Done() 23 | for v := 0; v < 100000; v++ { 24 | ps.AddServed() 25 | ps.AddBytes(1024) 26 | runtime.Gosched() 27 | } 28 | }() 29 | } 30 | 31 | wg.Wait() 32 | c, b := ps.GetStats() 33 | assert.Equal(t, 10000000, int(c), "unexpected client count") 34 | assert.Equal(t, 10240000000, int(b), "unexpected bytes count") 35 | } 36 | -------------------------------------------------------------------------------- /examples/go-base64.go: -------------------------------------------------------------------------------- 1 | // +build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "crypto/hmac" 7 | "crypto/sha1" 8 | "encoding/base64" 9 | "fmt" 10 | "strings" 11 | ) 12 | 13 | var CAMO_HOST = "https://img.example.com" 14 | 15 | func b64encode(data []byte) string { 16 | return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=") 17 | } 18 | 19 | func GenCamoUrl(hmacKey []byte, srcUrl string) string { 20 | if strings.HasPrefix(srcUrl, "https:") { 21 | return srcUrl 22 | } 23 | oBytes := []byte(srcUrl) 24 | mac := hmac.New(sha1.New, hmacKey) 25 | mac.Write(oBytes) 26 | macSum := b64encode(mac.Sum(nil)) 27 | encodedUrl := b64encode(oBytes) 28 | encurl := CAMO_HOST + "/" + macSum + "/" + encodedUrl 29 | return encurl 30 | } 31 | 32 | func main() { 33 | fmt.Println(GenCamoUrl([]byte("test"), "http://golang.org/doc/gopher/frontpage.png")) 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2018 Eli Janssen 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cactus/mlog v1.0.1 h1:86eVRQ7m9RaD2oayGiPcMz6uy8RJYKjI1zlejemkCUU= 2 | github.com/cactus/mlog v1.0.1/go.mod h1:AJd8Wr+LFfIG6gD8nF356EVpQ1i04/dgYfaMlMCj1ns= 3 | github.com/cactus/tai64 v1.0.0 h1:2G/693el0FjkhychJt8iBkCXa9OOxcs5Py5+6v3gypw= 4 | github.com/cactus/tai64 v1.0.0/go.mod h1:WhJw2EH0VDwR0Rzw4h03HV7pLkJIOJPXXs+gNx8eYz8= 5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/jessevdk/go-flags v1.3.0 h1:QmKsgik/Z5fJ11ZtlcA8F+XW9dNybBNFQ1rngF3MmdU= 8 | github.com/jessevdk/go-flags v1.3.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= 9 | github.com/pelletier/go-toml v1.1.0 h1:cmiOvKzEunMsAxyhXSzpL5Q1CRKpVv0KQsnAIcSEVYM= 10 | github.com/pelletier/go-toml v1.1.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= 11 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 13 | github.com/stretchr/testify v1.2.1 h1:52QO5WkIUcHGIR7EnGagH88x1bUzqGXTC5/1bDTUQ7U= 14 | github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 15 | -------------------------------------------------------------------------------- /pkg/router/httpdate.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package router 6 | 7 | import ( 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/cactus/mlog" 13 | ) 14 | 15 | const timeFormat = "Mon, 02 Jan 2006 15:04:05 GMT" 16 | 17 | // HTTPDate holds current date stamp formatting for HTTP date header 18 | type iHTTPDate struct { 19 | dateValue atomic.Value 20 | onceUpdater sync.Once 21 | } 22 | 23 | func (h *iHTTPDate) String() string { 24 | stamp := h.dateValue.Load() 25 | if stamp == nil { 26 | mlog.Print("got a nil datesamp. Trying to recover...") 27 | h.Update() 28 | return time.Now().UTC().Format(timeFormat) 29 | } 30 | return stamp.(string) 31 | } 32 | 33 | func (h *iHTTPDate) Update() { 34 | h.dateValue.Store(time.Now().UTC().Format(timeFormat)) 35 | } 36 | 37 | func newiHTTPDate() *iHTTPDate { 38 | d := &iHTTPDate{} 39 | d.Update() 40 | // spawn a single formattedDate updater 41 | d.onceUpdater.Do(func() { 42 | go func() { 43 | for range time.Tick(1 * time.Second) { 44 | d.Update() 45 | } 46 | }() 47 | }) 48 | return d 49 | } 50 | 51 | var formattedDate = newiHTTPDate() 52 | -------------------------------------------------------------------------------- /pkg/router/httpdate_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package router 6 | 7 | import ( 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestHTTPDateGoroutineUpdate(t *testing.T) { 15 | t.Parallel() 16 | d := newiHTTPDate() 17 | n := d.String() 18 | time.Sleep(2 * time.Second) 19 | l := d.String() 20 | assert.NotEqual(t, n, l, "Date did not update as expected: %s == %s", n, l) 21 | } 22 | 23 | func TestHTTPDateManualUpdate(t *testing.T) { 24 | t.Parallel() 25 | d := &iHTTPDate{} 26 | d.Update() 27 | n := d.String() 28 | time.Sleep(2 * time.Second) 29 | d.Update() 30 | l := d.String() 31 | assert.NotEqual(t, n, l, "Date did not update as expected: %s == %s", n, l) 32 | } 33 | 34 | func TestHTTPDateManualUpdateUninitialized(t *testing.T) { 35 | t.Parallel() 36 | d := &iHTTPDate{} 37 | 38 | n := d.String() 39 | time.Sleep(2 * time.Second) 40 | d.Update() 41 | l := d.String() 42 | assert.NotEqual(t, n, l, "Date did not update as expected: %s == %s", n, l) 43 | } 44 | 45 | func BenchmarkDataString(b *testing.B) { 46 | d := newiHTTPDate() 47 | b.ResetTimer() 48 | b.RunParallel(func(pb *testing.PB) { 49 | for pb.Next() { 50 | _ = d.String() 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/stats/stats.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package stats 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "sync/atomic" 11 | ) 12 | 13 | // ProxyStats is the counter container 14 | type ProxyStats struct { 15 | clients uint64 16 | bytes uint64 17 | } 18 | 19 | // AddServed increments the number of clients served counter 20 | func (ps *ProxyStats) AddServed() { 21 | atomic.AddUint64(&ps.clients, 1) 22 | } 23 | 24 | // AddBytes increments the number of bytes served counter 25 | func (ps *ProxyStats) AddBytes(bc int64) { 26 | if bc <= 0 { 27 | return 28 | } 29 | atomic.AddUint64(&ps.bytes, uint64(bc)) 30 | } 31 | 32 | // GetStats returns the stats: clients, bytes 33 | func (ps *ProxyStats) GetStats() (uint64, uint64) { 34 | psClients := atomic.LoadUint64(&ps.clients) 35 | psBytes := atomic.LoadUint64(&ps.bytes) 36 | return psClients, psBytes 37 | } 38 | 39 | // Handler returns an http.HandlerFunc that returns running totals and 40 | // stats about the server. 41 | func Handler(ps *ProxyStats) http.HandlerFunc { 42 | return func(w http.ResponseWriter, r *http.Request) { 43 | w.Header().Set("Content-Type", "text/plain; charset=utf-8") 44 | w.WriteHeader(200) 45 | c, b := ps.GetStats() 46 | fmt.Fprintf(w, "ClientsServed, BytesServed\n%d, %d\n", c, b) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /pkg/router/router.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package router 6 | 7 | import ( 8 | "net/http" 9 | "strings" 10 | ) 11 | 12 | // DumbRouter is a basic, special purpose, http router 13 | type DumbRouter struct { 14 | ServerName string 15 | CamoHandler http.Handler 16 | StatsHandler http.HandlerFunc 17 | AddHeaders map[string]string 18 | } 19 | 20 | // SetHeaders sets the headers on the response 21 | func (dr *DumbRouter) SetHeaders(w http.ResponseWriter) { 22 | h := w.Header() 23 | for k, v := range dr.AddHeaders { 24 | h.Set(k, v) 25 | } 26 | h.Set("Date", formattedDate.String()) 27 | h.Set("Server", dr.ServerName) 28 | } 29 | 30 | // HealthCheckHandler is HTTP handler for confirming the backend service 31 | // is available from an external client, such as a load balancer. 32 | func (dr *DumbRouter) HealthCheckHandler(w http.ResponseWriter, r *http.Request) { 33 | w.WriteHeader(200) 34 | } 35 | 36 | // ServeHTTP fulfills the http server interface 37 | func (dr *DumbRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) { 38 | // set some default headers 39 | dr.SetHeaders(w) 40 | 41 | if r.Method != "HEAD" && r.Method != "GET" { 42 | http.Error(w, "Method Not Allowed", 405) 43 | } 44 | 45 | components := strings.Split(r.URL.Path, "/") 46 | if len(components) == 3 { 47 | dr.CamoHandler.ServeHTTP(w, r) 48 | return 49 | } 50 | 51 | if r.URL.Path == "/healthcheck" { 52 | dr.HealthCheckHandler(w, r) 53 | return 54 | } 55 | 56 | if dr.StatsHandler != nil && r.URL.Path == "/status" { 57 | dr.StatsHandler(w, r) 58 | return 59 | } 60 | 61 | http.Error(w, "404 Not Found", 404) 62 | } 63 | -------------------------------------------------------------------------------- /examples/python3-base64-filtering.py: -------------------------------------------------------------------------------- 1 | # this example shows how filtering can be done on the url generation side. 2 | # this example through https urls (no proxying required), and only allows http 3 | # requests over port 80. 4 | 5 | import hashlib 6 | import hmac 7 | import base64 8 | from urllib.parse import urlsplit 9 | 10 | 11 | CAMO_HOST = 'https://img.example.com' 12 | 13 | 14 | def camo_url(hmac_key, image_url): 15 | url = urlsplit(image_url) 16 | 17 | if url.scheme == 'https': 18 | # pass through https, no need to proxy it to get security lock. 19 | # fast path. check this first. 20 | return image_url 21 | 22 | if url.scheme != 'http' or (':' in url.netloc and not url.netloc.endswith(':80')): 23 | # depending on application code, it may be more appropriate 24 | # to return a fixed url placeholder image of some kind (eg. 404 image url), 25 | # an empty string, or raise an exception that calling code handles. 26 | return "Nope!" 27 | 28 | hmac_key = hmac_key.encode() if isinstance(hmac_key, str) else hmac_key 29 | image_url = image_url.encode() if isinstance(image_url, str) else image_url 30 | 31 | b64digest = base64.urlsafe_b64encode( 32 | hmac.new(hmac_key, image_url, hashlib.sha1).digest() 33 | ).strip(b'=').decode('utf-8') 34 | b64url = base64.urlsafe_b64encode(image_url).strip(b'=').decode('utf-8') 35 | requrl = '%s/%s/%s' % (CAMO_HOST, b64digest, b64url) 36 | return requrl 37 | 38 | 39 | print(camo_url("test", "http://golang.org/doc/gopher/frontpage.png")) 40 | # https://img.example.org/D23vHLFHsOhPOcvdxeoQyAJTpvM/aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n 41 | print(camo_url("test", "http://golang.org:80/doc/gopher/frontpage.png")) 42 | # https://img.example.com/8_b8SZkMlTYfsGFtkZS7SyJn37k/aHR0cDovL2dvbGFuZy5vcmc6ODAvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n 43 | print(camo_url("test", "http://golang.org:8080/doc/gopher/frontpage.png")) 44 | # Nope! 45 | -------------------------------------------------------------------------------- /pkg/camo/misc.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package camo 6 | 7 | import ( 8 | "net" 9 | "os" 10 | "regexp" 11 | "strings" 12 | "syscall" 13 | ) 14 | 15 | func isBrokenPipe(err error) bool { 16 | if opErr, ok := err.(*net.OpError); ok { 17 | // >= go1.6 18 | if syscallErr, ok := opErr.Err.(*os.SyscallError); ok { 19 | switch syscallErr.Err { 20 | case syscall.EPIPE, syscall.ECONNRESET: 21 | return true 22 | default: 23 | return false 24 | } 25 | } 26 | 27 | // older go 28 | switch opErr.Err { 29 | case syscall.EPIPE, syscall.ECONNRESET: 30 | return true 31 | default: 32 | return false 33 | } 34 | } 35 | return false 36 | } 37 | 38 | func mustParseNetmask(s string) *net.IPNet { 39 | _, ipnet, err := net.ParseCIDR(s) 40 | if err != nil { 41 | panic(`misc: mustParseNetmask(` + s + `): ` + err.Error()) 42 | } 43 | return ipnet 44 | } 45 | 46 | func mustParseNetmasks(networks []string) []*net.IPNet { 47 | nets := make([]*net.IPNet, 0) 48 | for _, s := range networks { 49 | ipnet := mustParseNetmask(s) 50 | nets = append(nets, ipnet) 51 | } 52 | return nets 53 | } 54 | 55 | func isRejectedIP(ip net.IP) bool { 56 | if !ip.IsGlobalUnicast() { 57 | return true 58 | } 59 | 60 | // test whether address is ipv4 or ipv6, to pick the proper filter list 61 | // (otherwise address may be 16 byte representation in go but not an actual 62 | // ipv6 address. this also helps avoid accidentally matching the 63 | // "::ffff:0:0/96" netblock 64 | checker := rejectIPv4Networks 65 | if ip.To4() == nil { 66 | checker = rejectIPv6Networks 67 | } 68 | 69 | for _, ipnet := range checker { 70 | if ipnet.Contains(ip) { 71 | return true 72 | } 73 | } 74 | 75 | return false 76 | } 77 | 78 | func globToRegexp(globString string) (*regexp.Regexp, error) { 79 | gs := "^" + strings.Replace(globString, "*", ".*", 1) + "$" 80 | c, err := regexp.Compile(strings.TrimSpace(gs)) 81 | return c, err 82 | } 83 | -------------------------------------------------------------------------------- /pkg/camo/vars.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package camo 6 | 7 | import ( 8 | "regexp" 9 | ) 10 | 11 | // ValidReqHeaders are http request headers that are acceptable to pass from 12 | // the client to the remote server. Only those present and true, are forwarded. 13 | // Empty implies no filtering. 14 | var ValidReqHeaders = map[string]bool{ 15 | "Accept": true, 16 | "Accept-Charset": true, 17 | // images (aside from xml/svg), don't generally benefit (generally) from 18 | // compression 19 | "Accept-Encoding": false, 20 | "Accept-Language": true, 21 | "Cache-Control": true, 22 | "If-None-Match": true, 23 | "If-Modified-Since": true, 24 | "X-Forwarded-For": true, 25 | } 26 | 27 | // ValidRespHeaders are http response headers that are acceptable to pass from 28 | // the remote server to the client. Only those present and true, are forwarded. 29 | // Empty implies no filtering. 30 | var ValidRespHeaders = map[string]bool{ 31 | // Do not offer to accept range requests 32 | "Accept-Ranges": false, 33 | "Cache-Control": true, 34 | "Content-Encoding": true, 35 | "Content-Type": true, 36 | "Etag": true, 37 | "Expires": true, 38 | "Last-Modified": true, 39 | // override in response with either nothing, or ServerNameVer 40 | "Server": false, 41 | "Transfer-Encoding": true, 42 | } 43 | 44 | // networks to reject 45 | var rejectIPv4Networks = mustParseNetmasks( 46 | []string{ 47 | // ipv4 loopback 48 | "127.0.0.0/8", 49 | // ipv4 link local 50 | "169.254.0.0/16", 51 | // ipv4 rfc1918 52 | "10.0.0.0/8", 53 | "172.16.0.0/12", 54 | "192.168.0.0/16", 55 | }, 56 | ) 57 | 58 | var rejectIPv6Networks = mustParseNetmasks( 59 | []string{ 60 | // ipv6 loopback 61 | "::1/128", 62 | // ipv6 link local 63 | "fe80::/10", 64 | // old ipv6 site local 65 | "fec0::/10", 66 | // ipv6 ULA 67 | "fc00::/7", 68 | // ipv4 mapped onto ipv6 69 | "::ffff:0:0/96", 70 | }, 71 | ) 72 | 73 | // match for localhost 74 | var localhostRegex = regexp.MustCompile(`^localhost\.?(localdomain)?\.?$`) 75 | -------------------------------------------------------------------------------- /man/url-tool.1.mdoc: -------------------------------------------------------------------------------- 1 | .Dd May 22, 2014 2 | .Dt URL-TOOL \&1 "GO-CAMO MANUAL" 3 | .Os GO-CAMO VERSION 4 | .Sh NAME 5 | .Nm url-tool 6 | .Nd provides a simple way to generate signed URLs from the command line 7 | .Sh SYNOPSIS 8 | .Nm url-tool 9 | .Oo 10 | .Em OPTIONS Ns 11 | .Oc 12 | .Oo 13 | .Em OPTION-ARGUMENTS Ns 14 | .Oc 15 | .Sh DESCRIPTION 16 | .Sy url-tool 17 | provides a simple way to generate signed URLs from the command line 18 | compatible with 19 | .Xr go-camo 1 . 20 | .Sh OPTIONS 21 | .Bl -tag -width Ds 22 | .It Fl k Ns , Fl -key Ns = Ns Aq Ar hmac-key 23 | The HMAC key to use. 24 | .It Fl h Ns , Fl -help 25 | Show help output and exit 26 | .El 27 | .Sh COMMANDS 28 | url-tool has two subcommands. 29 | .Bl -tag -width Ds 30 | .It Cm encode Aq Ar url 31 | .Pp 32 | Available 33 | .Cm encode 34 | options: 35 | .Bl -tag -width Ds 36 | .It Fl b Ns , Fl -base Ns = Ns Aq Ar base 37 | The base encoding to use. Can be one of 38 | .Em hex 39 | or 40 | .Em base64 Ns . 41 | .It Fl -prefix Ns = Ns Aq Ar prefix 42 | Optional url prefix used by encode output. 43 | .El 44 | .It Cm decode Aq Ar url 45 | .El 46 | .Sh EXAMPLES 47 | Encode a url as hex: 48 | .Bd -literal 49 | $ ./url-tool -k "test" encode -p "https://img.example.org" "http://golang.org/doc/gopher/frontpage.png" 50 | https://img.example.org/0f6def1cb147b0e84f39cbddc5ea10c80253a6f3/687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67 51 | .Ed 52 | .Pp 53 | Encode a url as base64: 54 | .Bd -literal 55 | $ ./url-tool -k "test" encode -b base64 -p "https://img.example.org" "http://golang.org/doc/gopher/frontpage.png" 56 | https://img.example.org/D23vHLFHsOhPOcvdxeoQyAJTpvM/aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n 57 | .Ed 58 | .Pp 59 | Decode a hex url: 60 | .Bd -literal 61 | $ ./url-tool -k "test" decode "https://img.example.org/0f6def1cb147b0e84f39cbddc5ea10c80253a6f3/687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67" 62 | http://golang.org/doc/gopher/frontpage.png 63 | .Ed 64 | .Pp 65 | Decode a base64 url: 66 | .Bd -literal 67 | $ ./url-tool -k "test" decode "https://img.example.org/D23vHLFHsOhPOcvdxeoQyAJTpvM/aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n" 68 | http://golang.org/doc/gopher/frontpage.png 69 | .Ed 70 | .Sh WWW 71 | https://github.com/cactus/go-camo 72 | -------------------------------------------------------------------------------- /cmd/url-tool/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // url-tool 6 | package main 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "net/url" 12 | "os" 13 | "strings" 14 | 15 | "github.com/cactus/go-camo/pkg/camo/encoding" 16 | 17 | flags "github.com/jessevdk/go-flags" 18 | ) 19 | 20 | // EncodeCommand holds command options for the encode command 21 | type EncodeCommand struct { 22 | Base string `short:"b" long:"base" default:"hex" description:"Encode/Decode base. Either hex or base64"` 23 | Prefix string `short:"p" long:"prefix" default:"" description:"Optional url prefix used by encode output"` 24 | } 25 | 26 | // Execute runs the encode command 27 | func (c *EncodeCommand) Execute(args []string) error { 28 | if opts.HmacKey == "" { 29 | return errors.New("Empty HMAC") 30 | } 31 | 32 | if len(args) == 0 { 33 | return errors.New("No url argument provided") 34 | } 35 | 36 | oURL := args[0] 37 | if oURL == "" { 38 | return errors.New("No url argument provided") 39 | } 40 | 41 | hmacKeyBytes := []byte(opts.HmacKey) 42 | var outURL string 43 | switch c.Base { 44 | case "base64": 45 | outURL = encoding.B64EncodeURL(hmacKeyBytes, oURL) 46 | case "hex": 47 | outURL = encoding.HexEncodeURL(hmacKeyBytes, oURL) 48 | default: 49 | return errors.New("Invalid base provided") 50 | } 51 | fmt.Println(c.Prefix + outURL) 52 | return nil 53 | } 54 | 55 | // DecodeCommand holds command options for the decode command 56 | type DecodeCommand struct{} 57 | 58 | // Execute runs the decode command 59 | func (c *DecodeCommand) Execute(args []string) error { 60 | if opts.HmacKey == "" { 61 | return errors.New("Empty HMAC") 62 | } 63 | 64 | if len(args) == 0 { 65 | return errors.New("No url argument provided") 66 | } 67 | 68 | oURL := args[0] 69 | if oURL == "" { 70 | return errors.New("No url argument provided") 71 | } 72 | 73 | hmacKeyBytes := []byte(opts.HmacKey) 74 | 75 | u, err := url.Parse(oURL) 76 | if err != nil { 77 | return err 78 | } 79 | comp := strings.SplitN(u.Path, "/", 3) 80 | decURL, valid := encoding.DecodeURL(hmacKeyBytes, comp[1], comp[2]) 81 | if !valid { 82 | return errors.New("hmac is invalid") 83 | } 84 | fmt.Println(decURL) 85 | return nil 86 | } 87 | 88 | var opts struct { 89 | HmacKey string `short:"k" long:"key" description:"HMAC key"` 90 | } 91 | 92 | func main() { 93 | parser := flags.NewParser(&opts, flags.Default) 94 | parser.AddCommand("encode", "Encode a url and print result", 95 | "Encode a url and print result", &EncodeCommand{}) 96 | parser.AddCommand("decode", "Decode a url and print result", 97 | "Decode a url and print result", &DecodeCommand{}) 98 | 99 | // parse said flags 100 | _, err := parser.Parse() 101 | if err != nil { 102 | if e, ok := err.(*flags.Error); ok { 103 | if e.Type == flags.ErrHelp { 104 | os.Exit(0) 105 | } 106 | } 107 | os.Exit(1) 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /tools/genversion.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package main 6 | 7 | import ( 8 | "bufio" 9 | "flag" 10 | "fmt" 11 | "log" 12 | "os" 13 | "path" 14 | "strings" 15 | "text/template" 16 | 17 | toml "github.com/pelletier/go-toml" 18 | "github.com/pelletier/go-toml/query" 19 | ) 20 | 21 | // VersionLicenseText is a text formatter container 22 | type VersionLicenseText struct { 23 | Dependencies []map[string]string 24 | BTestDependencies []map[string]string 25 | Pkg string 26 | } 27 | 28 | const tplText = ` 29 | // Copyright (c) 2012-2018 Eli Janssen 30 | // Use of this source code is governed by an MIT-style 31 | // license that can be found in the LICENSE file. 32 | 33 | // THIS FILE IS AUTOGENERATED. DO NOT EDIT! 34 | 35 | package {{.Pkg}} 36 | 37 | const licenseText = ` + "`" + ` 38 | This software is available under the MIT License at: 39 | https://github.com/cactus/go-camo 40 | 41 | Portions of this software utilize third party libraries: 42 | * Runtime dependencies: 43 | {{range .Dependencies}} {{if eq .last "true"}}└──{{else}}├──{{end}} {{.name}} ({{.license}} license) 44 | {{end}} 45 | * Test/Build only dependencies: 46 | {{range .BTestDependencies}} {{if eq .last "true"}}└──{{else}}├──{{end}} {{.name}} ({{.license}} license) 47 | {{end}} 48 | ` + "`" 49 | 50 | func main() { 51 | var output, input, pkg string 52 | flag.StringVar(&output, "output", "", "output file") 53 | flag.StringVar(&input, "input", "", "input file") 54 | flag.StringVar(&pkg, "pkg", "", "package name") 55 | flag.Parse() 56 | 57 | if input == "" { 58 | log.Fatal("Input option is required") 59 | } 60 | 61 | if output == "" { 62 | log.Fatal("Output option is required") 63 | } 64 | 65 | if pkg == "" { 66 | log.Fatal("Package option is required") 67 | } 68 | 69 | fmt.Printf("Generating %s based on %s\n", path.Base(output), path.Base(input)) 70 | 71 | t, err := template.New("fileTemplate").Parse(strings.TrimSpace(tplText)) 72 | if err != nil { 73 | log.Fatal(err) 74 | } 75 | 76 | config, err := toml.LoadFile(input) 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | 81 | dependencies := make([]map[string]string, 0) 82 | qc, _ := query.Compile("$.constraint[?(btestOnly)]") 83 | qc.SetFilter("btestOnly", func(node interface{}) bool { 84 | if tree, ok := node.(*toml.Tree); ok { 85 | return tree.Get("metadata.btest-only") != true 86 | } 87 | return false 88 | }) 89 | for _, dep := range qc.Execute(config).Values() { 90 | if d, ok := dep.(*toml.Tree); ok { 91 | dependencies = append(dependencies, map[string]string{ 92 | "name": d.Get("name").(string), 93 | "license": d.GetDefault("metadata.license", "Not Specified").(string), 94 | "last": "false", 95 | }) 96 | } 97 | } 98 | dependencies[len(dependencies)-1]["last"] = "true" 99 | 100 | btestDependencies := make([]map[string]string, 0) 101 | qc.SetFilter("btestOnly", func(node interface{}) bool { 102 | if tree, ok := node.(*toml.Tree); ok { 103 | return tree.Get("metadata.btest-only") == true 104 | } 105 | return false 106 | }) 107 | for _, dep := range qc.Execute(config).Values() { 108 | if d, ok := dep.(*toml.Tree); ok { 109 | btestDependencies = append(btestDependencies, map[string]string{ 110 | "name": d.Get("name").(string), 111 | "license": d.GetDefault("metadata.license", "Not Specified").(string), 112 | "last": "false", 113 | }) 114 | } else { 115 | fmt.Println("not a tree") 116 | } 117 | } 118 | btestDependencies[len(btestDependencies)-1]["last"] = "true" 119 | 120 | f, err := os.Create(output) 121 | if err != nil { 122 | log.Fatal(err) 123 | } 124 | defer f.Close() 125 | 126 | writer := bufio.NewWriter(f) 127 | defer writer.Flush() 128 | 129 | data := &VersionLicenseText{ 130 | Dependencies: dependencies, 131 | BTestDependencies: btestDependencies, 132 | Pkg: pkg, 133 | } 134 | 135 | err = t.Execute(writer, data) 136 | if err != nil { 137 | log.Fatal(err) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /pkg/camo/encoding/url.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package encoding 6 | 7 | import ( 8 | "crypto/hmac" 9 | "crypto/sha1" 10 | "crypto/subtle" 11 | "encoding/base64" 12 | "encoding/hex" 13 | "fmt" 14 | "strings" 15 | 16 | "github.com/cactus/mlog" 17 | ) 18 | 19 | // DecoderFunc is a function type that defines a url decoder. 20 | type DecoderFunc func([]byte, string, string) (string, error) 21 | 22 | // EncoderFunc is a function type that defines a url encoder. 23 | type EncoderFunc func([]byte, string) string 24 | 25 | func validateURL(hmackey *[]byte, macbytes *[]byte, urlbytes *[]byte) error { 26 | mac := hmac.New(sha1.New, *hmackey) 27 | mac.Write(*urlbytes) 28 | macSum := mac.Sum(nil) 29 | 30 | // ensure lengths are equal. if not, return false 31 | if len(macSum) != len(*macbytes) { 32 | return fmt.Errorf("mismatched length") 33 | } 34 | 35 | if subtle.ConstantTimeCompare(macSum, *macbytes) != 1 { 36 | return fmt.Errorf("invalid mac") 37 | } 38 | return nil 39 | } 40 | 41 | func b64encode(data []byte) string { 42 | return strings.TrimRight(base64.URLEncoding.EncodeToString(data), "=") 43 | } 44 | 45 | func b64decode(str string) ([]byte, error) { 46 | padChars := (4 - (len(str) % 4)) % 4 47 | for i := 0; i < padChars; i++ { 48 | str = str + "=" 49 | } 50 | decBytes, ok := base64.URLEncoding.DecodeString(str) 51 | return decBytes, ok 52 | } 53 | 54 | // HexDecodeURL ensures the url is properly verified via HMAC, and then 55 | // unencodes the url, returning the url (if valid) and whether the 56 | // HMAC was verified. 57 | func HexDecodeURL(hmackey []byte, hexdig string, hexURL string) (string, error) { 58 | urlBytes, err := hex.DecodeString(hexURL) 59 | if err != nil { 60 | return "", fmt.Errorf("bad url decode") 61 | } 62 | macBytes, err := hex.DecodeString(hexdig) 63 | if err != nil { 64 | return "", fmt.Errorf("bad mac decode") 65 | } 66 | 67 | if err = validateURL(&hmackey, &macBytes, &urlBytes); err != nil { 68 | return "", fmt.Errorf("invalid signature: %s", err) 69 | } 70 | return string(urlBytes), nil 71 | } 72 | 73 | // HexEncodeURL takes an HMAC key and a url, and returns url 74 | // path partial consisitent of signature and encoded url. 75 | func HexEncodeURL(hmacKey []byte, oURL string) string { 76 | oBytes := []byte(oURL) 77 | mac := hmac.New(sha1.New, hmacKey) 78 | mac.Write(oBytes) 79 | macSum := hex.EncodeToString(mac.Sum(nil)) 80 | encodedURL := hex.EncodeToString(oBytes) 81 | hexURL := "/" + macSum + "/" + encodedURL 82 | return hexURL 83 | } 84 | 85 | // B64DecodeURL ensures the url is properly verified via HMAC, and then 86 | // unencodes the url, returning the url (if valid) and whether the 87 | // HMAC was verified. 88 | func B64DecodeURL(hmackey []byte, encdig string, encURL string) (string, error) { 89 | urlBytes, err := b64decode(encURL) 90 | if err != nil { 91 | return "", fmt.Errorf("bad url decode") 92 | } 93 | macBytes, err := b64decode(encdig) 94 | if err != nil { 95 | return "", fmt.Errorf("bad mac decode") 96 | } 97 | 98 | if err := validateURL(&hmackey, &macBytes, &urlBytes); err != nil { 99 | return "", fmt.Errorf("invalid signature: %s", err) 100 | } 101 | return string(urlBytes), nil 102 | } 103 | 104 | // B64EncodeURL takes an HMAC key and a url, and returns url 105 | // path partial consisitent of signature and encoded url. 106 | func B64EncodeURL(hmacKey []byte, oURL string) string { 107 | oBytes := []byte(oURL) 108 | mac := hmac.New(sha1.New, hmacKey) 109 | mac.Write(oBytes) 110 | macSum := b64encode(mac.Sum(nil)) 111 | encodedURL := b64encode(oBytes) 112 | encURL := "/" + macSum + "/" + encodedURL 113 | return encURL 114 | } 115 | 116 | // DecodeURL ensures the url is properly verified via HMAC, and then 117 | // unencodes the url, returning the url (if valid) and whether the 118 | // HMAC was verified. Tries either HexDecode or B64Decode, depending on the 119 | // length of the encoded hmac. 120 | func DecodeURL(hmackey []byte, encdig string, encURL string) (string, bool) { 121 | var decoder DecoderFunc 122 | if len(encdig) == 40 { 123 | decoder = HexDecodeURL 124 | } else { 125 | decoder = B64DecodeURL 126 | } 127 | 128 | urlBytes, err := decoder(hmackey, encdig, encURL) 129 | if err != nil { 130 | mlog.Debugf("Bad Decode of URL: %s", err) 131 | return "", false 132 | } 133 | return urlBytes, true 134 | } 135 | -------------------------------------------------------------------------------- /man/go-camo.1.mdoc: -------------------------------------------------------------------------------- 1 | .Dd May 26, 2014 2 | .Dt GO-CAMO \&1 "GO-CAMO MANUAL" 3 | .Os GO-CAMO VERSION 4 | .Sh NAME 5 | .Nm go-camo 6 | .Nd \&Go version of Camo server 7 | .Sh SYNOPSIS 8 | .Nm go-camo 9 | .Oo 10 | .Em OPTIONS Ns 11 | .Oc 12 | .Oo 13 | .Em OPTION-ARGUMENTS Ns 14 | .Oc 15 | .Sh DESCRIPTION 16 | .Sy go-camo 17 | is an implementation of Camo in Go. 18 | .Pp 19 | Camo is a special type of image proxy that proxies non-secure images over 20 | SSL/TLS. This prevents mixed content warnings on secure pages. 21 | .Pp 22 | It works in conjunction with back-end code to rewrite image URLs and sign them 23 | with an HMAC. 24 | .Sh ENVIRONMENT VARS 25 | .Bl -tag -width Ds 26 | .It Sy GOCAMO_HMAC 27 | The HMAC key to use. 28 | .El 29 | .Pp 30 | .Em Note Ns 31 | : 32 | .Sx "OPTIONS" Ns 33 | , if provided, override those defined in environment variables. 34 | .Pp 35 | For exmaple, if the HMAC key is provided on the command line, it will override 36 | (if present), an HMAC key set in the environment var. 37 | .Sh OPTIONS 38 | .Bl -tag -width Ds 39 | .It Fl k Ns , Fl -key Ns = Ns Aq Ar hmac-key 40 | The HMAC key to use. 41 | .It Fl H Ns , Fl -header Ns = Ns Aq Ar header 42 | Extra header to return for each response. This option can be used multiple 43 | times to add multiple headers. 44 | .Pp 45 | See 46 | .Sx "ADD_HEADERS" 47 | for more info. 48 | .It Fl -stats 49 | Enable stats collection and reporting functionality. 50 | .Pp 51 | If stats flag is provided, then the service will track bytes and clients 52 | served, and offer them up at an http endpoint /status via HTPT GET request. 53 | .Pp 54 | See 55 | .Sx "STATS" 56 | for more info. 57 | .It Fl -no-log-ts 58 | Do not add a timestamp to logging output. 59 | .It Fl -allow-list Ns = Ns Aq Ar file 60 | Path to a text file that contains a list (one per line) of regex host matches 61 | to allow. 62 | .Pp 63 | If an allow list is defined, and a request does not match one of the listed 64 | host regex, then the request is denied. 65 | .It Fl -max-size Ns = Ns Aq Ar size 66 | Max response image size in KB. Default: 5120 67 | .It Fl -timeout Ns = Ns Aq Ar time 68 | Timeout value for upstream response. Format is "4s" where 69 | .Em s 70 | means seconds. 71 | Default: 4s 72 | .It Fl -max-redirects 73 | Maximum number of redirects to follow. Default: 3 74 | .It Fl -no-fk 75 | Disable frontend http keep-alive support. 76 | .It Fl -no-bk 77 | Disable backend http keep-alive support. 78 | .It Fl -listen Ns = Ns Aq Ar address:port 79 | Address and port to listen to, as a string of "address:port". 80 | Default: "0.0.0.0:8080" 81 | .It Fl -ssl-listen Ns = Ns Aq Ar address:port 82 | Address and port to listen via SSL to, as a string of "address:port". 83 | .It Fl -ssl-key Ns = Ns Aq Ar ssl-key-file 84 | Path to ssl private key. Default: key.pem 85 | .It Fl -ssl-cert Ns = Ns Aq Ar ssl-cert-file 86 | Path to ssl certificate. Default: cert.pem 87 | .It Fl v Ns , Fl -verbose 88 | Show verbose (debug) level log output 89 | .It Fl V Ns , Fl -version 90 | Print version and exit; specify twice to show license information 91 | .It Fl h Ns , Fl -help 92 | Show help output and exit 93 | .El 94 | .Sh ADD_HEADERS 95 | Additional default headers (headers sent on every reply) can be set with the 96 | .Fl H Ns , Fl -header 97 | flag. This option can be used multiple times. 98 | .Pp 99 | The list of default headers sent are: 100 | .Bd -literal 101 | X-Content-Type-Options: nosniff 102 | X-XSS-Protection: 1; mode=block 103 | Content-Security-Policy: default-src 'none'` 104 | .Ed 105 | .Pp 106 | Additional headers are added to the above set. 107 | .Pp 108 | As an example, if you wanted to return an 109 | .Em Strict-Transport-Security 110 | and an 111 | .Em X-Frame-Options 112 | header by default, you could add this to the command line: 113 | .Bd -literal 114 | -H "Strict-Transport-Security: max-age=16070400" \\ 115 | -H "X-Frame-Options: deny" 116 | .Ed 117 | .Sh STATS 118 | If the 119 | .Fl -stats 120 | flag is provided, then the service will track bytes and 121 | clients served, and offer them up at an http endpoint 122 | .Qo Li /status Qc 123 | via HTTP GET 124 | request. 125 | .Pp 126 | The output format is show as an example: 127 | .Bd -literal 128 | ClientsServed, BytesServed 129 | 4, 27300 130 | .Ed 131 | .Sh EXAMPLES 132 | Listen on loopback port 8080 with a upstream timeout of 6 seconds: 133 | .Bd -literal 134 | go-camo -k BEEFBEEFBEEF --listen=127.0.0.1:8080 \e 135 | --timeout=6s 136 | .Ed 137 | .Pp 138 | Set HMAC key via env var, and an HSTS header: 139 | .Bd -literal 140 | export GOCAMO_HMAC=BEEFBEEFBEEF 141 | go-camo --listen=127.0.0.1:8080 \e 142 | --timeout=6s \e 143 | -H "Strict-Transport-Security: max-age=16070400" 144 | .Ed 145 | .Sh WWW 146 | .Bl -tag -width Ds 147 | .It Lk https://github.com/cactus/go-camo 148 | .El 149 | .Sh SEE ALSO 150 | .Bl -tag -width Ds 151 | .It Lk https://github.com/atmos/camo 152 | .El 153 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # environment 2 | BUILDDIR := ${CURDIR}/build 3 | TARBUILDDIR := ${BUILDDIR}/tar 4 | ARCH := $(shell go env GOHOSTARCH) 5 | OS := $(shell go env GOHOSTOS) 6 | GOVER := $(shell go version | awk '{print $$3}' | tr -d '.') 7 | SIGN_KEY ?= ${HOME}/.signify/go-camo.sec 8 | GO := vgo 9 | 10 | # app specific info 11 | APP_NAME := go-camo 12 | APP_VER := $(shell git describe --always --tags|sed 's/^v//') 13 | VERSION_VAR := main.ServerVersion 14 | 15 | # flags and build configuration 16 | GOTEST_FLAGS := -cpu=1,2 17 | GOBUILD_DEPFLAGS := -tags netgo 18 | GOBUILD_LDFLAGS ?= -s -w 19 | GOBUILD_FLAGS := ${GOBUILD_DEPFLAGS} -ldflags "${GOBUILD_LDFLAGS} -X ${VERSION_VAR}=${APP_VER}" 20 | 21 | # cross compile defs 22 | CC_BUILD_ARCHES = darwin/amd64 freebsd/amd64 linux/amd64 23 | CC_OUTPUT_TPL := ${BUILDDIR}/bin/{{.Dir}}.{{.OS}}-{{.Arch}} 24 | 25 | define HELP_OUTPUT 26 | Available targets: 27 | help this help 28 | clean clean up 29 | all build binaries and man pages 30 | test run tests 31 | cover run tests with cover output 32 | build build all binaries 33 | man build all man pages 34 | tar build release tarball 35 | cross-tar cross compile and build release tarballs 36 | endef 37 | export HELP_OUTPUT 38 | 39 | .PHONY: help clean build test cover man man-copy all tar cross-tar 40 | 41 | help: 42 | @echo "$$HELP_OUTPUT" 43 | 44 | clean: 45 | @rm -rf "${BUILDDIR}" 46 | 47 | setup: 48 | @if [ -z "$(shell which vgo)" ]; then \ 49 | echo "* 'vgo' command not found."; \ 50 | echo " install (or otherwise ensure presence in PATH)"; \ 51 | echo " go get golang.org/x/vgo"; \ 52 | exit 1;\ 53 | fi 54 | 55 | setup-gox: 56 | @if [ -z "$(shell which gox)" ]; then \ 57 | echo "* 'gox' command not found."; \ 58 | echo " install (or otherwise ensure presence in PATH)"; \ 59 | echo " go get github.com/mitchellh/gox"; \ 60 | exit 1;\ 61 | fi 62 | 63 | build: setup 64 | @[ -d "${BUILDDIR}/bin" ] || mkdir -p "${BUILDDIR}/bin" 65 | @echo "Building..." 66 | @echo "...go-camo..." 67 | @env CGO_ENABLED=0 ${GO} build ${GOBUILD_FLAGS} -o "${BUILDDIR}/bin/go-camo" ./cmd/go-camo 68 | @echo "...url-tool..." 69 | @env CGO_ENABLED=0 ${GO} build ${GOBUILD_FLAGS} -o "${BUILDDIR}/bin/url-tool" ./cmd/url-tool 70 | @echo "done!" 71 | 72 | test: setup 73 | @echo "Running tests..." 74 | @${GO} test ${GOTEST_FLAGS} ./... 75 | 76 | generate: setup 77 | @echo "Running generate..." 78 | @${GO} generate ./cmd/go-camo 79 | 80 | cover: setup 81 | @echo "Running tests with coverage..." 82 | @${GO} test -cover ${GOTEST_FLAGS} ./... 83 | 84 | ${BUILDDIR}/man/%: man/%.mdoc 85 | @[ -d "${BUILDDIR}/man" ] || mkdir -p "${BUILDDIR}/man" 86 | @cat $< | sed -E "s#.Os (.*) VERSION#.Os \1 ${APP_VER}#" > $@ 87 | 88 | man: $(patsubst man/%.mdoc,${BUILDDIR}/man/%,$(wildcard man/*.1.mdoc)) 89 | 90 | tar: all 91 | @echo "Building tar..." 92 | @mkdir -p ${TARBUILDDIR}/${APP_NAME}-${APP_VER}/bin 93 | @mkdir -p ${TARBUILDDIR}/${APP_NAME}-${APP_VER}/man 94 | @cp ${BUILDDIR}/bin/${APP_NAME} ${TARBUILDDIR}/${APP_NAME}-${APP_VER}/bin/${APP_NAME} 95 | @cp ${BUILDDIR}/bin/url-tool ${TARBUILDDIR}/${APP_NAME}-${APP_VER}/bin/url-tool 96 | @cp ${BUILDDIR}/man/*.[1-9] ${TARBUILDDIR}/${APP_NAME}-${APP_VER}/man/ 97 | @tar -C ${TARBUILDDIR} -czf ${TARBUILDDIR}/${APP_NAME}-${APP_VER}.${GOVER}.${OS}-${ARCH}.tar.gz ${APP_NAME}-${APP_VER} 98 | @rm -rf "${TARBUILDDIR}/${APP_NAME}-${APP_VER}" 99 | 100 | cross-tar: man setup setup-gox 101 | @echo "Building (cross-compile: ${CC_BUILD_ARCHES})..." 102 | @echo "...go-camo..." 103 | @env gox -gocmd="${GO}" -output="${CC_OUTPUT_TPL}" -osarch="${CC_BUILD_ARCHES}" ${GOBUILD_FLAGS} ./cmd/go-camo 104 | @echo 105 | 106 | @echo "...url-tool..." 107 | @env gox -gocmd="${GO}" -output="${CC_OUTPUT_TPL}" -osarch="${CC_BUILD_ARCHES}" ${GOBUILD_FLAGS} ./cmd/url-tool 108 | @echo 109 | 110 | @echo "...creating tar files..." 111 | @(for x in $(subst /,-,${CC_BUILD_ARCHES}); do \ 112 | echo "making tar for ${APP_NAME}.$${x}"; \ 113 | XDIR="${GOVER}.$${x}"; \ 114 | ODIR="${TARBUILDDIR}/$${XDIR}/${APP_NAME}-${APP_VER}"; \ 115 | mkdir -p $${ODIR}/{bin,man}/; \ 116 | cp ${BUILDDIR}/bin/${APP_NAME}.$${x} $${ODIR}/bin/${APP_NAME}; \ 117 | cp ${BUILDDIR}/bin/url-tool.$${x} $${ODIR}/bin/url-tool; \ 118 | cp ${BUILDDIR}/man/*.[1-9] $${ODIR}/man/; \ 119 | tar -C ${TARBUILDDIR}/$${XDIR} -czf ${TARBUILDDIR}/${APP_NAME}-${APP_VER}.$${XDIR}.tar.gz ${APP_NAME}-${APP_VER}; \ 120 | rm -rf "${TARBUILDDIR}/$${XDIR}/"; \ 121 | done) 122 | 123 | @echo "done!" 124 | 125 | release-sign: 126 | @echo "signing release tarballs" 127 | @(cd build/tar; shasum -a 256 go-camo-*.tar.gz > SHA256; \ 128 | signify -S -s ${SIGN_KEY} -m SHA256; \ 129 | sed -i.bak -E 's#^(.*:).*#\1 go-camo-${APP_VER} SHA256#' SHA256.sig; \ 130 | rm -f SHA256.sig.bak; \ 131 | ) 132 | 133 | all: build man 134 | -------------------------------------------------------------------------------- /pkg/camo/encoding/url_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package encoding 6 | 7 | import ( 8 | "fmt" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type enctesto struct { 15 | encoder EncoderFunc 16 | hmac string 17 | edig string 18 | eURL string 19 | sURL string 20 | } 21 | 22 | var enctests = []enctesto{ 23 | // hex 24 | {HexEncodeURL, "test", "0f6def1cb147b0e84f39cbddc5ea10c80253a6f3", 25 | "687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67", 26 | "http://golang.org/doc/gopher/frontpage.png"}, 27 | 28 | // base64 29 | {B64EncodeURL, "test", "D23vHLFHsOhPOcvdxeoQyAJTpvM", 30 | "aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n", 31 | "http://golang.org/doc/gopher/frontpage.png"}, 32 | } 33 | 34 | func TestEncoder(t *testing.T) { 35 | t.Parallel() 36 | for _, p := range enctests { 37 | hmacKey := []byte(p.hmac) 38 | // test specific encoder 39 | encodedURL := p.encoder(hmacKey, p.sURL) 40 | assert.Equal(t, encodedURL, fmt.Sprintf("/%s/%s", p.edig, p.eURL), "encoded url does not match") 41 | } 42 | } 43 | 44 | type dectesto struct { 45 | decoder DecoderFunc 46 | hmac string 47 | edig string 48 | eURL string 49 | sURL string 50 | } 51 | 52 | var dectests = []dectesto{ 53 | // hex 54 | {HexDecodeURL, "test", "0f6def1cb147b0e84f39cbddc5ea10c80253a6f3", 55 | "687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67", 56 | "http://golang.org/doc/gopher/frontpage.png"}, 57 | 58 | // base64 59 | {B64DecodeURL, "test", "D23vHLFHsOhPOcvdxeoQyAJTpvM", 60 | "aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n", 61 | "http://golang.org/doc/gopher/frontpage.png"}, 62 | } 63 | 64 | func TestDecoder(t *testing.T) { 65 | t.Parallel() 66 | for _, p := range dectests { 67 | hmacKey := []byte(p.hmac) 68 | // test specific decoder 69 | encodedURL, err := p.decoder(hmacKey, p.edig, p.eURL) 70 | assert.Nil(t, err, "decoded url failed to verify") 71 | assert.Equal(t, encodedURL, p.sURL, "decoded url does not match") 72 | 73 | // also test generic "guessing" decoder 74 | encodedURL, ok := DecodeURL(hmacKey, p.edig, p.eURL) 75 | assert.True(t, ok, "decoded url failed to verify") 76 | assert.Equal(t, encodedURL, p.sURL, "decoded url does not match") 77 | } 78 | } 79 | 80 | func BenchmarkHexEncoder(b *testing.B) { 81 | for i := 0; i < b.N; i++ { 82 | HexEncodeURL([]byte("test"), "http://golang.org/doc/gopher/frontpage.png") 83 | } 84 | } 85 | 86 | func BenchmarkB64Encoder(b *testing.B) { 87 | for i := 0; i < b.N; i++ { 88 | B64EncodeURL([]byte("test"), "http://golang.org/doc/gopher/frontpage.png") 89 | } 90 | } 91 | 92 | func BenchmarkHexDecoder(b *testing.B) { 93 | for i := 0; i < b.N; i++ { 94 | HexDecodeURL([]byte("test"), "0f6def1cb147b0e84f39cbddc5ea10c80253a6f3", "687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67") 95 | } 96 | } 97 | 98 | func BenchmarkB64Decoder(b *testing.B) { 99 | for i := 0; i < b.N; i++ { 100 | B64DecodeURL([]byte("test"), "D23vHLFHsOhPOcvdxeoQyAJTpvM", "aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n") 101 | } 102 | } 103 | 104 | func BenchmarkGuessingDecoderHex(b *testing.B) { 105 | for i := 0; i < b.N; i++ { 106 | DecodeURL([]byte("test"), "0f6def1cb147b0e84f39cbddc5ea10c80253a6f3", "687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67") 107 | } 108 | } 109 | 110 | func BenchmarkGuessingDecoderB64(b *testing.B) { 111 | for i := 0; i < b.N; i++ { 112 | DecodeURL([]byte("test"), "D23vHLFHsOhPOcvdxeoQyAJTpvM", "aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n") 113 | } 114 | } 115 | 116 | var baddectests = []dectesto{ 117 | // hex 118 | {HexDecodeURL, "test", "000", 119 | "687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67", ""}, 120 | {HexDecodeURL, "test", "0f6def1cb147b0e84f39cbddc5ea10c80253a6f3", 121 | "000000000000000000000000000000000000000000000000000000000000000000000000000000000000", ""}, 122 | 123 | // base64 124 | {B64DecodeURL, "test", "000", 125 | "aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n", ""}, 126 | {B64DecodeURL, "test", "D23vHLFHsOhPOcvdxeoQyAJTpvM", 127 | "00000000000000000000000000000000000000000000000000000000", ""}, 128 | 129 | // mixmatch 130 | // hex 131 | {HexDecodeURL, "test", "0f6def1cb147b0e84f39cbddc5ea10c80253a6f3", 132 | "aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n", 133 | "http://golang.org/doc/gopher/frontpage.png"}, 134 | 135 | // base64 136 | {B64DecodeURL, "test", "D23vHLFHsOhPOcvdxeoQyAJTpvM", 137 | "687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67", 138 | "http://golang.org/doc/gopher/frontpage.png"}, 139 | } 140 | 141 | func TestBadDecodes(t *testing.T) { 142 | t.Parallel() 143 | for _, p := range baddectests { 144 | hmacKey := []byte(p.hmac) 145 | // test specific decoder 146 | encodedURL, err := p.decoder(hmacKey, p.edig, p.eURL) 147 | assert.NotNil(t, err, "decoded url verfied when it shouldn't have") 148 | assert.Equal(t, encodedURL, "", "decoded url result not empty") 149 | 150 | // also test generic "guessing" decoder 151 | encodedURL, ok := DecodeURL(hmacKey, p.edig, p.eURL) 152 | assert.False(t, ok, "decoded url verfied when it shouldn't have") 153 | assert.Equal(t, encodedURL, "", "decoded url result not empty") 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | ## HEAD 5 | * switch to vgo. makes building outside GOPATH easy. 6 | probably breaks heroku builds, as godep is assumed? 7 | 8 | ## v1.1.2 2018-07-30 9 | * fix SSRF leak, where certain requests would not match defined and custom ip 10 | blacklists as expected 11 | 12 | ## v1.1.1 2018-07-18 13 | * change `/healthcheck` response to 200 instead of 204. 14 | solves configuration issue with some loadbalancers. 15 | 16 | ## v1.1.0 2018-07-16 17 | * add flag to allow `video/*` as content type (disabled by default) 18 | * allow setting custom server name 19 | * add flag to expose the current version version in http response header 20 | (similar to how it is done for `-V` cli output) 21 | * change root route to return 404 22 | * add `/healthcheck` route that returns 204 status (no body content) 23 | useful for load balancers to check that service is running 24 | 25 | ## 1.0.18 2018-05-15 26 | * change repo layout and build pipeline to dep/gox/GOPATH style 27 | * lint fixes and minor struct alignment changes (minor optimization) 28 | * update mlog dependency 29 | * build with go-1.10.2 30 | 31 | ## 1.0.17 2018-01-25 32 | * update dependency versions to current 33 | * include deps in tree (ease build for heroku) 34 | * minor makefile cleanup 35 | * rebuild with go-1.9.3 36 | 37 | ## 1.0.16 2017-08-29 38 | * rebuild with go-1.9 39 | 40 | ## 1.0.15 2017-02-18 41 | * rebuild with go-1.8 42 | * strip binaries as part of default build 43 | 44 | ## 1.0.14 2017-02-15 45 | * Pass through ETag header from server. The previous omission was 46 | inconsistent with passing the if-none-match client request header. 47 | 48 | ## 1.0.13 2017-01-22 49 | * resolve potential resource leak with redirection failures and http response 50 | body closing 51 | 52 | ## 1.0.12 2017-01-16 53 | * better address rejection logic 54 | 55 | ## 1.0.11 2017-01-16 56 | * resolve hostname and check against rfc1918 (best effort blocking of dns rebind attacks) 57 | * fix regex match bug with 172.16.0.0/12 addresses (over eager match) 58 | 59 | ## 1.0.10 2017-01-03 60 | * apply a more friendly default content-security-policy 61 | 62 | ## 1.0.9 2016-11-27 63 | * just a rebuild of 1.0.8 with go 1.7.3 64 | 65 | ## 1.0.8 2016-08-20 66 | * update go version support 67 | * build release with go1.7 68 | 69 | ## 1.0.7 2016-04-18 70 | * conver to different logging mechanism (mlog) 71 | * fix a go16 logging issue 72 | * add --no-log-ts command line option 73 | 74 | ## 1.0.6 2016-04-07 75 | * use sync/atomic for internal stats counters 76 | * reorganize some struct memory layout a little 77 | * add -VV license info output 78 | * move simple-server to its own repo 79 | * more performant stats (replaced mutex with sync/atomic) 80 | * fewer spawned goroutines when using stats 81 | 82 | ## 1.0.5 2016-02-18 83 | * Build release with go1.6 84 | * Switch to building with gb 85 | 86 | ## 1.0.4 2015-08-28 87 | * Minor change for go1.5 with proxy timeout 504 88 | 89 | ## 1.0.3 2015-04-25 90 | * revert to stdlib http client 91 | 92 | ## 1.0.2 2015-03-08 93 | * fix issue with http date header generation 94 | 95 | ## 1.0.1 2014-12-16 96 | * optimization for allow-list checks 97 | * keepalive options fix 98 | 99 | ## 1.0.0 2014-06-22 100 | 101 | * minor code organization changes 102 | * fix for heroku build issue with example code 103 | 104 | ## 0.6.0 2014-06-13 105 | 106 | * use simple router instead of gorilla/mux to reduce overhead 107 | and increase performance. 108 | * move some code from camo proxy into the simple router 109 | 110 | ## 0.5.0 2014-06-02 111 | 112 | * some minor changes to Makefile/building 113 | * add support for HTTP HEAD requests 114 | * add support for adding custom default response headers 115 | * return custom headers on 404s as well. 116 | * enable http keepalives on upstream/backends 117 | * add support for disable http keepalives on frontend/backend separately 118 | * upgrade library deps 119 | * various bug fixes 120 | 121 | ## 0.4.0 2014-05-23 122 | 123 | * remove config support (use env or cli flags) 124 | * turn allowlist into a cli flag to parse a plain text file vs json config 125 | * clean ups/general code hygiene 126 | 127 | ## 0.3.0 2014-05-13 128 | 129 | * Transparent base64 url support 130 | 131 | ## 0.2.0 2014-04-17 132 | 133 | * Remove NoFollowRedirects and add MaxRedirects 134 | * Use https://github.com/mreiferson/go-httpclient to handle timeouts more 135 | cleanly 136 | 137 | ## 0.1.3 2013-06-24 138 | 139 | * fix bug in loop prevention 140 | * bump max idle conn count 141 | * keep idle conn trimmer running 142 | 143 | ## 0.1.2 2013-03-30 144 | 145 | * Add ReadTimeout to http.Server, to close excessive keepalive goroutines 146 | 147 | ## 0.1.1 2013-02-27 148 | 149 | * optimize date header generation to use a ticker 150 | * performance improvements 151 | * fix a few subtle race conditions with stats 152 | 153 | ## 0.1.0 2013-01-19 154 | 155 | * Refactor logging a bit 156 | * Move encoding functionality into a submodule to reduce import size (and 157 | thus resultant binary size) for url-tool 158 | * Prevent request loop 159 | * Remove custom Denylist support. Filtering should be done on signed url 160 | generation. rfc1918 filtering retained and internalized so as do reduce 161 | internal network exposue surface and avoid non-routable effort. 162 | * Inverted redirect boolean. Redirects are now followed by default, and 163 | the flag `no-follow` was learned. 164 | * Use new flag parsing library for nicer help and cleaner usage. 165 | * Specify a fallback accept header if none is provided by client. 166 | 167 | ## 0.0.4 2012-09-02 168 | 169 | * Refactor Stats code out of camoproxy 170 | * Make stats an optional flag in go-camo 171 | * Minor documentation cleanup 172 | * Clean up excessive logging on client initiated broken pipe 173 | 174 | ## 0.0.3 2012-08-05 175 | 176 | * organize and clean up code 177 | * make header filters exported 178 | * start filtering response headers 179 | * add default Server name 180 | * fix bug dealing with header filtering logic 181 | * add cli utility to encode/decode urls for testing, etc. 182 | * change repo layout to be friendlier for Go development/building 183 | * timeout flag is now a duration (15s, 10m, 1h, etc) 184 | * X-Forwarded-For support 185 | * Added more info to readme 186 | 187 | 188 | ## 0.0.2 2012-07-12 189 | 190 | * documentation cleanup 191 | * code reorganization 192 | * some cleanup of command flag text 193 | * logging code simplification 194 | 195 | 196 | ## 0.0.1 2012-07-07 197 | 198 | * initial release 199 | -------------------------------------------------------------------------------- /cmd/go-camo/main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // go-camo daemon (go-camod) 6 | package main 7 | 8 | //go:generate vgo run ../../tools/genversion.go -pkg $GOPACKAGE -input ../../Gopkg.toml -output main_vers_gen.go 9 | 10 | import ( 11 | "fmt" 12 | "io/ioutil" 13 | "net/http" 14 | "os" 15 | "runtime" 16 | "strconv" 17 | "strings" 18 | "time" 19 | 20 | "github.com/cactus/go-camo/pkg/camo" 21 | "github.com/cactus/go-camo/pkg/router" 22 | "github.com/cactus/go-camo/pkg/stats" 23 | 24 | "github.com/cactus/mlog" 25 | flags "github.com/jessevdk/go-flags" 26 | ) 27 | 28 | var ( 29 | // ServerVersion holds the server version string 30 | ServerVersion = "no-version" 31 | ) 32 | 33 | func main() { 34 | var gmx int 35 | if gmxEnv := os.Getenv("GOMAXPROCS"); gmxEnv != "" { 36 | gmx, _ = strconv.Atoi(gmxEnv) 37 | } else { 38 | gmx = runtime.NumCPU() 39 | } 40 | runtime.GOMAXPROCS(gmx) 41 | 42 | // command line flags 43 | var opts struct { 44 | Version []bool `short:"V" long:"version" description:"Print version and exit; specify twice to show license information"` 45 | AddHeaders []string `short:"H" long:"header" description:"Extra header to return for each response. This option can be used multiple times to add multiple headers"` 46 | HMACKey string `short:"k" long:"key" description:"HMAC key"` 47 | SSLKey string `long:"ssl-key" description:"ssl private key (key.pem) path"` 48 | SSLCert string `long:"ssl-cert" description:"ssl cert (cert.pem) path"` 49 | AllowList string `long:"allow-list" description:"Text file of hostname allow regexes (one per line)"` 50 | BindAddress string `long:"listen" default:"0.0.0.0:8080" description:"Address:Port to bind to for HTTP"` 51 | BindAddressSSL string `long:"ssl-listen" description:"Address:Port to bind to for HTTPS/SSL/TLS"` 52 | MaxSize int64 `long:"max-size" default:"5120" description:"Max allowed response size (KB)"` 53 | ReqTimeout time.Duration `long:"timeout" default:"4s" description:"Upstream request timeout"` 54 | MaxRedirects int `long:"max-redirects" default:"3" description:"Maximum number of redirects to follow"` 55 | Stats bool `long:"stats" description:"Enable Stats"` 56 | NoLogTS bool `long:"no-log-ts" description:"Do not add a timestamp to logging"` 57 | DisableKeepAlivesFE bool `long:"no-fk" description:"Disable frontend http keep-alive support"` 58 | DisableKeepAlivesBE bool `long:"no-bk" description:"Disable backend http keep-alive support"` 59 | AllowContentVideo bool `long:"allow-content-video" description:"Additionally allow 'video/*' content"` 60 | Verbose bool `short:"v" long:"verbose" description:"Show verbose (debug) log level output"` 61 | ServerName string `long:"server-name" default:"go-camo" description:"Value to use for the HTTP server field"` 62 | ExposeServerVersion bool `long:"expose-server-version" description:"Include the server version in the HTTP server response header"` 63 | } 64 | 65 | // parse said flags 66 | _, err := flags.Parse(&opts) 67 | if err != nil { 68 | if e, ok := err.(*flags.Error); ok { 69 | if e.Type == flags.ErrHelp { 70 | os.Exit(0) 71 | } 72 | } 73 | os.Exit(1) 74 | } 75 | 76 | // set the server name 77 | ServerName := opts.ServerName 78 | 79 | // setup the server response field 80 | ServerResponse := opts.ServerName 81 | 82 | if opts.ExposeServerVersion { 83 | ServerResponse = fmt.Sprintf("%s %s", opts.ServerName, ServerVersion) 84 | } 85 | 86 | // setup -V version output 87 | if len(opts.Version) > 0 { 88 | fmt.Printf("%s %s (%s,%s-%s)\n", ServerName, ServerVersion, runtime.Version(), runtime.Compiler, runtime.GOARCH) 89 | if len(opts.Version) > 1 { 90 | fmt.Printf("\n%s\n", strings.TrimSpace(licenseText)) 91 | } 92 | os.Exit(0) 93 | } 94 | 95 | // start out with a very bare logger that only prints 96 | // the message (no special format or log elements) 97 | mlog.SetFlags(0) 98 | 99 | config := camo.Config{} 100 | if hmacKey := os.Getenv("GOCAMO_HMAC"); hmacKey != "" { 101 | config.HMACKey = []byte(hmacKey) 102 | } 103 | 104 | // flags override env var 105 | if opts.HMACKey != "" { 106 | config.HMACKey = []byte(opts.HMACKey) 107 | } 108 | 109 | if len(config.HMACKey) == 0 { 110 | mlog.Fatal("HMAC key required") 111 | } 112 | 113 | if opts.BindAddress == "" && opts.BindAddressSSL == "" { 114 | mlog.Fatal("One of listen or ssl-listen required") 115 | } 116 | 117 | if opts.BindAddressSSL != "" && opts.SSLKey == "" { 118 | mlog.Fatal("ssl-key is required when specifying ssl-listen") 119 | } 120 | if opts.BindAddressSSL != "" && opts.SSLCert == "" { 121 | mlog.Fatal("ssl-cert is required when specifying ssl-listen") 122 | } 123 | 124 | // set keepalive options 125 | config.DisableKeepAlivesBE = opts.DisableKeepAlivesBE 126 | config.DisableKeepAlivesFE = opts.DisableKeepAlivesFE 127 | 128 | // additonal content types to allow 129 | config.AllowContentVideo = opts.AllowContentVideo 130 | 131 | if opts.AllowList != "" { 132 | b, err := ioutil.ReadFile(opts.AllowList) 133 | if err != nil { 134 | mlog.Fatal("Could not read allow-list", err) 135 | } 136 | config.AllowList = strings.Split(string(b), "\n") 137 | } 138 | 139 | AddHeaders := map[string]string{ 140 | "X-Content-Type-Options": "nosniff", 141 | "X-XSS-Protection": "1; mode=block", 142 | "Content-Security-Policy": "default-src 'none'; img-src data:; style-src 'unsafe-inline'", 143 | } 144 | 145 | for _, v := range opts.AddHeaders { 146 | s := strings.SplitN(v, ":", 2) 147 | if len(s) != 2 { 148 | mlog.Printf("ignoring bad header: '%s'", v) 149 | continue 150 | } 151 | 152 | s0 := strings.TrimSpace(s[0]) 153 | s1 := strings.TrimSpace(s[1]) 154 | 155 | if len(s0) == 0 || len(s1) == 0 { 156 | mlog.Printf("ignoring bad header: '%s'", v) 157 | continue 158 | } 159 | AddHeaders[s[0]] = s[1] 160 | } 161 | 162 | // now configure a standard logger 163 | mlog.SetFlags(mlog.Lstd) 164 | if opts.NoLogTS { 165 | mlog.SetFlags(mlog.Flags() ^ mlog.Ltimestamp) 166 | } 167 | 168 | if opts.Verbose { 169 | mlog.SetFlags(mlog.Flags() | mlog.Ldebug) 170 | mlog.Debug("debug logging enabled") 171 | } 172 | 173 | // convert from KB to Bytes 174 | config.MaxSize = opts.MaxSize * 1024 175 | config.RequestTimeout = opts.ReqTimeout 176 | config.MaxRedirects = opts.MaxRedirects 177 | config.ServerName = ServerName 178 | 179 | proxy, err := camo.New(config) 180 | if err != nil { 181 | mlog.Fatal("Error creating camo", err) 182 | } 183 | 184 | dumbrouter := &router.DumbRouter{ 185 | ServerName: ServerResponse, 186 | AddHeaders: AddHeaders, 187 | CamoHandler: proxy, 188 | } 189 | 190 | if opts.Stats { 191 | ps := &stats.ProxyStats{} 192 | proxy.SetMetricsCollector(ps) 193 | mlog.Printf("Enabling stats at /status") 194 | dumbrouter.StatsHandler = stats.Handler(ps) 195 | } 196 | 197 | http.Handle("/", dumbrouter) 198 | 199 | if opts.BindAddress != "" { 200 | mlog.Printf("Starting server on: %s", opts.BindAddress) 201 | go func() { 202 | srv := &http.Server{ 203 | Addr: opts.BindAddress, 204 | ReadTimeout: 30 * time.Second} 205 | mlog.Fatal(srv.ListenAndServe()) 206 | }() 207 | } 208 | if opts.BindAddressSSL != "" { 209 | mlog.Printf("Starting TLS server on: %s", opts.BindAddressSSL) 210 | go func() { 211 | srv := &http.Server{ 212 | Addr: opts.BindAddressSSL, 213 | ReadTimeout: 30 * time.Second} 214 | mlog.Fatal(srv.ListenAndServeTLS(opts.SSLCert, opts.SSLKey)) 215 | }() 216 | } 217 | 218 | // just block. listen and serve will exit the program if they fail/return 219 | // so we just need to block to prevent main from exiting. 220 | select {} 221 | } 222 | -------------------------------------------------------------------------------- /pkg/camo/proxy_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | package camo 6 | 7 | import ( 8 | "fmt" 9 | "net/http" 10 | "net/http/httptest" 11 | "testing" 12 | "time" 13 | 14 | "github.com/cactus/go-camo/pkg/camo/encoding" 15 | "github.com/cactus/go-camo/pkg/router" 16 | 17 | "github.com/stretchr/testify/assert" 18 | ) 19 | 20 | var camoConfig = Config{ 21 | HMACKey: []byte("0x24FEEDFACEDEADBEEFCAFE"), 22 | MaxSize: 5120 * 1024, 23 | RequestTimeout: time.Duration(10) * time.Second, 24 | MaxRedirects: 3, 25 | ServerName: "go-camo", 26 | AllowContentVideo: false, 27 | } 28 | 29 | func makeReq(testURL string) (*http.Request, error) { 30 | k := []byte(camoConfig.HMACKey) 31 | hexURL := encoding.B64EncodeURL(k, testURL) 32 | out := "http://example.com" + hexURL 33 | req, err := http.NewRequest("GET", out, nil) 34 | if err != nil { 35 | return nil, fmt.Errorf("Error building req url '%s': %s", testURL, err.Error()) 36 | } 37 | return req, nil 38 | } 39 | 40 | func processRequest(req *http.Request, status int, camoConfig Config) (*httptest.ResponseRecorder, error) { 41 | camoServer, err := New(camoConfig) 42 | if err != nil { 43 | return nil, fmt.Errorf("Error building Camo: %s", err.Error()) 44 | } 45 | 46 | router := &router.DumbRouter{ 47 | AddHeaders: map[string]string{"X-Go-Camo": "test"}, 48 | ServerName: camoConfig.ServerName, 49 | CamoHandler: camoServer, 50 | } 51 | 52 | record := httptest.NewRecorder() 53 | router.ServeHTTP(record, req) 54 | if got, want := record.Code, status; got != want { 55 | return record, fmt.Errorf("response code = %d, wanted %d", got, want) 56 | } 57 | return record, nil 58 | } 59 | 60 | func makeTestReq(testURL string, status int, config Config) (*httptest.ResponseRecorder, error) { 61 | req, err := makeReq(testURL) 62 | if err != nil { 63 | return nil, err 64 | } 65 | record, err := processRequest(req, status, config) 66 | if err != nil { 67 | return record, err 68 | } 69 | return record, nil 70 | } 71 | 72 | func TestNotFound(t *testing.T) { 73 | t.Parallel() 74 | req, err := http.NewRequest("GET", "http://example.com/favicon.ico", nil) 75 | assert.Nil(t, err) 76 | 77 | record, err := processRequest(req, 404, camoConfig) 78 | if assert.Nil(t, err) { 79 | assert.Equal(t, 404, record.Code, "Expected 404 but got '%d' instead", record.Code) 80 | assert.Equal(t, "404 Not Found\n", record.Body.String(), "Expected 404 response body but got '%s' instead", record.Body.String()) 81 | // validate headers 82 | assert.Equal(t, "test", record.HeaderMap.Get("X-Go-Camo"), "Expected custom response header not found") 83 | assert.Equal(t, "go-camo", record.HeaderMap.Get("Server"), "Expected 'Server' response header not found") 84 | } 85 | } 86 | 87 | func TestSimpleValidImageURL(t *testing.T) { 88 | t.Parallel() 89 | testURL := "http://www.google.com/images/srpr/logo11w.png" 90 | record, err := makeTestReq(testURL, 200, camoConfig) 91 | if assert.Nil(t, err) { 92 | // validate headers 93 | assert.Equal(t, "test", record.HeaderMap.Get("X-Go-Camo"), "Expected custom response header not found") 94 | assert.Equal(t, "go-camo", record.HeaderMap.Get("Server"), "Expected 'Server' response header not found") 95 | } 96 | } 97 | 98 | func TestGoogleChartURL(t *testing.T) { 99 | t.Parallel() 100 | testURL := "http://chart.apis.google.com/chart?chs=920x200&chxl=0:%7C2010-08-13%7C2010-09-12%7C2010-10-12%7C2010-11-11%7C1:%7C0%7C0%7C0%7C0%7C0%7C0&chm=B,EBF5FB,0,0,0&chco=008Cd6&chls=3,1,0&chg=8.3,20,1,4&chd=s:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA&chxt=x,y&cht=lc" 101 | _, err := makeTestReq(testURL, 200, camoConfig) 102 | assert.Nil(t, err) 103 | } 104 | 105 | func TestChunkedImageFile(t *testing.T) { 106 | t.Parallel() 107 | testURL := "https://www.igvita.com/posts/12/spdyproxy-diagram.png" 108 | _, err := makeTestReq(testURL, 200, camoConfig) 109 | assert.Nil(t, err) 110 | } 111 | 112 | func TestFollowRedirects(t *testing.T) { 113 | t.Parallel() 114 | testURL := "http://cl.ly/1K0X2Y2F1P0o3z140p0d/boom-headshot.gif" 115 | _, err := makeTestReq(testURL, 200, camoConfig) 116 | assert.Nil(t, err) 117 | } 118 | 119 | func TestStrangeFormatRedirects(t *testing.T) { 120 | t.Parallel() 121 | testURL := "http://cl.ly/DPcp/Screen%20Shot%202012-01-17%20at%203.42.32%20PM.png" 122 | _, err := makeTestReq(testURL, 200, camoConfig) 123 | assert.Nil(t, err) 124 | } 125 | 126 | func TestRedirectsWithPathOnly(t *testing.T) { 127 | t.Parallel() 128 | testURL := "http://httpbin.org/redirect-to?url=%2Fredirect-to%3Furl%3Dhttp%3A%2F%2Fwww.google.com%2Fimages%2Fsrpr%2Flogo11w.png" 129 | _, err := makeTestReq(testURL, 200, camoConfig) 130 | assert.Nil(t, err) 131 | } 132 | 133 | func TestFollowTempRedirects(t *testing.T) { 134 | t.Parallel() 135 | testURL := "http://httpbin.org/redirect-to?url=http://www.google.com/images/srpr/logo11w.png" 136 | _, err := makeTestReq(testURL, 200, camoConfig) 137 | assert.Nil(t, err) 138 | } 139 | 140 | func TestBadContentType(t *testing.T) { 141 | t.Parallel() 142 | testURL := "http://httpbin.org/response-headers?Content-Type=what" 143 | _, err := makeTestReq(testURL, 400, camoConfig) 144 | assert.Nil(t, err) 145 | } 146 | 147 | func TestVideoContentTypeAllowed(t *testing.T) { 148 | t.Parallel() 149 | 150 | camoConfigWithVideo := Config{ 151 | HMACKey: []byte("0x24FEEDFACEDEADBEEFCAFE"), 152 | MaxSize: 180 * 1024, 153 | RequestTimeout: time.Duration(10) * time.Second, 154 | MaxRedirects: 3, 155 | ServerName: "go-camo", 156 | AllowContentVideo: true, 157 | } 158 | 159 | testURL := "http://mirrors.standaloneinstaller.com/video-sample/small.mp4" 160 | _, err := makeTestReq(testURL, 200, camoConfigWithVideo) 161 | assert.Nil(t, err) 162 | } 163 | 164 | func Test404InfiniRedirect(t *testing.T) { 165 | t.Parallel() 166 | testURL := "http://httpbin.org/redirect/4" 167 | _, err := makeTestReq(testURL, 404, camoConfig) 168 | assert.Nil(t, err) 169 | } 170 | 171 | func Test404URLWithoutHTTPHost(t *testing.T) { 172 | t.Parallel() 173 | testURL := "/picture/Mincemeat/Pimp.jpg" 174 | _, err := makeTestReq(testURL, 404, camoConfig) 175 | assert.Nil(t, err) 176 | } 177 | 178 | func Test404ImageLargerThan5MB(t *testing.T) { 179 | t.Parallel() 180 | testURL := "http://apod.nasa.gov/apod/image/0505/larryslookout_spirit_big.jpg" 181 | _, err := makeTestReq(testURL, 404, camoConfig) 182 | assert.Nil(t, err) 183 | } 184 | 185 | func Test404HostNotFound(t *testing.T) { 186 | t.Parallel() 187 | testURL := "http://flabergasted.cx" 188 | _, err := makeTestReq(testURL, 404, camoConfig) 189 | assert.Nil(t, err) 190 | } 191 | 192 | func Test404OnExcludes(t *testing.T) { 193 | t.Parallel() 194 | testURL := "http://iphone.internal.example.org/foo.cgi" 195 | _, err := makeTestReq(testURL, 404, camoConfig) 196 | assert.Nil(t, err) 197 | } 198 | 199 | func Test404OnNonImageContent(t *testing.T) { 200 | t.Parallel() 201 | testURL := "https://github.com/atmos/cinderella/raw/master/bootstrap.sh" 202 | _, err := makeTestReq(testURL, 404, camoConfig) 203 | assert.Nil(t, err) 204 | } 205 | 206 | func Test404On10xIpRange(t *testing.T) { 207 | t.Parallel() 208 | testURL := "http://10.0.0.1/foo.cgi" 209 | _, err := makeTestReq(testURL, 404, camoConfig) 210 | assert.Nil(t, err) 211 | } 212 | 213 | func Test404On169Dot254Net(t *testing.T) { 214 | t.Parallel() 215 | testURL := "http://169.254.0.1/foo.cgi" 216 | _, err := makeTestReq(testURL, 404, camoConfig) 217 | assert.Nil(t, err) 218 | } 219 | 220 | func Test404On172Dot16Net(t *testing.T) { 221 | t.Parallel() 222 | for i := 16; i < 32; i++ { 223 | testURL := "http://172.%d.0.1/foo.cgi" 224 | _, err := makeTestReq(fmt.Sprintf(testURL, i), 404, camoConfig) 225 | assert.Nil(t, err) 226 | } 227 | } 228 | 229 | func Test404On192Dot168Net(t *testing.T) { 230 | t.Parallel() 231 | testURL := "http://192.168.0.1/foo.cgi" 232 | _, err := makeTestReq(testURL, 404, camoConfig) 233 | assert.Nil(t, err) 234 | } 235 | 236 | func Test404OnLocalhost(t *testing.T) { 237 | t.Parallel() 238 | testURL := "http://localhost/foo.cgi" 239 | record, err := makeTestReq(testURL, 404, camoConfig) 240 | if assert.Nil(t, err) { 241 | assert.Equal(t, "Bad url host\n", record.Body.String(), "Expected 404 response body but got '%s' instead", record.Body.String()) 242 | } 243 | } 244 | 245 | func Test404OnLocalhostWithPort(t *testing.T) { 246 | t.Parallel() 247 | testURL := "http://localhost:80/foo.cgi" 248 | record, err := makeTestReq(testURL, 404, camoConfig) 249 | if assert.Nil(t, err) { 250 | assert.Equal(t, "Bad url host\n", record.Body.String(), "Expected 404 response body but got '%s' instead", record.Body.String()) 251 | } 252 | } 253 | 254 | // Test will fail if dns relay implements dns rebind prevention 255 | func Test404OnLoopback(t *testing.T) { 256 | t.Parallel() 257 | testURL := "http://i.i.com.com/foo.cgi" 258 | record, err := makeTestReq(testURL, 404, camoConfig) 259 | if assert.Nil(t, err) { 260 | assert.Equal(t, "Denylist host failure\n", record.Body.String(), "Expected 404 response body but got '%s' instead", record.Body.String()) 261 | } 262 | } 263 | 264 | func TestSupplyAcceptIfNoneGiven(t *testing.T) { 265 | t.Parallel() 266 | testURL := "http://images.anandtech.com/doci/6673/OpenMoboAMD30_575px.png" 267 | req, err := makeReq(testURL) 268 | req.Header.Del("Accept") 269 | assert.Nil(t, err) 270 | _, err = processRequest(req, 200, camoConfig) 271 | assert.Nil(t, err) 272 | } 273 | 274 | func TestTimeout(t *testing.T) { 275 | t.Parallel() 276 | c := Config{ 277 | HMACKey: []byte("0x24FEEDFACEDEADBEEFCAFE"), 278 | MaxSize: 5120 * 1024, 279 | RequestTimeout: time.Duration(500) * time.Millisecond, 280 | MaxRedirects: 3, 281 | ServerName: "go-camo", 282 | noIPFiltering: true, 283 | } 284 | cc := make(chan bool, 1) 285 | received := make(chan bool) 286 | ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 287 | received <- true 288 | <-cc 289 | r.Close = true 290 | w.Write([]byte("ok")) 291 | 292 | })) 293 | defer ts.Close() 294 | 295 | req, err := makeReq(ts.URL) 296 | assert.Nil(t, err) 297 | 298 | errc := make(chan error, 1) 299 | go func() { 300 | code := 504 301 | _, err := processRequest(req, code, c) 302 | errc <- err 303 | }() 304 | 305 | select { 306 | case <-received: 307 | select { 308 | case e := <-errc: 309 | assert.Nil(t, e) 310 | cc <- true 311 | case <-time.After(1 * time.Second): 312 | cc <- true 313 | t.Errorf("timeout didn't fire in time") 314 | } 315 | case <-time.After(1 * time.Second): 316 | var err error 317 | select { 318 | case e := <-errc: 319 | err = e 320 | default: 321 | } 322 | if err != nil { 323 | assert.Nil(t, err, "test didn't hit backend as expected") 324 | } 325 | t.Errorf("test didn't hit backend as expected") 326 | } 327 | 328 | close(cc) 329 | } 330 | -------------------------------------------------------------------------------- /pkg/camo/proxy.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2012-2018 Eli Janssen 2 | // Use of this source code is governed by an MIT-style 3 | // license that can be found in the LICENSE file. 4 | 5 | // Package camo provides an HTTP proxy server with content type 6 | // restrictions as well as regex host allow list support. 7 | package camo 8 | 9 | import ( 10 | "errors" 11 | "io" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "regexp" 16 | "strings" 17 | "time" 18 | 19 | "github.com/cactus/go-camo/pkg/camo/encoding" 20 | 21 | "github.com/cactus/mlog" 22 | ) 23 | 24 | // Config holds configuration data used when creating a Proxy with New. 25 | type Config struct { 26 | // HMACKey is a byte slice to be used as the hmac key 27 | HMACKey []byte 28 | // AllowList is a list of string represenstations of regex (not compiled 29 | // regex) that are used as a whitelist filter. If an AllowList is present, 30 | // then anything not matching is dropped. If no AllowList is present, 31 | // no Allow filtering is done. 32 | AllowList []string 33 | // Server name used in Headers and Via checks 34 | ServerName string 35 | // MaxSize is the maximum valid image size response (in bytes). 36 | MaxSize int64 37 | // MaxRedirects is the maximum number of redirects to follow. 38 | MaxRedirects int 39 | // Request timeout is a timeout for fetching upstream data. 40 | RequestTimeout time.Duration 41 | // Keepalive enable/disable 42 | DisableKeepAlivesFE bool 43 | DisableKeepAlivesBE bool 44 | // additional content types to allow 45 | AllowContentVideo bool 46 | // no ip filtering (test mode) 47 | noIPFiltering bool 48 | } 49 | 50 | // ProxyMetrics interface for Proxy to use for stats/metrics. 51 | // This must be goroutine safe, as AddBytes and AddServed will be called from 52 | // many goroutines. 53 | type ProxyMetrics interface { 54 | AddBytes(bc int64) 55 | AddServed() 56 | } 57 | 58 | // A Proxy is a Camo like HTTP proxy, that provides content type 59 | // restrictions as well as regex host allow list support. 60 | type Proxy struct { 61 | // compiled allow list regex 62 | allowList []*regexp.Regexp 63 | acceptTypesRe []*regexp.Regexp 64 | metrics ProxyMetrics 65 | client *http.Client 66 | config *Config 67 | acceptTypesString string 68 | } 69 | 70 | // ServerHTTP handles the client request, validates the request is validly 71 | // HMAC signed, filters based on the Allow list, and then proxies 72 | // valid requests to the desired endpoint. Responses are filtered for 73 | // proper image content types. 74 | func (p *Proxy) ServeHTTP(w http.ResponseWriter, req *http.Request) { 75 | if p.metrics != nil { 76 | p.metrics.AddServed() 77 | } 78 | 79 | if p.config.DisableKeepAlivesFE { 80 | w.Header().Set("Connection", "close") 81 | } 82 | 83 | if req.Header.Get("Via") == p.config.ServerName { 84 | http.Error(w, "Request loop failure", http.StatusNotFound) 85 | return 86 | } 87 | 88 | // split path and get components 89 | components := strings.Split(req.URL.Path, "/") 90 | if len(components) < 3 { 91 | http.Error(w, "Malformed request path", http.StatusNotFound) 92 | return 93 | } 94 | sigHash, encodedURL := components[1], components[2] 95 | 96 | mlog.Debugm("client request", mlog.Map{"req": req}) 97 | 98 | sURL, ok := encoding.DecodeURL(p.config.HMACKey, sigHash, encodedURL) 99 | if !ok { 100 | http.Error(w, "Bad Signature", http.StatusForbidden) 101 | return 102 | } 103 | 104 | mlog.Debugm("signed client url", mlog.Map{"url": sURL}) 105 | 106 | u, err := url.Parse(sURL) 107 | if err != nil { 108 | mlog.Debugm("url parse error", mlog.Map{"err": err}) 109 | http.Error(w, "Bad url", http.StatusBadRequest) 110 | return 111 | } 112 | 113 | uHostname := strings.ToLower(u.Hostname()) 114 | if uHostname == "" || localhostRegex.MatchString(uHostname) { 115 | http.Error(w, "Bad url host", http.StatusNotFound) 116 | return 117 | } 118 | 119 | // filtering 120 | if !p.config.noIPFiltering { 121 | // if allowList is set, require match 122 | for _, rgx := range p.allowList { 123 | if rgx.MatchString(uHostname) { 124 | http.Error(w, "Allowlist host failure", http.StatusNotFound) 125 | return 126 | } 127 | } 128 | 129 | // filter out rejected networks 130 | if ip := net.ParseIP(uHostname); ip != nil { 131 | if isRejectedIP(ip) { 132 | http.Error(w, "Denylist host failure", http.StatusNotFound) 133 | return 134 | } 135 | } else { 136 | if ips, err := net.LookupIP(uHostname); err == nil { 137 | for _, ip := range ips { 138 | if isRejectedIP(ip) { 139 | http.Error(w, "Denylist host failure", http.StatusNotFound) 140 | return 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | nreq, err := http.NewRequest(req.Method, sURL, nil) 148 | if err != nil { 149 | mlog.Debugm("could not create NewRequest", mlog.Map{"err": err}) 150 | http.Error(w, "Error Fetching Resource", http.StatusBadGateway) 151 | return 152 | } 153 | 154 | // filter headers 155 | p.copyHeader(&nreq.Header, &req.Header, &ValidReqHeaders) 156 | if req.Header.Get("X-Forwarded-For") == "" { 157 | hostIp, _, err := net.SplitHostPort(req.RemoteAddr) 158 | if err == nil { 159 | // add forwarded for header, as long as it isn't a private 160 | // ip address (use isRejectedIP to get private filtering for free) 161 | if ip := net.ParseIP(hostIp); ip != nil { 162 | if !isRejectedIP(ip) { 163 | nreq.Header.Add("X-Forwarded-For", hostIp) 164 | } 165 | } 166 | } 167 | } 168 | 169 | // add/squash an accept header if the client didn't send one 170 | nreq.Header.Set("Accept", p.acceptTypesString) 171 | 172 | nreq.Header.Add("User-Agent", p.config.ServerName) 173 | nreq.Header.Add("Via", p.config.ServerName) 174 | 175 | mlog.Debugm("built outgoing request", mlog.Map{"req": nreq}) 176 | 177 | resp, err := p.client.Do(nreq) 178 | 179 | if resp != nil { 180 | defer resp.Body.Close() 181 | } 182 | 183 | if err != nil { 184 | mlog.Debugm("could not connect to endpoint", mlog.Map{"err": err}) 185 | // this is a bit janky, but better than peeling off the 186 | // 3 layers of wrapped errors and trying to get to net.OpErr and 187 | // still having to rely on string comparison to find out if it is 188 | // a net.errClosing or not. 189 | errString := err.Error() 190 | // go 1.5 changes this to http.httpError 191 | // go 1.4 has this as net.OpError 192 | // and the error strings are different depending on which version too. 193 | if strings.Contains(errString, "timeout") || strings.Contains(errString, "Client.Timeout") { 194 | http.Error(w, "Error Fetching Resource", http.StatusGatewayTimeout) 195 | } else if strings.Contains(errString, "use of closed") { 196 | http.Error(w, "Error Fetching Resource", http.StatusBadGateway) 197 | } else { 198 | // some other error. call it a not found (camo compliant) 199 | http.Error(w, "Error Fetching Resource", http.StatusNotFound) 200 | } 201 | return 202 | } 203 | 204 | mlog.Debugm("response from upstream", mlog.Map{"resp": resp}) 205 | 206 | // check for too large a response 207 | if resp.ContentLength > p.config.MaxSize { 208 | mlog.Debugm("content length exceeded", mlog.Map{"url": sURL}) 209 | http.Error(w, "Content length exceeded", http.StatusNotFound) 210 | return 211 | } 212 | 213 | switch resp.StatusCode { 214 | case 200: 215 | // check content type 216 | match := false 217 | for _, re := range p.acceptTypesRe { 218 | if re.MatchString(resp.Header.Get("Content-Type")) { 219 | match = true 220 | break 221 | } 222 | } 223 | if !match { 224 | mlog.Debugm("Unsupported content-type returned", mlog.Map{"type": u}) 225 | http.Error(w, "Unsupported content-type returned", http.StatusBadRequest) 226 | return 227 | } 228 | case 300: 229 | http.Error(w, "Multiple choices not supported", http.StatusNotFound) 230 | return 231 | case 301, 302, 303, 307: 232 | // if we get a redirect here, we either disabled following, 233 | // or followed until max depth and still got one (redirect loop) 234 | http.Error(w, "Not Found", http.StatusNotFound) 235 | return 236 | case 304: 237 | h := w.Header() 238 | p.copyHeader(&h, &resp.Header, &ValidRespHeaders) 239 | w.WriteHeader(304) 240 | return 241 | case 404: 242 | http.Error(w, "Not Found", http.StatusNotFound) 243 | return 244 | case 500, 502, 503, 504: 245 | // upstream errors should probably just 502. client can try later. 246 | http.Error(w, "Error Fetching Resource", http.StatusBadGateway) 247 | return 248 | default: 249 | http.Error(w, "Not Found", http.StatusNotFound) 250 | return 251 | } 252 | 253 | h := w.Header() 254 | p.copyHeader(&h, &resp.Header, &ValidRespHeaders) 255 | w.WriteHeader(resp.StatusCode) 256 | 257 | // since this uses io.Copy from the respBody, it is streaming 258 | // from the request to the response. This means it will nearly 259 | // always end up with a chunked response. 260 | bW, err := io.Copy(w, resp.Body) 261 | if err != nil { 262 | // only log broken pipe errors at debug level 263 | if isBrokenPipe(err) { 264 | mlog.Debugm("error writing response", mlog.Map{"err": err}) 265 | } else { 266 | // unknown error and not a broken pipe 267 | mlog.Printm("error writing response", mlog.Map{"err": err}) 268 | } 269 | 270 | // we may have written some bytes before the error 271 | if p.metrics != nil && bW != 0 { 272 | p.metrics.AddBytes(bW) 273 | } 274 | return 275 | } 276 | 277 | if p.metrics != nil { 278 | p.metrics.AddBytes(bW) 279 | } 280 | mlog.Debugm("response to client", mlog.Map{"resp": w}) 281 | } 282 | 283 | // copy headers from src into dst 284 | // empty filter map will result in no filtering being done 285 | func (p *Proxy) copyHeader(dst, src *http.Header, filter *map[string]bool) { 286 | f := *filter 287 | filtering := false 288 | if len(f) > 0 { 289 | filtering = true 290 | } 291 | 292 | for k, vv := range *src { 293 | if x, ok := f[k]; filtering && (!ok || !x) { 294 | continue 295 | } 296 | for _, v := range vv { 297 | dst.Add(k, v) 298 | } 299 | } 300 | } 301 | 302 | // SetMetricsCollector sets a proxy metrics (ProxyMetrics interface) for 303 | // the proxy 304 | func (p *Proxy) SetMetricsCollector(pm ProxyMetrics) { 305 | p.metrics = pm 306 | } 307 | 308 | // New returns a new Proxy. An error is returned if there was a failure 309 | // to parse the regex from the passed Config. 310 | func New(pc Config) (*Proxy, error) { 311 | tr := &http.Transport{ 312 | Dial: (&net.Dialer{ 313 | Timeout: 3 * time.Second, 314 | KeepAlive: 30 * time.Second, 315 | }).Dial, 316 | TLSHandshakeTimeout: 3 * time.Second, 317 | MaxIdleConnsPerHost: 8, 318 | DisableKeepAlives: pc.DisableKeepAlivesBE, 319 | // no need for compression with images 320 | // some xml/svg can be compressed, but apparently some clients can 321 | // exhibit weird behavior when those are compressed 322 | DisableCompression: true, 323 | } 324 | 325 | // spawn an idle conn trimmer 326 | go func() { 327 | // prunes every 5 minutes. this is just a guess at an 328 | // initial value. very busy severs may want to lower this... 329 | for { 330 | time.Sleep(5 * time.Minute) 331 | tr.CloseIdleConnections() 332 | } 333 | }() 334 | 335 | client := &http.Client{ 336 | Transport: tr, 337 | // timeout 338 | Timeout: pc.RequestTimeout, 339 | } 340 | client.CheckRedirect = func(req *http.Request, via []*http.Request) error { 341 | if len(via) >= pc.MaxRedirects { 342 | return errors.New("Too many redirects") 343 | } 344 | return nil 345 | } 346 | 347 | var allow []*regexp.Regexp 348 | var c *regexp.Regexp 349 | var err error 350 | // compile allow list 351 | for _, v := range pc.AllowList { 352 | c, err = regexp.Compile(strings.TrimSpace(v)) 353 | if err != nil { 354 | return nil, err 355 | } 356 | allow = append(allow, c) 357 | } 358 | 359 | acceptTypes := []string{"image/*"} 360 | // add additional accept types 361 | if pc.AllowContentVideo { 362 | acceptTypes = append(acceptTypes, "video/*") 363 | } 364 | 365 | var acceptTypesRe []*regexp.Regexp 366 | for _, v := range acceptTypes { 367 | c, err = globToRegexp(v) 368 | if err != nil { 369 | return nil, err 370 | } 371 | acceptTypesRe = append(acceptTypesRe, c) 372 | } 373 | 374 | return &Proxy{ 375 | client: client, 376 | config: &pc, 377 | allowList: allow, 378 | acceptTypesString: strings.Join(acceptTypes, ", "), 379 | acceptTypesRe: acceptTypesRe}, nil 380 | 381 | } 382 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-camo 2 | ======= 3 | 4 | [![Current Release](https://img.shields.io/github/release/cactus/go-camo.svg)](http://github.com/cactus/go-camo/releases) 5 | [![TravisCI](https://img.shields.io/travis/cactus/go-camo.svg)](https://travis-ci.org/cactus/go-camo) 6 | [![License](https://img.shields.io/github/license/cactus/go-camo.svg)](https://github.com/cactus/go-camo/blob/master/LICENSE.md) 7 | 8 | 9 | ## Contents 10 | 11 | * [About](#about) 12 | * [How it works](#how-it-works) 13 | * [Differences from Camo](#differences-from-camo) 14 | * [Installing pre-built binaries](#installing-pre-built-binaries) 15 | * [Building](#building) 16 | * [Running](#running) 17 | * [Running on Heroku](#running-on-heroku) 18 | * [Securing an installation](#securing-an-installation) 19 | * [Configuring](#configuring) 20 | * [Additional tools](#additional-tools) 21 | * [Alternative Implementations](#alternative-implementations) 22 | * [Changelog](#changelog) 23 | * [License](#license) 24 | 25 | ## About 26 | 27 | Go version of [Camo][1] server. 28 | 29 | [Camo][1] is a special type of image proxy that proxies non-secure images over 30 | SSL/TLS. This prevents mixed content warnings on secure pages. 31 | 32 | It works in conjunction with back-end code that rewrites image URLs and signs 33 | them with an [HMAC][4]. 34 | 35 | ## How it works 36 | 37 | The general steps are as follows: 38 | 39 | 1. A client requests a page from the web app. 40 | 2. The original URL in the content is parsed. 41 | 3. An HMAC signature of the url is generated. 42 | 4. The url and hmac are encoded. 43 | 5. The encoded url and hmac are placed into the expected format, creating 44 | the signed url. 45 | 6. The signed url replaces the original image URL. 46 | 7. The web app returns the content to the client. 47 | 8. The client requets the signed url from Go-Camo. 48 | 9. Go-Camo validates the HMAC, decodes the URL, then requests the content 49 | from the origin server and streams it to the client. 50 | 51 | ```text 52 | +----------+ request +-------------+ 53 | | |----------------------------->| | 54 | | | | | 55 | | | | web-app | 56 | | | img src=https://go-camo/url | | 57 | | |<-----------------------------| | 58 | | | +-------------+ 59 | | client | 60 | | | https://go-camo/url +-------------+ http://some/img 61 | | |----------------------------->| |---------------> 62 | | | | | 63 | | | | go-camo | 64 | | | img data | | img data 65 | | |<-----------------------------| |<--------------- 66 | | | +-------------+ 67 | +----------+ 68 | ``` 69 | 70 | Go-Camo supports both hex and base64 encoded urls at the same time. 71 | 72 | | encoding | tradeoffs | 73 | | -------- | ----------------------------------------- | 74 | | hex | longer, case insensitive, slightly faster | 75 | | base64 | shorter, case sensitive, slightly slower | 76 | 77 | Benchmark results with go1.8: 78 | 79 | ```text 80 | BenchmarkHexEncoder-2 500000 2505 ns/op 81 | BenchmarkB64Encoder-2 500000 2576 ns/op 82 | BenchmarkHexDecoder-2 500000 2542 ns/op 83 | BenchmarkB64Decoder-2 500000 2687 ns/op 84 | ``` 85 | 86 | For examples of url generation, see the [examples](examples/) directory. 87 | 88 | While Go-Camo will support proxying HTTPS images as well, for performance 89 | reasons you may choose to filter HTTPS requests out from proxying, and let the 90 | client simply fetch those as they are. The linked code examples do this. 91 | 92 | Note that it is recommended to front Go-Camo with a CDN when possible. 93 | 94 | ## Differences from Camo 95 | 96 | * Go-Camo supports 'Path Format' url format only. Camo's "Query 97 | String Format" is not supported. 98 | * Go-Camo supports "allow regex host filters". 99 | * Go-Camo supports client http keep-alives. 100 | * Go-Camo provides native SSL support. 101 | * Go-Camo provides native HTTP/2 support (if built using >=go1.6). 102 | * Go-Camo supports using more than one os thread (via GOMAXPROCS) without the 103 | need of multiple instances or additional proxying. 104 | * Go-Camo builds to a static binary. This makes deploying to large numbers 105 | of servers a snap. 106 | * Go-Camo supports both Hex and Base64 urls. Base64 urls are smaller, but 107 | case sensitive. 108 | * Go-Camo supports HTTP HEAD requests. 109 | * Go-Camo allows custom default headers to be added -- useful for things 110 | like adding [HSTS][10] headers. 111 | 112 | ## Installing pre-built binaries 113 | 114 | Download the tarball appropriate for your OS/ARCH from [releases][13]. 115 | Extract, and copy files to desired locations. 116 | 117 | ## Building 118 | 119 | Building requires: 120 | 121 | * make 122 | * git 123 | * go (latest version recommended. At least version >= 1.9) 124 | 125 | Additionally required, if cross compiling: 126 | 127 | * [gox](https://github.com/mitchellh/gox) 128 | 129 | Building: 130 | 131 | ```text 132 | # first clone the repo 133 | $ git clone git@github.com:cactus/go-camo 134 | $ cd go-camo 135 | 136 | # show make targets 137 | $ make 138 | Available targets: 139 | help this help 140 | clean clean up 141 | all build binaries and man pages 142 | test run tests 143 | cover run tests with cover output 144 | build build all 145 | man build all man pages 146 | tar build release tarball 147 | cross-tar cross compile and build release tarballs 148 | 149 | # build all binaries (into ./bin/) and man pages (into ./man/) 150 | # strips debug symbols by default 151 | $ make all 152 | 153 | # do not strip debug symbols 154 | $ make all GOBUILD_LDFLAGS="" 155 | ``` 156 | 157 | ## Running 158 | 159 | ```text 160 | $ go-camo -k "somekey" 161 | ``` 162 | 163 | Go-Camo does not daemonize on its own. For production usage, it is recommended 164 | to launch in a process supervisor, and drop privileges as appropriate. 165 | 166 | Examples of supervisors include: [daemontools][5], [runit][6], [upstart][7], 167 | [launchd][8], [systemd][19], and many more. 168 | 169 | For the reasoning behind lack of daemonization, see [daemontools/why][9]. In 170 | addition, the code is much simpler because of it. 171 | 172 | ## Running on Heroku 173 | 174 | In order to use this on Heroku with the provided Procfile, you need to: 175 | 176 | 1. Create an app specifying the https://github.com/kr/heroku-buildpack-go 177 | buildpack 178 | 2. Set `GOCAMO_HMAC` to the key you are using 179 | 180 | ## Securing an installation 181 | 182 | go-camo will generally do what you tell it to with regard to fetching signed 183 | urls. There is some limited support for trying to prevent [dns 184 | rebinding][15] attacks. 185 | 186 | go-camo will attempt to reject any address matching an rfc1918 network block, 187 | or a private scope ipv6 address, be it in the url or via resulting hostname 188 | resolution. Do note, however, that this does not provide protecton for a 189 | network that uses public address space (ipv4 or ipv6), or some of the 190 | [more exotic][16] ipv6 addresses. 191 | 192 | The list of networks rejected include... 193 | 194 | | Network | Description | 195 | | ----------------- | ----------------------------- | 196 | | `127.0.0.0/8` | loopback | 197 | | `169.254.0.0/16` | ipv4 link local | 198 | | `10.0.0.0/8` | rfc1918 | 199 | | `172.16.0.0/12` | rfc1918 | 200 | | `192.168.0.0/16` | rfc1918 | 201 | | `::1/128` | ipv6 loopback | 202 | | `fe80::/10` | ipv6 link local | 203 | | `fec0::/10` | deprecated ipv6 site-local | 204 | | `fc00::/7` | ipv6 ULA | 205 | | `::ffff:0:0/96` | IPv4-mapped IPv6 address | 206 | 207 | More generally, it is recommended to either: 208 | 209 | * Run go-camo on an isolated instance (physical, vlans, firewall rules, etc). 210 | * Run a local resolver for go-camo that returns NXDOMAIN responses for 211 | addresses in blacklisted ranges (for example unbound's `private-address` 212 | functionality). This is also useful to help prevent dns rebinding in 213 | general. 214 | 215 | ## Configuring 216 | 217 | ### Environment Vars 218 | 219 | * `GOCAMO_HMAC` - HMAC key to use. 220 | 221 | ### Command line flags 222 | 223 | ```text 224 | $ go-camo -h 225 | Usage: 226 | go-camo [OPTIONS] 227 | 228 | Application Options: 229 | -V, --version Print version and exit; specify twice to show license 230 | information 231 | -H, --header= Extra header to return for each response. This option can 232 | be used multiple times to add multiple headers 233 | -k, --key= HMAC key 234 | --ssl-key= ssl private key (key.pem) path 235 | --ssl-cert= ssl cert (cert.pem) path 236 | --allow-list= Text file of hostname allow regexes (one per line) 237 | --listen= Address:Port to bind to for HTTP (default: 0.0.0.0:8080) 238 | --ssl-listen= Address:Port to bind to for HTTPS/SSL/TLS 239 | --max-size= Max allowed response size (KB) (default: 5120) 240 | --timeout= Upstream request timeout (default: 4s) 241 | --max-redirects= Maximum number of redirects to follow (default: 3) 242 | --stats Enable Stats 243 | --no-log-ts Do not add a timestamp to logging 244 | --no-fk Disable frontend http keep-alive support 245 | --no-bk Disable backend http keep-alive support 246 | --allow-content-video Additionally allow 'video/*' content 247 | -v, --verbose Show verbose (debug) log level output 248 | --server-name= Value to use for the HTTP server field (default: go-camo) 249 | --expose-server-version Include the server version in the HTTP server response 250 | header 251 | 252 | Help Options: 253 | -h, --help Show this help message 254 | 255 | ``` 256 | 257 | If an allow-list file is defined, that file is read and each line converted 258 | into a hostname regex. If a request does not match one of the listed host 259 | regex, then the request is denied. 260 | 261 | If stats flag is provided, then the service will track bytes and clients 262 | served, and offer them up at an http endpoint `/status` via HTTP GET request. 263 | 264 | If the HMAC key is provided on the command line, it will override (if present), 265 | an HMAC key set in the environment var. 266 | 267 | Additional default headers (sent on every response) can also be set. The 268 | `-H, --header` argument may be specified many times. 269 | 270 | The list of default headers sent are: 271 | 272 | ```text 273 | X-Content-Type-Options: nosniff 274 | X-XSS-Protection: 1; mode=block 275 | Content-Security-Policy: default-src 'none'; img-src data:; style-src 'unsafe-inline' 276 | ``` 277 | 278 | As an example, if you wanted to return a `Strict-Transport-Security` header 279 | by default, you could add this to the command line: 280 | 281 | ```text 282 | -H "Strict-Transport-Security: max-age=16070400" 283 | ``` 284 | 285 | ## Additional tools 286 | 287 | Go-Camo includes a couple of additional tools. 288 | 289 | ### url-tool 290 | 291 | The `url-tool` utility provides a simple way to generate signed URLs from the command line. 292 | 293 | ```text 294 | $ url-tool -h 295 | Usage: 296 | url-tool [OPTIONS] 297 | 298 | Application Options: 299 | -k, --key= HMAC key 300 | -p, --prefix= Optional url prefix used by encode output 301 | 302 | Help Options: 303 | -h, --help Show this help message 304 | 305 | Available commands: 306 | decode Decode a url and print result 307 | encode Encode a url and print result 308 | ``` 309 | 310 | Example usage: 311 | 312 | ```text 313 | # hex 314 | $ url-tool -k "test" encode -p "https://img.example.org" "http://golang.org/doc/gopher/frontpage.png" 315 | https://img.example.org/0f6def1cb147b0e84f39cbddc5ea10c80253a6f3/687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67 316 | 317 | $ url-tool -k "test" decode "https://img.example.org/0f6def1cb147b0e84f39cbddc5ea10c80253a6f3/687474703a2f2f676f6c616e672e6f72672f646f632f676f706865722f66726f6e74706167652e706e67" 318 | http://golang.org/doc/gopher/frontpage.png 319 | 320 | # base64 321 | $ url-tool -k "test" encode -b base64 -p "https://img.example.org" "http://golang.org/doc/gopher/frontpage.png" 322 | https://img.example.org/D23vHLFHsOhPOcvdxeoQyAJTpvM/aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n 323 | 324 | $ url-tool -k "test" decode "https://img.example.org/D23vHLFHsOhPOcvdxeoQyAJTpvM/aHR0cDovL2dvbGFuZy5vcmcvZG9jL2dvcGhlci9mcm9udHBhZ2UucG5n" 325 | http://golang.org/doc/gopher/frontpage.png 326 | ``` 327 | 328 | ### simple-server 329 | 330 | The `simple-server` utility has moved to its [own repo][14]. 331 | 332 | ## Alternative Implementations 333 | 334 | * [MrSaints][17]' go-camo [fork][18] - supports proxying additional content 335 | types (fonts/css). 336 | 337 | ## Changelog 338 | 339 | See `CHANGELOG.md` 340 | 341 | ## License 342 | 343 | Released under the [MIT 344 | license](http://www.opensource.org/licenses/mit-license.php). See `LICENSE.md` 345 | file for details. 346 | 347 | [1]: https://github.com/atmos/camo 348 | [3]: http://golang.org/doc/install 349 | [4]: http://en.wikipedia.org/wiki/HMAC 350 | [5]: http://cr.yp.to/daemontools.html 351 | [6]: http://smarden.org/runit/ 352 | [7]: http://upstart.ubuntu.com/ 353 | [8]: http://launchd.macosforge.org/ 354 | [9]: http://cr.yp.to/daemontools/faq/create.html#why 355 | [10]: https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security 356 | [11]: https://github.com/cactus/go-camo/issues/6 357 | [12]: https://codereview.appspot.com/151730045#msg10 358 | [13]: https://github.com/cactus/go-camo/releases 359 | [14]: https://github.com/cactus/static-server 360 | [15]: https://en.wikipedia.org/wiki/DNS_rebinding 361 | [16]: https://en.wikipedia.org/wiki/IPv6_address#Special_addresses 362 | [17]: https://github.com/MrSaints 363 | [18]: https://github.com/arachnys/go-camo 364 | [19]: https://www.freedesktop.org/wiki/Software/systemd/ 365 | --------------------------------------------------------------------------------