├── .gitignore ├── LICENSE ├── README.md └── httptee.go /.gitignore: -------------------------------------------------------------------------------- 1 | /httptee 2 | 3 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 4 | *.o 5 | *.a 6 | *.so 7 | 8 | # Folders 9 | _obj 10 | _test 11 | 12 | # Architecture specific extensions/prefixes 13 | *.[568vq] 14 | [568vq].out 15 | 16 | *.cgo1.go 17 | *.cgo2.c 18 | _cgo_defun.c 19 | _cgo_gotypes.go 20 | _cgo_export.* 21 | 22 | _testmain.go 23 | 24 | *.exe 25 | *.test 26 | *.prof 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jacob Wirth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # httptee 2 | Tee http diff server for comparing how backends respond to requests. 3 | 4 | **httptee** will respond to all requests by proxying them to the *base* backend. 5 | The requests are also sent to the *compare* backend to be diffed. 6 | 7 | ## Usage 8 | ``` 9 | $ go install github.com/xthexder/httptee 10 | 11 | $ httptee --help 12 | Usage of httptee: 13 | -addr=":8080": TCP address to bind 14 | -base=":8081": main upstream host to proxy 15 | -compare=":8082": secondary upstream host to proxy 16 | -verbose=false: verbose logging 17 | ``` 18 | 19 | ## Example 20 | Server: 21 | ``` 22 | $ httptee -base="cache01.nyc.frustra.org:80" -compare="cache01.sfo.frustra.org:80" 23 | INFO[0000] Listening on: :8080 24 | INFO[0002] Request: 25 | GET / HTTP/1.1 26 | Host: localhost:8080 27 | User-Agent: curl/7.42.0 28 | Accept: */* 29 | 30 | 31 | INFO[0002] Response code: HTTP/1.1 301 Moved Permanently 32 | INFO[0002] Headers differ: 33 | - X-Served-By: cache01.nyc 34 | X-Varnish: 31037300 35 | + X-Served-By: cache01.sfo 36 | X-Varnish: 10491072 37 | ``` 38 | 39 | Request: 40 | ``` 41 | $ curl -i localhost:8080 42 | HTTP/1.1 301 Moved Permanently 43 | Date: Tue, 19 May 2015 03:47:02 GMT 44 | Server: Varnish 45 | X-Varnish: 31097580 46 | Vary: Accept-Encoding 47 | X-Served-By: cache01.nyc 48 | Location: https://frustra.org 49 | Content-Length: 21 50 | Connection: keep-alive 51 | 52 | 301 Moved Permanently 53 | ``` 54 | -------------------------------------------------------------------------------- /httptee.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "io" 7 | "net" 8 | "sort" 9 | "strings" 10 | "time" 11 | 12 | log "github.com/Sirupsen/logrus" 13 | "github.com/sergi/go-diff/diffmatchpatch" 14 | ) 15 | 16 | var base, compare string 17 | 18 | func PrintDiff(diff []diffmatchpatch.Diff) { 19 | output := "" 20 | for _, change := range diff { 21 | switch change.Type { 22 | case diffmatchpatch.DiffDelete: 23 | output += "- " + change.Text 24 | case diffmatchpatch.DiffInsert: 25 | output += "+ " + change.Text 26 | } 27 | } 28 | if len(output) > 0 { 29 | log.Println("Headers differ:\n" + output) 30 | } 31 | } 32 | 33 | func Handle(conn *net.TCPConn) { 34 | log.Debug("Connection from ", conn.RemoteAddr().String()) 35 | 36 | conna, err := net.Dial("tcp", base) 37 | if err != nil { 38 | log.Warn("base backend: ", err) 39 | conn.Close() 40 | return 41 | } 42 | connb, err := net.Dial("tcp", compare) 43 | if err != nil { 44 | log.Warn("compare backend: ", err) 45 | } 46 | defer conna.Close() 47 | done := make(chan struct{}) 48 | 49 | request := new(bytes.Buffer) 50 | 51 | go func() { 52 | defer conn.Close() 53 | buffa := new(bytes.Buffer) 54 | buffb := new(bytes.Buffer) 55 | reader := io.TeeReader(conna, buffa) 56 | go func() { 57 | if connb == nil { 58 | return 59 | } 60 | defer connb.Close() 61 | buf := make([]byte, 32*1024) 62 | for { 63 | connb.SetReadDeadline(time.Now().Add(1 * time.Second)) 64 | n, err := connb.Read(buf) 65 | if err != nil { 66 | break 67 | } 68 | buffb.Write(buf[:n]) 69 | if n == 0 { // Just assume we're at the end 70 | break 71 | } 72 | } 73 | close(done) 74 | }() 75 | io.Copy(conn, reader) 76 | if connb != nil { 77 | <-done 78 | 79 | splita := append(strings.SplitN(buffa.String(), "\r\n\r\n", 2), "", "") 80 | splitb := append(strings.SplitN(buffb.String(), "\r\n\r\n", 2), "", "") 81 | linesa := strings.Split(splita[0], "\r\n") 82 | linesb := strings.Split(splitb[0], "\r\n") 83 | if len(linesa) > 0 && len(linesb) > 0 { 84 | sort.Strings(linesa[1:]) 85 | sort.Strings(linesb[1:]) 86 | respa := strings.Join(linesa[1:], "\r\n") + "\r\n" 87 | respb := strings.Join(linesb[1:], "\r\n") + "\r\n" 88 | differ := diffmatchpatch.New() 89 | charsa, charsb, fulltext := differ.DiffLinesToChars(respa, respb) 90 | diff := differ.DiffMain(charsa, charsb, false) 91 | diff = differ.DiffCharsToLines(diff, fulltext) 92 | 93 | log.Println("Request:\n" + request.String()) 94 | if linesa[0] != linesb[0] { 95 | log.Println("Response code differs:\n- " + linesa[0] + "\n+ " + linesb[0]) 96 | } else { 97 | log.Println("Response code: " + linesa[0]) 98 | } 99 | PrintDiff(diff) 100 | if splita[1] != splitb[1] { 101 | log.Println("Response body differs") 102 | } 103 | } else { 104 | log.Warn("Response missing") 105 | } 106 | } 107 | log.Debug("Done") 108 | }() 109 | 110 | if connb != nil { 111 | reader1 := io.TeeReader(conn, request) 112 | reader2 := io.TeeReader(reader1, connb) 113 | io.Copy(conna, reader2) 114 | } else { 115 | reader := io.TeeReader(conn, request) 116 | io.Copy(conna, reader) 117 | } 118 | } 119 | 120 | func main() { 121 | var addr string 122 | var verbose bool 123 | 124 | flag.StringVar(&addr, "addr", ":8080", "TCP address to bind") 125 | flag.StringVar(&base, "base", ":8081", "main upstream host to proxy") 126 | flag.StringVar(&compare, "compare", ":8082", "secondary upstream host to proxy") 127 | flag.BoolVar(&verbose, "verbose", false, "verbose logging") 128 | flag.Parse() 129 | 130 | if verbose { 131 | log.SetLevel(log.DebugLevel) 132 | } 133 | 134 | tcpaddr, err := net.ResolveTCPAddr("tcp", addr) 135 | if err != nil { 136 | panic(err) 137 | } 138 | 139 | listener, err := net.ListenTCP("tcp", tcpaddr) 140 | if err != nil { 141 | panic(err) 142 | } 143 | 144 | log.Println("Listening on: " + tcpaddr.String()) 145 | 146 | for { 147 | conn, err := listener.AcceptTCP() 148 | if err != nil { 149 | log.Warn("accept: ", err) 150 | continue 151 | } 152 | go Handle(conn) 153 | } 154 | } 155 | --------------------------------------------------------------------------------