diff options
Diffstat (limited to 'arbiter')
-rw-r--r-- | arbiter/.golangci.yml | 18 | ||||
-rw-r--r-- | arbiter/Dockerfile | 18 | ||||
-rw-r--r-- | arbiter/arbiter.go | 505 | ||||
-rw-r--r-- | arbiter/go.mod | 17 | ||||
-rw-r--r-- | arbiter/go.sum | 41 | ||||
-rwxr-xr-x | arbiter/tests.sh | 4 |
6 files changed, 0 insertions, 603 deletions
diff --git a/arbiter/.golangci.yml b/arbiter/.golangci.yml deleted file mode 100644 index 878e031..0000000 --- a/arbiter/.golangci.yml +++ /dev/null @@ -1,18 +0,0 @@ -run: - concurrency: 16 - timeout: 5m - modules-download-mode: readonly - allow-parallel-runners: true - allow-serial-runners: true - go: '1.22' -linters-settings: - depguard: - rules: - srcs: - listMode: "Strict" - allow: - - $gostd - - github.com/go-redis/redis/v8 - - github.com/gorilla/mux - - github.com/rs/zerolog - - github.com/rs/zerolog/log diff --git a/arbiter/Dockerfile b/arbiter/Dockerfile deleted file mode 100644 index a512798..0000000 --- a/arbiter/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM golang:1.22-alpine3.19 as builder -RUN apk update && apk upgrade -RUN apk add go git -COPY go.* /arbiter/ -RUN cd /arbiter && go mod download -COPY *.go /arbiter/ -RUN cd /arbiter && go build - -FROM alpine:3.19 as certbuilder -RUN apk add openssl -WORKDIR /certs -RUN openssl req -nodes -new -x509 -subj="/C=US/ST=Denial/L=springfield/O=Dis/CN=localhost" -keyout server.key -out server.cert - -# FROM gcr.io/distroless/static-debian10 -FROM alpine:3.19 -COPY --from=certbuilder /certs /certs -COPY --from=builder /arbiter/arbiter /arbiter/ -ENTRYPOINT ["/arbiter/arbiter"] diff --git a/arbiter/arbiter.go b/arbiter/arbiter.go deleted file mode 100644 index 6145a45..0000000 --- a/arbiter/arbiter.go +++ /dev/null @@ -1,505 +0,0 @@ -package main - -import ( - "context" - "crypto/tls" - "encoding/json" - "errors" - "flag" - "io" - "net/http" - "net/url" - "os" - "os/signal" - "strconv" - "sync" - "time" - - "github.com/go-redis/redis/v8" - "github.com/gorilla/mux" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -var ( - errBadLogic = errors.New("we should not be here") - errUnexpectedParam = errors.New("got unexpected parameter") - errUnknownDeployment = errors.New("unknown deployment kind") -) - -const ( - serverDeploymentType = "SERVER_DEPLOYMENT_TYPE" - coingeckoAPIURLv3 = "https://api.coingecko.com/api/v3" - coincapAPIURLv2 = "https://api.coincap.io/v2" - getTimeout = 5 - httpClientTimeout = 5 - serverTLSReadTimeout = 15 - serverTLSWriteTimeout = 15 - defaultGracefulShutdown = 15 -) - -// https://docs.coincap.io/ -type CoinCapAssetGetResponseData struct { - ID string `json:"id"` - Rank string `json:"rank"` - Symbol string `json:"symbol"` - Name string `json:"name"` - Supply string `json:"supply"` - MaxSupply string `json:"maxSupply"` - MarketCapUsd string `json:"marketCapUsd"` - VolumeUsd24Hr string `json:"volumeUsd24Hr"` - PriceUsd string `json:"priceUsd"` - ChangePercent24Hr string `json:"changePercent24Hr"` - Vwap24Hr string `json:"vwap24Hr"` -} - -type priceResponseData struct { - Name string `json:"name"` - Price float64 `json:"price"` - Unit string `json:"unit"` - Err string `json:"err"` - IsSuccessful bool `json:"isSuccessful"` -} - -type CoinCapAssetGetResponse struct { - Data CoinCapAssetGetResponseData `json:"data"` - TimeStamp int64 `json:"timestamp"` -} - -type HTTPHandlerFunc func(http.ResponseWriter, *http.Request) - -type HTTPHandler struct { - name string - function HTTPHandlerFunc -} - -type priceChanStruct struct { - name string - price float64 -} - -type errorChanStruct struct { - hasError bool - err error -} - -func GetProxiedClient() *http.Client { - transport := &http.Transport{ - DisableKeepAlives: true, - Proxy: http.ProxyFromEnvironment, - } - client := &http.Client{ - Transport: transport, - Timeout: httpClientTimeout * time.Second, - CheckRedirect: nil, - Jar: nil, - } - - return client -} - -// OWASP: https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html -func addSecureHeaders(writer *http.ResponseWriter) { - (*writer).Header().Set("Cache-Control", "no-store") - (*writer).Header().Set("Content-Security-Policy", "default-src https;") - (*writer).Header().Set("Strict-Transport-Security", "max-age=63072000;") - (*writer).Header().Set("X-Content-Type-Options", "nosniff") - (*writer).Header().Set("X-Frame-Options", "DENY") - (*writer).Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") -} - -func getPriceFromCoinGecko( - ctx context.Context, - name, unit string, - wg *sync.WaitGroup, - priceChan chan<- priceChanStruct, - errChan chan<- errorChanStruct, -) { - defer wg.Done() - - priceFloat := 0. - - params := "/simple/price?ids=" + url.QueryEscape(name) + "&" + - "vs_currencies=" + url.QueryEscape(unit) - path := coingeckoAPIURLv3 + params - - client := GetProxiedClient() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil) - if err != nil { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err) - - return - } - - resp, err := client.Do(req) - if err != nil { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err).Send() - - return - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err).Send() - } - - jsonBody := make(map[string]interface{}) - - err = json.Unmarshal(body, &jsonBody) - if err != nil { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err).Send() - } - - price, isOk := jsonBody[name].(map[string]interface{}) - if !isOk { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err) - - return - } - - log.Info().Msg(string(body)) - - priceFloat, isOk = price[unit].(float64) - if !isOk { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err) - - return - } - - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: false, err: nil} -} - -func getPriceFromCoinCap( - ctx context.Context, - name string, - wg *sync.WaitGroup, - priceChan chan<- priceChanStruct, - errChan chan<- errorChanStruct, -) { - defer wg.Done() - - priceFloat := 0. - - params := "/assets/" + url.QueryEscape(name) - path := coincapAPIURLv2 + params - - client := GetProxiedClient() - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, path, nil) - if err != nil { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err) - - return - } - - resp, err := client.Do(req) - if err != nil { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err).Send() - - return - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err) - } - - var coinCapAssetGetResponse CoinCapAssetGetResponse - - err = json.Unmarshal(body, &coinCapAssetGetResponse) - if err != nil { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err).Send() - } - - priceFloat, err = strconv.ParseFloat(coinCapAssetGetResponse.Data.PriceUsd, 64) - if err != nil { - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: true, err: err} - - log.Error().Err(err).Send() - } - - log.Info().Msg(string(body)) - - priceChan <- priceChanStruct{name: name, price: priceFloat} - errChan <- errorChanStruct{hasError: false, err: nil} -} - -func arbHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Add("Content-Type", "application/json") - - if r.Method != http.MethodGet { - http.Error(w, "Method is not supported.", http.StatusNotFound) - } - - addSecureHeaders(&w) - - var name string - - var unit string - - params := r.URL.Query() - for key, value := range params { - switch key { - case "name": - name = value[0] - case "unit": - unit = value[0] - default: - log.Error().Err(errUnexpectedParam) - } - } - - priceChan := make(chan priceChanStruct, 1) - errChan := make(chan errorChanStruct, 1) - - var waitGroup sync.WaitGroup - - ctx, cancel := context.WithTimeout(context.Background(), getTimeout*time.Second) - defer cancel() - - waitGroup.Add(1) - - //nolint:contextcheck - getPriceFromCoinGecko(ctx, name, unit, &waitGroup, priceChan, errChan) - waitGroup.Wait() - - select { - case err := <-errChan: - if err.hasError { - log.Error().Err(err.err) - } - default: - log.Error().Err(errBadLogic).Send() - } - - var price priceChanStruct - select { - case priceCh := <-priceChan: - price = priceCh - default: - log.Error().Err(errBadLogic) - } - - responseData := priceResponseData{ - Name: price.name, - Price: price.price, - Unit: "USD", - Err: "", - IsSuccessful: true, - } - - jsonResp, err := json.Marshal(responseData) - if err != nil { - cancel() - //nolint:gocritic - log.Fatal().Err(err) - } - - _, err = w.Write(jsonResp) - if err != nil { - cancel() - log.Fatal().Err(err) - } -} - -func coincapHandler(w http.ResponseWriter, r *http.Request) { - if r.Method != http.MethodGet { - http.Error(w, "Method is not supported.", http.StatusNotFound) - } - - w.Header().Add("Content-Type", "application/json") - - addSecureHeaders(&w) - - var name string - - params := r.URL.Query() - for key, value := range params { - switch key { - case "name": - name = value[0] - default: - log.Error().Err(errUnexpectedParam).Send() - } - } - - priceChan := make(chan priceChanStruct, 1) - errChan := make(chan errorChanStruct, 1) - - var waitGroup sync.WaitGroup - - waitGroup.Add(1) - - ctx, cancel := context.WithTimeout(context.Background(), getTimeout*time.Second) - defer cancel() - - getPriceFromCoinCap(ctx, name, &waitGroup, priceChan, errChan) - waitGroup.Wait() - - select { - case err := <-errChan: - if err.hasError { - log.Error().Err(err.err) - } - default: - log.Error().Err(errBadLogic) - } - - var price priceChanStruct - select { - case priceCh := <-priceChan: - price = priceCh - default: - log.Error().Err(errBadLogic) - } - - responseData := priceResponseData{ - Name: price.name, - Price: price.price, - Unit: "USD", - Err: "", - IsSuccessful: true, - } - - jsonResp, err := json.Marshal(responseData) - if err != nil { - cancel() - - log.Fatal().Err(err).Send() - } - - _, err = w.Write(jsonResp) - if err != nil { - cancel() - log.Fatal().Err(err) - } -} - -func setupLogging() { - zerolog.TimeFieldFormat = zerolog.TimeFormatUnix -} - -func startServer(gracefulWait time.Duration, - handlers []HTTPHandler, - serverDeploymentType string, port string, -) { - route := mux.NewRouter() - cfg := &tls.Config{ - MinVersion: tls.VersionTLS13, - } - - srv := &http.Server{ - Addr: "0.0.0.0:" + port, - WriteTimeout: time.Second * serverTLSWriteTimeout, - ReadTimeout: time.Second * serverTLSReadTimeout, - Handler: route, - TLSConfig: cfg, - } - - for i := range len(handlers) { - route.HandleFunc(handlers[i].name, handlers[i].function) - } - - go func() { - var certPath, keyPath string - - switch os.Getenv(serverDeploymentType) { - case "deployment": - certPath = "/etc/letsencrypt/live/api.terminaldweller.com/fullchain.pem" - keyPath = "/etc/letsencrypt/live/api.terminaldweller.com/privkey.pem" - case "test": - certPath = "/certs/server.cert" - keyPath = "/certs/server.key" - default: - log.Error().Err(errUnknownDeployment).Send() - } - - if err := srv.ListenAndServeTLS(certPath, keyPath); err != nil { - log.Error().Err(err) - } - }() - - c := make(chan os.Signal, 1) - - signal.Notify(c, os.Interrupt) - <-c - - ctx, cancel := context.WithTimeout(context.Background(), gracefulWait) - defer cancel() - - if err := srv.Shutdown(ctx); err != nil { - log.Error().Err(err) - } - - log.Info().Msg("gracefully shut down the server") -} - -func main() { - var gracefulWait time.Duration - - var rdb *redis.Client - - flag.DurationVar( - &gracefulWait, - "gracefulwait", - time.Second*defaultGracefulShutdown, - "the duration to wait during the graceful shutdown", - ) - - flagPort := flag.String("port", "8009", "determines the port the server will listen on") - redisDB := flag.Int64("redisdb", 1, "determines the db number") - redisAddress := flag.String("redisaddress", "redis:6379", "determines the address of the redis instance") - redisPassword := flag.String("redispassword", "", "determines the password of the redis db") - flag.Parse() - - rdb = redis.NewClient(&redis.Options{ - Addr: *redisAddress, - Password: *redisPassword, - DB: int(*redisDB), - }) - defer rdb.Close() - - setupLogging() - - handlerFuncs := []HTTPHandler{ - {name: "/crypto/v1/arb/gecko", function: arbHandler}, - {name: "/crypto/v1/arb/coincap", function: coincapHandler}, - } - - startServer(gracefulWait, handlerFuncs, serverDeploymentType, *flagPort) -} diff --git a/arbiter/go.mod b/arbiter/go.mod deleted file mode 100644 index a7a87c6..0000000 --- a/arbiter/go.mod +++ /dev/null @@ -1,17 +0,0 @@ -module arbiter - -go 1.22 - -require ( - github.com/go-redis/redis/v8 v8.11.5 - github.com/gorilla/mux v1.8.1 - github.com/rs/zerolog v1.31.0 -) - -require ( - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - golang.org/x/sys v0.15.0 // indirect -) diff --git a/arbiter/go.sum b/arbiter/go.sum deleted file mode 100644 index 85a6437..0000000 --- a/arbiter/go.sum +++ /dev/null @@ -1,41 +0,0 @@ -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= -github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= -github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= -github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= -github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= -golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/arbiter/tests.sh b/arbiter/tests.sh deleted file mode 100755 index be923f3..0000000 --- a/arbiter/tests.sh +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/env sh - -curl -k -X GET "https://localhost:8009/crypto/v1/arb/gecko?name=ethereum&unit=usd" -curl -k -X GET "https://localhost:8009/crypto/v1/arb/coincap?name=ethereum" |