New NTP Client (#277)

* WIP

* Added check for time out of sync

* merged upstream/master

* added tests

* Increased configuration options for NTPclient and test coverage

* removed unneeded config save at end of ntp update

* Added test for empty response to confirm it will loop

* formatting correction

* converted to pointer to allow for default allowance settings to be checked

* added readme for NTP server

* corrected some formatting

* updated configtest negativedifference value

* gofmt config_test.go for correct import order

* corrected typo value in test

* bugfix for windows newline and changes based on PR feedback

* added minus sign to output

* fixed negative number input

* Fixed spelling mistakes and removed redundant test

* reverted back to a positive number in the config instead of negative for allowednegativedifference

* restructured code for cleaner output
This commit is contained in:
Andrew
2019-04-18 10:08:19 +10:00
committed by Adrian Gallagher
parent 400c1cc84d
commit 071f4f68a8
8 changed files with 273 additions and 0 deletions

View File

@@ -211,6 +211,23 @@ comm method and add in your contact list if available.
},
```
## Configure Network Time Server
+ To configure and enable a NTP server you need to set the "enabled" field to one of 3 values -1 is disabled 0 is enabled and alert at start up 1 is enabled and warn at start up
servers are configured by the pool array and attempted first to last allowedDifference and allowedNegativeDifference are how far ahead and behind is acceptable for the time to be out in nanoseconds
```js
"ntpclient": {
"enabled": 0,
"pool": [
"pool.ntp.org:123"
],
"allowedDifference": 0,
"allowedNegativeDifference": 0
},
```
### Please click GoDocs chevron above to view current GoDoc information for this package
## Contribution

View File

@@ -1,14 +1,17 @@
package config
import (
"bufio"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"os"
"path"
"runtime"
"strconv"
"strings"
"sync"
"time"
@@ -32,6 +35,8 @@ const (
configPairsLastUpdatedWarningThreshold = 30 // 30 days
configDefaultHTTPTimeout = time.Second * 15
configMaxAuthFailres = 3
defaultNTPAllowedDifference = 50000000
defaultNTPAllowedNegativeDifference = 50000000
)
// Constants here hold some messages
@@ -104,6 +109,7 @@ type Config struct {
GlobalHTTPTimeout time.Duration `json:"globalHTTPTimeout"`
Logging log.Logging `json:"logging"`
Profiler ProfilerConfig `json:"profiler"`
NTPClient NTPClientConfig `json:"ntpclient"`
Currency CurrencyConfig `json:"currencyConfig"`
Communications CommunicationsConfig `json:"communications"`
Portfolio portfolio.Base `json:"portfolioAddresses"`
@@ -122,6 +128,13 @@ type ProfilerConfig struct {
Enabled bool `json:"enabled"`
}
type NTPClientConfig struct {
Level int `json:"enabled"`
Pool []string `json:"pool"`
AllowedDifference *time.Duration `json:"allowedDifference"`
AllowedNegativeDifference *time.Duration `json:"allowedNegativeDifference"`
}
// ExchangeConfig holds all the information needed for each enabled Exchange.
type ExchangeConfig struct {
Name string `json:"name"`
@@ -1086,6 +1099,62 @@ func (c *Config) CheckLoggerConfig() error {
return nil
}
// CheckNTPConfig() checks for missing or incorrectly configured NTPClient and recreates with known safe defaults
func (c *Config) CheckNTPConfig() {
m.Lock()
defer m.Unlock()
if c.NTPClient.AllowedDifference == nil || *c.NTPClient.AllowedDifference == 0 {
c.NTPClient.AllowedDifference = new(time.Duration)
*c.NTPClient.AllowedDifference = defaultNTPAllowedDifference
}
if c.NTPClient.AllowedNegativeDifference == nil || *c.NTPClient.AllowedNegativeDifference <= 0 {
c.NTPClient.AllowedNegativeDifference = new(time.Duration)
*c.NTPClient.AllowedNegativeDifference = defaultNTPAllowedNegativeDifference
}
if len(c.NTPClient.Pool) < 1 {
log.Warn("NTPClient enabled with no servers configured enabling default pool")
c.NTPClient.Pool = []string{"pool.ntp.org:123"}
}
}
// DisableNTPCheck() allows the user to change how they are prompted for timesync alerts
func (c *Config) DisableNTPCheck(input io.Reader) (string, error) {
m.Lock()
defer m.Unlock()
reader := bufio.NewReader(input)
log.Warn("Your system time is out of sync this may cause issues with trading")
log.Warn("How would you like to show future notifications? (a)lert / (w)arn / (d)isable \n")
var answered = false
for ok := true; ok; ok = (!answered) {
answer, err := reader.ReadString('\n')
if err != nil {
return "", err
}
answer = strings.TrimRight(answer, "\r\n")
switch answer {
case "a":
c.NTPClient.Level = 0
answered = true
return "Time sync has been set to alert", nil
case "w":
c.NTPClient.Level = 1
answered = true
return "Time sync has been set to warn only", nil
case "d":
c.NTPClient.Level = -1
answered = true
return "Future notications for out time sync have been disabled", nil
}
}
return "", errors.New("something went wrong NTPCheck should never make it this far")
}
// GetFilePath returns the desired config file or the default config file name
// based on if the application is being run under test or normal mode.
func GetFilePath(file string) (string, error) {

View File

@@ -1,11 +1,13 @@
package config
import (
"strings"
"testing"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/currency"
log "github.com/thrasher-/gocryptotrader/logger"
"github.com/thrasher-/gocryptotrader/ntpclient"
)
const (
@@ -967,3 +969,61 @@ func TestCheckLoggerConfig(t *testing.T) {
t.Errorf("Failed to create logger with user settings: reason: %v", err)
}
}
func TestDisableNTPCheck(t *testing.T) {
c := GetConfig()
err := c.LoadConfig(ConfigTestFile)
if err != nil {
t.Fatal(err)
}
warn, err := c.DisableNTPCheck(strings.NewReader("w\n"))
if err != nil {
t.Fatalf("test failed to create ntpclient failed reason: %v", err)
}
if warn != "Time sync has been set to warn only" {
t.Errorf("failed expected %v got %v", "Time sync has been set to warn only", warn)
}
alert, _ := c.DisableNTPCheck(strings.NewReader("a\n"))
if alert != "Time sync has been set to alert" {
t.Errorf("failed expected %v got %v", "Time sync has been set to alert", alert)
}
disable, _ := c.DisableNTPCheck(strings.NewReader("d\n"))
if disable != "Future notications for out time sync have been disabled" {
t.Errorf("failed expected %v got %v", "Future notications for out time sync have been disabled", disable)
}
_, err = c.DisableNTPCheck(strings.NewReader(" "))
if err.Error() != "EOF" {
t.Errorf("failed expected EOF got: %v", err)
}
}
func TestCheckNTPConfig(t *testing.T) {
c := GetConfig()
c.NTPClient.Level = 0
c.NTPClient.Pool = nil
c.NTPClient.AllowedNegativeDifference = nil
c.NTPClient.AllowedDifference = nil
c.CheckNTPConfig()
_, err := ntpclient.NTPClient(c.NTPClient.Pool)
if err != nil {
t.Fatalf("test failed to create ntpclient failed reason: %v", err)
}
if c.NTPClient.Pool[0] != "pool.ntp.org:123" {
t.Error("ntpclient with no valid pool should default to pool.ntp.org ")
}
if c.NTPClient.AllowedDifference == nil {
t.Error("ntpclient with nil alloweddifference should default to sane value")
}
if c.NTPClient.AllowedNegativeDifference == nil {
t.Error("ntpclient with nil allowednegativedifference should default to sane value")
}
}

View File

@@ -12,6 +12,14 @@
"profiler": {
"enabled": false
},
"ntpclient": {
"enabled": 0,
"pool": [
"pool.ntp.org:123"
],
"allowedDifference": 50000000,
"allowedNegativeDifference": 50000000
},
"currencyConfig": {
"forexProviders": [
{

26
main.go
View File

@@ -9,6 +9,7 @@ import (
"runtime"
"strconv"
"syscall"
"time"
"github.com/thrasher-/gocryptotrader/common"
"github.com/thrasher-/gocryptotrader/communications"
@@ -17,6 +18,7 @@ import (
"github.com/thrasher-/gocryptotrader/currency/coinmarketcap"
exchange "github.com/thrasher-/gocryptotrader/exchanges"
log "github.com/thrasher-/gocryptotrader/logger"
"github.com/thrasher-/gocryptotrader/ntpclient"
"github.com/thrasher-/gocryptotrader/portfolio"
)
@@ -103,6 +105,30 @@ func main() {
log.Errorf("Failed to setup logger reason: %s", err)
}
if bot.config.NTPClient.Level != -1 {
bot.config.CheckNTPConfig()
NTPTime, errNTP := ntpclient.NTPClient(bot.config.NTPClient.Pool)
currentTime := time.Now()
if errNTP != nil {
log.Warnf("NTPClient failed to create: %v", errNTP)
} else {
NTPcurrentTimeDifference := NTPTime.Sub(currentTime)
configNTPTime := *bot.config.NTPClient.AllowedDifference
configNTPNegativeTime := (*bot.config.NTPClient.AllowedNegativeDifference - (*bot.config.NTPClient.AllowedNegativeDifference * 2))
if NTPcurrentTimeDifference > configNTPTime || NTPcurrentTimeDifference < configNTPNegativeTime {
log.Warnf("Time out of sync (NTP): %v | (time.Now()): %v | (Difference): %v | (Allowed): +%v / %v", NTPTime, currentTime, NTPcurrentTimeDifference, configNTPTime, configNTPNegativeTime)
if bot.config.NTPClient.Level == 0 {
disable, errNTP := bot.config.DisableNTPCheck(os.Stdin)
if errNTP != nil {
log.Errorf("failed to disable ntp time check reason: %v", err)
} else {
log.Info(disable)
}
}
}
}
}
AdjustGoMaxProcs()
log.Debugf("Bot '%s' started.\n", bot.config.Name)
log.Debugf("Bot dry run mode: %v.\n", common.IsEnabled(bot.dryRun))

59
ntpclient/ntpclient.go Normal file
View File

@@ -0,0 +1,59 @@
package ntpclient
import (
"encoding/binary"
"errors"
"net"
"time"
log "github.com/thrasher-/gocryptotrader/logger"
)
type ntppacket struct {
Settings uint8 // leap yr indicator, ver number, and mode
Stratum uint8 // stratum of local clock
Poll int8 // poll exponent
Precision int8 // precision exponent
RootDelay uint32 // root delay
RootDispersion uint32 // root dispersion
ReferenceID uint32 // reference id
RefTimeSec uint32 // reference timestamp sec
RefTimeFrac uint32 // reference timestamp fractional
OrigTimeSec uint32 // origin time secs
OrigTimeFrac uint32 // origin time fractional
RxTimeSec uint32 // receive time secs
RxTimeFrac uint32 // receive time frac
TxTimeSec uint32 // transmit time secs
TxTimeFrac uint32 // transmit time frac
}
// NTPClient create's a new NTPClient and returns local based on ntp servers provided timestamp
func NTPClient(pool []string) (time.Time, error) {
for i := range pool {
con, err := net.Dial("udp", pool[i])
if err != nil {
log.Warnf("Unable to connect to hosts %v attempting next", pool[i])
continue
}
defer con.Close()
con.SetDeadline(time.Now().Add(5 * time.Second))
req := &ntppacket{Settings: 0x1B}
if err := binary.Write(con, binary.BigEndian, req); err != nil {
continue
}
rsp := &ntppacket{}
if err := binary.Read(con, binary.BigEndian, rsp); err != nil {
continue
}
secs := float64(rsp.TxTimeSec) - 2208988800
nanos := (int64(rsp.TxTimeFrac) * 1e9) >> 32
return time.Unix(int64(secs), nanos), nil
}
return time.Unix(0, 0), errors.New("no valid time servers")
}

View File

@@ -0,0 +1,25 @@
package ntpclient
import (
"testing"
)
func TestNTPClient(t *testing.T) {
pool := []string{"pool.ntp.org:123", "0.pool.ntp.org:123"}
_, err := NTPClient(pool)
if err != nil {
t.Fatalf("failed to get time %v", err)
}
invalidpool := []string{"pool.thisisinvalid.org"}
_, err = NTPClient(invalidpool)
if err == nil {
t.Errorf("failed to get time %v", err)
}
firstInvalid := []string{"pool.thisisinvalid.org", "pool.ntp.org:123", "0.pool.ntp.org:123"}
_, err = NTPClient(firstInvalid)
if err != nil {
t.Errorf("failed to get time %v", err)
}
}

View File

@@ -12,6 +12,15 @@
"profiler": {
"enabled": false
},
"ntpclient": {
"enabled": 0,
"pool": [
"0.pool.ntp.org:123",
"pool.ntp.org:123"
],
"allowedDifference": 50000000,
"allowedNegativeDifference": 50000000
},
"currencyConfig": {
"forexProviders": [
{