package main import ( "context" "crypto/tls" "encoding/json" "errors" "flag" "fmt" "io/ioutil" "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 ( flagPort = flag.String("port", "8009", "determines the port the server will listen on") flagInterval = flag.Float64("interval", 10, "In seconds, the delay between checking prices") redisDB = flag.Int64("redisdb", 1, "determines the db number") rdb *redis.Client redisAddress = flag.String("redisaddress", "redis:6379", "determines the address of the redis instance") redisPassword = flag.String("redispassword", "", "determines the password of the redis db") ) const ( SERVER_DEPLOYMENT_TYPE = "SERVER_DEPLOYMENT_TYPE" coingeckoAPIURLv3 = "https://api.coingecko.com/api/v3" coincapAPIURLv2 = "https://api.coincap.io/v2" ) // 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 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 } // OWASP: https://cheatsheetseries.owasp.org/cheatsheets/REST_Security_Cheat_Sheet.html func addSecureHeaders(w *http.ResponseWriter) { (*w).Header().Set("Cache-Control", "no-store") (*w).Header().Set("Content-Security-Policy", "default-src https;") (*w).Header().Set("Strict-Transport-Security", "max-age=63072000;") (*w).Header().Set("X-Content-Type-Options", "nosniff") (*w).Header().Set("X-Frame-Options", "DENY") (*w).Header().Set("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS") } //binance func getPriceFromBinance(name, unit string, wg *sync.WaitGroup, priceChan chan<- priceChanStruct, errChan chan<- errorChanStruct) { } //kucoin func getPriceFromKu(name, uni string, wg *sync.WaitGroup, priceChan chan<- priceChanStruct, errChan chan<- errorChanStruct) { } func getPriceFromCoinGecko( name, unit string, wg *sync.WaitGroup, priceChan chan<- priceChanStruct, errChan chan<- errorChanStruct) { defer wg.Done() params := "/simple/price?ids=" + url.QueryEscape(name) + "&" + "vs_currencies=" + url.QueryEscape(unit) path := coingeckoAPIURLv3 + params fmt.Println(path) resp, err := http.Get(path) if err != nil { priceChan <- priceChanStruct{name: name, price: 0.} errChan <- errorChanStruct{hasError: true, err: err} log.Error().Err(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { priceChan <- priceChanStruct{name: name, price: 0.} errChan <- errorChanStruct{hasError: true, err: err} log.Error().Err(err) } jsonBody := make(map[string]interface{}) err = json.Unmarshal(body, &jsonBody) if err != nil { priceChan <- priceChanStruct{name: name, price: 0.} errChan <- errorChanStruct{hasError: true, err: err} log.Error().Err(err) } price := jsonBody[name].(map[string]interface{})[unit].(float64) log.Info().Msg(string(body)) priceChan <- priceChanStruct{name: name, price: price} errChan <- errorChanStruct{hasError: false, err: nil} } func getPriceFromCoinCap( name, unit string, wg *sync.WaitGroup, priceChan chan<- priceChanStruct, errChan chan<- errorChanStruct) { defer wg.Done() params := "/assets/" + url.QueryEscape(name) path := coincapAPIURLv2 + params fmt.Println(path) resp, err := http.Get(path) if err != nil { priceChan <- priceChanStruct{name: name, price: 0.} errChan <- errorChanStruct{hasError: true, err: err} log.Error().Err(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { priceChan <- priceChanStruct{name: name, price: 0.} errChan <- errorChanStruct{hasError: true, err: err} log.Error().Err(err) } fmt.Println(string(body)) var coinCapAssetGetResponse CoinCapAssetGetResponse // jsonBody := make(map[string]interface{}) // err = json.Unmarshal(body, &jsonBody) err = json.Unmarshal(body, &coinCapAssetGetResponse) if err != nil { priceChan <- priceChanStruct{name: name, price: 0.} errChan <- errorChanStruct{hasError: true, err: err} log.Error().Err(err) } // price := jsonBody[name].(map[string]interface{})[unit].(float64) price, err := strconv.ParseFloat(coinCapAssetGetResponse.Data.PriceUsd, 64) if err != nil { priceChan <- priceChanStruct{name: name, price: 0.} errChan <- errorChanStruct{hasError: true, err: err} log.Error().Err(err) } fmt.Println(price) log.Info().Msg(string(body)) priceChan <- priceChanStruct{name: name, price: price} errChan <- errorChanStruct{hasError: false, err: nil} } func arbHandler(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") if r.Method != "GET" { 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(errors.New("Got unexpected parameter.")) } } priceChan := make(chan priceChanStruct, 1) errChan := make(chan errorChanStruct, 1) var wg sync.WaitGroup wg.Add(1) getPriceFromCoinGecko(name, unit, &wg, priceChan, errChan) wg.Wait() select { case err := <-errChan: if err.hasError != false { log.Error().Err(err.err) } default: log.Error().Err(errors.New("We shouldnt be here")) } var price priceChanStruct select { case priceCh := <-priceChan: price = priceCh default: log.Fatal().Err(errors.New("We shouldnt be here")) } json.NewEncoder(w).Encode(map[string]interface{}{ "name": price.name, "price": price.price, "unit": unit, "err": "", "isSuccessful": true, }) } func coincapHandler(w http.ResponseWriter, r *http.Request) { w.Header().Add("Content-Type", "application/json") if r.Method != "GET" { 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(errors.New("Got unexpected parameter.")) } } priceChan := make(chan priceChanStruct, 1) errChan := make(chan errorChanStruct, 1) var wg sync.WaitGroup wg.Add(1) getPriceFromCoinCap(name, unit, &wg, priceChan, errChan) wg.Wait() select { case err := <-errChan: if err.hasError != false { log.Error().Err(err.err) } default: log.Error().Err(errors.New("We shouldnt be here")) } var price priceChanStruct select { case priceCh := <-priceChan: price = priceCh default: log.Fatal().Err(errors.New("We shouldnt be here")) } json.NewEncoder(w).Encode(map[string]interface{}{ "name": price.name, "price": price.price, "unit": "USD", "err": "", "isSuccessful": true, }) } func setupLogging() { zerolog.TimeFieldFormat = zerolog.TimeFormatUnix } func startServer(gracefulWait time.Duration, handlers []HttpHandler, serverDeploymentType string, port string) { r := mux.NewRouter() cfg := &tls.Config{ MinVersion: tls.VersionTLS13, } srv := &http.Server{ Addr: "0.0.0.0:" + port, WriteTimeout: time.Second * 15, ReadTimeout: time.Second * 15, Handler: r, TLSConfig: cfg, } for i := 0; i < len(handlers); i++ { r.HandleFunc(handlers[i].name, handlers[i].function) } go func() { var certPath, keyPath string if os.Getenv(serverDeploymentType) == "deployment" { certPath = "/certs/fullchain1.pem" keyPath = "/certs/privkey1.pem" } else if os.Getenv(serverDeploymentType) == "test" { certPath = "/certs/server.cert" keyPath = "/certs/server.key" } else { log.Fatal().Err(errors.New(fmt.Sprintf("unknown deployment kind: %s", serverDeploymentType))) } if err := srv.ListenAndServeTLS(certPath, keyPath); err != nil { log.Fatal().Err(err) } }() c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt) <-c ctx, cancel := context.WithTimeout(context.Background(), gracefulWait) defer cancel() srv.Shutdown(ctx) log.Info().Msg("gracefully shut down the server") } func main() { var gracefulWait time.Duration flag.DurationVar(&gracefulWait, "gracefulwait", time.Second*15, "the duration to wait during the graceful shutdown") flag.Parse() rdb = redis.NewClient(&redis.Options{ Addr: *redisAddress, Password: *redisPassword, DB: int(*redisDB), }) defer rdb.Close() setupLogging() var handlerFuncs = []HttpHandler{ {name: "/crypto/v1/arb/gecko", function: arbHandler}, {name: "/crypto/v1/arb/coincap", function: coincapHandler}, } startServer(gracefulWait, handlerFuncs, SERVER_DEPLOYMENT_TYPE, *flagPort) }