diff --git a/.travis.yml b/.travis.yml index 31658f44..e2b93e23 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,9 @@ script: - GOOS=linux GOARCH=386 make binaries - make ci - make crossbuild +cache: + directories: + - vendor env: global: - secure: hQ9wa4Wx5eWfG4q8yySldLFrHMGfXnMz2JBB8YyHIxkXraDpr+ltqrraVSN2LaTdSAQcDxDKnCwnqRDZ34PX64UDZe8941qwPTFSjFmdz0VcOwStInPxioaLBJg14xcAUl55vLpx5Qkc3XcyRaKBSRJdi+cUqSjQL6MzlrR8ERIOJQzJ4l2VJ+e5X7D917dPEV5K4Z3Rha3ILTLCIQY6fygPnjRD8hNJjwMlW30vavUYs+fg9adK3M+o4vm0IXWqy7aqpaAWhfkJXH+tVM7dzXuql6l/dgFkN2uIB2WK4XJZbHrLirt8ceoL3SYLolMdm6ldG5exYROhuhh6jMlJ+Gx05/088URNxTMEmZpAdJHVYX4BvHIvkNb6x/TCYW+/+mbtGVtg27obsnXXrZERT8Jxe6V6NMhAUQYFjqcvOu8ATp4h2xwmBHhx3sMP8t9Y1owuqrPciSujzeG+XoDguxHwjun41YJJZx030kARV53EsM2wFoePVV1h9YXXCglUAyaVkQMBLeui3O11zFunpz4limPIXTNK9zDeW+krPaJWx3qi8Yn3DwUV1aozDfMHPCKg4nhwLfGPwQNwuzZPjvTAoVmW+YY5b9ZrLRFn7ndKlFFQp8nk3ssWxX5bPJrWd9UZQ1eu+gAV4u1DLpGLvGo1eqM1RWdXWl6jSpLRiHY= diff --git a/Gopkg.lock b/Gopkg.lock new file mode 100644 index 00000000..886fc10e --- /dev/null +++ b/Gopkg.lock @@ -0,0 +1,479 @@ +# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. + + +[[projects]] + branch = "master" + name = "github.com/Shopify/logrus-bugsnag" + packages = ["."] + revision = "577dee27f20dd8f1a529f82210094af593be12bd" + +[[projects]] + name = "github.com/alicebob/miniredis" + packages = [ + ".", + "server" + ] + revision = "955f929b3a68c092ee308c4b340b337e759f8288" + +[[projects]] + branch = "master" + name = "github.com/ant0ine/go-json-rest" + packages = [ + "rest", + "rest/trie" + ] + revision = "ebb33769ae013bd5f518a8bac348c310dea768b8" + +[[projects]] + name = "github.com/beorn7/perks" + packages = ["quantile"] + revision = "4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9" + +[[projects]] + name = "github.com/bugsnag/bugsnag-go" + packages = [ + ".", + "errors" + ] + revision = "036f1af2a63f8133e596d1c127c86100b4642ba1" + version = "v1.3.0" + +[[projects]] + name = "github.com/bugsnag/panicwrap" + packages = ["."] + revision = "dd8df9a3778aaebc569794383e5c4ce87d6fd89e" + +[[projects]] + name = "github.com/davecgh/go-spew" + packages = ["spew"] + revision = "346938d642f2ec3594ed81d874461961cd0faa76" + version = "v1.1.0" + +[[projects]] + name = "github.com/fsnotify/fsnotify" + packages = ["."] + revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" + version = "v1.4.7" + +[[projects]] + name = "github.com/garyburd/redigo" + packages = [ + "internal", + "redis" + ] + revision = "a69d19351219b6dd56f274f96d85a7014a2ec34e" + version = "v1.6.0" + +[[projects]] + name = "github.com/go-chi/chi" + packages = [ + ".", + "middleware" + ] + revision = "e83ac2304db3c50cf03d96a2fcd39009d458bc35" + version = "v3.3.2" + +[[projects]] + name = "github.com/go-test/deep" + packages = ["."] + revision = "9898238679c264cfb10411539f14a0553dc8b295" + version = "v1.0.0" + +[[projects]] + name = "github.com/golang/protobuf" + packages = ["proto"] + revision = "1e59b77b52bf8e4b449a57e6f79f21226d571845" + +[[projects]] + name = "github.com/gomodule/redigo" + packages = [ + "internal", + "redis" + ] + revision = "9c11da706d9b7902c6da69c592f75637793fe121" + version = "v2.0.0" + +[[projects]] + name = "github.com/hashicorp/hcl" + packages = [ + ".", + "hcl/ast", + "hcl/parser", + "hcl/printer", + "hcl/scanner", + "hcl/strconv", + "hcl/token", + "json/parser", + "json/scanner", + "json/token" + ] + revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" + +[[projects]] + branch = "master" + name = "github.com/jbenet/go-context" + packages = ["io"] + revision = "d14ea06fba99483203c19d92cfcd13ebe73135f4" + +[[projects]] + name = "github.com/jpillora/backoff" + packages = ["."] + revision = "8eab2debe79d12b7bd3d10653910df25fa9552ba" + version = "1.0.0" + +[[projects]] + branch = "master" + name = "github.com/kardianos/osext" + packages = ["."] + revision = "ae77be60afb1dcacde03767a8c37337fad28ac14" + +[[projects]] + name = "github.com/kevinburke/ssh_config" + packages = ["."] + revision = "802051befeb51da415c46972b5caf36e7c33c53d" + +[[projects]] + name = "github.com/magiconair/properties" + packages = ["."] + revision = "49d762b9817ba1c2e9d0c69183c2b4a8b8f1d934" + +[[projects]] + name = "github.com/mattn/go-colorable" + packages = ["."] + revision = "6cc8b475d4682021d75d2cbe2bc481bec4ce98e5" + +[[projects]] + branch = "master" + name = "github.com/mattn/go-isatty" + packages = ["."] + revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c" + +[[projects]] + name = "github.com/matttproud/golang_protobuf_extensions" + packages = ["pbutil"] + revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" + version = "v1.0.1" + +[[projects]] + branch = "master" + name = "github.com/mgutz/ansi" + packages = ["."] + revision = "9520e82c474b0a04dd04f8a40959027271bab992" + +[[projects]] + name = "github.com/mitchellh/go-homedir" + packages = ["."] + revision = "b8bc1bf767474819792c23f32d8286a45736f1c6" + +[[projects]] + name = "github.com/mitchellh/mapstructure" + packages = ["."] + revision = "b4575eea38cca1123ec2dc90c26529b5c5acfcff" + +[[projects]] + name = "github.com/patrickmn/go-cache" + packages = ["."] + revision = "a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0" + version = "v2.1.0" + +[[projects]] + name = "github.com/pelletier/go-buffruneio" + packages = ["."] + revision = "c37440a7cf42ac63b919c752ca73a85067e05992" + version = "v0.2.0" + +[[projects]] + name = "github.com/pelletier/go-toml" + packages = ["."] + revision = "9bf0212445a9fb4769aca5d2d86fbc381af39d52" + +[[projects]] + name = "github.com/philhofer/fwd" + packages = ["."] + revision = "bb6d471dc95d4fe11e432687f8b70ff496cf3136" + version = "v1.0.0" + +[[projects]] + name = "github.com/pkg/errors" + packages = ["."] + revision = "e881fd58d78e04cf6d0de1217f8707c8cc2249bc" + +[[projects]] + name = "github.com/pmezard/go-difflib" + packages = ["difflib"] + revision = "792786c7400a136282c1664665ae0a8db921c6c2" + version = "v1.0.0" + +[[projects]] + name = "github.com/prometheus/client_golang" + packages = [ + "prometheus", + "prometheus/promhttp" + ] + revision = "c5b7fccd204277076155f10851dad72b76a49317" + version = "v0.8.0" + +[[projects]] + name = "github.com/prometheus/client_model" + packages = ["go"] + revision = "99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c" + +[[projects]] + name = "github.com/prometheus/common" + packages = [ + "expfmt", + "internal/bitbucket.org/ww/goautoneg", + "model" + ] + revision = "89604d197083d4781071d3c65855d24ecfb0a563" + +[[projects]] + name = "github.com/prometheus/procfs" + packages = [ + ".", + "xfs" + ] + revision = "b15cd069a83443be3154b719d0cc9fe8117f09fb" + +[[projects]] + branch = "master" + name = "github.com/rafaeljusto/redigomock" + packages = ["."] + revision = "1d32475ae23cbe2e221e166e452da3279d3dc7bf" + +[[projects]] + name = "github.com/rs/xid" + packages = ["."] + revision = "5cbef93d023392114398b3b2e4a44d81d3f8e049" + +[[projects]] + name = "github.com/sergi/go-diff" + packages = ["diffmatchpatch"] + revision = "1744e2970ca51c86172c8190fadad617561ed6e7" + version = "v1.0.0" + +[[projects]] + name = "github.com/sirupsen/logrus" + packages = ["."] + revision = "d682213848ed68c0a260ca37d6dd5ace8423f5ba" + version = "v1.0.4" + +[[projects]] + name = "github.com/soheilhy/cmux" + packages = ["."] + revision = "e09e9389d85d8492d313d73d1469c029e710623f" + version = "v0.1.4" + +[[projects]] + name = "github.com/spf13/afero" + packages = [ + ".", + "mem" + ] + revision = "bb8f1927f2a9d3ab41c9340aa034f6b803f4359c" + version = "v1.0.2" + +[[projects]] + name = "github.com/spf13/cast" + packages = ["."] + revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4" + version = "v1.1.0" + +[[projects]] + branch = "master" + name = "github.com/spf13/jwalterweatherman" + packages = ["."] + revision = "7c0cea34c8ece3fbeb2b27ab9b59511d360fb394" + +[[projects]] + name = "github.com/spf13/pflag" + packages = ["."] + revision = "4c012f6dcd9546820e378d0bdda4d8fc772cdfea" + +[[projects]] + name = "github.com/spf13/viper" + packages = ["."] + revision = "aafc9e6bc7b7bb53ddaa75a5ef49a17d6e654be5" + +[[projects]] + name = "github.com/src-d/gcfg" + packages = [ + ".", + "scanner", + "token", + "types" + ] + revision = "f187355171c936ac84a82793659ebb4936bc1c23" + version = "v1.3.0" + +[[projects]] + name = "github.com/stretchr/testify" + packages = ["assert"] + revision = "87b1dfb5b2fa649f52695dd9eae19abe404a4308" + +[[projects]] + branch = "master" + name = "github.com/superfly/tlstest" + packages = ["."] + revision = "688815416c7e3edb2eb09d9dc14a31f9cb388cec" + +[[projects]] + name = "github.com/tinylib/msgp" + packages = ["msgp"] + revision = "b2b6a672cf1e5b90748f79b8b81fc8c5cf0571a1" + version = "1.0.2" + +[[projects]] + name = "github.com/ulule/limiter" + packages = ["."] + revision = "619f3ae8cc00f54934d27591e7010c8f6216c5ca" + version = "v1.0.0" + +[[projects]] + name = "github.com/x-cray/logrus-prefixed-formatter" + packages = ["."] + revision = "bb2702d423886830dee131692131d35648c382e2" + version = "v0.5.2" + +[[projects]] + name = "github.com/xanzy/ssh-agent" + packages = ["."] + revision = "ba9c9e33906f58169366275e3450db66139a31a9" + version = "v0.1.0" + +[[projects]] + name = "golang.org/x/crypto" + packages = [ + "cast5", + "curve25519", + "ed25519", + "ed25519/internal/edwards25519", + "openpgp", + "openpgp/armor", + "openpgp/elgamal", + "openpgp/errors", + "openpgp/packet", + "openpgp/s2k", + "ssh", + "ssh/agent", + "ssh/knownhosts", + "ssh/terminal" + ] + revision = "9419663f5a44be8b34ca85f08abc5fe1be11f8a3" + +[[projects]] + name = "golang.org/x/net" + packages = [ + "context", + "http2", + "http2/hpack", + "idna", + "lex/httplex" + ] + revision = "5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec" + +[[projects]] + name = "golang.org/x/sys" + packages = [ + "unix", + "windows" + ] + revision = "fff93fa7cd278d84afc205751523809c464168ab" + +[[projects]] + name = "golang.org/x/text" + packages = [ + "collate", + "collate/build", + "internal/colltab", + "internal/gen", + "internal/tag", + "internal/triegen", + "internal/ucd", + "language", + "secure/bidirule", + "transform", + "unicode/bidi", + "unicode/cldr", + "unicode/norm", + "unicode/rangetable" + ] + revision = "e19ae1496984b1c655b8044a65c0300a3c878dd3" + +[[projects]] + name = "gopkg.in/src-d/go-billy.v4" + packages = [ + ".", + "helper/chroot", + "helper/polyfill", + "osfs", + "util" + ] + revision = "e940f8b62a8e61adc71f69802c1cc8305b64ec96" + version = "v4.0.2" + +[[projects]] + name = "gopkg.in/src-d/go-git.v4" + packages = [ + ".", + "config", + "internal/revision", + "plumbing", + "plumbing/cache", + "plumbing/filemode", + "plumbing/format/config", + "plumbing/format/diff", + "plumbing/format/gitignore", + "plumbing/format/idxfile", + "plumbing/format/index", + "plumbing/format/objfile", + "plumbing/format/packfile", + "plumbing/format/pktline", + "plumbing/object", + "plumbing/protocol/packp", + "plumbing/protocol/packp/capability", + "plumbing/protocol/packp/sideband", + "plumbing/revlist", + "plumbing/storer", + "plumbing/transport", + "plumbing/transport/client", + "plumbing/transport/file", + "plumbing/transport/git", + "plumbing/transport/http", + "plumbing/transport/internal/common", + "plumbing/transport/server", + "plumbing/transport/ssh", + "storage", + "storage/filesystem", + "storage/filesystem/internal/dotgit", + "storage/memory", + "utils/binary", + "utils/diff", + "utils/ioutil", + "utils/merkletrie", + "utils/merkletrie/filesystem", + "utils/merkletrie/index", + "utils/merkletrie/internal/frame", + "utils/merkletrie/noder" + ] + revision = "bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8" + version = "v4.0.0" + +[[projects]] + name = "gopkg.in/warnings.v0" + packages = ["."] + revision = "ec4a0fea49c7b46c2aeb0b51aac55779c607e52b" + version = "v0.1.2" + +[[projects]] + name = "gopkg.in/yaml.v2" + packages = ["."] + revision = "d670f9405373e636a5a2765eea47fac0c9bc91a4" + version = "v2.0.0" + +[solve-meta] + analyzer-name = "dep" + analyzer-version = 1 + inputs-digest = "77b36588d1c8055287e97f9b93f9f96133579cb73c5bde193c9d8c6ed3bb7c46" + solver-name = "gps-cdcl" + solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml new file mode 100644 index 00000000..3168b6d4 --- /dev/null +++ b/Gopkg.toml @@ -0,0 +1,89 @@ +# Gopkg.toml example +# +# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md +# for detailed Gopkg.toml documentation. +# +# required = ["github.com/user/thing/cmd/thing"] +# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] +# +# [[constraint]] +# name = "github.com/user/project" +# version = "1.0.0" +# +# [[constraint]] +# name = "github.com/user/project2" +# branch = "dev" +# source = "github.com/myfork/project2" +# +# [[override]] +# name = "github.com/x/y" +# version = "2.4.0" +# +# [prune] +# non-go = false +# go-tests = true +# unused-packages = true + + +[[constraint]] + branch = "master" + name = "github.com/Shopify/logrus-bugsnag" + +[[constraint]] + name = "github.com/bugsnag/bugsnag-go" + version = "1.1.1" + +[[constraint]] + name = "github.com/go-test/deep" + version = "1.0.0" + +[[constraint]] + name = "github.com/jpillora/backoff" + version = "1.0.0" + +[[constraint]] + name = "github.com/prometheus/client_golang" + version = "0.8.0" + +[[constraint]] + name = "github.com/sirupsen/logrus" + version = "1.0.3" + +[[constraint]] + branch = "master" + name = "github.com/superfly/tlstest" + +[[constraint]] + name = "github.com/tinylib/msgp" + version = "1.0.2" + +[[constraint]] + name = "github.com/ulule/limiter" + version = "1.0.0" + +[[constraint]] + name = "github.com/x-cray/logrus-prefixed-formatter" + version = "0.5.2" + +[[constraint]] + name = "gopkg.in/src-d/go-git.v4" + version = "4.0.0" + +[prune] + go-tests = true + unused-packages = true + +[[constraint]] + name = "github.com/go-chi/chi" + version = "3.3.2" + +[[constraint]] + name = "github.com/rafaeljusto/redigomock" + branch = "master" + +[[constraint]] + name = "github.com/gomodule/redigo" + version = "2.0.0" +[[constraint]] + name = "github.com/soheilhy/cmux" + version = "0.1.4" diff --git a/Makefile b/Makefile index 0ab17f81..bdfc260b 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,8 @@ setup: ## install dependencies @go get -u github.com/golang/lint/golint @go get -u github.com/gordonklaus/ineffassign @go get -u github.com/client9/misspell/cmd/misspell - @go get -u github.com/Masterminds/glide - @glide install + @go get -u github.com/golang/dep/cmd/dep + @dep ensure # Depends on binaries because vet will silently fail if it can't load compiled # imports diff --git a/api/api.go b/api/api.go new file mode 100644 index 00000000..de6521c2 --- /dev/null +++ b/api/api.go @@ -0,0 +1,227 @@ +package api + +import ( + "context" + "encoding/json" + "io" + "net" + "net/http" + "strings" + "sync" + "time" + + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/gomodule/redigo/redis" + "github.com/sirupsen/logrus" + "github.com/superfly/wormhole/server" +) + +const ( + authHeader = "authorization" + authHeaderPrefix = "Token " + tlsEndpointPrefix = "tls:" +) + +type key uint8 + +const ( + keyBackendID key = iota +) + +var ( + errWrongAuthHeader = errorResponse{`Authorization header value should use "Token ..." format`} + errNotFound = errorResponse{`Not Found`} + errGenericServerProblem = errorResponse{`Internal Server Error`} +) + +// Handler handles the API HTTP server +type Handler struct { + logger *logrus.Entry + router chi.Router + redisPool *redis.Pool +} + +// NewServer ... +func NewServer(logger *logrus.Logger, redisPool *redis.Pool) *http.Server { + return &http.Server{Handler: NewHandler(logger, redisPool)} +} + +// NewHandler creates a new API handler +func NewHandler(logger *logrus.Logger, redisPool *redis.Pool) *Handler { + r := chi.NewRouter() + h := &Handler{logger: logger.WithFields(logrus.Fields{"prefix": "api"}), router: r, redisPool: redisPool} + r.Use(middleware.RequestLogger(&middleware.DefaultLogFormatter{ + Logger: logger, + })) + + r.Route("/api/v1", func(r chi.Router) { + r.Use(jsonMiddleware) + r.Use(h.authMiddleware) + + r.Get("/servers", h.servers) + r.Get("/backend/endpoints", h.endpoints) + }) + + return h +} + +type errorResponse struct { + Error string `json:"error"` +} + +func jsonMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.Header().Set("content-type", "application/json") + next.ServeHTTP(w, req) + }) +} + +func (h *Handler) authMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + auth := req.Header.Get(authHeader) + w.Header().Set("content-type", "application/json") + + if auth == "" || !strings.HasPrefix(auth, authHeaderPrefix) { + jsonResponse(w, errWrongAuthHeader, http.StatusUnauthorized) + return + } + + token := strings.TrimSpace(strings.TrimPrefix(auth, authHeaderPrefix)) + + if token == "" { + jsonResponse(w, errWrongAuthHeader, http.StatusUnauthorized) + return + } + + redisConn := h.redisPool.Get() + defer redisConn.Close() + + backendID, err := redis.String(redisConn.Do("HGET", "backend_tokens", token)) + if err != nil { + if err == redis.ErrNil { + jsonResponse(w, errNotFound, http.StatusUnauthorized) + return + } + h.logger.Error(err) + jsonResponse(w, errGenericServerProblem, http.StatusInternalServerError) + return + } + + next.ServeHTTP(w, req.WithContext(context.WithValue(req.Context(), keyBackendID, backendID))) + }) +} + +func jsonResponse(w http.ResponseWriter, j interface{}, code int) { + w.WriteHeader(code) + b, _ := json.Marshal(j) + w.Write(b) +} + +// ServeHTTP serves the wormhole API +func (h *Handler) ServeHTTP(w http.ResponseWriter, req *http.Request) { + h.router.ServeHTTP(w, req) +} + +func (h *Handler) servers(w http.ResponseWriter, req *http.Request) { + redisConn := h.redisPool.Get() + defer redisConn.Close() + + t := time.Now().Unix() + rawServers, err := redis.ByteSlices(redisConn.Do("ZREVRANGEBYSCORE", "servers", "+inf", t-30)) + if err != nil { + if err == redis.ErrNil { + jsonResponse(w, []struct{}{}, http.StatusOK) + return + } + h.logger.Error(err) + jsonResponse(w, errGenericServerProblem, http.StatusInternalServerError) + return + } + + servers := make([]server.Representation, 0) + for _, rs := range rawServers { + var s server.Representation + _, err := s.UnmarshalMsg(rs) + if err != nil { + h.logger.Error(err) + } + servers = append(servers, s) + } + + jsonResponse(w, servers, http.StatusOK) + +} + +func (h *Handler) endpoints(w http.ResponseWriter, req *http.Request) { + backendID := req.Context().Value(keyBackendID).(string) + + redisConn := h.redisPool.Get() + defer redisConn.Close() + + endpoints, err := redis.Strings(redisConn.Do("SMEMBERS", "backend:"+backendID+":endpoints")) + if err != nil { + if err == redis.ErrNil { + jsonResponse(w, errNotFound, http.StatusNotFound) + return + } + h.logger.Error(err) + jsonResponse(w, errGenericServerProblem, http.StatusInternalServerError) + return + } + + goodEndpoints := []map[string]string{} + + if len(endpoints) > 0 { + for _, ep := range endpoints { + if strings.HasPrefix(ep, tlsEndpointPrefix) { + m, err := redis.StringMap(redisConn.Do("HGETALL", "backend:"+backendID+":endpoint:"+ep)) + if err == nil { + goodEndpoints = append(goodEndpoints, map[string]string{ + "address": strings.TrimPrefix(ep, tlsEndpointPrefix), + "cluster": m["cluster"], + "region": m["region"], + "connected_at": m["connected_at"], + "last_seen_at": m["last_seen_at"], + }) + } + } + } + + } + jsonResponse(w, goodEndpoints, http.StatusOK) +} + +// SingleConnListener is a listener that accepts a single, stored, conn +type SingleConnListener struct { + listener net.Listener + conn net.Conn + done bool + doneMutex sync.Mutex +} + +// NewSingleConnListener creates a new single connection listener +func NewSingleConnListener(l net.Listener, c net.Conn) *SingleConnListener { + return &SingleConnListener{listener: l, conn: c, done: false} +} + +// Accept accepts a connection once and then EOFs +func (l *SingleConnListener) Accept() (net.Conn, error) { + l.doneMutex.Lock() + if l.done { + return nil, io.EOF + } + l.done = true + l.doneMutex.Unlock() + return l.conn, nil +} + +// Close does nothing +func (l *SingleConnListener) Close() error { + return nil +} + +// Addr defers to the stored original listener +func (l *SingleConnListener) Addr() net.Addr { + return l.listener.Addr() +} diff --git a/api/api_test.go b/api/api_test.go new file mode 100644 index 00000000..cfb5d3ed --- /dev/null +++ b/api/api_test.go @@ -0,0 +1,149 @@ +package api + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gomodule/redigo/redis" + "github.com/rafaeljusto/redigomock" + "github.com/sirupsen/logrus" + "github.com/stretchr/testify/assert" +) + +var ( + mockRedisConn *redigomock.Conn + mockRedisPool *redis.Pool + handler *Handler +) + +func TestAPIHandlerAuth(t *testing.T) { + // no auth + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1", nil) + handler.ServeHTTP(rr, req) + res := rr.Result() + + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + assert.Equal(t, "application/json", res.Header.Get("content-type")) + + // wrong format + rr = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/api/v1", nil) + req.Header.Set("authorization", "bad auth") + handler.ServeHTTP(rr, req) + res = rr.Result() + + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + assert.Equal(t, "application/json", res.Header.Get("content-type")) + + // wrong format + rr = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/api/v1", nil) + req.Header.Set("authorization", "Token ") + handler.ServeHTTP(rr, req) + res = rr.Result() + + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + assert.Equal(t, "application/json", res.Header.Get("content-type")) + + // wrong token + cmd := mockRedisConn.Command("HGET", "backend_tokens", "blah").ExpectError(redis.ErrNil) + + rr = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/api/v1", nil) + req.Header.Set("authorization", "Token blah") + handler.ServeHTTP(rr, req) + res = rr.Result() + + if mockRedisConn.Stats(cmd) != 1 { + t.Fatal("Command was not used") + } + + assert.Equal(t, http.StatusUnauthorized, res.StatusCode) + assert.Equal(t, "application/json", res.Header.Get("content-type")) + + // right token + cmd = mockRedisConn.Command("HGET", "backend_tokens", "test").Expect("123") + + rr = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/api/v1", nil) + req.Header.Set("authorization", "Token test") + handler.ServeHTTP(rr, req) + res = rr.Result() + + if mockRedisConn.Stats(cmd) != 1 { + t.Fatal("Command was not used") + } + + assert.Equal(t, http.StatusNotFound, res.StatusCode) + + // redis error + cmd = mockRedisConn.Command("HGET", "backend_tokens", "error").ExpectError(fmt.Errorf("error")) + + rr = httptest.NewRecorder() + req = httptest.NewRequest(http.MethodGet, "/api/v1", nil) + req.Header.Set("authorization", "Token error") + handler.ServeHTTP(rr, req) + res = rr.Result() + + if mockRedisConn.Stats(cmd) != 1 { + t.Fatal("Command was not used") + } + + assert.Equal(t, http.StatusInternalServerError, res.StatusCode) + assert.Equal(t, "application/json", res.Header.Get("content-type")) +} + +func TestAPIHandlerEndpoints(t *testing.T) { + cmd := mockRedisConn.Command("HGET", "backend_tokens", "testendpoints").Expect("123") + cmdEndpoints := mockRedisConn.Command("SMEMBERS", "backend:123:endpoints").ExpectStringSlice("tls:helloworld.wormhole.test:1234") + + now := time.Now().String() + cmdEndpoint := mockRedisConn.Command("HGETALL", "backend:123:endpoint:tls:helloworld.wormhole.test:1234").ExpectMap(map[string]string{ + "cluster": "wormhole.test", + "region": "test region", + "connected_at": now, + "last_seen_at": now, + }) + + rr := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/api/v1/backend/endpoints", nil) + req.Header.Set("authorization", "Token testendpoints") + handler.ServeHTTP(rr, req) + res := rr.Result() + + if mockRedisConn.Stats(cmd) != 1 { + t.Fatal("Command was not used") + } + if mockRedisConn.Stats(cmdEndpoints) != 1 { + t.Fatal("SMEMBERS Endpoints command was not used") + } + if mockRedisConn.Stats(cmdEndpoint) != 1 { + t.Fatal("HGETALL Endpoint command was not used") + } + + assert.Equal(t, http.StatusOK, res.StatusCode) + assert.Equal(t, "application/json", res.Header.Get("content-type")) + body, _ := ioutil.ReadAll(res.Body) + expectedBody, _ := json.Marshal([]map[string]string{{ + "address": "helloworld.wormhole.test:1234", + "cluster": "wormhole.test", + "region": "test region", + "connected_at": now, + "last_seen_at": now, + }}) + assert.JSONEq(t, string(expectedBody), string(body)) +} + +func init() { + mockRedisConn = redigomock.NewConn() + mockRedisPool = redis.NewPool(func() (redis.Conn, error) { + return mockRedisConn, nil + }, 10) + handler = NewHandler(logrus.New(), mockRedisPool) +} diff --git a/config/config.go b/config/config.go index 8ea4c4fe..46c2a216 100644 --- a/config/config.go +++ b/config/config.go @@ -139,6 +139,9 @@ type ServerConfig struct { // MetricsAPIPort used by HTTP server to serve metrics // Used by Prometheus to scrape wormhole server endpoint MetricsAPIPort string + + // Region represents the location of the wormhole server + Region string } // NewServerConfig parses config values collected from Viper and validates them @@ -153,6 +156,8 @@ func NewServerConfig() (*ServerConfig, error) { viper.SetDefault("shared_tls_forwarding_port", "443") viper.BindEnv("bugsnag_api_key", "BUGSNAG_API_KEY") + viper.BindEnv("region") + logger := logrus.New() logger.Formatter = new(prefixed.TextFormatter) logger.Level = parseLogLevel(viper.GetString("log_level")) @@ -192,6 +197,7 @@ func NewServerConfig() (*ServerConfig, error) { MetricsAPIPort: viper.GetString("metrics_api_port"), UseSharedPortForwarding: viper.GetBool("use_shared_port_forwarding"), SharedTLSForwardingPort: viper.GetString("shared_tls_forwarding_port"), + Region: viper.GetString("region"), Config: shared, } diff --git a/glide.lock b/glide.lock deleted file mode 100644 index e1c60feb..00000000 --- a/glide.lock +++ /dev/null @@ -1,311 +0,0 @@ -hash: 1aa2847713f8fea2ff189bd96061a43b2cac482fb2fcba3c58782af2a80c293d -updated: 2018-01-15T21:30:58.071567-08:00 -imports: -- name: github.com/ant0ine/go-json-rest - version: ebb33769ae013bd5f518a8bac348c310dea768b8 - subpackages: - - rest - - rest/trie -- name: github.com/beorn7/perks - version: 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 - subpackages: - - quantile -- name: github.com/bugsnag/bugsnag-go - version: 036f1af2a63f8133e596d1c127c86100b4642ba1 - subpackages: - - errors -- name: github.com/bugsnag/panicwrap - version: dd8df9a3778aaebc569794383e5c4ce87d6fd89e -- name: github.com/fsnotify/fsnotify - version: c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9 -- name: github.com/garyburd/redigo - version: 8873b2f1995f59d4bcdd2b0dc9858e2cb9bf0c13 - subpackages: - - internal - - redis -- name: github.com/golang/protobuf - version: 1e59b77b52bf8e4b449a57e6f79f21226d571845 - subpackages: - - proto -- name: github.com/hashicorp/hcl - version: 23c074d0eceb2b8a5bfdbb271ab780cde70f05a8 - subpackages: - - hcl/ast - - hcl/parser - - hcl/printer - - hcl/scanner - - hcl/strconv - - hcl/token - - json/parser - - json/scanner - - json/token -- name: github.com/jbenet/go-context - version: d14ea06fba99483203c19d92cfcd13ebe73135f4 - subpackages: - - io -- name: github.com/jpillora/backoff - version: 8eab2debe79d12b7bd3d10653910df25fa9552ba -- name: github.com/kardianos/osext - version: ae77be60afb1dcacde03767a8c37337fad28ac14 -- name: github.com/kevinburke/ssh_config - version: 802051befeb51da415c46972b5caf36e7c33c53d -- name: github.com/klauspost/cpuid - version: 09cded8978dc9e80714c4d85b0322337b0a1e5e0 -- name: github.com/magiconair/properties - version: 49d762b9817ba1c2e9d0c69183c2b4a8b8f1d934 -- name: github.com/mattn/go-colorable - version: 6cc8b475d4682021d75d2cbe2bc481bec4ce98e5 -- name: github.com/mattn/go-isatty - version: 6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c -- name: github.com/matttproud/golang_protobuf_extensions - version: c12348ce28de40eed0136aa2b644d0ee0650e56c - subpackages: - - pbutil -- name: github.com/mgutz/ansi - version: 9520e82c474b0a04dd04f8a40959027271bab992 -- name: github.com/mitchellh/go-homedir - version: b8bc1bf767474819792c23f32d8286a45736f1c6 -- name: github.com/mitchellh/mapstructure - version: b4575eea38cca1123ec2dc90c26529b5c5acfcff -- name: github.com/patrickmn/go-cache - version: a3647f8e31d79543b2d0f0ae2fe5c379d72cedc0 -- name: github.com/pelletier/go-buffruneio - version: c37440a7cf42ac63b919c752ca73a85067e05992 -- name: github.com/pelletier/go-toml - version: 9bf0212445a9fb4769aca5d2d86fbc381af39d52 -- name: github.com/philhofer/fwd - version: bb6d471dc95d4fe11e432687f8b70ff496cf3136 -- name: github.com/pkg/errors - version: e881fd58d78e04cf6d0de1217f8707c8cc2249bc -- name: github.com/prometheus/client_golang - version: c5b7fccd204277076155f10851dad72b76a49317 - subpackages: - - prometheus - - prometheus/promhttp -- name: github.com/prometheus/client_model - version: 99fa1f4be8e564e8a6b613da7fa6f46c9edafc6c - subpackages: - - go -- name: github.com/prometheus/common - version: 89604d197083d4781071d3c65855d24ecfb0a563 - subpackages: - - expfmt - - internal/bitbucket.org/ww/goautoneg - - model -- name: github.com/prometheus/procfs - version: b15cd069a83443be3154b719d0cc9fe8117f09fb - subpackages: - - xfs -- name: github.com/rs/xid - version: 5cbef93d023392114398b3b2e4a44d81d3f8e049 -- name: github.com/sergi/go-diff - version: 1744e2970ca51c86172c8190fadad617561ed6e7 - subpackages: - - diffmatchpatch -- name: github.com/Shopify/logrus-bugsnag - version: 577dee27f20dd8f1a529f82210094af593be12bd -- name: github.com/sirupsen/logrus - version: d682213848ed68c0a260ca37d6dd5ace8423f5ba -- name: github.com/spf13/afero - version: bb8f1927f2a9d3ab41c9340aa034f6b803f4359c - subpackages: - - mem -- name: github.com/spf13/cast - version: acbeb36b902d72a7a4c18e8f3241075e7ab763e4 -- name: github.com/spf13/jwalterweatherman - version: 7c0cea34c8ece3fbeb2b27ab9b59511d360fb394 -- name: github.com/spf13/pflag - version: 4c012f6dcd9546820e378d0bdda4d8fc772cdfea -- name: github.com/spf13/viper - version: aafc9e6bc7b7bb53ddaa75a5ef49a17d6e654be5 -- name: github.com/src-d/gcfg - version: f187355171c936ac84a82793659ebb4936bc1c23 - subpackages: - - scanner - - token - - types -- name: github.com/tinylib/msgp - version: b2b6a672cf1e5b90748f79b8b81fc8c5cf0571a1 - subpackages: - - msgp -- name: github.com/ulule/limiter - version: 619f3ae8cc00f54934d27591e7010c8f6216c5ca -- name: github.com/x-cray/logrus-prefixed-formatter - version: bb2702d423886830dee131692131d35648c382e2 -- name: github.com/xanzy/ssh-agent - version: ba9c9e33906f58169366275e3450db66139a31a9 -- name: golang.org/x/crypto - version: 9419663f5a44be8b34ca85f08abc5fe1be11f8a3 - subpackages: - - curve25519 - - ed25519 - - ed25519/internal/edwards25519 - - ssh - - ssh/agent - - ssh/knownhosts - - ssh/terminal -- name: golang.org/x/net - version: 5ccada7d0a7ba9aeb5d3aca8d3501b4c2a509fec - subpackages: - - context - - context/ctxhttp - - http2 - - http2/hpack - - idna - - lex/httplex -- name: golang.org/x/sys - version: fff93fa7cd278d84afc205751523809c464168ab - subpackages: - - unix - - windows -- name: golang.org/x/text - version: e19ae1496984b1c655b8044a65c0300a3c878dd3 - subpackages: - - secure/bidirule - - transform - - unicode/bidi - - unicode/norm -- name: gopkg.in/src-d/go-billy.v4 - version: e940f8b62a8e61adc71f69802c1cc8305b64ec96 - subpackages: - - helper/chroot - - helper/polyfill - - osfs - - util -- name: gopkg.in/src-d/go-git.v4 - version: bf3b1f1fb9e0a04d0f87511a7ded2562b48a19d8 - subpackages: - - config - - internal/revision - - plumbing - - plumbing/cache - - plumbing/filemode - - plumbing/format/config - - plumbing/format/diff - - plumbing/format/gitignore - - plumbing/format/idxfile - - plumbing/format/index - - plumbing/format/objfile - - plumbing/format/packfile - - plumbing/format/pktline - - plumbing/object - - plumbing/protocol/packp - - plumbing/protocol/packp/capability - - plumbing/protocol/packp/sideband - - plumbing/revlist - - plumbing/storer - - plumbing/transport - - plumbing/transport/client - - plumbing/transport/file - - plumbing/transport/git - - plumbing/transport/http - - plumbing/transport/internal/common - - plumbing/transport/server - - plumbing/transport/ssh - - storage - - storage/filesystem - - storage/filesystem/internal/dotgit - - storage/memory - - utils/binary - - utils/diff - - utils/ioutil - - utils/merkletrie - - utils/merkletrie/filesystem - - utils/merkletrie/index - - utils/merkletrie/internal/frame - - utils/merkletrie/noder -- name: gopkg.in/vmihailenco/msgpack.v2 - version: 200315d5c8aab339f5a5cd44219f4b50ad0782df -- name: gopkg.in/warnings.v0 - version: ec4a0fea49c7b46c2aeb0b51aac55779c607e52b -- name: gopkg.in/yaml.v2 - version: d670f9405373e636a5a2765eea47fac0c9bc91a4 -testImports: -- name: github.com/alicebob/miniredis - version: 955f929b3a68c092ee308c4b340b337e759f8288 - subpackages: - - server -- name: github.com/Azure/go-ansiterm - version: d6e3b3328b783f23731bc4d058875b0371ff8109 - subpackages: - - winterm -- name: github.com/cenk/backoff - version: 2ea60e5f094469f9e65adb9cd103795b73ae743e -- name: github.com/containerd/continuity - version: b2b946a77f5973f420514090d6f6dd58b08303f0 - subpackages: - - pathdriver -- name: github.com/davecgh/go-spew - version: ecdeabc65495df2dec95d7c4a4c3e021903035e5 - subpackages: - - spew -- name: github.com/docker/docker - version: 3c7990fb63c546e0d0443e4c74a1ee15b16d005c - subpackages: - - api/types - - api/types/blkiodev - - api/types/container - - api/types/filters - - api/types/mount - - api/types/network - - api/types/registry - - api/types/strslice - - api/types/swarm - - api/types/swarm/runtime - - api/types/versions - - opts - - pkg/archive - - pkg/fileutils - - pkg/homedir - - pkg/idtools - - pkg/ioutils - - pkg/jsonmessage - - pkg/longpath - - pkg/mount - - pkg/pools - - pkg/stdcopy - - pkg/system - - pkg/term - - pkg/term/windows -- name: github.com/docker/go-connections - version: 3ede32e2033de7505e6500d6c868c2b9ed9f169d - subpackages: - - nat -- name: github.com/docker/go-units - version: d59758554a3d3911fa25c0269de1ebe2f1912c39 -- name: github.com/fsouza/go-dockerclient - version: 413e380d74dfeddac90c0b89c598a1c7b19f5c54 -- name: github.com/go-test/deep - version: 9898238679c264cfb10411539f14a0553dc8b295 -- name: github.com/gogo/protobuf - version: 160de10b2537169b5ae3e7e221d28269ef40d311 - subpackages: - - proto -- name: github.com/Microsoft/go-winio - version: b7c3cf0d1286c7b62b140ed960a00b154c1b6fcc -- name: github.com/Nvveen/Gotty - version: cd527374f1e5bff4938207604a14f2e38a9cf512 -- name: github.com/opencontainers/go-digest - version: 279bed98673dd5bef374d3b6e4b09e2af76183bf -- name: github.com/opencontainers/image-spec - version: 577479e4dc273d3779f00c223c7e0dba4cd6b8b0 - subpackages: - - specs-go - - specs-go/v1 -- name: github.com/opencontainers/runc - version: ab4a8191679806893207d9c1e507a77ff46dafaa - subpackages: - - libcontainer/system - - libcontainer/user -- name: github.com/pmezard/go-difflib - version: 792786c7400a136282c1664665ae0a8db921c6c2 - subpackages: - - difflib -- name: github.com/stretchr/testify - version: 87b1dfb5b2fa649f52695dd9eae19abe404a4308 - subpackages: - - assert -- name: github.com/superfly/tlstest - version: 688815416c7e3edb2eb09d9dc14a31f9cb388cec -- name: gopkg.in/ory-am/dockertest.v3 - version: 59e045640952457da1046932eae73e20cb3afe70 diff --git a/glide.yaml b/glide.yaml deleted file mode 100644 index 7e5d24f9..00000000 --- a/glide.yaml +++ /dev/null @@ -1,32 +0,0 @@ -package: github.com/superfly/wormhole -import: -- package: github.com/jpillora/backoff -- package: github.com/x-cray/logrus-prefixed-formatter -- package: github.com/garyburd/redigo - version: ~1.0.0 -- package: gopkg.in/vmihailenco/msgpack.v2 - version: ~2.7.4 -- package: github.com/rs/xid -- package: github.com/klauspost/cpuid - version: ~1.0.0 -- package: gopkg.in/src-d/go-git.v4 - version: v4 -- package: github.com/spf13/viper -- package: github.com/Shopify/logrus-bugsnag -- package: github.com/bugsnag/bugsnag-go - version: ^1.1.1 -- package: github.com/prometheus/client_golang - version: ^0.8.0 - subpackages: - - prometheus -- package: github.com/ulule/limiter - version: ~1.x -- package: github.com/sirupsen/logrus - version: ^1.0.3 -- package: github.com/tinylib/msgp - version: ^1.0.2 -testImport: -- package: github.com/alicebob/miniredis -- package: github.com/go-test/deep -- package: github.com/stretchr/testify -- package: github.com/superfly/tlstest diff --git a/integration_test.go b/integration_test.go index 755f1796..71a0734a 100644 --- a/integration_test.go +++ b/integration_test.go @@ -12,7 +12,7 @@ import ( "github.com/superfly/wormhole" "github.com/superfly/wormhole/config" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" ) var fakeRedis *miniredis.Miniredis diff --git a/local/http2_handler_test.go b/local/http2_handler_test.go index 300f4b00..863d66ea 100644 --- a/local/http2_handler_test.go +++ b/local/http2_handler_test.go @@ -134,7 +134,7 @@ func TestHTTP2Handler(t *testing.T) { assert.NoError(t, err, "Should be no error creating test handler") t.Run("Test_dial", func(t *testing.T) { - sConnCh := make(chan *net.TCPConn) + sConnCh := make(chan net.Conn) go func(ln *net.TCPListener) { s, err := ln.AcceptTCP() diff --git a/net/single_port_listener_factory.go b/net/single_port_listener_factory.go index e968fb72..3d1937b3 100644 --- a/net/single_port_listener_factory.go +++ b/net/single_port_listener_factory.go @@ -119,6 +119,7 @@ func (sl *sharedPortTLSListenerFactory) populateCh() error { sl.logger.Errorf("Error finding ID from SNI: %+v", err) return } + sl.fLock.Lock() ch, ok := sl.forward[id] sl.fLock.Unlock() diff --git a/net/utils.go b/net/utils.go index 13103b9a..925c397a 100644 --- a/net/utils.go +++ b/net/utils.go @@ -3,12 +3,13 @@ package net import ( "crypto/tls" "fmt" - "golang.org/x/net/http2" "io" "net" "reflect" "strings" "time" + + "golang.org/x/net/http2" ) // CopyDirection describes the direction of data copying in full-duplex link @@ -52,7 +53,7 @@ type TLSWrapperFunc func(conn net.Conn, cfg *tls.Config) *tls.Conn // GenericTLSWrap takes a TCP connection, a tls config, and an upgrade function // and returns the new connection -func GenericTLSWrap(conn *net.TCPConn, cfg *tls.Config, tFunc TLSWrapperFunc) (*tls.Conn, error) { +func GenericTLSWrap(conn net.Conn, cfg *tls.Config, tFunc TLSWrapperFunc) (*tls.Conn, error) { var tConn *tls.Conn tCfg := cfg.Clone() @@ -92,7 +93,7 @@ func GenericTLSWrap(conn *net.TCPConn, cfg *tls.Config, tFunc TLSWrapperFunc) (* // While technically the golang implementation will allow us not to perform ALPN, // this breaks the http/2 spec. The goal here is to follow the RFC to the letter // as documented in http://httpwg.org/specs/rfc7540.html#starting -func HTTP2ALPNTLSWrap(conn *net.TCPConn, cfg *tls.Config, tFunc TLSWrapperFunc) (*tls.Conn, error) { +func HTTP2ALPNTLSWrap(conn net.Conn, cfg *tls.Config, tFunc TLSWrapperFunc) (*tls.Conn, error) { protoCfg := cfg.Clone() // TODO: append here protoCfg.NextProtos = []string{http2.NextProtoTLS} diff --git a/remote.go b/remote.go index ddc3a6d3..d83b92d7 100644 --- a/remote.go +++ b/remote.go @@ -1,17 +1,22 @@ package wormhole import ( + "crypto/tls" + "net" "net/url" "os" "os/signal" "syscall" "time" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/sirupsen/logrus" + "github.com/soheilhy/cmux" + "github.com/superfly/wormhole/api" "github.com/superfly/wormhole/config" wnet "github.com/superfly/wormhole/net" handler "github.com/superfly/wormhole/remote" + wserver "github.com/superfly/wormhole/server" "github.com/superfly/wormhole/session" tlsc "github.com/superfly/wormhole/tls" ) @@ -37,6 +42,15 @@ func StartRemote(cfg *config.ServerConfig) { log.Fatalf("Could not create listener factory: %+v", err) } + l, err := net.Listen("tcp", ":"+cfg.Port) + if err != nil { + log.Fatal(err) + } + + m := cmux.New(l) + httpL := m.Match(cmux.TLS()) + tcpL := m.Match(cmux.Any()) + switch cfg.Protocol { case config.SSH: h, err = handler.NewSSHHandler(cfg, registry, redisPool, listenerFactory) @@ -58,20 +72,32 @@ func StartRemote(cfg *config.ServerConfig) { } go handleDeath(h, registry) - server.ListenAndServe(":"+cfg.Port, h) -} -func listenerFactoryFromConfig(registry *session.Registry, cfg *config.ServerConfig) (wnet.ListenerFactory, error) { - multiPortFactoryArgs := &wnet.MultiPortTCPListenerFactoryArgs{ - BindAddr: "0.0.0.0", - Logger: cfg.Logger, + crt, err := tls.X509KeyPair(cfg.SharedPortTLSCert, cfg.SharedPortTLSPrivateKey) + if err != nil { + log.Fatal("could not parse tls key/value pair", err) } + tlsl := tls.NewListener(httpL, &tls.Config{ + Certificates: []tls.Certificate{crt}, + }) - listenerFactory, err := wnet.NewMultiPortTCPListenerFactory(multiPortFactoryArgs) + rep, err := wserver.Representation{Address: cfg.ClusterURL, Port: cfg.Port, Region: cfg.Region}.MarshalMsg(nil) if err != nil { - return nil, err + log.Fatal(err) } + go api.NewServer(cfg.Logger, redisPool).Serve(tlsl) + go server.Serve(tcpL, h) + go session.NewRedisStore(redisPool).Announce(rep) + if err := m.Serve(); err != nil { + log.Error("server error", err) + exitGracefully(h, registry) + } +} + +func listenerFactoryFromConfig(registry *session.Registry, cfg *config.ServerConfig) (wnet.ListenerFactory, error) { + var listenerFactory wnet.ListenerFactory + if cfg.UseSharedPortForwarding { tlsconf, err := tlsc.NewConfig(cfg.SharedPortTLSCert, cfg.SharedPortTLSPrivateKey, registry) if err != nil { @@ -94,10 +120,6 @@ func listenerFactoryFromConfig(registry *session.Registry, cfg *config.ServerCon Factory: sharedL, ShouldCleanup: true, }, - { - Factory: listenerFactory, - ShouldCleanup: true, - }, }, Logger: cfg.Logger, } @@ -164,11 +186,15 @@ func handleDeath(h handler.Handler, r *session.Registry) { signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func(c <-chan os.Signal) { for range c { - log.Print("Cleaning up before exit...") - h.Close() - r.Close() - log.Print("Cleaned up connections.") - os.Exit(1) + exitGracefully(h, r) } }(c) } + +func exitGracefully(h handler.Handler, r *session.Registry) { + log.Print("Cleaning up before exit...") + h.Close() + r.Close() + log.Print("Cleaned up connections.") + os.Exit(1) +} diff --git a/remote/handler.go b/remote/handler.go index 711481e2..afe5fc42 100644 --- a/remote/handler.go +++ b/remote/handler.go @@ -6,6 +6,6 @@ import "net" // It's the entry point for starting a session, managing a handshake, auth // and encryption (e.g. SSH) type Handler interface { - Serve(*net.TCPConn) + Serve(net.Conn) Close() } diff --git a/remote/http2_handler.go b/remote/http2_handler.go index 5881744f..0ece741a 100644 --- a/remote/http2_handler.go +++ b/remote/http2_handler.go @@ -5,7 +5,7 @@ import ( "io" "net" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/sirupsen/logrus" "github.com/superfly/wormhole/config" "github.com/superfly/wormhole/messages" @@ -51,7 +51,7 @@ func NewHTTP2Handler(cfg *config.ServerConfig, registry *session.Registry, pool // We are explicit with the *net.TCPConn since we need to be this way - and let the handler and // sessions handle wrapping in TLS. Having a TCPConn all the way down will highlight the dangers // of sending data over the socket without first wrapping in TLS -func (h *HTTP2Handler) Serve(conn *net.TCPConn) { +func (h *HTTP2Handler) Serve(conn net.Conn) { tlsConn, err := h.genericTLSWrap(conn) if err != nil { h.logger.Errorf("error establishing tls session: " + err.Error()) @@ -119,7 +119,7 @@ func (h *HTTP2Handler) Serve(conn *net.TCPConn) { } } -func (h *HTTP2Handler) genericTLSWrap(conn *net.TCPConn) (*tls.Conn, error) { +func (h *HTTP2Handler) genericTLSWrap(conn net.Conn) (*tls.Conn, error) { return wnet.GenericTLSWrap(conn, h.tlsConfig, tls.Server) } @@ -127,7 +127,7 @@ func (h *HTTP2Handler) genericTLSWrap(conn *net.TCPConn) (*tls.Conn, error) { // While technically the golang implementation will allow us not to perform ALPN, // this breaks the http/2 spec. The goal here is to follow the RFC to the letter // as documented in http://httpwg.org/specs/rfc7540.html#starting -func (h *HTTP2Handler) http2ALPNTLSWrap(conn *net.TCPConn) (*tls.Conn, error) { +func (h *HTTP2Handler) http2ALPNTLSWrap(conn net.Conn) (*tls.Conn, error) { return wnet.HTTP2ALPNTLSWrap(conn, h.tlsConfig, tls.Server) } diff --git a/remote/http2_handler_test.go b/remote/http2_handler_test.go index 6935e3b3..c19f9c0b 100644 --- a/remote/http2_handler_test.go +++ b/remote/http2_handler_test.go @@ -14,8 +14,9 @@ import ( "testing" "time" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/pkg/errors" + "github.com/rafaeljusto/redigomock" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/superfly/tlstest" @@ -24,18 +25,20 @@ import ( wnet "github.com/superfly/wormhole/net" "github.com/superfly/wormhole/session" "golang.org/x/net/http2" - "gopkg.in/ory-am/dockertest.v3" ) -var redisPool *redis.Pool -var registry *session.Registry -var serverTLSConfig *tls.Config -var clientTLSConfig *tls.Config -var listenerFactory wnet.ListenerFactory - -var serverTLSCert tls.Certificate -var serverCrtPEM []byte -var serverKeyPEM []byte +var ( + redisConn *redigomock.Conn + redisPool *redis.Pool + registry *session.Registry + serverTLSConfig *tls.Config + clientTLSConfig *tls.Config + listenerFactory wnet.ListenerFactory + + serverTLSCert tls.Certificate + serverCrtPEM []byte + serverKeyPEM []byte +) type testListenerFactory struct{} @@ -74,35 +77,35 @@ func TestMain(m *testing.M) { registry = session.NewRegistry(log.New()) - pool, err := dockertest.NewPool("") - if err != nil { - log.Fatalf("Dockertest could not connect to docker: %s", err) - } - - redisResource, err := pool.Run("redis", "4.0.1", []string{}) - if err != nil { - log.Fatalf("Could not create redis container") - } - - if err := pool.Retry(func() error { - var err error - c, err := redis.DialURL(fmt.Sprintf("redis://127.0.0.1:%s", redisResource.GetPort("6379/tcp"))) - if err != nil { - return err - } - _, err = c.Do("PING") - return err - }); err != nil { - log.Fatalf("Could not connect to redis container: %s", err) - } - - redisPool = newRedisPool(fmt.Sprintf("redis://localhost:%s", redisResource.GetPort("6379/tcp"))) + // pool, err := dockertest.NewPool("") + // if err != nil { + // log.Fatalf("Dockertest could not connect to docker: %s", err) + // } + + // redisResource, err := pool.Run("redis", "4.0.1", []string{}) + // if err != nil { + // log.Fatalf("Could not create redis container") + // } + + // if err := pool.Retry(func() error { + // var err error + // c, err := redis.DialURL(fmt.Sprintf("redis://127.0.0.1:%s", redisResource.GetPort("6379/tcp"))) + // if err != nil { + // return err + // } + // _, err = c.Do("PING") + // return err + // }); err != nil { + // log.Fatalf("Could not connect to redis container: %s", err) + // } + + // redisPool = newRedisPool(fmt.Sprintf("redis://localhost:%s", redisResource.GetPort("6379/tcp"))) code := m.Run() - if err := pool.Purge(redisResource); err != nil { - log.Fatalf("Could not purge redis: %s", err) - } + // if err := pool.Purge(redisResource); err != nil { + // log.Fatalf("Could not purge redis: %s", err) + // } os.Exit(code) } @@ -293,6 +296,20 @@ func wrapClientConn(cConn *net.TCPConn, tlsConf *tls.Config, alpn bool) (*tls.Co } func TestCreatesFullSession(t *testing.T) { + redisConn.Command("HMSET", redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData()) + + redisConn.Command("ZADD", redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData()) + + redisConn.Command("SADD", redigomock.NewAnyData(), redigomock.NewAnyData()) + + redisConn.Command("MULTI") + + redisConn.Command("HSET", redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData()) + + redisConn.Command("HMSET", redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData(), redigomock.NewAnyData()) + + redisConn.Command("EXEC") + h, err := newTestHTTP2Handler() assert.NoError(t, err, "Should be no error creating new HTTP2Handler") assert.NotNil(t, h, "Handler shouldn't be nil") @@ -364,3 +381,10 @@ func TestCreatesFullSession(t *testing.T) { // TODO: Test throughput // This is dependent on registering backend IDs with token upon creation like the SSH handler currently does } + +func init() { + redisConn = redigomock.NewConn() + redisPool = redis.NewPool(func() (redis.Conn, error) { + return redisConn, nil + }, 10) +} diff --git a/remote/server.go b/remote/server.go index 44e4a890..957657d9 100644 --- a/remote/server.go +++ b/remote/server.go @@ -1,7 +1,6 @@ package remote import ( - "fmt" "net" "github.com/sirupsen/logrus" @@ -12,35 +11,32 @@ type Server struct { Logger *logrus.Logger } -// ListenAndServe accepts incoming wormhole connections and passes them to the handler -func (s *Server) ListenAndServe(addr string, handler Handler) error { +// Serve accepts incoming wormhole connections and passes them to the handler +func (s *Server) Serve(listener net.Listener, handler Handler) error { log := s.Logger.WithFields(logrus.Fields{"prefix": "Server"}) - listener, err := s.newTCPListener(addr) - if err != nil { - return fmt.Errorf("Failed to listen on %s (%s)", addr, err.Error()) - } for { - tcpConn, err := listener.AcceptTCP() + conn, err := listener.Accept() if err != nil { log.Errorf("Failed to accept wormhole connection (%s)", err.Error()) break } - log.Debugln("Accepted wormhole TCP conn from:", tcpConn.RemoteAddr()) - go handler.Serve(tcpConn) + log.Println("Accepted wormhole TCP conn from:", conn.RemoteAddr()) + + go handler.Serve(conn) } return nil } -func (s *Server) newTCPListener(addr string) (*net.TCPListener, error) { - ln, err := net.Listen("tcp", addr) - if err != nil { - return nil, err - } - tcpLN, ok := ln.(*net.TCPListener) - if !ok { - return nil, fmt.Errorf("Could not create tcp listener") - } - return tcpLN, nil -} +// func (s *Server) newTCPListener(addr string) (*net.TCPListener, error) { +// ln, err := net.Listen("tcp", addr) +// if err != nil { +// return nil, err +// } +// tcpLN, ok := ln.(*net.TCPListener) +// if !ok { +// return nil, fmt.Errorf("Could not create tcp listener") +// } +// return tcpLN, nil +// } diff --git a/remote/ssh_handler.go b/remote/ssh_handler.go index e63084b9..6684e954 100644 --- a/remote/ssh_handler.go +++ b/remote/ssh_handler.go @@ -6,7 +6,7 @@ import ( "net" "time" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/sirupsen/logrus" "github.com/superfly/wormhole/config" wnet "github.com/superfly/wormhole/net" @@ -26,6 +26,7 @@ type SSHHandler struct { nodeID string localhost string clusterURL string + region string registry *session.Registry pool *redis.Pool logger *logrus.Entry @@ -54,6 +55,7 @@ func NewSSHHandler(cfg *config.ServerConfig, registry *session.Registry, pool *r registry: registry, localhost: cfg.Localhost, clusterURL: cfg.ClusterURL, + region: cfg.Region, pool: pool, config: config, logger: cfg.Logger.WithFields(logrus.Fields{"prefix": "SSHHandler"}), @@ -64,7 +66,7 @@ func NewSSHHandler(cfg *config.ServerConfig, registry *session.Registry, pool *r } // Serve accepts incoming wormhole connections and passes them to the handler -func (s *SSHHandler) Serve(conn *net.TCPConn) { +func (s *SSHHandler) Serve(conn net.Conn) { conn.RemoteAddr() ctx, err := s.limiter.Get(ipForConn(conn)) if err != nil { @@ -92,7 +94,7 @@ func makeConfig(key []byte) (*ssh.ServerConfig, error) { func (s *SSHHandler) sshSessionHandler(conn net.Conn) { // Before use, a handshake must be performed on the incoming net.Conn. - sess := session.NewSSHSession(s.logger.Logger, s.clusterURL, s.nodeID, s.pool, conn, s.config) + sess := session.NewSSHSession(s.logger.Logger, s.clusterURL, s.nodeID, s.region, s.pool, conn, s.config) err := sess.RequireStream() if err != nil { s.logger.WithField("client_addr", conn.RemoteAddr().String()).Errorln("error getting a stream:", err) diff --git a/remote/tcp_handler.go b/remote/tcp_handler.go index e2ca58dd..cbc44526 100644 --- a/remote/tcp_handler.go +++ b/remote/tcp_handler.go @@ -4,7 +4,7 @@ import ( "crypto/tls" "net" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/sirupsen/logrus" "github.com/superfly/wormhole/config" "github.com/superfly/wormhole/messages" @@ -53,7 +53,7 @@ func NewTCPHandler(cfg *config.ServerConfig, registry *session.Registry, pool *r } // Serve accepts incoming wormhole connections and passes them to the handler -func (h *TCPHandler) Serve(conn *net.TCPConn) { +func (h *TCPHandler) Serve(conn net.Conn) { var useConn net.Conn if h.tlsConfig != nil { var err error diff --git a/scripts/build.sh b/scripts/build.sh index a6c9e175..e215ae68 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -37,12 +37,12 @@ else BUILD_UPLOAD=${BUILD_UPLOAD:-false} fi -# download glide if it's not available -if [ ! -x "$(command -v glide)" ]; then - go get github.com/Masterminds/glide +# download dep if it's not available +if [ ! -x "$(command -v dep)" ]; then + go get github.com/golang/dep/cmd/dep fi -glide --debug install +dep ensure MD5='md5sum' unamestr=`uname` @@ -119,18 +119,19 @@ fi echo "Pushing binaries to S3" -go get -u github.com/minio/mc +curl -Lk https://dl.minio.io/client/mc/release/linux-amd64/mc > ./mc +chmod +x ./mc -mc config host add s3 https://s3.amazonaws.com $AWS_S3_ACCESS_KEY_ID $AWS_S3_SECRET_ACCESS_KEY +./mc config host add s3 https://s3.amazonaws.com $AWS_S3_ACCESS_KEY_ID $AWS_S3_SECRET_ACCESS_KEY echo "Pushing to s3/flyio-wormhole-builds/$VERSION/" -mc -q mirror --overwrite pkg/ s3/flyio-wormhole-builds/$VERSION +./mc -q mirror --overwrite pkg/ s3/flyio-wormhole-builds/$VERSION echo "Pushing to s3/flyio-wormhole-builds/$CHANNEL/" # also set the version as the latest # TODO: there must be a better way to copy/symlink objects in S3 instead of uploading again -mc -q mirror --overwrite pkg/ s3/flyio-wormhole-builds/$CHANNEL +./mc -q mirror --overwrite pkg/ s3/flyio-wormhole-builds/$CHANNEL echo "Building and pushing to Docker Hub" diff --git a/scripts/cert.pem b/scripts/cert.pem index 90f5d8ea..6bc1c8fe 100644 --- a/scripts/cert.pem +++ b/scripts/cert.pem @@ -1,34 +1,10 @@ -----BEGIN CERTIFICATE----- -MIIF9DCCA9ygAwIBAgIJAMSP/M6ISA+PMA0GCSqGSIb3DQEBBQUAMFkxCzAJBgNV -BAYTAkFVMRMwEQYDVQQIEwpTb21lLVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBX -aWRnaXRzIFB0eSBMdGQxEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xNzAyMDQwMDEx -MzJaFw0xODAyMDQwMDExMzJaMFkxCzAJBgNVBAYTAkFVMRMwEQYDVQQIEwpTb21l -LVN0YXRlMSEwHwYDVQQKExhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQxEjAQBgNV -BAMTCWxvY2FsaG9zdDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMAX -QKwzFGabn6V85oRjmlE0Lhljsgyh4RcuNETcGF3HXZZJqxPS2aqH4XkaC/IXA2I/ -LsCjdzBltvOsm9diYRZZe0Q6lpH/enwPfygdMbO9QH0ldq3atMTiCLHt+hDiK1/8 -gz3hGG7naSHBeXD8lEpXVPV0mARmJlRiiI7QW6pBQu19QYpuxg4x0Pq7QeWWfprE -rWz1DZE+pturtQklShAlRKuTiWrQ2ssGyK9lMj1BCxrob6TC3sAhQQWhHYu+cNrL -7M76SE4zmF3zOQ7pb69J6vd1u7OOC/xCO3DQQq6qUU5rvL7Rf7lUcMYEmtXoa+wK -N7C1W5CtRkqt9vxoWrD9jomKcks0ggwzX2H6738CJlLrCzFyZRxJoEgCgZKg8gD/ -uTTirUP+/w15y2YAjl2X9moZKShRuP/IwekQih++6prNzEbfjGt3+ZB1K36AfDSx -6mtVwf6wwhXmMmpx4EYCf3UKRPd8/mPk915HrgulMdPskrDK33LqR0Y9wLiSLV8T -/Cn/wZiXam3EZ6zx6NkuPFMiQ8IM5gKdksY58bCNuMd9/WzDB5CsEahDIY6p683/ -iGwwOADpwHxmZwdlQ+t+UOgkaFxq3QMnRppprE5o1bcKC0MTLmgTTGrTeQJ6sKCw -73QIAKHrlRx8H7ocLnRiiXmuWPMMI7p8J/R9u1qNAgMBAAGjgb4wgbswHQYDVR0O -BBYEFJV1lsSP08gS/42kfVKfdBSZPI5mMIGLBgNVHSMEgYMwgYCAFJV1lsSP08gS -/42kfVKfdBSZPI5moV2kWzBZMQswCQYDVQQGEwJBVTETMBEGA1UECBMKU29tZS1T -dGF0ZTEhMB8GA1UEChMYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMRIwEAYDVQQD -Ewlsb2NhbGhvc3SCCQDEj/zOiEgPjzAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEB -BQUAA4ICAQAzNcwrmTZ38fAtYZ619SkCdNyHPzemsJ7GV5HtXHc+gnf7w7hnKMdE -U+nDmJ2TCxXPuOMTiDfn1UCGmc+15EDZHW8C++R86ZkjwH+O2I8aFeJIPZ2rf8kV -5UgcKTUMIehYts8r4K+I7tSwkrTJGHuFn4pgtw44UF5qy+jTkySaIx8diM/upKZP -vGnG29h8r69SctNCJFTIyq8By68Q0Y8yHItCLgyi08TgLXslUDUUxoh9ZapieLUs -pizE/h6wqKLHkvkGpA6eNJxIRdVLEKNMdEvLTcK7wQ+tNF/F/Abe/Cw6EpvE9PA0 -J85zLM5CXZDKa3OhB6g9noGc8jO43paUQ+MsNZKo+7tb1Q2KR8R90BCWFJDkQ3IT -GC03+A4m1EsP3mtKZ9TAs74Asc2TI5P19Gv7Vtlqw65KdzMgiwt9lkPH6Nwk4OIM -5l9AVIo4hdhmqbtyWFvrdPdSBhORnG8zPfMIv77LG/5UeGrL6q2mzvZm89tLhgaF -OwdZEejbqSHQ+34QeavyszRB3mxPmjSLZtQ545BJkDGg9oGu2zX6dp+HQv2sNNaZ -1WSSK/HLL22bWETx/HCCg11mnDjIIG5mmygBY/ty0u6DbHnFiaUthHzK3CrjU35U -LYfLNB2z6HRHwB69fceb1UZqUMeZv9trw9Ti/ByCd1eBG+BvdSCAkg== +MIIBbTCCAROgAwIBAgIQNpfKC40S87skG1VGWUrODzAKBggqhkjOPQQDAjASMRAw +DgYDVQQKEwdBY21lIENvMB4XDTE4MDgyMDE4NDAyNFoXDTE5MDgyMDE4NDAyNFow +EjEQMA4GA1UEChMHQWNtZSBDbzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNVs +mCkRbKrf9M+cxZ5pCd/l4VbTY/Rs5gdg9Tks8ZriuJ2ekXVqbsuWGBjgnzwhQznE +ClY1encRv0rdufEsTYqjSzBJMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggr +BgEFBQcDATAMBgNVHRMBAf8EAjAAMBQGA1UdEQQNMAuCCWxvY2FsaG9zdDAKBggq +hkjOPQQDAgNIADBFAiAhTErnYc705unqx4QKR7gSzuvYOoDBWij8q4xEfwGQyQIh +AJJW4E5sNeRDvx46Pqyte69xUHkoB+3So2OCfYCLneA/ -----END CERTIFICATE----- diff --git a/scripts/key.pem b/scripts/key.pem index 4b76ce19..955d9771 100644 --- a/scripts/key.pem +++ b/scripts/key.pem @@ -1,51 +1,5 @@ ------BEGIN RSA PRIVATE KEY----- -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 ------END RSA PRIVATE KEY----- +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIMr8rVEL3OU67GX0LHnM357XpNi5zJY5R+Rls5dDZrXooAoGCCqGSM49 +AwEHoUQDQgAE1WyYKRFsqt/0z5zFnmkJ3+XhVtNj9GzmB2D1OSzxmuK4nZ6RdWpu +y5YYGOCfPCFDOcQKVjV6dxG/St258SxNig== +-----END EC PRIVATE KEY----- diff --git a/scripts/wormhole-server.sh b/scripts/wormhole-server.sh index 9191cfde..cbf7a1fa 100755 --- a/scripts/wormhole-server.sh +++ b/scripts/wormhole-server.sh @@ -22,8 +22,8 @@ export FLY_SHARED_TLS_FORWARDING_PORT=5442 export FLY_LOG_LEVEL=debug export FLY_REDIS_URL=redis://127.0.0.1:6379 export FLY_CLUSTER_URL=127.0.0.1 -export FLY_LOCALHOST=localhost -export FLY_NODE_ID=localhost +export FLY_LOCALHOST=wormhole.test +export FLY_NODE_ID=wormhole.test export FLY_TLS_CERT_FILE=$GOPATH/src/github.com/superfly/wormhole/scripts/cert.pem export FLY_TLS_PRIVATE_KEY_FILE=$GOPATH/src/github.com/superfly/wormhole/scripts/key.pem diff --git a/server/representation.go b/server/representation.go new file mode 100644 index 00000000..bc390a84 --- /dev/null +++ b/server/representation.go @@ -0,0 +1,10 @@ +package server + +//go:generate msgp + +// Representation ... +type Representation struct { + Address string `msg:"url" json:"address"` + Port string `msg:"port" json:"port"` + Region string `msg:"region" json:"region"` +} diff --git a/server/representation_gen.go b/server/representation_gen.go new file mode 100644 index 00000000..6c10790c --- /dev/null +++ b/server/representation_gen.go @@ -0,0 +1,147 @@ +package server + +// NOTE: THIS FILE WAS PRODUCED BY THE +// MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) +// DO NOT EDIT + +import ( + "github.com/tinylib/msgp/msgp" +) + +// DecodeMsg implements msgp.Decodable +func (z *Representation) DecodeMsg(dc *msgp.Reader) (err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, err = dc.ReadMapHeader() + if err != nil { + return + } + for zb0001 > 0 { + zb0001-- + field, err = dc.ReadMapKeyPtr() + if err != nil { + return + } + switch msgp.UnsafeString(field) { + case "url": + z.Address, err = dc.ReadString() + if err != nil { + return + } + case "port": + z.Port, err = dc.ReadString() + if err != nil { + return + } + case "region": + z.Region, err = dc.ReadString() + if err != nil { + return + } + default: + err = dc.Skip() + if err != nil { + return + } + } + } + return +} + +// EncodeMsg implements msgp.Encodable +func (z Representation) EncodeMsg(en *msgp.Writer) (err error) { + // map header, size 3 + // write "url" + err = en.Append(0x83, 0xa3, 0x75, 0x72, 0x6c) + if err != nil { + return + } + err = en.WriteString(z.Address) + if err != nil { + return + } + // write "port" + err = en.Append(0xa4, 0x70, 0x6f, 0x72, 0x74) + if err != nil { + return + } + err = en.WriteString(z.Port) + if err != nil { + return + } + // write "region" + err = en.Append(0xa6, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e) + if err != nil { + return + } + err = en.WriteString(z.Region) + if err != nil { + return + } + return +} + +// MarshalMsg implements msgp.Marshaler +func (z Representation) MarshalMsg(b []byte) (o []byte, err error) { + o = msgp.Require(b, z.Msgsize()) + // map header, size 3 + // string "url" + o = append(o, 0x83, 0xa3, 0x75, 0x72, 0x6c) + o = msgp.AppendString(o, z.Address) + // string "port" + o = append(o, 0xa4, 0x70, 0x6f, 0x72, 0x74) + o = msgp.AppendString(o, z.Port) + // string "region" + o = append(o, 0xa6, 0x72, 0x65, 0x67, 0x69, 0x6f, 0x6e) + o = msgp.AppendString(o, z.Region) + return +} + +// UnmarshalMsg implements msgp.Unmarshaler +func (z *Representation) UnmarshalMsg(bts []byte) (o []byte, err error) { + var field []byte + _ = field + var zb0001 uint32 + zb0001, bts, err = msgp.ReadMapHeaderBytes(bts) + if err != nil { + return + } + for zb0001 > 0 { + zb0001-- + field, bts, err = msgp.ReadMapKeyZC(bts) + if err != nil { + return + } + switch msgp.UnsafeString(field) { + case "url": + z.Address, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + return + } + case "port": + z.Port, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + return + } + case "region": + z.Region, bts, err = msgp.ReadStringBytes(bts) + if err != nil { + return + } + default: + bts, err = msgp.Skip(bts) + if err != nil { + return + } + } + } + o = bts + return +} + +// Msgsize returns an upper bound estimate of the number of bytes occupied by the serialized message +func (z Representation) Msgsize() (s int) { + s = 1 + 4 + msgp.StringPrefixSize + len(z.Address) + 5 + msgp.StringPrefixSize + len(z.Port) + 7 + msgp.StringPrefixSize + len(z.Region) + return +} diff --git a/server/representation_gen_test.go b/server/representation_gen_test.go new file mode 100644 index 00000000..47acc356 --- /dev/null +++ b/server/representation_gen_test.go @@ -0,0 +1,125 @@ +package server + +// NOTE: THIS FILE WAS PRODUCED BY THE +// MSGP CODE GENERATION TOOL (github.com/tinylib/msgp) +// DO NOT EDIT + +import ( + "bytes" + "testing" + + "github.com/tinylib/msgp/msgp" +) + +func TestMarshalUnmarshalRepresentation(t *testing.T) { + v := Representation{} + bts, err := v.MarshalMsg(nil) + if err != nil { + t.Fatal(err) + } + left, err := v.UnmarshalMsg(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after UnmarshalMsg(): %q", len(left), left) + } + + left, err = msgp.Skip(bts) + if err != nil { + t.Fatal(err) + } + if len(left) > 0 { + t.Errorf("%d bytes left over after Skip(): %q", len(left), left) + } +} + +func BenchmarkMarshalMsgRepresentation(b *testing.B) { + v := Representation{} + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.MarshalMsg(nil) + } +} + +func BenchmarkAppendMsgRepresentation(b *testing.B) { + v := Representation{} + bts := make([]byte, 0, v.Msgsize()) + bts, _ = v.MarshalMsg(bts[0:0]) + b.SetBytes(int64(len(bts))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + bts, _ = v.MarshalMsg(bts[0:0]) + } +} + +func BenchmarkUnmarshalRepresentation(b *testing.B) { + v := Representation{} + bts, _ := v.MarshalMsg(nil) + b.ReportAllocs() + b.SetBytes(int64(len(bts))) + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := v.UnmarshalMsg(bts) + if err != nil { + b.Fatal(err) + } + } +} + +func TestEncodeDecodeRepresentation(t *testing.T) { + v := Representation{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + + m := v.Msgsize() + if buf.Len() > m { + t.Logf("WARNING: Msgsize() for %v is inaccurate", v) + } + + vn := Representation{} + err := msgp.Decode(&buf, &vn) + if err != nil { + t.Error(err) + } + + buf.Reset() + msgp.Encode(&buf, &v) + err = msgp.NewReader(&buf).Skip() + if err != nil { + t.Error(err) + } +} + +func BenchmarkEncodeRepresentation(b *testing.B) { + v := Representation{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + en := msgp.NewWriter(msgp.Nowhere) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.EncodeMsg(en) + } + en.Flush() +} + +func BenchmarkDecodeRepresentation(b *testing.B) { + v := Representation{} + var buf bytes.Buffer + msgp.Encode(&buf, &v) + b.SetBytes(int64(buf.Len())) + rd := msgp.NewEndlessReader(buf.Bytes(), b) + dc := msgp.NewReader(rd) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + err := v.DecodeMsg(dc) + if err != nil { + b.Fatal(err) + } + } +} diff --git a/session/http2_session.go b/session/http2_session.go index 2bc88a1d..e1a4834d 100644 --- a/session/http2_session.go +++ b/session/http2_session.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "time" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/rs/xid" "github.com/sirupsen/logrus" "github.com/superfly/wormhole/messages" diff --git a/session/http2_session_test.go b/session/http2_session_test.go index 6c534fb6..3c430a46 100644 --- a/session/http2_session_test.go +++ b/session/http2_session_test.go @@ -18,7 +18,7 @@ import ( ) func newServerClientTLSConns(alpn bool) (serverTLSConn *tls.Conn, clientTLSConn *tls.Conn, err error) { - sConnCh := make(chan *net.TCPConn) + sConnCh := make(chan net.Conn) lnAddr := &net.TCPAddr{ IP: net.IPv4(127, 0, 0, 1), Port: 8085, @@ -56,14 +56,14 @@ func newServerClientTLSConns(alpn bool) (serverTLSConn *tls.Conn, clientTLSConn sTLSConnCh := make(chan *tls.Conn) - var wrapFunc func(*net.TCPConn, *tls.Config, wnet.TLSWrapperFunc) (*tls.Conn, error) + var wrapFunc func(net.Conn, *tls.Config, wnet.TLSWrapperFunc) (*tls.Conn, error) if alpn { wrapFunc = wnet.HTTP2ALPNTLSWrap } else { wrapFunc = wnet.GenericTLSWrap } - go func(sConn *net.TCPConn) { + go func(sConn net.Conn) { sTLSConn, err := wrapFunc(sConn, serverTLSConfig, tls.Server) if err != nil { log.Errorf("Error creating tls wrap server") diff --git a/session/session.go b/session/session.go index db2022ca..b6f5057f 100644 --- a/session/session.go +++ b/session/session.go @@ -20,6 +20,7 @@ type Session interface { Client() string ClientIP() string Cluster() string + Region() string Endpoints() []net.Addr AddEndpoint(endpoint net.Addr) Key() string @@ -42,6 +43,7 @@ type baseSession struct { clientAddr string endpoints []net.Addr ClusterURL string + RegionID string requiresClientAuth bool release *messages.Release @@ -85,6 +87,11 @@ func (s *baseSession) Cluster() string { return s.ClusterURL } +// Region returns a region identifier +func (s *baseSession) Region() string { + return s.RegionID +} + // Endpoints returns a list of endpoint addresses that have been registered for // this session. func (s *baseSession) Endpoints() []net.Addr { diff --git a/session/session_store.go b/session/session_store.go index 3ed4ed9e..4c55573a 100644 --- a/session/session_store.go +++ b/session/session_store.go @@ -4,7 +4,7 @@ import ( "net" "time" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" wnet "github.com/superfly/wormhole/net" ) @@ -26,6 +26,7 @@ type Store interface { BackendRequiresClientAuth(backendID string) (bool, error) ValidCertificate(backendID, fingerprint string) (bool, error) GetClientCAs(backendID string) ([]byte, error) + Announce(rep []byte) } // RedisStore is session persistence using Redis @@ -47,10 +48,11 @@ func (r *RedisStore) RegisterConnection(s Session) error { "node_id": s.NodeID(), "backend_id": s.BackendID(), "cluster": s.Cluster(), + "region": s.Region(), "client_addr": s.Client(), "agent": s.Agent(), - "connected_at": t.String(), - "last_seen_at": t.String(), + "connected_at": t.Format(time.RFC3339), + "last_seen_at": t.Format(time.RFC3339), } redisConn := r.pool.Get() defer redisConn.Close() @@ -101,8 +103,9 @@ func (r *RedisStore) RegisterEndpoint(s Session) error { "session_id": s.ID(), "backend_id": s.BackendID(), "cluster": s.Cluster(), - "connected_at": t.String(), - "last_seen_at": t.String(), + "region": s.Region(), + "connected_at": t.Format(time.RFC3339), + "last_seen_at": t.Format(time.RFC3339), } if extended, ok := endpointAddr.(wnet.ExtendedAddr); ok { @@ -142,9 +145,9 @@ func (r *RedisStore) RegisterHeartbeat(s Session) error { defer redisConn.Close() redisConn.Send("MULTI") - redisConn.Send("HSET", s.Key(), "last_seen_at", t.String()) + redisConn.Send("HSET", s.Key(), "last_seen_at", t.Format(time.RFC3339)) for _, endpointAddr := range s.Endpoints() { - redisConn.Send("HSET", endpointKey(s, endpointAddr), "last_seen_at", t.String()) + redisConn.Send("HSET", endpointKey(s, endpointAddr), "last_seen_at", t.Format(time.RFC3339)) } _, err := redisConn.Do("EXEC") return err @@ -206,6 +209,25 @@ func (r *RedisStore) ValidCertificate(backendID, fingerprint string) (bool, erro return redis.Bool(redisConn.Do("SISMEMBER", "backend:"+backendID+":valid_certificates", fingerprint)) } +// Announce announces the server on redis +// rep is a serialized representation of the current server +func (r *RedisStore) Announce(rep []byte) { + announce(r.pool, rep) + ticker := time.NewTicker(10 * time.Second) + for range ticker.C { + announce(r.pool, rep) + } +} + +const announceKey = "servers" + +func announce(pool *redis.Pool, rep []byte) { + redisConn := pool.Get() + defer redisConn.Close() + + redisConn.Do("ZADD", announceKey, time.Now().Unix(), rep) +} + func timeToScore(t time.Time) int64 { return t.UnixNano() / (int64(time.Millisecond) / int64(time.Nanosecond)) } diff --git a/session/setup_test.go b/session/setup_test.go index 4aaca7a7..d0175770 100644 --- a/session/setup_test.go +++ b/session/setup_test.go @@ -9,7 +9,7 @@ import ( "time" "github.com/alicebob/miniredis" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/superfly/tlstest" ) diff --git a/session/ssh_session.go b/session/ssh_session.go index 897a36b9..d073e2ac 100644 --- a/session/ssh_session.go +++ b/session/ssh_session.go @@ -9,7 +9,7 @@ import ( "strconv" "strings" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/prometheus/client_golang/prometheus" "github.com/rs/xid" "github.com/sirupsen/logrus" @@ -164,12 +164,13 @@ func (io instrumentedIO) Write(p []byte) (int, error) { } // NewSSHSession creates new SshSession struct -func NewSSHSession(logger *logrus.Logger, clusterURL, nodeID string, redisPool *redis.Pool, tcpConn net.Conn, config *ssh.ServerConfig) *SSHSession { +func NewSSHSession(logger *logrus.Logger, clusterURL, nodeID string, region string, redisPool *redis.Pool, tcpConn net.Conn, config *ssh.ServerConfig) *SSHSession { base := baseSession{ id: xid.New().String(), nodeID: nodeID, store: NewRedisStore(redisPool), ClusterURL: clusterURL, + RegionID: region, logger: logger.WithFields(logrus.Fields{"prefix": "SSHSession"}), } s := &SSHSession{ diff --git a/session/tcp_session.go b/session/tcp_session.go index f6fc0a5a..a4462039 100644 --- a/session/tcp_session.go +++ b/session/tcp_session.go @@ -7,7 +7,7 @@ import ( "sync/atomic" "time" - "github.com/garyburd/redigo/redis" + "github.com/gomodule/redigo/redis" "github.com/rs/xid" "github.com/sirupsen/logrus" "github.com/superfly/wormhole/messages" diff --git a/tls/config.go b/tls/config.go index 870aeb91..0143429b 100644 --- a/tls/config.go +++ b/tls/config.go @@ -52,10 +52,16 @@ func (c *Config) GetDefaultConfig() *tls.Config { } func (c *Config) getConfigForClient(helloInfo *tls.ClientHelloInfo) (*tls.Config, error) { + id := strings.Split(helloInfo.ServerName, ".")[0] if len(id) == 0 { return nil, fmt.Errorf("SNI has no ID") } + + if id == "api" { + return c.GetDefaultConfig(), nil + } + session := c.registry.GetSession(id) if session == nil { return nil, fmt.Errorf("Session (ID='%s') cannot be found", id) diff --git a/tls/config_test.go b/tls/config_test.go index 20257c82..ac265666 100644 --- a/tls/config_test.go +++ b/tls/config_test.go @@ -154,6 +154,10 @@ func (ts *testSession) Cluster() string { return "" } +func (ts *testSession) Region() string { + return "" +} + func (ts *testSession) Endpoints() []net.Addr { return []net.Addr{} }