diff options
Diffstat (limited to 'arbiter/arbiter.go')
-rw-r--r-- | arbiter/arbiter.go | 505 |
1 files changed, 0 insertions, 505 deletions
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) -} |