├── .github └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── LICENSE ├── Makefile ├── README.md ├── accesslog └── accesslog.go ├── chocon.go ├── go.mod ├── go.sum ├── pidfile └── pidfile.go ├── proxy ├── handler.go └── handler_test.go └── upstream └── upstream.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | on: [push] 3 | jobs: 4 | test: 5 | name: Build 6 | runs-on: ubuntu-latest 7 | strategy: 8 | matrix: 9 | go: ["1.17.x"] 10 | steps: 11 | - name: Set up Go 12 | uses: actions/setup-go@v1 13 | with: 14 | go-version: ${{ matrix.go }} 15 | id: go 16 | 17 | - name: Check out code into the Go module directory 18 | uses: actions/checkout@v2 19 | 20 | - name: test 21 | run: | 22 | make check 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | tags: 5 | - "v[0-9]+.[0-9]+.[0-9]+" 6 | jobs: 7 | goreleaser: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | 15 | - name: Setup Go 16 | uses: actions/setup-go@v2 17 | with: 18 | go-version: 1.17 19 | 20 | - name: Run GoReleaser 21 | uses: goreleaser/goreleaser-action@v2 22 | with: 23 | version: latest 24 | args: release --rm-dist 25 | env: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | chocon 27 | vendor/ 28 | dist/ 29 | *~ 30 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - binary: chocon 3 | goos: 4 | - darwin 5 | - linux 6 | goarch: 7 | - amd64 8 | - arm64 9 | - arm 10 | ignore: 11 | - goos: darwin 12 | goarch: arm64 13 | archives: 14 | - format: zip 15 | name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" 16 | release: 17 | github: 18 | owner: kazeburo 19 | name: chocon 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Masahiro Nagano 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | VERSION=0.12.5 2 | LDFLAGS=-ldflags "-w -s -X main.version=${VERSION}" 3 | 4 | all: chocon 5 | 6 | chocon: chocon.go 7 | go build $(LDFLAGS) chocon.go 8 | 9 | linux: chocon.go 10 | GOOS=linux GOARCH=amd64 go build $(LDFLAGS) chocon.go 11 | 12 | check: 13 | go test ./... 14 | 15 | fmt: 16 | go fmt ./... 17 | 18 | clean: 19 | rm -rf chocon chocon-*.tar.gz 20 | 21 | tag: 22 | git tag v${VERSION} 23 | git push origin v${VERSION} 24 | git push origin master 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # chocon 2 | 3 | **chocon** is a simple proxy server for persisting connections between upstream servers. 4 | 5 | # Requirements 6 | 7 | **chocon** requires Go1.11.3 or later. 8 | 9 | # Installation 10 | 11 | download from release page or build from source 12 | 13 | ## build from source 14 | 15 | git clone this repository and 16 | 17 | ``` 18 | $ make 19 | $ cp chocon /path/to/bin 20 | ``` 21 | 22 | # Run 23 | 24 | ``` 25 | chocon 26 | ``` 27 | 28 | # Usage 29 | 30 | ``` 31 | $ chocon -h 32 | Usage: 33 | chocon [OPTIONS] 34 | 35 | Application Options: 36 | -l, --listen= address to bind (default: 0.0.0.0) 37 | -p, --port= Port number to bind (default: 3000) 38 | --access-log-dir= directory to store logfiles 39 | --access-log-rotate= Number of rotation before remove logs (default: 30) 40 | --access-log-rotate-time= Interval minutes between file rotation (default: 1440) 41 | -v, --version Show version 42 | --pid-file= filename to store pid. disabled by default 43 | -c, --keepalive-conns= maximum keepalive connections for upstream (default: 2) 44 | --max-conns-per-host= maximum connections per host (default: 0) 45 | --read-timeout= timeout of reading request (default: 30) 46 | --write-timeout= timeout of writing response (default: 90) 47 | --proxy-read-timeout= timeout of reading response from upstream (default: 60) 48 | --shutdown-timeout= timeout to wait for all connections to be closed. (default: 1h) 49 | --upstream= upstream server: http://upstream-server/ 50 | --stsize= buffer size for http stats (default: 1000) 51 | --spfactor= sampling factor for http stats (default: 3) 52 | 53 | Help Options: 54 | -h, --help Show this help message 55 | 56 | ``` 57 | -------------------------------------------------------------------------------- /accesslog/accesslog.go: -------------------------------------------------------------------------------- 1 | package accesslog 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | "os" 7 | "strings" 8 | "time" 9 | 10 | rotatelogs "github.com/lestrrat-go/file-rotatelogs" 11 | "github.com/pkg/errors" 12 | "go.uber.org/zap" 13 | "go.uber.org/zap/zapcore" 14 | ) 15 | 16 | // AccessLog : 17 | type AccessLog struct { 18 | logger *zap.Logger 19 | } 20 | 21 | func logWriter(logDir string, logRotate int64, logRotateTime int64) (io.Writer, error) { 22 | if logDir == "stdout" { 23 | return os.Stdout, nil 24 | } else if logDir == "" { 25 | return os.Stderr, nil 26 | } else if logDir == "none" { 27 | return nil, nil 28 | } 29 | logFile := logDir 30 | linkName := logDir 31 | if !strings.HasSuffix(logDir, "/") { 32 | logFile += "/" 33 | linkName += "/" 34 | 35 | } 36 | logFile += "access_log.%Y%m%d%H%M" 37 | linkName += "current" 38 | 39 | rl, err := rotatelogs.New( 40 | logFile, 41 | rotatelogs.WithLinkName(linkName), 42 | rotatelogs.WithMaxAge(time.Duration(logRotate*60*logRotateTime)*time.Second), 43 | rotatelogs.WithRotationTime(time.Second*time.Duration(logRotateTime)*60), 44 | ) 45 | if err != nil { 46 | return nil, errors.Wrap(err, "rotatelogs.New failed") 47 | } 48 | return rl, nil 49 | } 50 | 51 | // New : 52 | func New(logDir string, logRotate int64, logRotateTime int64) (*AccessLog, error) { 53 | w, err := logWriter(logDir, logRotate, logRotateTime) 54 | if err != nil { 55 | return nil, err 56 | } 57 | if w == nil { 58 | return &AccessLog{}, nil 59 | } 60 | 61 | encoderConfig := zapcore.EncoderConfig{ 62 | EncodeLevel: zapcore.LowercaseLevelEncoder, 63 | EncodeTime: zapcore.ISO8601TimeEncoder, 64 | EncodeDuration: zapcore.StringDurationEncoder, 65 | EncodeCaller: zapcore.ShortCallerEncoder, 66 | } 67 | 68 | logger := zap.New( 69 | zapcore.NewCore( 70 | zapcore.NewJSONEncoder(encoderConfig), 71 | zapcore.AddSync(w), 72 | zapcore.InfoLevel, 73 | ), 74 | ) 75 | return &AccessLog{ 76 | logger: logger, 77 | }, nil 78 | } 79 | 80 | // WrapHandleFunc : 81 | func (al *AccessLog) WrapHandleFunc(h http.Handler) http.Handler { 82 | if al.logger == nil { 83 | return h 84 | } 85 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 86 | start := time.Now() 87 | ww := WrapWriter(w) 88 | defer func() { 89 | end := time.Now() 90 | ptime := end.Sub(start) 91 | remoteAddr := r.RemoteAddr 92 | if i := strings.LastIndexByte(remoteAddr, ':'); i > -1 { 93 | remoteAddr = remoteAddr[:i] 94 | } 95 | al.logger.Info( 96 | "-", 97 | zap.String("time", start.Format("2006/01/02 15:04:05 MST")), 98 | zap.String("remote_addr", remoteAddr), 99 | zap.String("method", r.Method), 100 | zap.String("uri", r.URL.Path), 101 | zap.Int("status", ww.GetCode()), 102 | zap.Int("size", ww.GetSize()), 103 | zap.String("ua", r.UserAgent()), 104 | zap.Float64("ptime", ptime.Seconds()), 105 | zap.String("host", r.Host), 106 | zap.String("chocon_req", w.Header().Get("X-Chocon-Id")), 107 | ) 108 | }() 109 | h.ServeHTTP(ww, r) 110 | }) 111 | } 112 | 113 | // Writer : 114 | type Writer struct { 115 | w http.ResponseWriter 116 | size int 117 | code int 118 | } 119 | 120 | // WrapWriter : 121 | func WrapWriter(w http.ResponseWriter) *Writer { 122 | return &Writer{ 123 | w: w, 124 | } 125 | } 126 | 127 | // Header : 128 | func (w *Writer) Header() http.Header { 129 | return w.w.Header() 130 | } 131 | 132 | // Write : 133 | func (w *Writer) Write(b []byte) (int, error) { 134 | w.size += len(b) 135 | return w.w.Write(b) 136 | } 137 | 138 | // WriteHeader : 139 | func (w *Writer) WriteHeader(statusCode int) { 140 | w.code = statusCode 141 | w.w.WriteHeader(statusCode) 142 | } 143 | 144 | // GetCode : 145 | func (w *Writer) GetCode() int { 146 | return w.code 147 | } 148 | 149 | // GetSize : 150 | func (w *Writer) GetSize() int { 151 | return w.size 152 | } 153 | -------------------------------------------------------------------------------- /chocon.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "log" 8 | "net" 9 | "net/http" 10 | "os" 11 | "os/signal" 12 | "runtime" 13 | "strings" 14 | "syscall" 15 | "time" 16 | 17 | stats_api "github.com/fukata/golang-stats-api-handler" 18 | "github.com/jessevdk/go-flags" 19 | "github.com/kazeburo/chocon/accesslog" 20 | "github.com/kazeburo/chocon/pidfile" 21 | "github.com/kazeburo/chocon/proxy" 22 | "github.com/kazeburo/chocon/upstream" 23 | ss "github.com/lestrrat/go-server-starter-listener" 24 | statsHTTP "github.com/mercari/go-httpstats" 25 | "go.uber.org/zap" 26 | ) 27 | 28 | var ( 29 | // version chocon version 30 | version string 31 | ) 32 | 33 | type cmdOpts struct { 34 | Listen string `short:"l" long:"listen" default:"0.0.0.0" description:"address to bind"` 35 | Port string `short:"p" long:"port" default:"3000" description:"Port number to bind"` 36 | LogDir string `long:"access-log-dir" default:"" description:"directory to store logfiles"` 37 | LogRotate int64 `long:"access-log-rotate" default:"30" description:"Number of rotation before remove logs"` 38 | LogRotateTime int64 `long:"access-log-rotate-time" default:"1440" description:"Interval minutes between file rotation"` 39 | Version bool `short:"v" long:"version" description:"Show version"` 40 | PidFile string `long:"pid-file" default:"" description:"filename to store pid. disabled by default"` 41 | KeepaliveConns int `short:"c" default:"2" long:"keepalive-conns" description:"maximum keepalive connections for upstream"` 42 | MaxConnsPerHost int `long:"max-conns-per-host" default:"0" description:"maximum connections per host"` 43 | ReadTimeout int `long:"read-timeout" default:"30" description:"timeout of reading request"` 44 | WriteTimeout int `long:"write-timeout" default:"90" description:"timeout of writing response"` 45 | ProxyReadTimeout int `long:"proxy-read-timeout" default:"60" description:"timeout of reading response from upstream"` 46 | ShutdownTimeout time.Duration `long:"shutdown-timeout" default:"1h" description:"timeout to wait for all connections to be closed."` 47 | Upstream string `long:"upstream" default:"" description:"upstream server: http://upstream-server/"` 48 | StatsBufsize int `long:"stsize" default:"1000" description:"buffer size for http stats"` 49 | StatsSpfactor int `long:"spfactor" default:"3" description:"sampling factor for http stats"` 50 | } 51 | 52 | func addStatsHandler(h http.Handler, mw *statsHTTP.Metrics) http.Handler { 53 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 54 | if strings.Index(r.URL.Path, "/.api/stats") == 0 { 55 | stats_api.Handler(w, r) 56 | } else if strings.Index(r.URL.Path, "/.api/http-stats") == 0 { 57 | d, err := mw.Data() 58 | if err != nil { 59 | http.Error(w, err.Error(), http.StatusInternalServerError) 60 | } 61 | if err := json.NewEncoder(w).Encode(d); err != nil { 62 | http.Error(w, err.Error(), http.StatusInternalServerError) 63 | } 64 | } else { 65 | h.ServeHTTP(w, r) 66 | } 67 | }) 68 | } 69 | 70 | func wrapLogHandler(h http.Handler, logDir string, logRotate int64, logRotateTime int64, logger *zap.Logger) http.Handler { 71 | al, err := accesslog.New(logDir, logRotate, logRotateTime) 72 | if err != nil { 73 | logger.Fatal("could not init accesslog", zap.Error(err)) 74 | } 75 | return al.WrapHandleFunc(h) 76 | } 77 | 78 | func wrapStatsHandler(h http.Handler, mw *statsHTTP.Metrics) http.Handler { 79 | return mw.WrapHandleFunc(h) 80 | } 81 | 82 | func makeTransport(keepaliveConns int, maxConnsPerHost int, proxyReadTimeout int) http.RoundTripper { 83 | return &http.Transport{ 84 | // inherited http.DefaultTransport 85 | Proxy: http.ProxyFromEnvironment, 86 | DialContext: (&net.Dialer{ 87 | Timeout: 30 * time.Second, 88 | KeepAlive: 30 * time.Second, 89 | }).DialContext, 90 | IdleConnTimeout: 30 * time.Second, 91 | TLSHandshakeTimeout: 10 * time.Second, 92 | ExpectContinueTimeout: 1 * time.Second, 93 | // self-customized values 94 | MaxIdleConnsPerHost: keepaliveConns, 95 | MaxConnsPerHost: maxConnsPerHost, 96 | ResponseHeaderTimeout: time.Duration(proxyReadTimeout) * time.Second, 97 | } 98 | } 99 | 100 | func printVersion() { 101 | fmt.Printf(`chocon %s 102 | Compiler: %s %s 103 | `, 104 | version, 105 | runtime.Compiler, 106 | runtime.Version()) 107 | } 108 | 109 | func main() { 110 | os.Exit(_main()) 111 | } 112 | 113 | func _main() int { 114 | opts := cmdOpts{} 115 | psr := flags.NewParser(&opts, flags.Default) 116 | _, err := psr.Parse() 117 | if err != nil { 118 | return 1 119 | } 120 | 121 | if opts.Version { 122 | printVersion() 123 | return 0 124 | } 125 | 126 | logger, _ := zap.NewProduction() 127 | upstream, err := upstream.New(opts.Upstream, logger) 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | 132 | if opts.PidFile != "" { 133 | err = pidfile.WritePid(opts.PidFile) 134 | if err != nil { 135 | log.Fatal(err) 136 | } 137 | } 138 | 139 | transport := makeTransport(opts.KeepaliveConns, opts.MaxConnsPerHost, opts.ProxyReadTimeout) 140 | var handler http.Handler = proxy.New(&transport, version, upstream, logger) 141 | 142 | statsChocon, err := statsHTTP.NewCapa(opts.StatsBufsize, opts.StatsSpfactor) 143 | if err != nil { 144 | log.Fatal(err) 145 | } 146 | handler = addStatsHandler(handler, statsChocon) 147 | handler = wrapLogHandler(handler, opts.LogDir, opts.LogRotate, opts.LogRotateTime, logger) 148 | handler = wrapStatsHandler(handler, statsChocon) 149 | 150 | server := http.Server{ 151 | Handler: handler, 152 | ReadTimeout: time.Duration(opts.ReadTimeout) * time.Second, 153 | WriteTimeout: time.Duration(opts.WriteTimeout) * time.Second, 154 | } 155 | 156 | idleConnsClosed := make(chan struct{}) 157 | go func() { 158 | sigChan := make(chan os.Signal, 1) 159 | signal.Notify(sigChan, syscall.SIGTERM) 160 | <-sigChan 161 | ctx, cancel := context.WithTimeout(context.Background(), opts.ShutdownTimeout) 162 | if es := server.Shutdown(ctx); es != nil { 163 | logger.Warn("Shutdown error", zap.Error(es)) 164 | } 165 | cancel() 166 | close(idleConnsClosed) 167 | }() 168 | 169 | l, err := ss.NewListener() 170 | if l == nil || err != nil { 171 | // Fallback if not running under Server::Starter 172 | l, err = net.Listen("tcp", fmt.Sprintf("%s:%s", opts.Listen, opts.Port)) 173 | if err != nil { 174 | logger.Fatal("Failed to listen to port", 175 | zap.Error(err), 176 | zap.String("listen", opts.Listen), 177 | zap.String("port", opts.Port)) 178 | } 179 | } 180 | if err := server.Serve(l); err != http.ErrServerClosed { 181 | logger.Error("Error in Serve", zap.Error(err)) 182 | return 1 183 | } 184 | 185 | <-idleConnsClosed 186 | return 0 187 | } 188 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kazeburo/chocon 2 | 3 | go 1.14 4 | 5 | require ( 6 | github.com/fukata/golang-stats-api-handler v1.0.0 7 | github.com/jessevdk/go-flags v1.5.0 8 | github.com/jonboulle/clockwork v0.2.2 // indirect 9 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible 10 | github.com/lestrrat-go/strftime v1.0.5 // indirect 11 | github.com/lestrrat/go-server-starter-listener v0.0.0-20150507032651-00dd68592c85 12 | github.com/mercari/go-httpstats v1.0.0 13 | github.com/montanaflynn/stats v0.6.6 // indirect 14 | github.com/pkg/errors v0.9.1 15 | github.com/rs/xid v1.3.0 16 | github.com/stretchr/testify v1.7.0 17 | go.uber.org/atomic v1.9.0 // indirect 18 | go.uber.org/multierr v1.7.0 // indirect 19 | go.uber.org/zap v1.19.1 20 | golang.org/x/sys v0.0.0-20210927052749-1cf2251ac284 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= 2 | github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/fukata/golang-stats-api-handler v1.0.0 h1:N6M25vhs1yAvwGBpFY6oBmMOZeJdcWnvA+wej8pKeko= 7 | github.com/fukata/golang-stats-api-handler v1.0.0/go.mod h1:1sIi4/rHq6s/ednWMZqTmRq3765qTUSs/c3xF6lj8J8= 8 | github.com/jessevdk/go-flags v1.5.0 h1:1jKYvbxEjfUl0fmqTCOfonvskHHXMjBySTLW4y9LFvc= 9 | github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4= 10 | github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= 11 | github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= 12 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 13 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 14 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 15 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 16 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 17 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8= 18 | github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is= 19 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4= 20 | github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA= 21 | github.com/lestrrat-go/strftime v1.0.5 h1:A7H3tT8DhTz8u65w+JRpiBxM4dINQhUXAZnhBa2xeOE= 22 | github.com/lestrrat-go/strftime v1.0.5/go.mod h1:E1nN3pCbtMSu1yjSVeyuRFVm/U0xoR76fd03sz+Qz4g= 23 | github.com/lestrrat/go-server-starter-listener v0.0.0-20150507032651-00dd68592c85 h1:gxayjLkXkf5th4qXa32uk8PngRdBRGOiH+cn2bamRCQ= 24 | github.com/lestrrat/go-server-starter-listener v0.0.0-20150507032651-00dd68592c85/go.mod h1:qWioISoOtEGapIqSRs4MTbygZUEd8fsWzdmYu5bY8Bs= 25 | github.com/mercari/go-httpstats v1.0.0 h1:A9IdJ0mVUbSaSzuqvLxtTBlBIUfr7q8NSgfmfzS++V4= 26 | github.com/mercari/go-httpstats v1.0.0/go.mod h1:VhmqSP+4fQD93VIDjvnoI0IC9VLhTRhX0KBPJv8G6DU= 27 | github.com/montanaflynn/stats v0.6.3/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= 28 | github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ= 29 | github.com/montanaflynn/stats v0.6.6/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= 30 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 31 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 32 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 33 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 34 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 35 | github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4= 36 | github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 37 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 38 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 39 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 40 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 41 | github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= 42 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 43 | go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= 44 | go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 45 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723 h1:sHOAIxRGBp443oHZIPB+HsUGaksVCXVQENPxwTfQdH4= 46 | go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 47 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 48 | go.uber.org/multierr v1.7.0 h1:zaiO/rmgFjbmCXdSYJWQcdvOCsthmdaHfr3Gm2Kx4Ec= 49 | go.uber.org/multierr v1.7.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= 50 | go.uber.org/zap v1.19.1 h1:ue41HOKd1vGURxrmeKIgELGb3jPW9DMUDGtsinblHwI= 51 | go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= 52 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 53 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 54 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de h1:5hukYrvBGR8/eNkX5mdUezrA6JiaEZDtJb9Ei+1LlBs= 55 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 56 | golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= 57 | golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 58 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 59 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 60 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 61 | golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 62 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 63 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 64 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 65 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 66 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 67 | golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 68 | golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 69 | golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 70 | golang.org/x/sys v0.0.0-20210927052749-1cf2251ac284 h1:lBPNCmq8u4zFP3huKCmUQ2Fx8kcY4X+O12UgGnyKsrg= 71 | golang.org/x/sys v0.0.0-20210927052749-1cf2251ac284/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 72 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 73 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 74 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 75 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 76 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 77 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 78 | golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= 79 | golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 80 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 81 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 82 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 83 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 84 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 85 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 86 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 87 | gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= 88 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 89 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 90 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 91 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 92 | -------------------------------------------------------------------------------- /pidfile/pidfile.go: -------------------------------------------------------------------------------- 1 | package pidfile 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/pkg/errors" 10 | ) 11 | 12 | // WritePid : write pid to given file 13 | func WritePid(pidfile string) error { 14 | dir, filename := filepath.Split(pidfile) 15 | tmpfile, err := ioutil.TempFile(dir, filename+".*") 16 | if err != nil { 17 | return errors.Wrap(err, "Could not create tempfile") 18 | } 19 | _, err = tmpfile.WriteString(fmt.Sprintf("%d", os.Getpid())) 20 | if err != nil { 21 | tmpfile.Close() 22 | os.Remove(tmpfile.Name()) 23 | return errors.Wrap(err, "Could not write pid to tempfile") 24 | } 25 | tmpfile.Close() 26 | err = os.Rename(tmpfile.Name(), pidfile) 27 | if err != nil { 28 | os.Remove(tmpfile.Name()) 29 | return errors.Wrap(err, "Could not rename pidfile") 30 | } 31 | return nil 32 | } 33 | -------------------------------------------------------------------------------- /proxy/handler.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | // this class is based on https://github.com/r7kamura/entoverse 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "io" 9 | "net" 10 | "net/http" 11 | "net/url" 12 | "strings" 13 | "sync" 14 | 15 | "github.com/kazeburo/chocon/upstream" 16 | "github.com/rs/xid" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | const ( 21 | proxyVerHeader = "X-Chocon-Ver" 22 | proxyIDHeader = "X-Chocon-Id" 23 | httpStatusClientClosedRequest = 499 24 | ) 25 | 26 | // These headers won't be copied from original request to proxy request. 27 | var ignoredHeaderNames = map[string]struct{}{ 28 | "Connection": struct{}{}, 29 | "Keep-Alive": struct{}{}, 30 | "Proxy-Authenticate": struct{}{}, 31 | "Proxy-Authorization": struct{}{}, 32 | "Te": struct{}{}, 33 | "Trailers": struct{}{}, 34 | "Transfer-Encoding": struct{}{}, 35 | "Upgrade": struct{}{}, 36 | } 37 | 38 | // Status for override http status 39 | type Status struct { 40 | Code int 41 | } 42 | 43 | // Proxy : Provide host-based proxy server. 44 | type Proxy struct { 45 | Version string 46 | Transport http.RoundTripper 47 | upstream *upstream.Upstream 48 | logger *zap.Logger 49 | } 50 | 51 | var pool = sync.Pool{ 52 | New: func() interface{} { return make([]byte, 32*1024) }, 53 | } 54 | 55 | // New : Create a request-based reverse-proxy. 56 | func New(transport *http.RoundTripper, version string, upstream *upstream.Upstream, logger *zap.Logger) *Proxy { 57 | return &Proxy{ 58 | Version: version, 59 | Transport: *transport, 60 | upstream: upstream, 61 | logger: logger, 62 | } 63 | } 64 | 65 | func (proxy *Proxy) ServeHTTP(writer http.ResponseWriter, originalRequest *http.Request) { 66 | proxyID := originalRequest.Header.Get(proxyIDHeader) 67 | if proxyID == "" { 68 | proxyID = xid.New().String() 69 | originalRequest.Header.Set(proxyIDHeader, proxyID) 70 | } 71 | writer.Header().Set(proxyIDHeader, proxyID) 72 | 73 | // If request has Via: ViaHeader, stop request 74 | if originalRequest.Header.Get(proxyVerHeader) != "" { 75 | writer.WriteHeader(http.StatusLoopDetected) 76 | return 77 | } 78 | 79 | // Create a new proxy request object by coping the original request. 80 | proxyRequest := proxy.copyRequest(originalRequest) 81 | status := &Status{Code: http.StatusOK} 82 | 83 | if proxy.upstream.Enabled() { 84 | h, ipwc, err := proxy.upstream.Get() 85 | defer proxy.upstream.Release(ipwc) 86 | if err != nil { 87 | status.Code = http.StatusBadGateway 88 | } 89 | proxyRequest.URL.Scheme = proxy.upstream.GetScheme() 90 | proxyRequest.URL.Host = h 91 | proxyRequest.Host = originalRequest.Host 92 | } else { 93 | // Set Proxied 94 | originalRequest.Header.Set(proxyVerHeader, proxy.Version) 95 | // Convert an original request into another proxy request. 96 | proxy.rewriteProxyHost(originalRequest, proxyRequest, status) 97 | } 98 | if status.Code != http.StatusOK { 99 | writer.WriteHeader(status.Code) 100 | return 101 | } 102 | 103 | // Convert a request into a response by using its Transport. 104 | response, err := proxy.Transport.RoundTrip(proxyRequest) 105 | if err != nil { 106 | logger := proxy.logger.With( 107 | zap.String("request_host", originalRequest.Host), 108 | zap.String("request_path", originalRequest.URL.Path), 109 | zap.String("proxy_host", proxyRequest.URL.Host), 110 | zap.String("proxy_scheme", proxyRequest.URL.Scheme), 111 | zap.String("proxy_id", proxyID), 112 | ) 113 | if netErr, ok := err.(net.Error); ok && netErr.Timeout() { 114 | logger.Error("ErrorFromProxy", zap.Error(err)) 115 | writer.WriteHeader(http.StatusGatewayTimeout) 116 | } else if err == context.Canceled || err == io.ErrUnexpectedEOF { 117 | logger.Error("ErrorFromProxy", 118 | zap.Error(fmt.Errorf("%v: seems client closed request", err)), 119 | ) 120 | // For custom status code 121 | http.Error(writer, "Client Closed Request", httpStatusClientClosedRequest) 122 | } else { 123 | logger.Error("ErrorFromProxy", zap.Error(err)) 124 | writer.WriteHeader(http.StatusBadGateway) 125 | } 126 | return 127 | } 128 | 129 | buf := pool.Get().([]byte) 130 | defer func() { 131 | response.Body.Close() 132 | pool.Put(buf) 133 | }() 134 | 135 | // Copy all header fields. 136 | nv := 0 137 | for _, vv := range response.Header { 138 | nv += len(vv) 139 | } 140 | sv := make([]string, nv) 141 | for k, vv := range response.Header { 142 | if k == proxyIDHeader { 143 | continue 144 | } 145 | n := copy(sv, vv) 146 | writer.Header()[k] = sv[:n:n] 147 | sv = sv[n:] 148 | } 149 | 150 | // Copy a status code. 151 | writer.WriteHeader(response.StatusCode) 152 | 153 | // Copy a response body. 154 | io.CopyBuffer(writer, response.Body, buf) 155 | } 156 | 157 | func (proxy *Proxy) rewriteProxyHost(r *http.Request, pr *http.Request, ps *Status) { 158 | if r.Host == "" { 159 | ps.Code = http.StatusBadRequest 160 | return 161 | } 162 | hostPortSplit := strings.Split(r.Host, ":") 163 | host := hostPortSplit[0] 164 | port := "" 165 | if len(hostPortSplit) > 1 { 166 | port = ":" + hostPortSplit[1] 167 | } 168 | hostSplit := strings.Split(host, ".") 169 | lastPartIndex := 0 170 | for i, hostPart := range hostSplit { 171 | if hostPart == "ccnproxy-ssl" || hostPart == "ccnproxy-secure" || hostPart == "ccnproxy-https" || hostPart == "ccnproxy" { 172 | lastPartIndex = i 173 | } 174 | } 175 | if lastPartIndex == 0 { 176 | ps.Code = http.StatusBadRequest 177 | return 178 | } 179 | 180 | pr.URL.Host = strings.Join(hostSplit[0:lastPartIndex], ".") + port 181 | pr.Host = pr.URL.Host 182 | if hostSplit[lastPartIndex] == "ccnproxy-https" || hostSplit[lastPartIndex] == "ccnproxy-secure" || hostSplit[lastPartIndex] == "ccnproxy-ssl" { 183 | pr.URL.Scheme = "https" 184 | } 185 | } 186 | 187 | // Create a new proxy request with some modifications from an original request. 188 | func (proxy *Proxy) copyRequest(originalRequest *http.Request) *http.Request { 189 | proxyRequest := new(http.Request) 190 | proxyURL := new(url.URL) 191 | *proxyRequest = *originalRequest 192 | *proxyURL = *originalRequest.URL 193 | proxyRequest.URL = proxyURL 194 | proxyRequest.Proto = "HTTP/1.1" 195 | proxyRequest.ProtoMajor = 1 196 | proxyRequest.ProtoMinor = 1 197 | proxyRequest.Close = false 198 | proxyRequest.Header = make(http.Header) 199 | proxyRequest.URL.Scheme = "http" 200 | proxyRequest.URL.Path = originalRequest.URL.Path 201 | 202 | // Copy all header fields except ignoredHeaderNames'. 203 | nv := 0 204 | for _, vv := range originalRequest.Header { 205 | nv += len(vv) 206 | } 207 | sv := make([]string, nv) 208 | for k, vv := range originalRequest.Header { 209 | if _, ok := ignoredHeaderNames[k]; ok { 210 | continue 211 | } 212 | n := copy(sv, vv) 213 | proxyRequest.Header[k] = sv[:n:n] 214 | sv = sv[n:] 215 | } 216 | 217 | return proxyRequest 218 | } 219 | -------------------------------------------------------------------------------- /proxy/handler_test.go: -------------------------------------------------------------------------------- 1 | package proxy 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "net/http" 7 | "net/url" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var ( 14 | dummyProxy *Proxy 15 | dummyRequest *http.Request 16 | dummyURL *url.URL 17 | ) 18 | 19 | func init() { 20 | dummyProxy = &Proxy{} 21 | var err error 22 | dummyRequest, err = createDummyRequest() 23 | if err != nil { 24 | log.Fatal(err) 25 | } 26 | } 27 | 28 | func createDummyRequest() (*http.Request, error) { 29 | dummyHeaders := http.Header{ 30 | "User-Agent": {"dummy-client"}, 31 | "X-Chocon-Test-Value": {"6"}, 32 | // ignored headers 33 | "Connection": {"Keep-Alive"}, 34 | "Keep-Alive": {"timeout=30, max=100"}, 35 | "Proxy-Authenticate": {"Basic"}, 36 | "Proxy-Authorization": {"Basic dummy"}, 37 | "Te": {"deflate"}, 38 | "Trailers": {"Expires"}, 39 | "Transfer-Encoding": {"chunked"}, 40 | "Upgrade": {"WebSocket"}, 41 | } 42 | dummyURL = &url.URL{ 43 | Scheme: "http", 44 | Path: "/dummy", 45 | } 46 | req, err := http.NewRequest("GET", "/dummy", nil) 47 | if err != nil { 48 | return nil, err 49 | } 50 | req.Header = dummyHeaders 51 | req.Close = true 52 | req.Proto = "HTTP/1.0" 53 | req.ProtoMajor = 1 54 | req.ProtoMinor = 0 55 | req.URL = dummyURL 56 | 57 | return req, nil 58 | } 59 | 60 | func TestCopyRequest(t *testing.T) { 61 | req := dummyProxy.copyRequest(dummyRequest) 62 | 63 | assert.Equal(t, req.Proto, "HTTP/1.1") 64 | assert.Equal(t, req.ProtoMajor, 1) 65 | assert.Equal(t, req.ProtoMinor, 1) 66 | assert.Equal(t, req.Close, false) 67 | assert.Equal(t, req.URL.Scheme, "http") 68 | assert.Equal(t, req.URL.Path, dummyURL.Path) 69 | 70 | assert.Equal(t, req.Header["User-Agent"][0], "dummy-client") 71 | assert.Equal(t, req.Header["X-Chocon-Test-Value"][0], "6") 72 | 73 | for k := range req.Header { 74 | if _, ok := ignoredHeaderNames[k]; ok { 75 | assert.Fail(t, fmt.Sprintf("header filed: %s must be removed", k)) 76 | } 77 | } 78 | } 79 | 80 | func BenchmarkCopyRequest(b *testing.B) { 81 | for i := 0; i < b.N; i++ { 82 | _ = dummyProxy.copyRequest(dummyRequest) 83 | } 84 | } 85 | 86 | func BenchmarkRewriteHost(b *testing.B) { 87 | status := &Status{Code: http.StatusOK} 88 | originalReq, _ := http.NewRequest("GET", "/dummy", nil) 89 | originalReq.URL.Host = "example.com.ccnproxy:3000" 90 | originalReq.Host = originalReq.URL.Host 91 | req, _ := http.NewRequest("GET", "/dummy", nil) 92 | req.URL.Scheme = "http" 93 | for n := 0; n < b.N; n++ { 94 | dummyProxy.rewriteProxyHost(originalReq, req, status) 95 | } 96 | } 97 | 98 | func TestRewriteHost(t *testing.T) { 99 | cases := []struct { 100 | originalReqHost string 101 | reqHost string 102 | scheme string 103 | }{ 104 | {"example.com.ccnproxy:3000", "example.com:3000", "http"}, 105 | {"example.com.ccnproxy", "example.com", "http"}, 106 | {"example.com.ccnproxy.local:3000", "example.com:3000", "http"}, 107 | {"example.com.ccnproxy.local", "example.com", "http"}, 108 | {"example.com.ccnproxy-ssl:3000", "example.com:3000", "https"}, 109 | {"example.com.ccnproxy-ssl", "example.com", "https"}, 110 | } 111 | 112 | for _, c := range cases { 113 | t.Run(c.originalReqHost, func(t *testing.T) { 114 | status := &Status{Code: http.StatusOK} 115 | originalReq, _ := http.NewRequest("GET", "/dummy", nil) 116 | req, _ := http.NewRequest("GET", "/dummy", nil) 117 | req.URL.Scheme = "http" 118 | originalReq.URL.Host = c.originalReqHost 119 | originalReq.Host = originalReq.URL.Host 120 | dummyProxy.rewriteProxyHost(originalReq, req, status) 121 | assert.Equal(t, status.Code, http.StatusOK) 122 | assert.Equal(t, req.Host, c.reqHost) 123 | assert.Equal(t, req.URL.Scheme, c.scheme) 124 | }) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /upstream/upstream.go: -------------------------------------------------------------------------------- 1 | package upstream 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "net" 7 | "net/url" 8 | "sort" 9 | "strings" 10 | "sync" 11 | "time" 12 | 13 | "github.com/pkg/errors" 14 | "go.uber.org/zap" 15 | ) 16 | 17 | // Upstream struct 18 | type Upstream struct { 19 | scheme string 20 | port string 21 | host string 22 | ipwcs []*IPwc 23 | csum string 24 | logger *zap.Logger 25 | mu sync.Mutex 26 | // current resolved record version 27 | version uint64 28 | } 29 | 30 | // IPwc : IP with counter 31 | type IPwc struct { 32 | ip string 33 | // # requerst in busy 34 | busy int64 35 | // resolved record version 36 | version uint64 37 | } 38 | 39 | // New : 40 | func New(upstream string, logger *zap.Logger) (*Upstream, error) { 41 | var h string 42 | var p string 43 | var err error 44 | u := new(url.URL) 45 | 46 | if upstream != "" { 47 | u, err = url.Parse(upstream) 48 | if err != nil { 49 | return nil, errors.Wrap(err, "upsteam url is invalid") 50 | } 51 | if u.Scheme != "http" && u.Scheme != "https" { 52 | return nil, errors.New("upsteam url is invalid: upsteam url scheme should be http or https") 53 | } 54 | if u.Host == "" { 55 | return nil, errors.New("upsteam url is invalid: no hostname") 56 | } 57 | 58 | hostPortSplit := strings.Split(u.Host, ":") 59 | h = hostPortSplit[0] 60 | p = "" 61 | if len(hostPortSplit) > 1 { 62 | p = hostPortSplit[1] 63 | } 64 | } 65 | 66 | um := &Upstream{ 67 | scheme: u.Scheme, 68 | host: h, 69 | port: p, 70 | version: 0, 71 | logger: logger, 72 | } 73 | 74 | if um.Enabled() { 75 | ctx := context.Background() 76 | ipwcs, err := um.RefreshIP(ctx) 77 | if err != nil { 78 | return nil, errors.Wrap(err, "failed initial resolv hostname") 79 | } 80 | if len(ipwcs) < 1 { 81 | return nil, errors.New("Could not resolv hostname") 82 | } 83 | go um.Run(ctx) 84 | } 85 | return um, nil 86 | } 87 | 88 | // Enabled : upstream is enabled 89 | func (u *Upstream) Enabled() bool { 90 | return u.scheme != "" 91 | } 92 | 93 | // GetScheme : get upstream's scheme 94 | func (u *Upstream) GetScheme() string { 95 | return u.scheme 96 | } 97 | 98 | // RefreshIP : resolve hostname 99 | func (u *Upstream) RefreshIP(ctx context.Context) ([]*IPwc, error) { 100 | u.mu.Lock() 101 | u.version++ 102 | u.mu.Unlock() 103 | 104 | ctx, cancel := context.WithTimeout(ctx, 10*time.Second) 105 | addrs, err := net.DefaultResolver.LookupIPAddr(ctx, u.host) 106 | cancel() 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | sort.Slice(addrs, func(i, j int) bool { 112 | return addrs[i].IP.String() > addrs[j].IP.String() 113 | }) 114 | 115 | ips := make([]string, len(addrs)) 116 | ipwcs := make([]*IPwc, len(addrs)) 117 | for i, ia := range addrs { 118 | ips[i] = ia.IP.String() 119 | ipwcs[i] = &IPwc{ 120 | ip: ia.IP.String(), 121 | version: u.version, 122 | busy: 0, 123 | } 124 | } 125 | csum := strings.Join(ips, ",") 126 | u.mu.Lock() 127 | defer u.mu.Unlock() 128 | if csum != u.csum { 129 | u.csum = csum 130 | u.ipwcs = ipwcs 131 | } 132 | 133 | return ipwcs, nil 134 | } 135 | 136 | // Run : resolv hostname in background 137 | func (u *Upstream) Run(ctx context.Context) { 138 | ticker := time.NewTicker(3 * time.Second) 139 | defer ticker.Stop() 140 | for { 141 | select { 142 | case <-ctx.Done(): 143 | return 144 | case _ = <-ticker.C: 145 | //for _, ipwc := range u.ipwcs { 146 | // log.Printf("%v", ipwc) 147 | //} 148 | _, err := u.RefreshIP(ctx) 149 | if err != nil { 150 | u.logger.Error("failed refresh ip", zap.Error(err)) 151 | } 152 | } 153 | } 154 | } 155 | 156 | // Get : wild 157 | func (u *Upstream) Get() (string, *IPwc, error) { 158 | u.mu.Lock() 159 | defer u.mu.Unlock() 160 | 161 | if len(u.ipwcs) < 1 { 162 | return "", &IPwc{}, errors.New("No upstream hosts") 163 | } 164 | 165 | sort.Slice(u.ipwcs, func(i, j int) bool { 166 | if u.ipwcs[i].busy == u.ipwcs[j].busy { 167 | return rand.Intn(2) == 0 168 | } 169 | return u.ipwcs[i].busy < u.ipwcs[j].busy 170 | }) 171 | 172 | u.ipwcs[0].busy++ 173 | h := u.ipwcs[0].ip 174 | if u.port != "" { 175 | h = h + ":" + u.port 176 | } 177 | ipwc := &IPwc{ 178 | ip: u.ipwcs[0].ip, 179 | version: u.ipwcs[0].version, 180 | } 181 | return h, ipwc, nil 182 | } 183 | 184 | // Release : decrement counter 185 | func (u *Upstream) Release(o *IPwc) { 186 | u.mu.Lock() 187 | defer u.mu.Unlock() 188 | for i, ipwc := range u.ipwcs { 189 | if ipwc.ip == o.ip && ipwc.version == o.version { 190 | u.ipwcs[i].busy = u.ipwcs[i].busy - 1 191 | } 192 | } 193 | } 194 | --------------------------------------------------------------------------------