<|start_filename|>servicecheck_test.go<|end_filename|>
package frontman

import (
	"testing"

	"github.com/stretchr/testify/require"
)

func TestDNSUDPCheck(t *testing.T) {
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.Sleep = 10
	fm := helperCreateFrontman(t, cfg)
	input := &Input{
		ServiceChecks: []ServiceCheck{{
			UUID: "dns-udp-check",
			Check: ServiceCheckData{
				Connect:  "8.8.8.8",
				Protocol: "udp",
				Service:  "dns",
				Port:     "53",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["net.udp.dns.53.success"])
}

func TestDNSTCPCheck(t *testing.T) {
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.Sleep = 10
	fm := helperCreateFrontman(t, cfg)
	input := &Input{
		ServiceChecks: []ServiceCheck{{
			UUID: "dns-tcp-check",
			Check: ServiceCheckData{
				Connect:  "8.8.8.8",
				Protocol: "tcp",
				Service:  "dns",
				Port:     "53",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["net.tcp.dns.53.success"])
}


<|start_filename|>pkg/utils/random.go<|end_filename|>
package utils

import "math/rand"

var letters = []byte("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")

// randomizedStr returns string of len strLen
// probability of all the letters will not be exactly the same
func RandomizedStr(strLen int) string {
	b := make([]byte, strLen)
	for i := range b {
		b[i] = letters[rand.Int63()%int64(len(letters))]
	}
	return string(b)
}


<|start_filename|>syslog.go<|end_filename|>
// +build !windows,!nacl,!plan9
// OS list copied from log/syslog

package frontman

import (
	"fmt"
	"log/syslog"
	"net/url"

	"github.com/sirupsen/logrus"
	lSyslog "github.com/sirupsen/logrus/hooks/syslog"
)

func addSyslogHook(syslogURL string) error {

	var network, raddr string

	if syslogURL != "local" {
		u, err := url.Parse(syslogURL)
		if err != nil {
			return fmt.Errorf("wrong format of syslogURL: %s", err.Error())
		}
		network = u.Scheme
		raddr = u.Host

		if u.Port() == "" {
			raddr += ":;514"
		}
	}

	hook, err := lSyslog.NewSyslogHook(network, raddr, syslog.LOG_DEBUG, "frontman")

	if err != nil {
		return err
	}

	logrus.AddHook(hook)

	return nil
}


<|start_filename|>types.go<|end_filename|>
package frontman

type ServiceName string

const (
	ProtocolICMP = "icmp"
	ProtocolTCP  = "tcp"
	ProtocolUDP  = "udp"
	ProtocolSSL  = "ssl"

	ServiceICMPPing = "ping"
)

type Results struct {
	Results  []Result               `json:"results"`
	HostInfo map[string]interface{} `json:"hostInfo,omitempty"`
}

type Result struct {
	CheckUUID        string                   `json:"checkUuid"`
	Timestamp        int64                    `json:"timestamp"`
	CheckType        string                   `json:"checkType"`
	Check            interface{}              `json:"check"` // *CheckData
	Measurements     map[string]interface{}   `json:"measurements"`
	Message          interface{}              `json:"message"`
	Node             string                   `json:"node,omitempty"` // filled in when result is coming from a neighbor
	NodeMeasurements []map[string]interface{} `json:"nodeMeasurements,omitempty"`
}

type MeasurementsMap map[string]interface{}


<|start_filename|>sendermode.go<|end_filename|>
package frontman

import (
	"bytes"
	"compress/gzip"
	"encoding/json"
	"fmt"
	"net/http"
	"net/url"
	"os"
	"sync/atomic"
	"time"

	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

func (fm *Frontman) postResultsToHub(results []Result) error {
	if len(results) == 0 {
		return nil
	}

	fm.offlineResultsLock.Lock()
	defer fm.offlineResultsLock.Unlock()
	fm.offlineResultsBuffer = append(fm.offlineResultsBuffer, results...)

	b, err := json.Marshal(Results{
		Results: fm.offlineResultsBuffer,
	})
	if err != nil {
		return err
	}

	// in case we have HubMaxOfflineBufferBytes set(>0) and buffer + results exceed HubMaxOfflineBufferBytes -> reset buffer
	if fm.Config.HubMaxOfflineBufferBytes > 0 && len(b) > fm.Config.HubMaxOfflineBufferBytes && len(fm.offlineResultsBuffer) > 0 {
		logrus.Errorf("hub_max_offline_buffer_bytes(%d bytes) exceed with %d results. Flushing the buffer...",
			fm.Config.HubMaxOfflineBufferBytes,
			len(results))

		fm.offlineResultsBuffer = results
		b, err = json.Marshal(Results{
			Results: fm.offlineResultsBuffer,
		})
		if err != nil {
			return err
		}
	}

	if fm.Config.HubURL == "" {
		return newEmptyFieldError("hub_url")
	} else if u, err := url.Parse(fm.Config.HubURL); err != nil {
		err = errors.WithStack(err)
		return newFieldError("hub_url", err)
	} else if u.Scheme != "http" && u.Scheme != "https" {
		err := errors.Errorf("wrong scheme '%s', URL must start with http:// or https://", u.Scheme)
		return newFieldError("hub_url", err)
	}

	var req *http.Request
	var bodyLength int

	if fm.Config.HubGzip {
		var buffer bytes.Buffer
		zw := gzip.NewWriter(&buffer)
		if _, err := zw.Write(b); err != nil {
			_ = zw.Close()
			return err
		}
		if err := zw.Close(); err != nil {
			return err
		}

		req, err = http.NewRequest("POST", fm.Config.HubURL, &buffer)
		bodyLength = buffer.Len()
		req.Header.Set("Content-Encoding", "gzip")
	} else {
		req, err = http.NewRequest("POST", fm.Config.HubURL, bytes.NewBuffer(b))
		bodyLength = len(b)
	}
	if err != nil {
		return err
	}

	req.Header.Add("User-Agent", fm.userAgent())

	if fm.Config.HubUser != "" {
		req.SetBasicAuth(fm.Config.HubUser, fm.Config.HubPassword)
	}

	started := time.Now()

	resp, err := fm.hubClient.Do(req)
	if err != nil {
		return err
	}

	defer resp.Body.Close()

	secondsSpent := float64(time.Since(started)) / float64(time.Second)
	logrus.Infof("Sent %d results to Hub.. Status %d. Spent %fs", len(fm.offlineResultsBuffer), resp.StatusCode, secondsSpent)

	if resp.StatusCode == 205 {
		logrus.Debugf("postResultsToHub hub returned 205")
		return ErrorHubResetContent{}
	}
	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
		logrus.Debugf("postResultsToHub failed with %v", resp.Status)
		return ErrorHubGeneral{resp.StatusCode, resp.Status}
	}

	// in case of successful POST, we reset the offline buffer
	fm.offlineResultsBuffer = []Result{}

	// Update frontman statistics
	fm.statsLock.Lock()
	fm.stats.BytesSentToHubTotal += uint64(bodyLength)
	fm.statsLock.Unlock()

	return nil
}

func (fm *Frontman) sendResultsChanToFile(outputFile *os.File) error {
	var results []Result
	var jsonEncoder = json.NewEncoder(outputFile)
	for res := range fm.resultsChan {
		results = append(results, res)
	}

	return jsonEncoder.Encode(results)
}

// posts results to hub, used by RunOnce
func (fm *Frontman) sendResultsChanToHub() error {
	var results []Result
	logrus.Infof("sendResultsChanToHub collecting %d results", len(fm.resultsChan))
	for res := range fm.resultsChan {
		results = append(results, res)
	}

	logrus.Infof("sendResultsChanToHub: sending %d results", len(results))
	err := fm.postResultsToHub(results)
	if err != nil {
		return fmt.Errorf("postResultsToHub: %s", err.Error())
	}

	fm.statsLock.Lock()
	fm.stats.CheckResultsSentToHub += uint64(len(results))
	fm.statsLock.Unlock()

	return nil
}

func (fm *Frontman) writeQueueStatsContinuous() {
	writeQueueStatsInterval := time.Millisecond * 200

	for {
		select {
		case <-fm.InterruptChan:
			logrus.Infof("writeQueueStatsContinuous interrupt caught, returning")
			return
		case <-time.After(writeQueueStatsInterval):
			fm.writeQueueStats()
		}
	}
}

func (fm *Frontman) pollResultsChan() {

	// chan polling forever until closed
	for res := range fm.resultsChan {
		fm.resultsLock.Lock()
		fm.results = append(fm.results, res)
		fm.resultsLock.Unlock()
	}

	logrus.Debugf("pollResultsChan resultsChan closed, returning")
}

// expire results who is past TTL
func (fm *Frontman) expireOldResults() {
	ttlDuration := time.Duration(fm.Config.CheckResultsTTL) * time.Second

	fm.resultsLock.Lock()
	currentResults := []Result{}
	for _, result := range fm.results {
		since := time.Since(time.Unix(result.Timestamp, 0))
		if since <= ttlDuration {
			currentResults = append(currentResults, result)
		} else {
			logrus.Debugf("result %s is %v old, expiring", result.CheckUUID, since)
		}
	}
	fm.results = currentResults
	fm.resultsLock.Unlock()
}

// sends results to hub continuously
func (fm *Frontman) sendResultsChanToHubQueue() {

	sendInterval := secToDuration(float64(fm.Config.SenderInterval))
	var sendResults []Result
	lastSentToHub := time.Unix(0, 0)

	for {
		if time.Since(lastSentToHub) >= sendInterval {
			lastSentToHub = time.Now()

			fm.expireOldResults()

			fm.resultsLock.Lock()
			if len(fm.results) >= fm.Config.SenderBatchSize {
				sendResults = fm.results[0:fm.Config.SenderBatchSize]
				fm.results = fm.results[fm.Config.SenderBatchSize:]
			} else {
				sendResults = fm.results
				fm.results = nil
			}
			fm.resultsLock.Unlock()

			currentSenders := int(atomic.LoadInt64(&fm.senderThreads))
			if currentSenders < fm.Config.SenderThreadConcurrency {
				if len(sendResults) > 0 {
					logrus.Infof("sendResultsChanToHubQueue: sending %v results", len(sendResults))
					fm.TerminateQueue.Add(1)

					atomic.AddInt64(&fm.senderThreads, 1)
					go func(r []Result) {
						defer fm.TerminateQueue.Done()
						defer atomic.AddInt64(&fm.senderThreads, -1)

						err := fm.postResultsToHub(r)

						if err == nil {
							fm.statsLock.Lock()
							fm.stats.CheckResultsSentToHub += uint64(len(r))
							fm.statsLock.Unlock()
						} else {
							switch err.(type) {
							case ErrorHubResetContent:
								logrus.Debugf("result queue cleared")
								fm.resultsLock.Lock()
								fm.results = []Result{}
								fm.resultsLock.Unlock()

							case ErrorHubGeneral:
								if !fm.Config.DiscardOnHTTPResponseError {
									// If the hub doesn't respond with 2XX, the results remain in the queue.
									fm.resultsLock.Lock()
									fm.results = append(fm.results, r...)
									fm.resultsLock.Unlock()
								}
							default:
								if !fm.Config.DiscardOnHTTPConnectError {
									fm.resultsLock.Lock()
									fm.results = append(fm.results, r...)
									fm.resultsLock.Unlock()
								}
							}
							logrus.Errorf("postResultsToHub error: %s", err.Error())
						}
					}(sendResults)
				} else {
					logrus.Infof("sendResultsChanToHubQueue: nothing to do. outgoing queue empty.")
				}
			} else {
				logrus.Errorf("Too few concurrent sender threads (%d)", currentSenders)
			}
		}

		select {
		case <-fm.InterruptChan:
			fm.resultsLock.RLock()
			logrus.Infof("sendResultsChanToHubQueue interrupt caught, posting last %d results", len(fm.results))
			if err := fm.postResultsToHub(fm.results); err != nil {
				logrus.Error(err)
			}
			fm.resultsLock.RUnlock()
			return
		case <-time.After(250 * time.Millisecond):
			continue
		}
	}
}

func (fm *Frontman) writeQueueStats() {
	if fm.Config.QueueStatsFile == "" {
		return
	}

	fm.ipc.mutex.RLock()
	ipcLen := len(fm.ipc.uuids)
	fm.ipc.mutex.RUnlock()

	fm.checksLock.RLock()
	checksLen := len(fm.checks)
	fm.checksLock.RUnlock()

	fm.resultsLock.RLock()
	resultsLen := len(fm.results)
	fm.resultsLock.RUnlock()

	data, err := json.Marshal(map[string]int{
		"checks_queue":       checksLen,
		"checks_in_progress": ipcLen,
		"results_queue":      resultsLen,
		"ts":                 int(time.Now().UnixNano())})

	if err != nil {
		logrus.Error("writeQueueStats Marshal", err)
		return
	}

	go func(b []byte) {
		f, err := os.Create(fm.Config.QueueStatsFile)
		if err != nil {
			logrus.Error("writeQueueStats Create", err)
			return
		}
		defer f.Close()
		_, err = f.Write(b)
		if err != nil {
			logrus.Error("writeQueueStats Write", err)
		}
	}(data)
}


<|start_filename|>service_windows.go<|end_filename|>
// +build windows

package frontman

import (
	"github.com/kardianos/service"
)

func updateServiceConfig(fm *Frontman, username string) {
	// nothing to do
}

func (fm *Frontman) configureServiceEnabledState(s service.Service) {
	// nothing to do
}


<|start_filename|>service.go<|end_filename|>
package frontman

import (
	"fmt"
	"runtime"
	"strings"

	"github.com/kardianos/service"
	"github.com/sirupsen/logrus"
)

func tryStartService(s service.Service) {
	logrus.Info("Starting service...")
	err := s.Start()
	if err != nil {
		logrus.WithError(err).Warningf("Frontman service(%s) startup failed", s.Platform())
	}
}

func tryInstallService(s service.Service, assumeYesPtr *bool) {
	const maxAttempts = 3
	for attempt := 1; attempt <= maxAttempts; attempt++ {
		err := s.Install()
		// Check error case where the service already exists
		switch {
		case err != nil && strings.Contains(err.Error(), "already exists"):
			if attempt == maxAttempts {
				logrus.Fatalf("Giving up after %d attempts", maxAttempts)
			}

			var osSpecificNote string
			if runtime.GOOS == "windows" {
				osSpecificNote = " Windows Services Manager application must be closed before proceeding!"
			}

			fmt.Printf("Frontman service(%s) already installed: %s\n", s.Platform(), err.Error())
			if *assumeYesPtr || AskForConfirmation("Do you want to overwrite it?"+osSpecificNote) {
				logrus.Info("Trying to override old service unit...")
				err = s.Stop()
				if err != nil {
					logrus.WithError(err).Warnln("Failed to stop the service")
				}

				// lets try to uninstall despite of this error
				err := s.Uninstall()
				if err != nil {
					logrus.WithError(err).Fatalln("Failed to uninstall the service")
				}
			}
		case err != nil:
			logrus.WithError(err).Fatalf("Frontman service(%s) installation failed", s.Platform())
		default:
			logrus.Infof("Frontman service(%s) has been installed.", s.Platform())
			return
		}
	}
}

func (fm *Frontman) tryUpgradeServiceUnit(s service.Service) {
	_, err := s.Status()
	if err == service.ErrNotInstalled {
		logrus.Error("Can't upgrade service: service is not installed")
		return
	}

	fm.configureServiceEnabledState(s)

	err = s.Stop()
	if err != nil {
		logrus.WithError(err).Warnln("Failed to stop the service")
	}

	err = s.Uninstall()
	if err != nil {
		logrus.WithError(err).Fatalln("Failed to uninstall the service")
	}

	err = s.Install()
	if err != nil {
		logrus.WithError(err).Fatalf("Frontman service(%s) unit upgrade failed", s.Platform())
	}

	logrus.Infof("Frontman service(%s) unit has been upgraded.", s.Platform())
}


<|start_filename|>healthcheck.go<|end_filename|>
package frontman

import (
	"fmt"
	"strings"
	"sync"
	"time"

	"github.com/go-ping/ping"
	"github.com/sirupsen/logrus"
)

// HealthCheck runs before any other check to ensure that the host itself and its network are healthly.
// This is useful to confirm a stable internet connection to avoid false alerts due to network outages.
func (fm *Frontman) HealthCheck() error {
	hcfg := fm.Config.HealthChecks
	if len(hcfg.ReferencePingHosts) == 0 {
		return nil
	}
	if hcfg.ReferencePingCount == 0 {
		return nil
	}
	timeout := secToDuration(hcfg.ReferencePingTimeout)
	if timeout == 0 {
		// use the default timeout
		timeout = 500 * time.Millisecond
	}
	failC := make(chan string, len(hcfg.ReferencePingHosts))

	wg := new(sync.WaitGroup)
	for _, addr := range hcfg.ReferencePingHosts {
		pinger, err := ping.NewPinger(addr)
		if err != nil {
			logrus.WithError(err).Warningln("failed to parse host for ICMP ping")
			continue
		}
		pinger.Timeout = timeout
		pinger.Count = hcfg.ReferencePingCount
		wg.Add(1)
		go func(addr string) {
			defer wg.Done()
			pinger.Run()
			if pinger.Statistics().PacketLoss > 0 {
				failC <- addr
			}
		}(addr)
	}
	go func() {
		wg.Wait()
		close(failC)
	}()

	failedHosts := []string{}
	for host := range failC {
		failedHosts = append(failedHosts, host)
	}

	fm.statsLock.Lock()
	fm.stats.HealthChecksPerformed++
	fm.stats.HealthChecksLastTimestamp = uint64(time.Now().Unix())
	fm.statsLock.Unlock()

	if len(failedHosts) > 0 {
		return fmt.Errorf("host(s) failed to respond to ICMP ping: %s", strings.Join(failedHosts, ", "))
	}

	return nil
}


<|start_filename|>ssl.go<|end_filename|>
package frontman

import (
	"context"
	"crypto/tls"
	"crypto/x509"
	"fmt"
	"math"
	"net"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
)

const timeoutPortLookup = time.Second * 3

func certName(cert *x509.Certificate) string {
	return fmt.Sprintf("'%s' issued by %s", cert.Subject.CommonName, cert.Issuer.CommonName)
}

func (fm *Frontman) runSSLCheck(hostname string, port int, service string) (m MeasurementsMap, err error) {
	service = strings.ToLower(service)

	if net.ParseIP(hostname) != nil {
		hostname = ""
	}

	if port == 0 {
		ctx, cancel := context.WithTimeout(context.Background(), timeoutPortLookup)
		defer cancel()

		if p, exists := defaultPortByService[service]; exists {
			port = p
		} else if p, lerr := net.DefaultResolver.LookupPort(ctx, "tcp", service); p > 0 {
			port = p
		} else if lerr != nil {
			err = fmt.Errorf("failed to auto-determine port for '%s': %s", service, lerr.Error())
			return
		}
	}

	prefix := fmt.Sprintf("net.tcp.ssl.%d.", port)

	m = MeasurementsMap{
		prefix + "success": 0,
	}

	addr := fmt.Sprintf("%s:%d", hostname, port)
	dialer := net.Dialer{Timeout: secToDuration(fm.Config.NetTCPTimeout)}
	connection, err := tls.DialWithDialer(
		&dialer,
		"tcp",
		addr,
		&tls.Config{ServerName: hostname},
	)
	if err != nil {
		logrus.Debugf("serviceCheck: SSL check %s for '%s' failed: %s", addr, hostname, err.Error())
		if strings.HasPrefix(err.Error(), "tls:") {
			err = fmt.Errorf("service doesn't support SSL")
		} else {
			err = fmt.Errorf(strings.TrimPrefix(err.Error(), "x509: "))
		}
		return
	}

	defer connection.Close()

	remainingValidity, firstCertToExpire := findCertRemainingValidity(connection.ConnectionState().VerifiedChains)
	m[prefix+"expiryDaysRemaining"] = remainingValidity

	if remainingValidity <= float64(fm.Config.SSLCertExpiryThreshold) {
		err = fmt.Errorf("certificate will expire soon: %s", certName(firstCertToExpire))
		return
	}

	m[prefix+"success"] = 1
	return
}

func findCertRemainingValidity(certChains [][]*x509.Certificate) (float64, *x509.Certificate) {
	var remainingValidity float64
	var firstToExpire *x509.Certificate

	// find chain with max remaining validity
	for _, chain := range certChains {
		chainRemainingValidity, c := findChainRemainingValidity(chain)
		if chainRemainingValidity > remainingValidity {
			remainingValidity = chainRemainingValidity
			firstToExpire = c
		}
	}
	return remainingValidity, firstToExpire
}

func findChainRemainingValidity(chain []*x509.Certificate) (float64, *x509.Certificate) {
	var min = math.MaxFloat64
	var firstToExpire *x509.Certificate

	// find cert that will expire first
	for _, cert := range chain {
		remainingValidity := time.Until(cert.NotAfter).Hours() / 24
		if remainingValidity < min {
			min = remainingValidity
			firstToExpire = cert
		}
	}
	return min, firstToExpire
}


<|start_filename|>frontman_test.go<|end_filename|>
package frontman

import (
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
)

func helperCreateFrontman(t *testing.T, cfg *Config) *Frontman {
	t.Helper()
	fm, err := New(cfg, DefaultCfgPath, "1.2.3")
	assert.Nil(t, err)
	fm.ipc = newIPC()
	return fm
}

func TestFrontmanHubInput(t *testing.T) {
	hub := NewMockHub("localhost:9100")
	go hub.Serve()

	cfg, err := HandleAllConfigSetup(DefaultCfgPath)
	assert.Nil(t, err)

	cfg.HubURL = hub.URL() + "/?serviceChecks=1&webChecks=1"
	cfg.LogLevel = "debug"
	cfg.Sleep = 10          // delay between each round of checks
	cfg.SenderBatchSize = 2 // number of results to send to hub at once
	cfg.SenderInterval = 0
	cfg.ICMPTimeout = 0.1
	cfg.HTTPCheckTimeout = 0.1
	cfg.SleepDurationAfterCheck = 0
	cfg.SleepDurationEmptyQueue = 0
	cfg.Nodes = make(map[string]Node)

	fm := helperCreateFrontman(t, cfg)

	go fm.Run("", nil)

	// stop after some time
	time.Sleep(2000 * time.Millisecond)
	close(fm.InterruptChan)

	fm.statsLock.Lock()
	assert.Equal(t, true, fm.stats.BytesSentToHubTotal > 0)
	assert.Equal(t, true, fm.stats.BytesFetchedFromHubTotal > 0)
	assert.Equal(t, true, fm.stats.ChecksPerformedTotal > 0)
	assert.Equal(t, uint64(2), fm.stats.ChecksFetchedFromHub)
	assert.Equal(t, true, fm.stats.CheckResultsSentToHub > 0)
	fm.statsLock.Unlock()

	fm.ipc.mutex.RLock()
	assert.Equal(t, true, len(fm.ipc.uuids) > 0)
	fm.ipc.mutex.RUnlock()
}


<|start_filename|>snmp.go<|end_filename|>
package frontman

import (
	"fmt"
	"math"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
	"github.com/soniah/gosnmp"
)

const (
	protocolSNMPv1 = "v1"
	protocolSNMPv2 = "v2"
	protocolSNMPv3 = "v3"

	maxRepetitions = 255
)

// used to calculate delta from last snmp bandwidth measure
type snmpBandwidthMeasure struct {
	timestamp   time.Time
	name        string
	ifOutOctets uint
	ifInOctets  uint
}

type snmpOidDeltaMeasure struct {
	timestamp time.Time
	name      string
	val       uint
}

type snmpPorterrorsMeasure struct {
	timestamp         time.Time
	name              string
	ifInErrors        uint
	ifOutErrors       uint
	ifInDiscards      uint
	ifOutDiscards     uint
	ifInUnknownProtos uint
}

func (check SNMPCheck) uniqueID() string {
	return check.UUID
}

func (check SNMPCheck) run(fm *Frontman) (*Result, error) {

	res := &Result{
		Node:      fm.Config.NodeName,
		CheckType: "snmpCheck",
		CheckUUID: check.UUID,
		Check:     check.Check,
		Timestamp: time.Now().Unix(),
	}

	if check.UUID == "" {
		return res, fmt.Errorf("missing checkUuid key")
	}
	if check.Check.Connect == "" {
		return res, fmt.Errorf("missing check.connect key")
	}

	var done = make(chan map[string]interface{})
	var err error
	go func() {
		var results map[string]interface{}
		results, err = fm.runSNMPProbe(&check.Check)
		successKey := "snmpCheck." + check.Check.Preset + ".success"
		if err != nil {
			results[successKey] = 0
		} else {
			results[successKey] = 1
		}
		done <- results
	}()

	// Warning: do not rely on serviceCheckEmergencyTimeout as it leak goroutines(until it will be finished)
	// instead use individual timeouts inside all checks
	select {
	case m := <-done:
		res.Measurements = m
		return res, err
	case <-time.After(serviceCheckEmergencyTimeout):
		logrus.Debugf("snmpCheck: %s got unexpected timeout after %.0fs", check.UUID, serviceCheckEmergencyTimeout.Seconds())
		return res, fmt.Errorf("got unexpected timeout")
	}
}

func (fm *Frontman) runSNMPProbe(check *SNMPCheckData) (map[string]interface{}, error) {

	check.ValueType = strings.ToLower(check.ValueType)
	if check.ValueType == "" {
		check.ValueType = "raw"
	}

	m := make(map[string]interface{})
	params, err := buildSNMPParameters(check)
	if err != nil {
		return m, err
	}

	err = params.Connect()
	if err != nil {
		return m, fmt.Errorf("connect err: %v", err)
	}
	defer params.Conn.Close()

	// do a simple snmp probe to make sure we are authenticated (work around issue https://github.com/soniah/gosnmp/issues/196)
	authRes, err := params.Get([]string{"1.3.6.1.2.1.1.1.0"})
	if err != nil {
		return m, fmt.Errorf("get err: %v", err)
	}
	if err := getErrorFromVariables(authRes.Variables); err != nil {
		return m, err
	}

	oids, form, err := check.presetToOids()
	if err != nil {
		return m, err
	}

	var packets []gosnmp.SnmpPDU
	switch form {
	case "bulk":
		logrus.Debugln("snmp: GetBulk", oids)
		result, err := params.GetBulk(oids, uint8(len(oids)), maxRepetitions)
		if err != nil {
			return m, fmt.Errorf("get bulk err: %v", err)
		}
		packets = result.Variables
	case "walk":
		for _, oid := range oids {
			logrus.Debugln("snmp: BulkWalkAll", oid)
			pdus, err := params.BulkWalkAll(oid)
			if err != nil {
				return m, fmt.Errorf("bulk walk all err: %v", err)
			}
			packets = append(packets, pdus...)
		}
	case "single":
		logrus.Debugln("snmp: Get", oids)
		result, err := params.Get(oids)
		if err != nil {
			return m, fmt.Errorf("get err: %v", err)
		}
		packets = append(packets, result.Variables...)
	}

	return fm.prepareSNMPResult(check, packets)
}

// getErrorFromVariables returns an error if any of the oid:s in the packets contains a recognized oid error
func getErrorFromVariables(packets []gosnmp.SnmpPDU) error {
	for _, variable := range packets {
		if err := oidToError(variable.Name); err != nil {
			return err
		}
	}
	return nil
}

type snmpResult struct {
	key string
	val interface{}
}

func (fm *Frontman) prepareSNMPResult(check *SNMPCheckData, packets []gosnmp.SnmpPDU) (map[string]interface{}, error) {
	res := make(map[int][]snmpResult)
	for _, variable := range packets {
		if err := oidToError(variable.Name); err != nil {
			return make(map[string]interface{}), err
		}
		if ignoreSNMPOid(variable.Name) {
			continue
		}
		var prefix string
		var suffix int
		var err error

		if check.Preset == "oid" {
			prefix = variable.Name
		} else {
			prefix, suffix, err = oidToHumanReadable(variable.Name)
			if err != nil {
				// skip unrecognized oid:s
				continue
			}
		}

		switch variable.Type {
		case gosnmp.OctetString:
			val := ""
			if (check.ValueType == "raw" && oidShouldBeHexString(variable.Name)) || check.ValueType == "hex" {
				// format hex as "99:aa:bb:cc:dd"
				val = fmt.Sprintf("% x", variable.Value.([]byte))
				val = strings.ReplaceAll(val, " ", ":")
			} else {
				val = string(variable.Value.([]byte))
			}
			res[suffix] = append(res[suffix], snmpResult{key: prefix, val: val})

		case gosnmp.TimeTicks, gosnmp.Integer, gosnmp.Counter32, gosnmp.Gauge32:
			res[suffix] = append(res[suffix], snmpResult{key: prefix, val: variable.Value})

		case gosnmp.Null:
			res[suffix] = append(res[suffix], snmpResult{key: prefix, val: ""})

		case gosnmp.NoSuchInstance:
			return make(map[string]interface{}), fmt.Errorf("no such instance")

		default:
			logrus.Debugf("SNMP unhandled return type %#v for %s: %d", variable.Type, prefix, variable.Value)
		}
	}
	return fm.filterSNMPResult(check, res)
}

const (
	ifOperStatusUp       = 1
	ifTypeEthernetCsmacd = 6
)

// returns true if snmpResult is part of a network interface that should be excluded
func (kv snmpResult) shouldExcludeInterface() bool {
	if kv.key == "ifOperStatus" && kv.val.(int) != ifOperStatusUp {
		return true
	}
	if kv.key == "ifType" && kv.val.(int) != ifTypeEthernetCsmacd {
		return true
	}
	return false
}

type jsonFloat64 float64

func (value jsonFloat64) MarshalJSON() ([]byte, error) {
	if math.IsNaN(float64(value)) {
		return []byte("null"), nil
	}
	return []byte(fmt.Sprintf("%f", value)), nil
}

// filters the snmp results according to preset
func (fm *Frontman) filterSNMPResult(check *SNMPCheckData, res map[int][]snmpResult) (map[string]interface{}, error) {
	m := make(map[string]interface{})
	switch check.Preset {
	case "bandwidth":
		prevMeasures := fm.previousSNMPBandwidthMeasure
		fm.previousSNMPBandwidthMeasure = nil
		for idx, iface := range res {
			skip := false
			for _, kv := range iface {
				if kv.shouldExcludeInterface() {
					if kv.key == "ifOperStatus" && kv.val.(int) != ifOperStatusUp {
						logrus.Debug("Excluding interface ", idx, " since status is ", kv.val)
					}
					if kv.key == "ifType" && kv.val.(int) != ifTypeEthernetCsmacd {
						logrus.Debug("Excluding interface ", idx, " since type is ", kv.val)
					}
					skip = true
					break
				}
			}
			if skip {
				continue
			}
			m[fmt.Sprint(idx)] = fm.filterSNMPBandwidthResult(idx, iface, prevMeasures)
		}

	case "porterrors":
		prevMeasures := fm.previousSNMPPorterrorsMeasure
		fm.previousSNMPPorterrorsMeasure = nil
		for idx, iface := range res {
			skip := false
			for _, kv := range iface {
				if kv.shouldExcludeInterface() {
					if kv.key == "ifOperStatus" && kv.val.(int) != ifOperStatusUp {
						logrus.Debug("Excluding interface ", idx, " since status is ", kv.val)
					}
					if kv.key == "ifType" && kv.val.(int) != ifTypeEthernetCsmacd {
						logrus.Debug("Excluding interface ", idx, " since type is ", kv.val)
					}
					skip = true
					break
				}
			}
			if skip {
				continue
			}
			m[fmt.Sprint(idx)] = fm.filterSNMPPorterrorsResult(idx, iface, prevMeasures)
		}

	case "oid":
		if len(res) != 1 {
			logrus.Debug("unexpected oid result length:", res)
		} else {
			for idx := range res {
				for _, r := range res[idx] {
					m[r.key] = fm.filterSNMPOidDeltaResult(check, r)
					break
				}
			}
		}

	default:
		// flatten
		if len(res) != 1 {
			return m, fmt.Errorf("unexpected index count %d", len(res))
		}
		for _, x := range res[0] {
			m[x.key] = x.val
		}
	}
	return m, nil
}

func (fm *Frontman) filterSNMPOidDeltaResult(check *SNMPCheckData, r snmpResult) map[string]interface{} {
	m := make(map[string]interface{})

	// pass-through values for easy consumption by frontend
	m["name"] = check.Name
	m["oid"] = check.Oid
	m["value_type"] = check.ValueType
	m["unit"] = check.Unit

	if check.ValueType == "raw" || check.ValueType == "hex" {
		m["value"] = r.val.(string)
		return m
	}

	prevMeasures := fm.previousSNMPOidDeltaMeasure
	fm.previousSNMPOidDeltaMeasure = nil

	val := r.val.(uint)

	switch check.ValueType {
	case "delta":
		// calculate delta from previous measure
		for _, measure := range prevMeasures {
			if measure.name == check.Oid {
				m["value"] = jsonFloat64(deltaFloat(float64(measure.val), float64(val)))
				break
			}
		}
	case "delta_per_sec":
		// calculate delta per second from previous measure
		for _, measure := range prevMeasures {
			if measure.name == check.Oid {
				delaySeconds := float64(time.Since(measure.timestamp) / time.Second)
				delta := deltaFloat(float64(measure.val), float64(val))
				m["value"] = jsonFloat64(delta / delaySeconds)
				break
			}
		}
	default:
		logrus.Warnf("snmpCheck: invalid value_type '%s'", check.ValueType)
	}

	fm.previousSNMPOidDeltaMeasure = append(fm.previousSNMPOidDeltaMeasure, snmpOidDeltaMeasure{
		timestamp: time.Now(),
		name:      check.Oid,
		val:       val,
	})

	return m
}

func (fm *Frontman) filterSNMPBandwidthResult(idx int, iface []snmpResult, prevMeasures []snmpBandwidthMeasure) map[string]interface{} {
	m := make(map[string]interface{})

	ifIn := uint(0)
	ifOut := uint(0)
	ifName := ""
	ifSpeedInBytes := uint(0)
	for _, x := range iface {
		key := x.key
		switch x.key {
		case "ifOperStatus", "ifType":
			continue
		case "ifName":
			ifName = x.val.(string)
		case "ifInOctets":
			ifIn = x.val.(uint)
		case "ifOutOctets":
			ifOut = x.val.(uint)
		case "ifSpeed":
			key = "ifSpeed_mbps"
			if x.val.(uint) > 0 {
				x.val = x.val.(uint) / 1000000 // megabits
				ifSpeedInBytes = (x.val.(uint) * 1000000) / 8
			}
		}
		m[key] = x.val
	}
	m["ifIndex"] = idx

	// calculate delta from previous measure
	for _, measure := range prevMeasures {
		if measure.name == ifName {
			delaySeconds := float64(time.Since(measure.timestamp)) / float64(time.Second)
			inDelta := float64(delta(measure.ifInOctets, ifIn))
			outDelta := float64(delta(measure.ifOutOctets, ifOut))
			m["ifIn_Bps"] = jsonFloat64(math.Round(inDelta / delaySeconds))
			m["ifOut_Bps"] = jsonFloat64(math.Round(outDelta / delaySeconds))
			if ifSpeedInBytes > 0 {
				inPct := (inDelta / (float64(ifSpeedInBytes) * delaySeconds)) * 100
				outPct := (outDelta / (float64(ifSpeedInBytes) * delaySeconds)) * 100
				m["ifInUtilization_percent"] = jsonFloat64(math.Round(inPct*100) / 100)
				m["ifOutUtilization_percent"] = jsonFloat64(math.Round(outPct*100) / 100)
			}
			break
		}
	}

	fm.previousSNMPBandwidthMeasure = append(fm.previousSNMPBandwidthMeasure, snmpBandwidthMeasure{
		timestamp:   time.Now(),
		name:        ifName,
		ifInOctets:  ifIn,
		ifOutOctets: ifOut,
	})

	return m
}

func (fm *Frontman) filterSNMPPorterrorsResult(idx int, iface []snmpResult, prevMeasures []snmpPorterrorsMeasure) map[string]interface{} {
	m := make(map[string]interface{})

	ifInErrors := uint(0)
	ifOutErrors := uint(0)
	ifInDiscards := uint(0)
	ifOutDiscards := uint(0)
	ifInUnknownProtos := uint(0)
	ifName := ""

	for _, x := range iface {
		key := x.key
		switch x.key {
		case "ifOperStatus", "ifType":
			continue
		case "ifName":
			ifName = x.val.(string)
		case "ifInErrors":
			ifInErrors = x.val.(uint)
		case "ifOutErrors":
			ifOutErrors = x.val.(uint)
		case "ifInDiscards":
			ifInDiscards = x.val.(uint)
		case "ifOutDiscards":
			ifOutDiscards = x.val.(uint)
		case "ifInUnknownProtos":
			ifInUnknownProtos = x.val.(uint)
		}
		m[key] = x.val
	}
	m["ifIndex"] = idx

	// calculate delta from previous measure
	for _, measure := range prevMeasures {
		if measure.name == ifName {
			delaySeconds := float64(time.Since(measure.timestamp)) / float64(time.Second)
			inErrorsDelta := float64(delta(measure.ifInErrors, ifInErrors))
			outErrorsDelta := float64(delta(measure.ifOutErrors, ifOutErrors))
			m["ifInErrors_delta"] = uint(math.Round(inErrorsDelta / delaySeconds))
			m["ifOutErrors_delta"] = uint(math.Round(outErrorsDelta / delaySeconds))

			inDiscardsDelta := float64(delta(measure.ifInDiscards, ifInDiscards))
			outDiscardsDelta := float64(delta(measure.ifOutDiscards, ifOutDiscards))
			m["ifInDiscards_delta"] = uint(math.Round(inDiscardsDelta / delaySeconds))
			m["ifOutDiscards_delta"] = uint(math.Round(outDiscardsDelta / delaySeconds))

			inUnknownProtosDelta := float64(delta(measure.ifInUnknownProtos, ifInUnknownProtos))
			m["ifInUnknownProtos_delta"] = uint(math.Round(inUnknownProtosDelta / delaySeconds))
			break
		}
	}

	fm.previousSNMPPorterrorsMeasure = append(fm.previousSNMPPorterrorsMeasure, snmpPorterrorsMeasure{
		timestamp:         time.Now(),
		name:              ifName,
		ifInErrors:        ifInErrors,
		ifOutErrors:       ifOutErrors,
		ifInDiscards:      ifInDiscards,
		ifOutDiscards:     ifOutDiscards,
		ifInUnknownProtos: ifInUnknownProtos,
	})
	return m
}

func delta(v1, v2 uint) uint {
	if v1 < v2 {
		return v2 - v1
	}
	return v1 - v2
}

func deltaFloat(v1, v2 float64) float64 {
	if v1 < v2 {
		return v2 - v1
	}
	return v1 - v2
}

// generates gosnmp parameters for the given check configuration
func buildSNMPParameters(check *SNMPCheckData) (*gosnmp.GoSNMP, error) {
	if check.Timeout < 5 {
		check.Timeout = 5
	}
	params := &gosnmp.GoSNMP{
		Target:  check.Connect,
		Port:    check.Port,
		Timeout: time.Duration(check.Timeout) * time.Second,
	}
	switch check.Protocol {
	case protocolSNMPv1:
		params.Version = gosnmp.Version1
		params.Community = check.Community
	case protocolSNMPv2:
		params.Version = gosnmp.Version2c
		params.Community = check.Community
	case protocolSNMPv3:
		var err error
		params.Version = gosnmp.Version3
		params.SecurityModel = gosnmp.UserSecurityModel
		params.SecurityParameters, err = buildSNMPSecurityParameters(check)
		if err != nil {
			return nil, err
		}
		switch check.SecurityLevel {
		case "noAuthNoPriv":
			params.MsgFlags = gosnmp.NoAuthNoPriv
		case "authNoPriv":
			params.MsgFlags = gosnmp.AuthNoPriv
		case "authPriv":
			params.MsgFlags = gosnmp.AuthPriv
		default:
			return nil, fmt.Errorf("invalid security_level configuration value '%s'", check.SecurityLevel)
		}
	default:
		return nil, fmt.Errorf("invalid protocol '%s'", check.Protocol)
	}
	return params, nil
}

func buildSNMPSecurityParameters(check *SNMPCheckData) (sp *gosnmp.UsmSecurityParameters, err error) {
	// 8+ password length required by the SNMPv3 USM
	if check.SecurityLevel != "noAuthNoPriv" && len(check.AuthenticationPassword) < 8 {
		return nil, fmt.Errorf("authentication_password must be at least 8 characters")
	}
	sp = &gosnmp.UsmSecurityParameters{
		UserName: check.Username,
	}
	switch check.AuthenticationProtocol {
	case "sha", "sha1":
		sp.AuthenticationProtocol = gosnmp.SHA
	case "md5":
		sp.AuthenticationProtocol = gosnmp.MD5
	case "":
		sp.AuthenticationProtocol = gosnmp.NoAuth
	default:
		return sp, fmt.Errorf("invalid authentication_protocol '%s'", check.AuthenticationProtocol)
	}

	switch check.PrivacyProtocol {
	case "des":
		sp.PrivacyProtocol = gosnmp.DES
	case "":
		sp.PrivacyProtocol = gosnmp.NoPriv
	default:
		return sp, fmt.Errorf("invalid privacy_protocol '%s'", check.PrivacyProtocol)
	}
	if check.SecurityLevel == "authPriv" && len(check.PrivacyPassword) < 8 {
		return nil, fmt.Errorf("privacy_password must be at least 8 characters")
	}

	switch check.SecurityLevel {
	case "noAuthNoPriv":
		sp.AuthenticationProtocol = gosnmp.NoAuth
	case "authNoPriv":
		sp.AuthenticationPassphrase = check.AuthenticationPassword
	case "authPriv":
		sp.AuthenticationPassphrase = check.AuthenticationPassword
		sp.PrivacyPassphrase = check.PrivacyPassword
	default:
		err = fmt.Errorf("invalid security_level configuration value '%s'", check.SecurityLevel)
	}
	return
}

// returns true if oid should be ignored
func ignoreSNMPOid(name string) bool {
	switch name {
	case ".1.3.6.1.2.1.1.2.0", // sysObjectID
		".1.3.6.1.2.1.1.7.0": // sysServices
		return true
	}
	return false
}

// returns true if oid should be formatted as a hex string (aa:bb:cc:dd)
func oidShouldBeHexString(name string) bool {
	prefix, _, _ := oidToHumanReadable(name)
	switch prefix {
	case ".1.3.6.1.2.1.2.2.1.6": // ifPhysAddress
		return true
	}
	return false
}

// returns human readable error if OID is a error
func oidToError(name string) (err error) {
	switch name {
	case ".1.3.6.1.6.3.15.1.1.3.0":
		// usmStatsUnknownUserNames
		err = errors.New("unknown user name")
	case ".1.3.6.1.6.3.15.1.1.5.0":
		// usmStatsWrongDigests
		err = errors.New("wrong digests, possibly wrong password")
	}
	return
}

// map OID to a human readable key
func oidToHumanReadable(name string) (prefix string, suffix int, err error) {
	idx := strings.LastIndex(name, ".")
	if idx == -1 {
		err = errors.New("separator missing from input")
		return
	}
	suffix, err = strconv.Atoi(name[idx+1:])
	if err != nil {
		return
	}
	name = name[0:idx]

	switch name {
	case ".1.3.6.1.2.1.1.1":
		prefix = "system.description"
	case ".1.3.6.1.2.1.1.4":
		prefix = "system.contact"
	case ".1.3.6.1.2.1.1.6":
		prefix = "system.location"
	case ".1.3.6.1.2.1.1.3":
		prefix = "system.uptime_s"
	case ".1.3.6.1.2.1.1.5":
		prefix = "system.hostname"

	// IF-MIB
	case ".1.3.6.1.2.1.2.2.1.8":
		prefix = "ifOperStatus"
	case ".1.3.6.1.2.1.2.2.1.3":
		prefix = "ifType"
	case ".1.3.6.1.2.1.31.1.1.1.1":
		prefix = "ifName"
	case ".1.3.6.1.2.1.2.2.1.2":
		prefix = "ifDescr"
	case ".1.3.6.1.2.1.2.2.1.5":
		prefix = "ifSpeed"
	case ".1.3.6.1.2.1.31.1.1.1.18":
		prefix = "ifAlias"
	case ".1.3.6.1.2.1.31.1.1.1.15":
		prefix = "ifHighSpeed"
	case ".1.3.6.1.2.1.2.2.1.10":
		prefix = "ifInOctets"
	case ".1.3.6.1.2.1.2.2.1.16":
		prefix = "ifOutOctets"
	case ".1.3.6.1.2.1.2.2.1.14":
		prefix = "ifInErrors"
	case ".1.3.6.1.2.1.2.2.1.20":
		prefix = "ifOutErrors"
	case ".1.3.6.1.2.1.2.2.1.13":
		prefix = "ifInDiscards"
	case ".1.3.6.1.2.1.2.2.1.19":
		prefix = "ifOutDiscards"
	case ".1.3.6.1.2.1.2.2.1.15":
		prefix = "ifInUnknownProtos"

	default:
		prefix = name
		err = fmt.Errorf("unrecognized OID %s", name)
	}
	return
}

// returns a collection of oids for the given preset
func (check *SNMPCheckData) presetToOids() (oids []string, form string, err error) {
	switch check.Preset {
	case "basedata":
		oids = []string{
			".1.3.6.1.2.1.1.1.0", // STRING: SG350-10 10-Port Gigabit Managed Switch
			".1.3.6.1.2.1.1.3.0", // Timeticks: (575618700) 66 days, 14:56:27.00
			".1.3.6.1.2.1.1.4.0", // STRING: <EMAIL>
			".1.3.6.1.2.1.1.5.0", // STRING: switch-cloudradar
			".1.3.6.1.2.1.1.6.0", // STRING: Office Berlin
		}
		form = "single"
	case "bandwidth":
		oids = []string{
			".1.3.6.1.2.1.2.2.1.8",    // IF-MIB::ifOperStatus (1=up)
			".1.3.6.1.2.1.2.2.1.3",    // IF-MIB::ifType (6=ethernetCsmacd)
			".1.3.6.1.2.1.31.1.1.1.1", // IF-MIB::ifName
			".1.3.6.1.2.1.2.2.1.2",    // IF-MIB::ifDescr
			".1.3.6.1.2.1.2.2.1.5",    // IF-MIB::ifSpeed
			".1.3.6.1.2.1.2.2.1.10",   // IF-MIB::ifInOctets
			".1.3.6.1.2.1.2.2.1.16",   // IF-MIB::ifOutOctets
		}
		form = "walk"
	case "oid":
		oids = []string{check.Oid}
		form = "single"

	case "porterrors":
		oids = []string{
			".1.3.6.1.2.1.2.2.1.8",    // IF-MIB::ifOperStatus (1=up)
			".1.3.6.1.2.1.2.2.1.3",    // IF-MIB::ifType (6=ethernetCsmacd)
			".1.3.6.1.2.1.31.1.1.1.1", // IF-MIB::ifName
			".1.3.6.1.2.1.2.2.1.14",   // IF-MIB::ifInErrors
			".1.3.6.1.2.1.2.2.1.20",   // IF-MIB::ifOutErrors
			".1.3.6.1.2.1.2.2.1.13",   // IF-MIB::ifInDiscards
			".1.3.6.1.2.1.2.2.1.19",   // IF-MIB::ifOutDiscards
			".1.3.6.1.2.1.2.2.1.15",   // IF-MIB::ifInUnknownProtos
		}
		form = "walk"
	default:
		err = fmt.Errorf("unrecognized preset %s", check.Preset)
	}
	return
}


<|start_filename|>config.go<|end_filename|>
package frontman

import (
	"bytes"
	"fmt"
	"net/url"
	"os"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"github.com/cloudradar-monitoring/toml"
	"github.com/sirupsen/logrus"
)

const (
	defaultLogLevel = "error"

	IOModeFile = "file"
	IOModeHTTP = "http"

	minHubRequestTimeout     = 1
	maxHubRequestTimeout     = 600
	defaultHubRequestTimeout = 30

	minSelfUpdatesCheckInterval = 600
)

var configAutogeneratedHeadline = []byte(
	`# This is an auto-generated config to connect with the cloudradar service
# To see all options of frontman run frontman -p
#
`)

var DefaultCfgPath string
var defaultLogPath string
var rootCertsPath string
var defaultStatsFilePath string
var defaultQueueStatsFilePath string

type MinValuableConfig struct {
	LogLevel    LogLevel `toml:"log_level" comment:"\"debug\", \"info\", \"error\" verbose level; can be overridden with -v flag"`
	IOMode      string   `toml:"io_mode" comment:"\"file\" or \"http\" – where frontman gets checks to perform and post results"`
	HubURL      string   `toml:"hub_url" commented:"true"`
	HubUser     string   `toml:"hub_user" commented:"true"`
	HubPassword string   `toml:"hub_password" commented:"true"`

	HubProxy         string `toml:"hub_proxy" commented:"true"`
	HubProxyUser     string `toml:"hub_proxy_user" commented:"true"`
	HubProxyPassword string `toml:"hub_proxy_password" commented:"true"`

	// new configs should use host_info, keep system_fields to support older configs
	SystemFields []string `toml:"system_fields" commented:"true" comment:"DEPRECATED: use host_info instead"`
	HostInfo     []string `toml:"host_info" commented:"true"`
}

type Config struct {
	NodeName       string  `toml:"node_name" comment:"Name of the Frontman\nUsed to identify group measurements if multiple frontmen run in grouped-mode (ask_nodes)"`
	Sleep          float64 `toml:"sleep" comment:"delay before starting a new round of checks in second\nsleep refers to the start timestamp of the check round.\nIf sleep is 30 seconds and the round takes 25 seconds frontman waits 5 seconds to start the next round.\nIf sleep is less than the round takes, there is no delay."`
	PidFile        string  `toml:"pid" comment:"path to pid file"`
	LogFile        string  `toml:"log,omitempty" comment:"path to log file"`
	LogSyslog      string  `toml:"log_syslog" comment:"\"local\" for local unix socket or URL e.g. \"udp://localhost:514\" for remote syslog server"`
	StatsFile      string  `toml:"stats_file" comment:"Path to the file where we write frontman statistics"`
	QueueStatsFile string  `toml:"queue_stats_file" comment:"Path to the file where we write frontman queue statistics"`

	MinValuableConfig

	HubGzip                  bool `toml:"hub_gzip" comment:"enable gzip when sending results to the HUB"`
	HubRequestTimeout        int  `toml:"hub_request_timeout" comment:"time limit in seconds for requests made to Hub.\nThe timeout includes connection time, any redirects, and reading the response body.\nMin: 1, Max: 600. default: 30"`
	HubMaxOfflineBufferBytes int  `toml:"hub_max_offline_buffer_bytes" commented:"true"`

	ICMPTimeout            float64 `toml:"icmp_timeout" comment:"ICMP ping timeout in seconds"`
	NetTCPTimeout          float64 `toml:"net_tcp_timeout" comment:"TCP timeout in seconds"`
	NetUDPTimeout          float64 `toml:"net_udp_timeout" comment:"UDP timeout in seconds"`
	HTTPCheckTimeout       float64 `toml:"http_check_timeout" comment:"HTTP time in seconds"`
	HTTPCheckMaxRedirects  int     `toml:"max_redirects" comment:"Limit the number of HTTP redirects to follow"`
	IgnoreSSLErrors        bool    `toml:"ignore_ssl_errors"`
	SSLCertExpiryThreshold int     `toml:"ssl_cert_expiry_threshold" comment:"Min days remain on the SSL cert to pass the check"`

	SenderBatchSize int `toml:"sender_batch_size" comment:"Do not send back more than N results per POST request"`

	SenderInterval float64 `toml:"sender_interval" comment:"Make a pause of N seconds between POST requests when processing the result queue"`

	SenderThreadConcurrency int `toml:"sender_thread_concurrency" comment:"The maximum number of concurrent http connections for sending\nresults to the hub."`

	DiscardOnHTTPResponseError bool `toml:"discard_on_http_response_error" comment:"Discard unsent data of the current sender thread\nif the hub responds with a status code other than 2XX."`
	DiscardOnHTTPConnectError  bool `toml:"discard_on_http_connect_error" comment:"Discard unsent data from the current sender thread\nif the hub connection times out, name resolution fails,\nor cannot be established for other reasons."`

	CheckResultsTTL float64 `toml:"check_results_ttl" comment:"Keep check results not longer than the TTL (seconds) in the internal queues.\nIf TTL is exceeded check results are discarded."`

	SleepDurationAfterCheck float64 `toml:"sleep_duration_after_check" comment:"Time in seconds to sleep between each check being dispatched for execution"`
	SleepDurationEmptyQueue float64 `toml:"sleep_duration_empty_queue" comment:"Time in seconds to sleep when the check queue is empty"`

	HealthChecks HealthCheckConfig `toml:"health_checks" comment:"Frontman can verify a reliable internet uplink by pinging some reference hosts before each check round starts.\nPing all hosts of the list.\nOnly if frontman gets a positive answer form all of them, frontman continues.\nOtherwise, the entire check round is skipped. No data is sent back.\nFailed health checks are recorded to the log.\nOnly 0% packet loss is considered as a positive check result. Pings are performed in parallel.\nDisabled by default. Enable by declaring reference_ping_hosts targets\n"`

	HTTPListener HTTPListenerConfig `toml:"http_listener" comment:"Perform checks requested via HTTP POST requests"`

	FailureConfirmation      int     `toml:"failure_confirmation" comment:"In case a web or service check fails, frontman will check again after a short delay (seconds). The failure must be confirmed N times.\nfailure_confirmation = 0 switches off the confirmation of failures\nDoes not affect snmp checks"`
	FailureConfirmationDelay float64 `toml:"failure_confirmation_delay" comment:"Delay in seconds"`

	Node  NodeConfig      `toml:"node" comment:"Node configuration"`
	Nodes map[string]Node `toml:"nodes" comment:"Frontman can execute a failed check on other frontmen - ideally on different locations -\nto confirm the check fails everywhere.\nOnly if the check fails on all of them it's considered as failed and sent back to the hub.\nIf the check succeeds on one frontman this check result is sent back\nRequires the HTTP listener enabled on the foreign frontman\nExample:\n[nodes]\n  [nodes.1]\n  url = \"https://frontman-1.example.com:9955\"\n  username = \"frontman\"\n  password = \"<PASSWORD>\"\n  verify_ssl = true"`

	Updates UpdatesConfig `toml:"self_update" comment:"Control how frontman installs self-updates. Windows-only"`
}

type NodeConfig struct {
	NodeTimeout     float64  `toml:"node_timeout" comment:"Set the maximum time in seconds frontman should spend trying to connect a node"`
	NodeCacheErrors float64  `toml:"node_cache_errors" comment:"Cache errors for N seconds. If the connection to a node fails for whatever reason, this node is not asked again, until the error cache has expired"`
	ForwardExcept   []string `toml:"forward_except" comment:"Do not forward failed checks to the foreign node(s) if the message contains one of the following regular expression.\nMatching is case insensitive."`
	ForwardLog      string   `toml:"forward_log" comment:"Log all checks forwarded to foreign node(s).\nThe log contains the check ID, the check type, and the message of the local check result."`
}

type Node struct {
	URL       string `toml:"url" comment:"URL of frontman node"`
	Username  string `toml:"username" comment:"Username"`
	Password  string `toml:"password" comment:"Password"`
	VerifySSL bool   `toml:"verify_ssl"`
}

type HealthCheckConfig struct {
	ReferencePingHosts   []string `toml:"reference_ping_hosts" comment:"Ping all hosts of the list. Only if frontman gets a positive answer form all of them, frontman continues.\nOnly 0% packet loss is considered as a positive check result. Pings are performed in parallel."`
	ReferencePingTimeout float64  `toml:"reference_ping_timeout" comment:"Maximum time (seconds) to wait for the response"`
	ReferencePingCount   int      `toml:"reference_ping_count" comment:"Number of request packets to send to each host."`
}

type HTTPListenerConfig struct {
	HTTPListen       string `toml:"http_listen" comment:"HTTP Listener\nPerform checks requested via HTTP POST requests on '/check'\nExamples:\nhttp_listen = \"http://0.0.0.0:9090\"   # for unencrypted http connections\nhttp_listen = \"https://0.0.0.0:8443\"  # for encrypted https connections\nexecute \"sudo setcap cap_net_bind_service=+ep /usr/bin/frontman\" to use ports < 1024\nExecuting SNMP check through the HTTP Listener is not supported."`
	HTTPTLSKey       string `toml:"http_tls_key" comment:"Private key for https connections"`
	HTTPTLSCert      string `toml:"http_tls_cert" comment:"Certificate for https connections"`
	HTTPAuthUser     string `toml:"http_auth_user" comment:"Username for the http basic authentication. If omitted authentication is disabled"`
	HTTPAuthPassword string `toml:"http_auth_password" comment:"Password for the http basic authentication."`
	HTTPAccessLog    string `toml:"http_access_log" comment:"Log http requests. On windows slash must be escaped like \"C:\\\\access.log\""`
}

type UpdatesConfig struct {
	Enabled       bool   `toml:"enabled" comment:"Set 'false' to disable self-updates"`
	URL           string `toml:"url" comment:"URL for updates feed"`
	CheckInterval uint32 `toml:"check_interval" comment:"Frontman will check for new versions every N seconds"`
}

func (u *UpdatesConfig) Validate() error {
	if u.CheckInterval < minSelfUpdatesCheckInterval {
		return fmt.Errorf("check_interval must be greater than %d seconds", minSelfUpdatesCheckInterval)
	}
	return nil
}

func (u *UpdatesConfig) GetCheckInterval() time.Duration {
	return time.Duration(int64(u.CheckInterval) * int64(time.Second))
}

func init() {
	ex, err := os.Executable()
	if err != nil {
		panic(err)
	}
	exPath := filepath.Dir(ex)

	switch runtime.GOOS {
	case "windows":
		DefaultCfgPath = filepath.Join(exPath, "./frontman.conf")
		defaultLogPath = filepath.Join(exPath, "./frontman.log")
		defaultStatsFilePath = "C:\\Windows\\temp\\frontman.stats"
		defaultQueueStatsFilePath = "C:\\Windows\\temp\\frontman.queuestats"
	case "darwin":
		DefaultCfgPath = os.Getenv("HOME") + "/.frontman/frontman.conf"
		defaultLogPath = os.Getenv("HOME") + "/.frontman/frontman.log"
		defaultStatsFilePath = "/tmp/frontman.stats"
		defaultQueueStatsFilePath = "/tmp/frontman.queuestats"
	default:
		rootCertsPath = "/etc/frontman/cacert.pem"
		DefaultCfgPath = "/etc/frontman/frontman.conf"
		defaultLogPath = "/var/log/frontman/frontman.log"
		defaultStatsFilePath = "/tmp/frontman.stats"
		defaultQueueStatsFilePath = "/tmp/frontman.queuestats"
	}
}

func NewConfig() *Config {
	cfg := &Config{
		MinValuableConfig:          *NewMinimumConfig(),
		NodeName:                   "Frontman",
		LogFile:                    defaultLogPath,
		StatsFile:                  defaultStatsFilePath,
		QueueStatsFile:             defaultQueueStatsFilePath,
		ICMPTimeout:                0.1,
		Sleep:                      30,
		SenderBatchSize:            100,
		SenderInterval:             2,
		SenderThreadConcurrency:    10,
		DiscardOnHTTPResponseError: false,
		DiscardOnHTTPConnectError:  false,
		CheckResultsTTL:            30,
		SleepDurationAfterCheck:    0.005,
		SleepDurationEmptyQueue:    0.2,
		HTTPCheckMaxRedirects:      10,
		HTTPCheckTimeout:           15,
		NetTCPTimeout:              3,
		NetUDPTimeout:              3,
		SSLCertExpiryThreshold:     7,
		HealthChecks: HealthCheckConfig{
			ReferencePingTimeout: 1,
			ReferencePingCount:   1,
		},
		HubRequestTimeout: defaultHubRequestTimeout,
		Updates: UpdatesConfig{
			Enabled:       false,
			CheckInterval: 21600,
		},
		Node: NodeConfig{
			NodeTimeout:     3,
			NodeCacheErrors: 10,
			ForwardExcept: []string{
				"bad status code",
				"certificate.*(expire|unknown)",
				"(tls|ssl) (error|failed|handshake)",
				"service.*support (ssl|tls)",
				"failed to verify .* service",
				"connection.*refused",
				"no such host",
				"x509",
				"pattern.*extraxcted text",
			},
		},
	}
	if runtime.GOOS == "windows" {
		cfg.Updates.Enabled = true
		cfg.Updates.URL = SelfUpdatesFeedURL
	}
	return cfg
}

func NewMinimumConfig() *MinValuableConfig {
	mvc := &MinValuableConfig{
		IOMode:       IOModeHTTP,
		LogLevel:     defaultLogLevel,
		SystemFields: []string{},
		HostInfo:     []string{"uname", "os_kernel", "os_family", "os_arch", "cpu_model", "fqdn", "memory_total_B"},
	}
	mvc.applyEnv(false)
	return mvc
}

func secToDuration(secs float64) time.Duration {
	return time.Duration(secs * 1000000000)
}

func (mvc *MinValuableConfig) applyEnv(force bool) {
	if val, ok := os.LookupEnv("FRONTMAN_HUB_URL"); ok && ((mvc.HubURL == "") || force) {
		mvc.HubURL = val
	}
	if val, ok := os.LookupEnv("FRONTMAN_HUB_USER"); ok && ((mvc.HubUser == "") || force) {
		mvc.HubUser = val
	}
	if val, ok := os.LookupEnv("FRONTMAN_HUB_PASSWORD"); ok && ((mvc.HubPassword == "") || force) {
		mvc.HubPassword = val
	}
}

func (cfg *Config) DumpToml() string {
	buff := &bytes.Buffer{}
	enc := toml.NewEncoder(buff)
	err := enc.Encode(cfg)

	if err != nil {
		logrus.Errorf("DumpConfigToml error: %s", err.Error())
		return ""
	}

	return buff.String()
}

// TryUpdateConfigFromFile applies values from file in configFilePath to cfg if given file exists.
// it rewrites all cfg keys that present in the file
func (cfg *Config) TryUpdateConfigFromFile(configFilePath string) error {
	_, err := os.Stat(configFilePath)
	if err != nil {
		return err
	}

	_, err = toml.DecodeFile(configFilePath, cfg)
	return err
}

// SaveConfigFile saves config file as toml
func SaveConfigFile(mvc *MinValuableConfig, configFilePath string) error {
	f, err := os.Create(configFilePath)
	if err != nil {
		return fmt.Errorf("failed to open the config file '%s': %s", configFilePath, err.Error())
	}
	defer f.Close()

	if _, err = f.Write(configAutogeneratedHeadline); err != nil {
		return fmt.Errorf("failed to write headline to config file")
	}

	err = toml.NewEncoder(f).Encode(mvc)
	if err != nil {
		return fmt.Errorf("failed to encode config to file")
	}

	return nil
}

// GenerateDefaultConfigFile creates a default frontman.toml and writes to to disk
func GenerateDefaultConfigFile(mvc *MinValuableConfig, configFilePath string) error {
	var err error

	if _, err = os.Stat(configFilePath); os.IsExist(err) {
		return fmt.Errorf("config already exists at path: %s", configFilePath)
	}

	absFile, err := filepath.Abs(configFilePath)
	if err != nil {
		return err
	}
	path := filepath.Dir(absFile)
	if _, err := os.Stat(path); os.IsNotExist(err) {
		err = os.MkdirAll(path, os.ModePerm)
		if err != nil {
			return err
		}
	}
	return SaveConfigFile(mvc, configFilePath)
}

// auto-corrects some config values
func (cfg *Config) sanitize() error {
	err := cfg.Updates.Validate()
	if err != nil {
		return fmt.Errorf("invalid [updates] config: %s", err.Error())
	}

	if cfg.HubProxy != "" {
		if !strings.HasPrefix(cfg.HubProxy, "http") {
			cfg.HubProxy = "http://" + cfg.HubProxy
		}

		if _, err := url.Parse(cfg.HubProxy); err != nil {
			return fmt.Errorf("failed to parse 'hub_proxy' URL")
		}
	}

	if cfg.HubRequestTimeout < minHubRequestTimeout || cfg.HubRequestTimeout > maxHubRequestTimeout {
		cfg.HubRequestTimeout = defaultHubRequestTimeout
		return fmt.Errorf("hub_request_timeout must be between %d and %d", minHubRequestTimeout, maxHubRequestTimeout)
	}

	// backwards compatibility with old configs. system_fields is deprecated!
	cfg.HostInfo = append(cfg.HostInfo, cfg.SystemFields...)

	return nil
}

// HandleAllConfigSetup prepares config for Frontman with parameters specified in file
// if config file not exists default one created in form of MinValuableConfig
func HandleAllConfigSetup(configFilePath string) (*Config, error) {
	cfg := NewConfig()

	err := cfg.TryUpdateConfigFromFile(configFilePath)
	if os.IsNotExist(err) {
		mvc := NewMinimumConfig()
		if err = GenerateDefaultConfigFile(mvc, configFilePath); err != nil {
			return nil, err
		}

		cfg.MinValuableConfig = *mvc
	} else if err != nil {
		if strings.Contains(err.Error(), "cannot load TOML value of type int64 into a Go float") {
			return nil, fmt.Errorf("config load '%s' error: please use numbers with a decimal point for numerical values", configFilePath)
		}

		return nil, fmt.Errorf("config load '%s' error: %s", configFilePath, err.Error())
	}

	return cfg, nil
}


<|start_filename|>pkg/stats/stats.go<|end_filename|>
package stats

import "time"

// FrontmanStats holds application stats
type FrontmanStats struct {
	BytesSentToHubTotal      uint64
	BytesFetchedFromHubTotal uint64

	ChecksPerformedTotal  uint64
	ChecksFetchedFromHub  uint64
	CheckResultsSentToHub uint64

	HubErrorsTotal        uint64
	HubLastErrorMessage   string
	HubLastErrorTimestamp uint64

	InternalErrorsTotal        uint64
	InternalLastErrorMessage   string
	InternalLastErrorTimestamp uint64

	HealthChecksPerformed     uint64
	HealthChecksLastTimestamp uint64

	Uptime    uint64
	StartedAt time.Time `json:"-"` // Used to calculate Uptime
}


<|start_filename|>http.go<|end_filename|>
package frontman

import (
	"encoding/json"
	"fmt"
	"net/http"
	"os"
	"path/filepath"
	"strings"

	"github.com/gorilla/handlers"
	"github.com/sirupsen/logrus"
)

func pingHandler(w http.ResponseWriter, req *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	_, _ = w.Write([]byte(`{"alive": true}`))
}

func (fm *Frontman) checkHandler(w http.ResponseWriter, req *http.Request) {
	if req.Method != "POST" {
		w.WriteHeader(http.StatusMethodNotAllowed)
		return
	}
	if req.Header.Get("Content-Type") != "application/json" {
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")

	decoder := json.NewDecoder(req.Body)
	var inputConfig Input
	err := decoder.Decode(&inputConfig)

	if err != nil {
		logrus.Errorf("json decode error: '%s'", err.Error())
		w.WriteHeader(http.StatusInternalServerError)
		return
	}

	fm.resultsLock.Lock()

	// perform the checks, collect result and pass it back as json
	fm.resultsChan = make(chan Result, 100)
	fm.processInput(inputConfig.asChecks(), false)

	// close it so we can iterate it
	close(fm.resultsChan)

	res := []Result{}
	for elem := range fm.resultsChan {
		res = append(res, elem)
	}

	fm.resultsLock.Unlock()

	enc, _ := json.Marshal(res)
	_, _ = w.Write(enc)
}

func (listener *HTTPListenerConfig) middlewareLogging(h http.Handler) http.Handler {
	var logFile *os.File
	if listener.HTTPAccessLog != "" {
		absFile, err := filepath.Abs(listener.HTTPAccessLog)
		if err != nil {
			logrus.Fatal(err)
		}
		path := filepath.Dir(absFile)
		if _, err := os.Stat(path); os.IsNotExist(err) {
			logrus.Info("Creating directory for http log:", path)
			err = os.MkdirAll(path, os.ModePerm)
			if err != nil {
				logrus.Fatal(err)
			}
		}
		logFile, err = os.OpenFile(absFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
		if err != nil {
			logrus.Error(err)
		}
	} else {
		logFile = os.Stdout
	}
	return handlers.LoggingHandler(logFile, h)
}

func (listener HTTPListenerConfig) middlewareAuth(fn http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		user, pass, _ := r.BasicAuth()
		if user != listener.HTTPAuthUser || pass != listener.HTTPAuthPassword {
			http.Error(w, "Unauthorized.", http.StatusUnauthorized)
			return
		}
		fn(w, r)
	}
}

// ServeWeb starts the webserver as configured under [http_listener] section of frontman.conf
func (fm *Frontman) ServeWeb() error {
	pos := strings.Index(fm.Config.HTTPListener.HTTPListen, "://")
	if pos == -1 {
		return fmt.Errorf("invalid address in http_listen: '%s'", fm.Config.HTTPListener.HTTPListen)
	}
	protocol := fm.Config.HTTPListener.HTTPListen[0:pos]
	address := fm.Config.HTTPListener.HTTPListen[pos+3:]
	logrus.Info("http_listener listening on ", protocol+"://"+address)
	http.Handle("/ping", fm.Config.HTTPListener.middlewareLogging(http.HandlerFunc(pingHandler)))
	http.Handle("/check", fm.Config.HTTPListener.middlewareLogging(fm.Config.HTTPListener.middlewareAuth(fm.checkHandler)))
	var err error
	switch protocol {
	case "http":
		err = http.ListenAndServe(address, nil)
	case "https":
		err = http.ListenAndServeTLS(address, fm.Config.HTTPListener.HTTPTLSCert, fm.Config.HTTPListener.HTTPTLSKey, nil)
	default:
		return fmt.Errorf("invalid protocol: '%s'", protocol)
	}
	return err
}


<|start_filename|>pkg/notification/toast_fallback.go<|end_filename|>
// +build !windows

package notification

import (
	"errors"
)

func SendErrorNotification(_, _ string) error {
	return errors.New("implemented only or Windows")
}

func SendSuccessNotification(_, _ string) error {
	return errors.New("implemented only or Windows")
}


<|start_filename|>Dockerfile<|end_filename|>
FROM alpine:3.9

ARG FRONTMAN_VERSION

ENV FRONTMAN_HUB_URL=https://hub.cloudradar.io/checks/
# User and password should be passed via -e when starting the container
#ENV FRONTMAN_HUB_USER=XXXXXXXXX
#ENV FRONTMAN_HUB_PASSWORD=<PASSWORD>

RUN apk update && apk add ca-certificates

RUN wget https://github.com/cloudradar-monitoring/frontman/releases/download/${FRONTMAN_VERSION}/frontman_${FRONTMAN_VERSION}_Linux_x86_64.tar.gz && \
    tar xf frontman_${FRONTMAN_VERSION}_Linux_x86_64.tar.gz && \
    mv frontman /usr/local/bin/ && \
    mkdir /etc/frontman && \
    mv example.config.toml /etc/frontman/config.toml && \
    rm -rf frontman_${FRONTMAN_VERSION}_Linux_x86_64 && \
    rm frontman_${FRONTMAN_VERSION}_Linux_x86_64.tar.gz

CMD ["/usr/local/bin/frontman"]


<|start_filename|>check.go<|end_filename|>
package frontman

import (
	"encoding/json"
	"sync"

	"github.com/sirupsen/logrus"
)

type Check interface {
	// run always returns a *Result, even in case of failure
	run(fm *Frontman) (*Result, error)

	// uniqueUD returns the check UUID
	uniqueID() string
}

type Input struct {
	ServiceChecks []ServiceCheck `json:"serviceChecks"`
	WebChecks     []WebCheck     `json:"webChecks"`
	SNMPChecks    []SNMPCheck    `json:"snmpChecks,omitempty"`
}

type ServiceCheck struct {
	UUID  string           `json:"checkUuid"`
	Check ServiceCheckData `json:"check"`
}

type ServiceCheckData struct {
	Connect  string      `json:"connect,omitempty"`
	Service  string      `json:"service,omitempty"`
	Protocol string      `json:"protocol,omitempty"`
	Port     json.Number `json:"port,omitempty"`
}

type WebCheck struct {
	UUID  string       `json:"checkUuid"`
	Check WebCheckData `json:"check"`
}

type WebCheckData struct {
	Method                  string            `json:"method"`
	URL                     string            `json:"url"`
	PostData                string            `json:"postData,omitempty"`
	ExpectedHTTPStatus      int               `json:"expectedHttpStatus,omitempty"`
	SearchHTMLSource        bool              `json:"searchHtmlSource"`
	ExpectedPattern         string            `json:"expectedPattern,omitempty"`
	ExpectedPatternPresence string            `json:"expectedPatternPresence,omitempty"`
	DontFollowRedirects     bool              `json:"dontFollowRedirects"`
	IgnoreSSLErrors         *bool             `json:"ignoreSSLErrors,omitempty"`
	Timeout                 float64           `json:"timeout,omitempty"`
	Headers                 map[string]string `json:"headers,omitempty"`
}

type SNMPCheck struct {
	UUID  string        `json:"checkUuid"`
	Check SNMPCheckData `json:"check"`
}

type SNMPCheckData struct {
	Connect                string   `json:"connect"`
	Port                   uint16   `json:"port"`
	Timeout                float64  `json:"timeout"`
	Protocol               string   `json:"protocol"`
	Community              string   `json:"community,omitempty"` // v1, v2
	Preset                 string   `json:"preset"`
	Oids                   []string `json:"oids,omitempty"`
	SecurityLevel          string   `json:"security_level,omitempty"`          // v3
	Username               string   `json:"username,omitempty"`                // v3
	AuthenticationProtocol string   `json:"authentication_protocol,omitempty"` // v3
	AuthenticationPassword string   `json:"authentication_password,omitempty"` // v3
	PrivacyProtocol        string   `json:"privacy_protocol,omitempty"`        // v3
	PrivacyPassword        string   `json:"privacy_password,omitempty"`        // v3

	// values used by "oid" preset
	Oid       string `json:"oid,omitempty"`
	Name      string `json:"name,omitempty"`
	ValueType string `json:"value_type,omitempty"` /// auto (default), hex, delta, delta_per_sec
	Unit      string `json:"unit,omitempty"`
}

// used to keep track of in-progress checks being run
type inProgressChecks struct {
	mutex sync.RWMutex
	uuids map[string]bool
}

func newIPC() inProgressChecks {
	return inProgressChecks{
		uuids: make(map[string]bool),
	}
}

func (ipc *inProgressChecks) add(uuid string) {
	ipc.mutex.Lock()
	defer ipc.mutex.Unlock()
	ipc.uuids[uuid] = true
}

func (ipc *inProgressChecks) remove(uuid string) {
	ipc.mutex.Lock()
	defer ipc.mutex.Unlock()
	delete(ipc.uuids, uuid)
}

func (ipc *inProgressChecks) isInProgress(uuid string) bool {
	ipc.mutex.RLock()
	defer ipc.mutex.RUnlock()

	if b, ok := ipc.uuids[uuid]; ok && b {
		return true
	}
	return false
}

// returns the slice index for the first check in `checks` that is not already in progress, false if none found
func (fm *Frontman) getIndexOfFirstCheckNotInProgress() (int, bool) {
	fm.checksLock.RLock()
	defer fm.checksLock.RUnlock()

	for idx, c := range fm.checks {
		if !fm.ipc.isInProgress(c.uniqueID()) {
			return idx, true
		}
		logrus.Infof("Skipping request for check %v. Check still in progress.", c.uniqueID())
	}
	return 0, false
}


<|start_filename|>hostinfo_test.go<|end_filename|>
package frontman

import (
	"fmt"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestHostInfoResults(t *testing.T) {
	cfg, err := HandleAllConfigSetup(DefaultCfgPath)
	assert.Nil(t, err)

	// system_fields is deprecated, but we test to make sure system_fields is treated as host_info
	cfg.HostInfo = []string{"uname", "os_kernel", "os_family", "os_arch", "cpu_model", "fqdn"}
	cfg.SystemFields = []string{"hostname", "memory_total_B"}
	fm := helperCreateFrontman(t, cfg)

	v, err := fm.HostInfoResults()
	assert.Nil(t, err)
	assert.Equal(t, 8, len(v))
	fmt.Printf("%+v\n", v)
}


<|start_filename|>build-win.bat<|end_filename|>
SET build_dir=C:\Users\hero\ci\frontman_ci\build_msi\%1
SET frontman_version=%2
SET cert_pass=%3

SET PATH=%PATH%;C:\Program Files (x86)\WiX Toolset v3.11\bin;c:\Program Files (x86)\Windows Kits\10\bin\10.0.17134.0\x86;C:\Program Files\go-msi
CD %build_dir%


COPY dist\frontman_windows_amd64\frontman.exe frontman.exe
go-msi make --src pkg-scripts\msi-templates --msi dist/_frontman_64.msi --version %frontman_version% --arch amd64
DEL frontman.exe

COPY dist\_frontman_64.msi C:\Users\hero\ci\frontman_64.msi


<|start_filename|>http_test.go<|end_filename|>
package frontman

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestPingHandler(t *testing.T) {
	req, err := http.NewRequest("GET", "/ping", nil)
	assert.Equal(t, nil, err)

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(pingHandler)
	handler.ServeHTTP(rr, req)
	assert.Equal(t, http.StatusOK, rr.Code)

	expected := `{"alive": true}`
	assert.Equal(t, expected, rr.Body.String())
}

func TestHttpCheckHandler(t *testing.T) {
	checks := `{
		"webChecks": [{
			"checkUUID": "web_head_status_matched",
			"check": { "url": "https://www.google.com", "method": "head", "expectedHttpStatus": 200}
		  }]
	  }`

	cfg := NewConfig()
	fm := helperCreateFrontman(t, cfg)

	reader := strings.NewReader(checks)
	req, err := http.NewRequest("POST", "/check", reader)
	assert.Equal(t, nil, err)
	req.Header.Set("Content-Type", "application/json")

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(fm.checkHandler)
	handler.ServeHTTP(rr, req)
	assert.Equal(t, http.StatusOK, rr.Code)

	data := rr.Body.Bytes()
	assert.Equal(t, true, len(data) > 2)

	var f interface{}
	err = json.Unmarshal(data, &f)
	assert.Equal(t, nil, err)

	dec1 := f.([]interface{})
	dec := dec1[0].(map[string]interface{})
	measurements := dec["measurements"].(map[string]interface{})

	assert.Equal(t, nil, dec["message"])
	assert.Equal(t, "webCheck", dec["checkType"])
	assert.Equal(t, "web_head_status_matched", dec["checkUuid"])
	assert.Equal(t, 200., measurements["http.head.httpStatusCode"])
	assert.Equal(t, 1., measurements["http.head.success"])
	assert.Equal(t, map[string]interface{}{
		"dontFollowRedirects": false,
		"expectedHttpStatus":  200.,
		"method":              "head",
		"searchHtmlSource":    false,
		"url":                 "https://www.google.com",
	}, dec["check"])
}

func TestHttpCheckInvalidHost(t *testing.T) {
	checks := `{
		"webChecks": [{
			"checkUUID": "webcheck_broken",
			"check": { "url": "http://notfound.site.com/", "method": "get", "expectedHttpStatus": 200}
		  }]
	  }`

	cfg := NewConfig()
	fm := helperCreateFrontman(t, cfg)

	reader := strings.NewReader(checks)
	req, err := http.NewRequest("POST", "/check", reader)
	assert.Equal(t, nil, err)
	req.Header.Set("Content-Type", "application/json")

	rr := httptest.NewRecorder()
	handler := http.HandlerFunc(fm.checkHandler)
	handler.ServeHTTP(rr, req)
	assert.Equal(t, http.StatusOK, rr.Code)

	data := rr.Body.Bytes()
	assert.Equal(t, true, len(data) > 2)

	var f interface{}
	err = json.Unmarshal(data, &f)
	assert.Equal(t, nil, err)

	dec1 := f.([]interface{})
	dec := dec1[0].(map[string]interface{})

	assert.NotEqual(t, nil, dec["measurements"])
	measurements := dec["measurements"].(map[string]interface{})

	assert.Equal(t, "webCheck", dec["checkType"])
	assert.Equal(t, "webcheck_broken", dec["checkUuid"])
	assert.Equal(t, 0., measurements["http.get.success"])
}


<|start_filename|>pkg/winui/winui_service.go<|end_filename|>
// +build windows

package winui

import (
	"context"
	"strings"
	"time"

	"github.com/pkg/errors"
	"golang.org/x/sys/windows/svc"
	"golang.org/x/sys/windows/svc/mgr"
)

func (ui *UI) reloadService() (err error) {
	ctx := context.Background()

	m, err2 := mgr.Connect()
	if err2 != nil {
		err = errors.New("Failed to connect to Windows Service Manager")
		return
	}
	defer m.Disconnect()

	s, err := m.OpenService("frontman")
	if err != nil {
		err = errors.New("Failed to find Frontman service")
		return
	}
	defer s.Close()

	ui.SaveButton.SetText("Stopping the service...")

	if err2 := stopService(ctx, s); err2 != nil {
		err = errors.New("Failed to stop Frontman service")
		return
	}

	ui.SaveButton.SetText("Starting the service...")
	if err2 := startService(ctx, s); err2 != nil {
		err = errors.New("Failed to start Frontman service")
		return
	}

	ui.StatusBar.SetText("Status: successfully connected to the Hub")
	ui.StatusBar.SetIcon(ui.SuccessIcon)
	return
}

func startService(ctx context.Context, s *mgr.Service) error {
	err := s.Start("is", "manual-started")
	if err != nil {
		err = errors.Wrap(err, "could not schedule a service to start")
		return err
	}

	return waitServiceState(ctx, s, svc.Running)
}

func stopService(ctx context.Context, s *mgr.Service) error {
	status, err := s.Control(svc.Stop)
	if err != nil {
		if strings.Contains(err.Error(), "has not been started") {
			return nil
		}
		err = errors.Wrap(err, "could not schedule a service to stop")
		return err
	}
	if status.State == svc.Stopped {
		return nil
	}
	return waitServiceState(ctx, s, svc.Stopped)
}

// waitServiceState checks the current state of a service and waits until it will match
// the expectedState, or a context deadline appearing first.
func waitServiceState(ctx context.Context, s *mgr.Service, expectedState svc.State) error {
	for {
		select {
		case <-ctx.Done():
			if ctx.Err() == context.DeadlineExceeded {
				err := errors.Wrap(ctx.Err(), "timeout waiting for service to stop")
				return err
			}
			return nil
		default:
			currentStatus, err := s.Query()
			if err != nil {
				err := errors.Wrap(err, "could not retrieve service status")
				return err
			}
			if currentStatus.State == expectedState {
				return nil
			}
			time.Sleep(300 * time.Millisecond)
		}
	}
}


<|start_filename|>cli.go<|end_filename|>
package frontman

import (
	"bufio"
	"context"
	"fmt"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"

	"github.com/kardianos/service"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"

	"github.com/cloudradar-monitoring/frontman/pkg/notification"
	"github.com/cloudradar-monitoring/selfupdate"
)

var (
	serviceName = "frontman"
)

// HandleFlagTest handles -t flag for the frontman executable
// returns exit code
func (fm *Frontman) HandleFlagTest() int {
	ctx := context.Background()
	err := fm.CheckHubCredentials(ctx, "hub_url", "hub_user", "hub_password")
	if err != nil {
		if runtime.GOOS == "windows" {
			// ignore toast error to make the main error clear for user
			// toast error probably means toast not supported on the system
			_ = notification.SendErrorNotification("Hub connection check failed", err.Error())
		}
		logrus.WithError(err).Errorln("Hub connection check failed")
		systemService, err := getServiceFromFlags(fm, "", "")
		if err != nil {
			logrus.WithError(err).Fatalln("Failed to get system service")
		}

		status, err := systemService.Status()
		if err != nil {
			// service seems not installed
			// no need to show the tip on how to restart it
			return 1
		}

		systemManager := service.ChosenSystem()
		if status == service.StatusRunning || status == service.StatusStopped {
			restartCmdSpec := getSystemMangerCommand(systemManager.String(), serviceName, "restart")
			logrus.WithFields(logrus.Fields{
				"restartCmd": restartCmdSpec,
			}).Infoln("Fix the config and then restart the service")
		}

		return 1
	}

	if runtime.GOOS == "windows" {
		_ = notification.SendSuccessNotification("Hub connection check is done", "")
	}
	logrus.Infoln("Hub connection check is done and credentials are correct!")
	return 0
}

// returns exit code
func (fm *Frontman) HandleServiceCommand(check, start, stop, restart bool) int {
	svc, err := getServiceFromFlags(fm, "", "")
	if err != nil {
		logrus.WithError(err).Fatalln("can't find service")
	}

	var status service.Status
	if status, err = svc.Status(); err != nil && err != service.ErrNotInstalled {
		logrus.WithError(err).Fatalln("can't get service status")
	}

	if check {
		switch status {
		case service.StatusRunning:
			fmt.Println("running")
		case service.StatusStopped:
			fmt.Println("stopped")
		default:
			// unknown, failed
			fmt.Println("unknown")
		}
	}

	if stop && (status == service.StatusRunning) {
		if err = svc.Stop(); err != nil {
			fmt.Println(err)
			return 1
		}

		fmt.Println("stopped")
		return 0
	} else if stop {
		fmt.Println("service is not running")
		return 0
	}

	if start {
		if status == service.StatusRunning {
			fmt.Println("already")
			return 1
		}

		if err = svc.Start(); err != nil {
			fmt.Println(err)
			return 1
		}

		fmt.Println("started")
		return 0
	}

	if restart {
		if err = svc.Restart(); err != nil {
			fmt.Println(err)
			return 1
		}
		fmt.Println("restarted")
		return 0
	}

	return 0
}

// returns exit code
func (fm *Frontman) HandleFlagServiceUpgrade(cfgPath string, serviceUpgradeFlag *bool, serviceInstallUserPtr *string) int {

	installUser := ""
	if serviceInstallUserPtr != nil {
		installUser = *serviceInstallUserPtr
	}

	systemService, err := getServiceFromFlags(fm, cfgPath, installUser)
	if err != nil {
		logrus.WithError(err).Fatalln("Failed to get system service")
	}

	updateServiceConfig(fm, installUser)
	fm.tryUpgradeServiceUnit(systemService)

	return 0
}

// returns exit code
func (fm *Frontman) HandleFlagServiceUninstall() int {

	systemService, err := getServiceFromFlags(fm, "", "")
	if err != nil {
		fmt.Println("Failed to get system service: ", err.Error())
		return 1
	}

	status, err := systemService.Status()
	if err != nil {
		fmt.Println("Failed to get service status: ", err.Error())
	}

	if status == service.StatusRunning {
		err = systemService.Stop()
		if err != nil {
			// don't exit here, just write a warning and try to uninstall
			fmt.Println("Failed to stop the running service: ", err.Error())
		}
	}

	err = systemService.Uninstall()
	if err != nil {
		fmt.Println("Failed to uninstall the service: ", err.Error())
		return 1
	}

	return 0
}

// returns exit code
func (fm *Frontman) HandleFlagServiceInstall(systemManager service.System, username string, serviceInstallPtr *bool, cfgPath string, assumeYesPtr *bool) int {

	s, err := getServiceFromFlags(fm, cfgPath, username)
	if err != nil {
		fmt.Println(err.Error())
		return 1
	}

	updateServiceConfig(fm, username)
	tryInstallService(s, assumeYesPtr)
	tryStartService(s)

	fmt.Printf("Log file located at: %s\n", fm.Config.LogFile)
	fmt.Printf("Config file located at: %s\n", cfgPath)

	if fm.Config.HubURL == "" {
		fmt.Printf(`*** Attention: 'hub_url' config param is empty.\n
*** You need to put the right credentials from your Cloudradar account into the config and then restart the service\n\n`)
	}

	fmt.Printf("Run this command to restart the service: %s\n\n", getSystemMangerCommand(systemManager.String(), fm.serviceConfig.Name, "restart"))
	return 0
}

// returns error code
func (fm *Frontman) HandleFlagOneRunOnlyMode(inputFile string, output *os.File) int {

	logrus.Debug("OneRunOnlyMode invoked (-r)")

	if err := fm.HealthCheck(); err != nil {
		fm.HealthCheckPassedPreviously = false
		logrus.WithError(err).Errorln("Health checks are not passed. Skipping other checks.")
		return 1
	}
	if !fm.HealthCheckPassedPreviously {
		fm.HealthCheckPassedPreviously = true
		logrus.Infoln("All health checks are positive. Resuming normal operation.")
	}

	err := fm.RunOnce(inputFile, output)
	if err != nil {
		fmt.Println(err)
		return 1
	}
	return 0
}

// returns exit code
func (fm *Frontman) HandleFlagPrintStats() int {

	buff, err := ioutil.ReadFile(fm.Config.StatsFile)
	if err != nil {
		fmt.Printf("Could not read stats file: %s\n", fm.Config.StatsFile)
		return 1
	}

	fmt.Printf("%s", buff)
	return 0
}

func (fm *Frontman) HandleFlagPrintConfig() {
	var configHeadline = "# Please refer to https://github.com/cloudradar-monitoring/frontman/blob/master/example.config.toml\n" +
		"# for a fully documented configuration example\n" +
		"#\n"

	fmt.Println(configHeadline)
	fmt.Println(fm.Config.DumpToml())
}

func (fm *Frontman) HandleFlagLogLevel(logLevel string) error {
	if logLevel == string(LogLevelError) || logLevel == string(LogLevelInfo) || logLevel == string(LogLevelDebug) {
		fm.SetLogLevel(LogLevel(logLevel))
	} else if logLevel != "" {
		return fmt.Errorf("Invalid log level: \"%s\". Set to default: \"%s\"", logLevel, fm.Config.LogLevel)
	}
	return nil
}

func (fm *Frontman) WritePidFileIfNeeded() error {
	if fm.Config.PidFile != "" && runtime.GOOS != "windows" {
		err := ioutil.WriteFile(fm.Config.PidFile, []byte(strconv.Itoa(os.Getpid())), 0664)
		if err != nil {
			return fmt.Errorf("Failed to write pid file at: %s", fm.Config.PidFile)
		}
	}
	return nil
}

func (fm *Frontman) RemovePidFileIfNeeded() {
	if fm.Config.PidFile != "" && runtime.GOOS != "windows" {
		err := os.Remove(fm.Config.PidFile)
		if err != nil {
			logrus.Errorf("Failed to remove pid file at: %s", fm.Config.PidFile)
		}
	}
}

// returns exit code
func HandleFlagDaemonizeMode() int {
	err := rerunDetached()
	if err != nil {
		fmt.Println("Failed to fork process: ", err.Error())
		return 1
	}
	return 0
}

func rerunDetached() error {
	cwd, err := os.Getwd()
	if err != nil {
		return err
	}

	cmd := exec.Command(os.Args[0], os.Args[1:]...)
	cmd.Dir = cwd
	cmd.Env = append(os.Environ(), "FRONTMAN_FORK=1")

	err = cmd.Start()
	if err != nil {
		return err
	}

	fmt.Printf("Frontman will continue in background...\nPID %d", cmd.Process.Pid)

	return cmd.Process.Release()
}

// returns exit code
func (fm *Frontman) RunUnderOsServiceManager() int {
	systemService, err := getServiceFromFlags(fm, "", "")
	if err != nil {
		fmt.Println(err)
		return 1
	}

	// we are running under OS service manager
	err = systemService.Run()
	if err != nil {
		fmt.Println(err.Error())
		return 1
	}
	return 0
}

func AskForConfirmation(s string) bool {
	reader := bufio.NewReader(os.Stdin)

	for {
		fmt.Printf("%s [y/n]: ", s)

		response, err := reader.ReadString('\n')
		if err != nil {
			logrus.Fatal(err)
		}

		response = strings.ToLower(strings.TrimSpace(response))

		if response == "y" || response == "yes" {
			return true
		} else if response == "n" || response == "no" {
			return false
		}
	}
}

func getServiceFromFlags(fm *Frontman, configPath, userName string) (service.Service, error) {
	prg := &serviceWrapper{Frontman: fm}

	fm.serviceConfig = service.Config{
		Name:        serviceName,
		DisplayName: "CloudRadar Frontman",
		Description: "A versatile open source monitoring agent developed by cloudradar.io. It monitors your local intranet.",
	}

	if configPath != "" {
		if !filepath.IsAbs(configPath) {
			var err error
			configPath, err = filepath.Abs(configPath)
			if err != nil {
				return nil, fmt.Errorf("failed to get absolute path to config at '%s': %s", configPath, err)
			}
		}
		fm.serviceConfig.Arguments = []string{"-c", configPath}
	}

	if userName != "" {
		fm.serviceConfig.UserName = userName
	}

	return service.New(prg, &fm.serviceConfig)
}

func getSystemMangerCommand(manager string, service string, command string) string {
	switch manager {
	case "unix-systemv":
		return "sudo service " + service + " " + command
	case "linux-upstart":
		return "sudo initctl " + command + " " + service
	case "linux-systemd":
		return "sudo systemctl " + command + " " + service + ".service"
	case "darwin-launchd":
		switch command {
		case "stop":
			command = "unload"
		case "start":
			command = "load"
		case "restart":
			return "sudo launchctl unload " + service + " && sudo launchctl load " + service
		}
		return "sudo launchctl " + command + " " + service
	case "windows-service":
		return "sc " + command + " " + service
	default:
		return ""
	}
}

// serviceWrapper provides context and methods that satisfies service.Interface
// in order to run Frontman under OS Service Manager
type serviceWrapper struct {
	Frontman *Frontman
}

func (sw *serviceWrapper) Start(s service.Service) error {
	go func() {
		sw.Frontman.Run("", nil)
		sw.Frontman.DoneChan <- true
	}()

	return nil
}

func (sw *serviceWrapper) Stop(s service.Service) error {

	logrus.Println("Finishing the batch and stop the service...")
	close(sw.Frontman.InterruptChan)
	sw.Frontman.TerminateQueue.Wait()

	<-sw.Frontman.DoneChan
	return nil
}

// returns exit code
func HandleFlagUpdate(assumeYes *bool) int {

	updates, err := printAvailableUpdates()
	if err != nil {
		fmt.Println(err.Error())
		return 1
	}

	if len(updates) == 0 {
		return 0
	}

	proceedInstallation := (assumeYes != nil && *assumeYes) || AskForConfirmation("Proceed installation?")
	if !proceedInstallation {
		return 0
	}

	fmt.Println("Downloading...")

	err = selfupdate.DownloadAndInstallUpdate(updates[len(updates)-1])
	if err != nil {
		fmt.Println(err.Error())
		return 1
	}
	fmt.Println("Installer executed. Exiting.")
	return 0
}

// returns exit code
func HandleFlagSearchUpdates() int {
	_, err := printAvailableUpdates()
	if err != nil {
		fmt.Println(err.Error())
		return 1
	}
	return 0
}

func printAvailableUpdates() ([]*selfupdate.UpdateInfo, error) {
	fmt.Println("Searching updates...")

	updates, err := selfupdate.ListAvailableUpdates()
	if err != nil {
		return nil, errors.Wrapf(err, "while listing updates")
	}

	if len(updates) == 0 {
		fmt.Println("No updates available")
	} else {
		fmt.Println("Available updates:")
		for _, u := range updates {
			fmt.Printf("\t%s\n", u.Version.Original())
		}
	}
	return updates, nil
}

func HandleFlagInputOutput(inputFile string, outputFile string, oneRunOnlyMode bool) *os.File {

	if inputFile == "" {
		return nil
	}

	var output *os.File
	var err error

	// Set output to stdout
	if outputFile == "-" {
		logrus.SetOutput(ioutil.Discard)
		output = os.Stdout
		return output
	}

	// Try to create the output file directory if it does not exist
	if _, err := os.Stat(outputFile); os.IsNotExist(err) {
		dir := filepath.Dir(outputFile)
		if _, err := os.Stat(dir); os.IsNotExist(err) {
			err = os.MkdirAll(dir, 0644)
			if err != nil {
				logrus.WithError(err).Fatalf("Failed to create the output file directory: '%s'", dir)
			}
		}
	}

	mode := os.O_WRONLY | os.O_CREATE

	if oneRunOnlyMode {
		mode |= os.O_TRUNC
	} else {
		mode |= os.O_APPEND
	}

	if outputFile != "" {
		output, err = os.OpenFile(outputFile, mode, 0644)
		if err != nil {
			logrus.WithError(err).Fatalf("Failed to open the output file: '%s'", outputFile)
		}
	}
	return output
}


<|start_filename|>ping_ours.go<|end_filename|>
package frontman

import (
	"runtime"
	"time"

	"github.com/go-ping/ping"

	"github.com/pkg/errors"
	"golang.org/x/net/icmp"
)

func CheckIfRawICMPAvailable() bool {
	conn, err := icmp.ListenPacket("ip4:1", "0.0.0.0")
	if err != nil {
		return false
	}

	conn.Close()
	return true
}

func CheckIfRootlessICMPAvailable() bool {
	conn, err := icmp.ListenPacket("udp4", "")
	if err != nil {
		return false
	}

	conn.Close()
	return true
}

func (fm *Frontman) runPing(addr string) (m map[string]interface{}, err error) {
	prefix := "net.icmp.ping."
	m = make(map[string]interface{})

	pinger, err := ping.NewPinger(addr)
	if err != nil {
		return
	}

	if CheckIfRawICMPAvailable() || runtime.GOOS == "windows" {
		pinger.SetPrivileged(true)
	}

	pinger.Timeout = secToDuration(fm.Config.ICMPTimeout)
	pinger.Count = 5

	pinger.Run()

	var total time.Duration

	stats := pinger.Statistics()
	for _, rtt := range stats.Rtts {
		total += rtt
	}

	if pinger.PacketsSent > 0 {
		m[prefix+"packetLoss_percent"] = float64(pinger.PacketsSent-pinger.PacketsRecv) / float64(pinger.PacketsSent) * 100
	}
	if (len(stats.Rtts)) > 0 {
		m[prefix+"roundTripTime_s"] = total.Seconds() / float64(len(stats.Rtts))
	}
	success := 0
	if pinger.PacketsRecv > 0 {
		success = 1
	} else {
		err = errors.New("no packets received")
	}

	m[prefix+"success"] = success

	return
}


<|start_filename|>cmd/frontman/main.go<|end_filename|>
package main

import (
	"flag"
	"fmt"
	"io/ioutil"
	_ "net/http/pprof"
	"os"
	"os/signal"
	"runtime"
	"runtime/pprof"
	"syscall"

	"github.com/kardianos/service"
	log "github.com/sirupsen/logrus"

	"github.com/cloudradar-monitoring/frontman"
	"github.com/cloudradar-monitoring/frontman/pkg/winui"
)

var exitCode = 0

func exit() {
	os.Exit(exitCode)
}

func main() {
	// exit will be called last (FILO defer order)
	defer exit()

	systemManager := service.ChosenSystem()

	var serviceInstallUserPtr *string
	var serviceInstallPtr *bool
	var settingsPtr *bool
	var searchUpdatesPtr *bool
	var updatePtr *bool

	// Setup flag pointers
	inputFilePtr := flag.String("i", "", "JSON file to read the list (required)")
	outputFilePtr := flag.String("o", "", "file to write the results (default ./results.out)")
	cfgPathPtr := flag.String("c", frontman.DefaultCfgPath, "config file path")
	testConfigPtr := flag.Bool("t", false, "test the Hub config and exit")
	logLevelPtr := flag.String("v", "", "log level – overrides the level in config file (values \"error\",\"info\",\"debug\")")
	daemonizeModePtr := flag.Bool("d", false, "daemonize – run the process in background")
	oneRunOnlyModePtr := flag.Bool("r", false, "one run only – perform checks once and exit. Overwrites output file")
	serviceUninstallPtr := flag.Bool("u", false, fmt.Sprintf("stop and uninstall the system service(%s)", systemManager.String()))
	printConfigPtr := flag.Bool("p", false, "print the active config")
	versionPtr := flag.Bool("version", false, "show the frontman version")
	statsPtr := flag.Bool("stats", false, "show the frontman stats")
	assumeYesPtr := flag.Bool("y", false, "automatic yes to prompts. Assume 'yes' as answer to all prompts and run non-interactively")
	serviceStatusPtr := flag.Bool("service_status", false, "check service status")
	serviceStartPtr := flag.Bool("service_start", false, "start service")
	serviceStopPtr := flag.Bool("service_stop", false, "stop service")
	serviceRestartPtr := flag.Bool("service_restart", false, "restart service")
	serviceUpgradePtr := flag.Bool("service_upgrade", false, "upgrade service unit configuration")
	cpuProfile := flag.String("cpuprofile", "", "write cpu profile to file")

	// some OS specific flags
	if runtime.GOOS == "windows" {
		serviceInstallPtr = flag.Bool("s", false, fmt.Sprintf("install and start the system service(%s)", systemManager.String()))
		settingsPtr = flag.Bool("x", false, "open the settings UI")
		updatePtr = flag.Bool("update", false, "look for updates and apply them. Requires confirmation. Use -y to suppress the confirmation.")
		searchUpdatesPtr = flag.Bool("search-updates", false, "look for updates and print available")
	} else {
		serviceInstallUserPtr = flag.String("s", "", fmt.Sprintf("username to install and start the system service(%s)", systemManager.String()))
	}

	flag.Parse()
	// version should be handled first to ensure it will be accessible in case of fatal errors before
	if *versionPtr {
		handleFlagVersion()
		return
	}

	if *cpuProfile != "" {
		fmt.Println("Starting CPU profile")
		f, err := os.Create(*cpuProfile)
		if err != nil {
			log.Fatal(err)
		}
		pprof.StartCPUProfile(f)
		defer func() {
			fmt.Println("Ending CPU profile")
			pprof.StopCPUProfile()
		}()
	}

	// check some incompatible flags
	if serviceInstallUserPtr != nil && *serviceInstallUserPtr != "" ||
		serviceInstallPtr != nil && *serviceInstallPtr {
		if *inputFilePtr != "" {
			fmt.Println("Input file(-i) flag can't be used together with service install(-s) flag")
			exitCode = 1
			return
		}

		if *outputFilePtr != "" {
			fmt.Println("Output file(-o) flag can't be used together with service install(-s) flag")
			exitCode = 1
			return
		}

		if *serviceUninstallPtr {
			fmt.Println("Service uninstall(-u) flag can't be used together with service install(-s) flag")
			exitCode = 1
			return
		}

		if *serviceStartPtr || *serviceRestartPtr || *serviceStopPtr || *serviceStatusPtr {
			fmt.Println("Service management flags can't be used together with service install(-s) flag")
			exitCode = 1
			return
		}
	}

	cfg, err := frontman.HandleAllConfigSetup(*cfgPathPtr)
	if err != nil {
		log.Fatalf("Failed to handle frontman configuration: %s", err)
	}

	fm, err := frontman.New(cfg, *cfgPathPtr, frontman.Version)
	if err != nil {
		log.Fatalf("Failed to initialize frontman: %s", err)
	}

	if statsPtr != nil && *statsPtr {
		exitCode = fm.HandleFlagPrintStats()
		return
	}

	if printConfigPtr != nil && *printConfigPtr {
		fm.HandleFlagPrintConfig()
		return
	}

	if searchUpdatesPtr != nil && *searchUpdatesPtr {
		exitCode = frontman.HandleFlagSearchUpdates()
		return
	}

	if updatePtr != nil && *updatePtr {
		exitCode = frontman.HandleFlagUpdate(assumeYesPtr)
		return
	}

	if testConfigPtr != nil && *testConfigPtr {
		exitCode = fm.HandleFlagTest()
		return
	}

	if settingsPtr != nil && *settingsPtr {
		handleFlagSettings(fm)
		return
	}

	setDefaultLogFormatter()

	// log level set in flag has a precedence. If specified we need to set it ASAP
	if err := fm.HandleFlagLogLevel(*logLevelPtr); err != nil {
		log.Warn(err)
	}

	printOSSpecificWarnings()

	if oneRunOnlyModePtr != nil && !*oneRunOnlyModePtr {
		if err := fm.WritePidFileIfNeeded(); err != nil {
			log.Error(err)
		}
		defer fm.RemovePidFileIfNeeded()
	}

	winui.HandleFeedback(fm, *cfgPathPtr)

	log.Info("frontman " + frontman.Version + " started")

	if !*oneRunOnlyModePtr && !*testConfigPtr && *inputFilePtr == "" && *outputFilePtr == "" && cfg.HTTPListener.HTTPListen != "" {
		go func() {
			if err := fm.ServeWeb(); err != nil {
				log.Fatal(err)
			}
		}()
	}

	if !service.Interactive() {
		exitCode = fm.RunUnderOsServiceManager()
		return
	}

	if (serviceStatusPtr != nil && *serviceStatusPtr) ||
		(serviceStartPtr != nil && *serviceStartPtr) ||
		(serviceStopPtr != nil && *serviceStopPtr) ||
		(serviceRestartPtr != nil && *serviceRestartPtr) {
		exitCode = fm.HandleServiceCommand(*serviceStatusPtr, *serviceStartPtr, *serviceStopPtr, *serviceRestartPtr)
		return
	}

	if serviceUpgradePtr != nil && *serviceUpgradePtr {
		exitCode = fm.HandleFlagServiceUpgrade(*cfgPathPtr, serviceUpgradePtr, serviceInstallUserPtr)
		return
	}

	if serviceUninstallPtr != nil && *serviceUninstallPtr {
		exitCode = fm.HandleFlagServiceUninstall()
		return
	}

	if (serviceInstallUserPtr != nil && *serviceInstallUserPtr != "") || (serviceInstallPtr != nil && *serviceInstallPtr) {
		exitCode = fm.HandleFlagServiceInstall(systemManager, *serviceInstallUserPtr, serviceInstallPtr, *cfgPathPtr, assumeYesPtr)
		return
	}

	if *daemonizeModePtr && os.Getenv("FRONTMAN_FORK") != "1" {
		exitCode = frontman.HandleFlagDaemonizeMode()
		return
	}

	if *inputFilePtr != "" && *outputFilePtr == "" {
		fmt.Println("Output(-o) flag can be only used together with input(-i)")
		exitCode = 1
		return
	}

	output := frontman.HandleFlagInputOutput(*inputFilePtr, *outputFilePtr, *oneRunOnlyModePtr)
	if output != nil {
		defer output.Close()
	}

	if *oneRunOnlyModePtr {
		exitCode = fm.HandleFlagOneRunOnlyMode(*inputFilePtr, output)
		return
	}

	// nothing resulted in os.Exit
	// so lets use the default continuous run mode and wait for interrupt
	signalChan := make(chan os.Signal, 1)
	signal.Notify(signalChan,
		syscall.SIGHUP,
		syscall.SIGINT,  // ctrl-C
		syscall.SIGTERM) // kill <pid>

	go func() {
		fm.Run(*inputFilePtr, output)
		fm.DoneChan <- true
	}()

	//  Handle interrupts
	select {
	case sig := <-signalChan:
		log.Infof("Got %s signal. Finishing the batch and exit...", sig.String())
		close(fm.InterruptChan)
		fm.TerminateQueue.Wait()
		log.Infof("Stopped")
		return
	case <-fm.DoneChan:
		return
	}
}

func printOSSpecificWarnings() {
	var osNotice string
	if runtime.GOOS == "windows" && !frontman.CheckIfRawICMPAvailable() {
		osNotice = "!!! You need to run frontman as administrator in order to use ICMP ping on Windows !!!"
	}
	if runtime.GOOS == "linux" && !frontman.CheckIfRootlessICMPAvailable() && !frontman.CheckIfRawICMPAvailable() {
		osNotice = `⚠️ In order to perform rootless ICMP Ping on Linux you need to run this command first:
			sudo setcap cap_net_raw=+ep /usr/bin/frontman"`
	}
	if osNotice != "" {
		// print to console without log formatting
		fmt.Println(osNotice)

		// disable logging to stderr temporarily
		log.SetOutput(ioutil.Discard)
		log.Error(osNotice)
		log.SetOutput(os.Stderr)
	}
}

func handleFlagVersion() {
	fmt.Printf("frontman v%s released under MIT license. https://github.com/cloudradar-monitoring/frontman/\n", frontman.Version)
}

func handleFlagSettings(fm *frontman.Frontman) {
	winui.WindowsShowSettingsUI(fm, false)
}

func setDefaultLogFormatter() {
	textFormat := log.TextFormatter{FullTimestamp: true}
	if runtime.GOOS == "windows" {
		textFormat.DisableColors = true
	}
	log.SetFormatter(&textFormat)
}


<|start_filename|>node.go<|end_filename|>
package frontman

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"net/url"
	"path"
	"regexp"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
)

// checks if given node recently failed
func (fm *Frontman) nodeRecentlyFailed(node *Node) bool {
	limit := time.Second * time.Duration(fm.Config.Node.NodeCacheErrors)

	fm.failedNodeLock.Lock()
	defer fm.failedNodeLock.Unlock()

	if when, ok := fm.failedNodes[node.URL]; ok {
		if time.Since(when) < limit {
			logrus.Debugf("skipping recently failed node %s", node.URL)
			return true
		}
	}

	return false
}

// marks a node as temporarily failing
func (fm *Frontman) markNodeFailure(node *Node, data []byte) {
	fm.failedNodeLock.Lock()
	defer fm.failedNodeLock.Unlock()
	fm.failedNodes[node.URL] = time.Now()
	fm.failedNodeCache[node.URL] = data
}

// returns the most recent cached node failure response
func (fm *Frontman) getCachedNodeFailure(node *Node) []byte {
	fm.failedNodeLock.Lock()
	defer fm.failedNodeLock.Unlock()

	if n, ok := fm.failedNodeCache[node.URL]; ok {
		return n
	}
	return nil
}

// asking other nodes to try a failed check
func (fm *Frontman) askNodes(check Check, res *Result) {

	var data []byte

	if len(fm.Config.Nodes) < 1 {
		return
	}

	msg := res.Message.(string)
	// only forward if result message don't match ForwardExcept config
	if len(fm.Config.Node.ForwardExcept) > 0 {
		for _, rexp := range fm.Config.Node.ForwardExcept {
			// case insensitive match
			irexp := "(?i)" + rexp
			match, err := regexp.MatchString(irexp, msg)
			if err != nil {
				logrus.Error("forward_except regexp error ", err)
			} else if match {
				logrus.Infof("forward_except matched on '%s', won't forward %s", rexp, msg)
				return
			}
		}
	}

	uuid := ""
	checkType := ""
	if c, ok := check.(ServiceCheck); ok {
		if c.Check.Protocol == "ssl" {
			// ssl checks are excluded from "ask node" feature
			return
		}
		uuid = c.UUID
		checkType = "serviceCheck"
		req := &Input{ServiceChecks: []ServiceCheck{c}}
		data, _ = json.Marshal(req)
	}
	if c, ok := check.(WebCheck); ok {
		uuid = c.UUID
		checkType = "webCheck"
		req := &Input{WebChecks: []WebCheck{c}}
		data, _ = json.Marshal(req)
	}
	if c, ok := check.(SNMPCheck); ok {
		uuid = c.UUID
		checkType = "snmpCheck"
		req := &Input{SNMPChecks: []SNMPCheck{c}}
		data, _ = json.Marshal(req)
	}

	var nodeResults []string
	var succeededNodes []string
	var failedNodes []string
	failedNodeMessage := make(map[string]string)

	for i := range fm.Config.Nodes {
		node := fm.Config.Nodes[i]
		if fm.nodeRecentlyFailed(&node) {
			logrus.Warnf("Skipping recently failed node %s", node.URL)
			failedNodes = append(failedNodes, node.URL)
			if failure := fm.getCachedNodeFailure(&node); failure != nil {
				failureText, _ := json.Marshal(failure)
				nodeResults = append(nodeResults, string(failureText))
			}
			continue
		}

		url, err := url.Parse(node.URL)
		if err != nil {
			logrus.Warnf("Invalid node url in config: '%s': %s", node.URL, err.Error())
			continue
		}
		url.Path = path.Join(url.Path, "check")
		logrus.Debugf("askNodes asking %s (%s)", node.URL, check.uniqueID())

		client := &http.Client{
			Timeout: time.Duration(fm.Config.Node.NodeTimeout) * time.Second,
		}
		if !node.VerifySSL {
			client.Transport = &http.Transport{
				TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
			}
		}

		fm.logForward(fmt.Sprintf("Forwarding check %s, type %s, msg '%s' to %s", uuid, checkType, msg, node.URL))
		req, _ := http.NewRequest("POST", url.String(), bytes.NewBuffer(data))
		req.SetBasicAuth(node.Username, node.Password)
		req.Header.Set("Content-Type", "application/json")
		resp, err := client.Do(req)
		if err != nil {
			logrus.Debugf("askNodes failed: %s (%s)", err.Error(), check.uniqueID())
			fm.markNodeFailure(&node, nil)
		} else {
			defer resp.Body.Close()

			body, _ := ioutil.ReadAll(resp.Body)
			if resp.StatusCode == http.StatusOK {
				nodeResults = append(nodeResults, string(body))
			} else {
				logrus.Errorf("askNodes received HTTP %v from %s", resp.StatusCode, node.URL)
				fm.markNodeFailure(&node, body)
			}
		}
	}

	if len(nodeResults) == 0 {
		// all nodes failed, use original measure
		logrus.Debugf("askNodes received no successful results (%s)", check.uniqueID())
		return
	}

	bestDuration := 999.

	// select the fastest result, fall back to first result if we fail
	resultID := 0
	for currID, resp := range nodeResults {

		var selected []interface{}
		if err := json.Unmarshal([]byte(resp), &selected); err != nil {
			logrus.Errorf("unmarshal of node result '%v' failed: %v", resp, err)
			continue
		}

		// recognize response type and check relevant values
		if l1, ok := selected[0].(map[string]interface{}); ok {

			nodeName := ""
			if n, ok := l1["node"].(string); ok {
				nodeName = n
			}

			nodeMessage := ""
			if n, ok := l1["message"].(string); ok {
				nodeMessage = n
			}

			if l2, ok := l1["measurements"].(map[string]interface{}); ok {

				successKey := ""
				for key := range l2 {
					lastPeriod := strings.LastIndex(key, ".")
					if lastPeriod == -1 {
						continue
					}
					switch key[lastPeriod+1:] {
					case "success":
						successKey = key
					}
				}
				if successKey == "" {
					continue
				}

				if success, ok := l2[successKey].(float64); ok {
					if int(success) == 1 {
						succeededNodes = append(succeededNodes, nodeName)
					} else {
						failedNodeMessage[nodeName] = nodeMessage
						failedNodes = append(failedNodes, nodeName)
						continue
					}
				}

				useKey := ""
				for key := range l2 {
					lastPeriod := strings.LastIndex(key, ".")
					if lastPeriod == -1 {
						continue
					}
					switch key[lastPeriod+1:] {
					case "roundTripTime_s", "totalTimeSpent_s", "connectTime_s":
						useKey = key
					}
				}
				if useKey == "" {
					continue
				}
				if duration, ok := l2[useKey].(float64); ok {
					if duration < bestDuration {
						resultID = currID
						bestDuration = duration
					}
				}
			}
		}
	}

	var fastestResult []Result
	if err := json.Unmarshal([]byte(nodeResults[resultID]), &fastestResult); err != nil {
		logrus.Errorf("askNodes unmarshal of fastest node result '%v' failed: %v", nodeResults[resultID], err)
	}
	if len(fastestResult) < 1 {
		logrus.Warning("askNodes no results gathered from node")
		return
	}

	locallMeasurement := *res

	// make the fastest node measurement the main result
	*res = fastestResult[0]

	fastestMsg := ""
	if f, ok := res.Message.(string); ok {
		fastestMsg = f
	}

	// append all node messages to Message response
	nodeMsg := fm.Config.NodeName + ": " + fastestMsg + "\n"
	for _, v := range failedNodes {
		nodeMsg += fmt.Sprintf("%s: %s\n", v, failedNodeMessage[v])
	}
	for _, v := range succeededNodes {
		nodeMsg += fmt.Sprintf("%s: check succeeded\n", v)
	}

	(*res).Message = nodeMsg

	logrus.Debug("askNodes succeess:", nodeMsg)

	// combine the other measurments with the failing measurement
	for idx := range nodeResults {
		if idx == resultID {
			continue
		}

		var result []Result
		if err := json.Unmarshal([]byte(nodeResults[idx]), &result); err != nil {
			logrus.Error(err)
		}

		var out []map[string]interface{}
		inrec, _ := json.Marshal(result)
		json.Unmarshal(inrec, &out)

		(*res).NodeMeasurements = append((*res).NodeMeasurements, out...)
	}

	var locallMeasurementInterface map[string]interface{}
	tmp, _ := json.Marshal(locallMeasurement)
	json.Unmarshal(tmp, &locallMeasurementInterface)

	(*res).NodeMeasurements = append((*res).NodeMeasurements, locallMeasurementInterface)
}

func (fm *Frontman) logForward(s string) {
	if fm.forwardLog == nil {
		return
	}
	t := time.Now()
	s = t.Format(time.RFC3339) + " " + s + "\n"
	fm.forwardLog.WriteString(s)
}


<|start_filename|>Makefile<|end_filename|>
.PHONY: all

PROJECT_DIR=/go/src/github.com/cloudradar-monitoring/frontman

ifeq ($(RELEASE_MODE),)
  RELEASE_MODE=release-candidate
endif
ifeq ($(RELEASE_MODE),release-candidate)
  SELF_UPDATES_FEED_URL="https://repo.cloudradar.io/windows/frontman/feed/rolling"
endif
ifeq ($(RELEASE_MODE),stable)
  SELF_UPDATES_FEED_URL="https://repo.cloudradar.io/windows/frontman/feed/stable"
endif

# Go parameters
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
GORUN=$(GOCMD) run
BINARY_NAME=frontman

all: test build

build:
	$(GOBUILD) -v ./cmd/frontman/...

test:
	$(GOTEST) -v ./...

test-short:
	$(GOTEST) -short -v ./...

clean:
	$(GOCLEAN)
	rm -f $(BINARY_NAME)

run:
	$(GORUN) -v ./cmd/frontman/...

goimports:
	goimports -l $$(find . -type f -name '*.go' -not -path "./vendor/*")

goreleaser-precheck:
	@if [ -z ${SELF_UPDATES_FEED_URL} ]; then echo "SELF_UPDATES_FEED_URL is empty"; exit 1; fi

goreleaser-rm-dist: goreleaser-precheck
	SELF_UPDATES_FEED_URL=$(SELF_UPDATES_FEED_URL) goreleaser --rm-dist

goreleaser-snapshot: goreleaser-precheck
	SELF_UPDATES_FEED_URL=$(SELF_UPDATES_FEED_URL) goreleaser --snapshot --rm-dist



<|start_filename|>ping_test.go<|end_filename|>
package frontman

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestPing(t *testing.T) {
	cfg, err := HandleAllConfigSetup(DefaultCfgPath)

	cfg.ICMPTimeout = 0.2
	assert.Nil(t, err)
	fm := helperCreateFrontman(t, cfg)

	_, _ = fm.runPing("8.8.8.8")
}


<|start_filename|>pkg/utils/gzipreader/gzipreader.go<|end_filename|>
package gzipreader

import (
	"compress/gzip"
	"io"
)

// GzipReader wraps a ReadCloser
// call gzip.NewReader on the first call to Read
type GzipReader struct {
	Reader  io.ReadCloser
	zreader *gzip.Reader
	zerr    error
}

func (gz *GzipReader) Read(p []byte) (n int, err error) {
	if gz.zreader == nil {
		if gz.zerr == nil {
			gz.zreader, gz.zerr = gzip.NewReader(gz.Reader)
		}
		if gz.zerr != nil {
			return 0, gz.zerr
		}
	}

	return gz.zreader.Read(p)
}

func (gz *GzipReader) Close() error {
	return gz.Reader.Close()
}


<|start_filename|>mockhub.go<|end_filename|>
package frontman

// a mock hub used in tests, similar to https://bitbucket.org/cloudradar/debug_hub/

import (
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"math/rand"
	"net/http"
	"strconv"
)

type MockHub struct {
	address string

	// if set, responds with this status code on all requests. useful for testing 401 and such
	ResponseStatusCode int
}

func NewMockHub(address string) *MockHub {
	return &MockHub{
		address: address,
	}
}

func (hub *MockHub) URL() string {
	return "http://" + hub.address
}

// returns some mocked checks
func (hub *MockHub) indexHandler(w http.ResponseWriter, r *http.Request) {
	if hub.ResponseStatusCode != 0 {
		log.Printf("Responding to request with status code %d", hub.ResponseStatusCode)
		w.WriteHeader(hub.ResponseStatusCode)
		return
	}
	switch r.Method {
	case "GET":
		hub.getHandler(w, r)
	case "POST":
		hub.postHandler(w, r)
	}
}

func (hub *MockHub) postHandler(w http.ResponseWriter, r *http.Request) {
	// simulate reset content
	//w.WriteHeader(205)

	// simulate slow hub
	//time.Sleep(500 * time.Millisecond)

	data, err := ioutil.ReadAll(r.Body)
	if err != nil {
		log.Fatal(err)
	}

	log.Println("MockHub: got post data", string(data))
}

func (hub *MockHub) getHandler(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")

	query := r.URL.Query()
	serviceChecksS := query.Get("serviceChecks")
	webChecksS := query.Get("webChecks")

	serviceChecks, err := strconv.ParseInt(serviceChecksS, 10, 64)
	if err != nil {
		serviceChecks = 5
	}

	webChecks, err := strconv.ParseInt(webChecksS, 10, 64)
	if err != nil {
		webChecks = 5
	}

	log.Println("responding with", serviceChecks, "serviceChecks and", webChecks, "webChecks")

	checks := Input{
		ServiceChecks: mockServiceChecks(int(serviceChecks)),
		WebChecks:     mockWebChecks(int(webChecks)),
	}

	data, _ := json.Marshal(checks)
	w.Write(data)
}

func (hub *MockHub) Serve() {

	http.HandleFunc("/", hub.indexHandler)
	log.Println("mock hub listening at", hub.URL())
	http.ListenAndServe(hub.address, nil)
}

func mockServiceChecks(n int) []ServiceCheck {
	res := []ServiceCheck{}
	for i := 0; i < n; i++ {
		res = append(res, randomServiceCheck())
	}
	return res
}

func mockWebChecks(n int) []WebCheck {
	res := []WebCheck{}
	for i := 0; i < n; i++ {
		if i%5 == 0 {
			// every fifth web check should be a lame web check
			res = append(res, lameWebCheck())
		} else {
			res = append(res, randomWebCheck())
		}
	}
	return res
}

func randomWebCheck() WebCheck {
	methods := []string{"get", "post", "head"}
	statuses := []int{200, 404}
	patterns := []string{"running", "welcome", "yyy"}
	return WebCheck{
		UUID: randomUUID(),
		Check: WebCheckData{
			URL:                fmt.Sprintf("https://h%d.hostgum.eu/", rand.Intn(1000)),
			Method:             methods[rand.Intn(len(methods))],
			ExpectedHTTPStatus: statuses[rand.Intn(len(statuses))],
			ExpectedPattern:    patterns[rand.Intn(len(patterns))],
		},
	}
}

func lameWebCheck() WebCheck {
	return WebCheck{
		UUID: randomUUID(),
		Check: WebCheckData{
			URL:                fmt.Sprintf("https://h1.hostgum.eu/sleep.php?t=%d", 5+rand.Intn(45)),
			Method:             "get",
			ExpectedHTTPStatus: 200,
			ExpectedPattern:    "slept",
			Timeout:            randomFloat(30, 60),
		},
	}
}

func randomFloat(min, max float64) float64 {
	return min + rand.Float64()*(max-min)
}

func randomUUID() string {
	b := make([]byte, 16)
	_, err := rand.Read(b)
	if err != nil {
		log.Fatal(err)
	}
	return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
}

func randomServiceCheck() ServiceCheck {
	switch rand.Intn(2) {
	case 0:
		return randomPingCheck()
	default:
		return randomTcpCheck()
	}
}

func randomPingCheck() ServiceCheck {
	return ServiceCheck{
		UUID: randomUUID(),
		Check: ServiceCheckData{
			Connect:  randomConnect(),
			Service:  "ping",
			Protocol: "icmp",
		},
	}
}

func randomTcpCheck() ServiceCheck {
	services := []string{
		"http",
		"https",
		"pop3",
		"imap",
		"smtp",
	}
	return ServiceCheck{
		UUID: randomUUID(),
		Check: ServiceCheckData{
			Connect:  fmt.Sprintf("h%d.hostgum.eu", rand.Intn(1000)),
			Protocol: "tcp",
			Service:  services[rand.Intn(len(services))],
		},
	}
}

func randomConnect() string {
	pool := []string{
		"www.google.com",
		"8.8.8.8",
		"8.8.4.4",
		"1.1.1.1",
		"h1.hostgum.eu",
		"lameduck.hostgum.eu",
		"not_exists_domain1234.com",
		"github.com",
		"dns.google",
		"192.168.3.11",
		"172.16.31.10",
		"172.16.58.3",
		"172.16.17.32",
		"ns1.artechinfo.in",
		"pb6abf7bd.szokff01.ap.so-net.ne.jp",
		"172.16.31.10",
		"dns.prhs.ptc.edu.tw",
		"192.168.127.12",
	}
	return pool[rand.Intn(len(pool))]
}


<|start_filename|>pkg/iax/iax.go<|end_filename|>
package iax

/* IAX packet structure:
struct ast_iax2_full_hdr {
	unsigned short scallno;	// Source call number -- high bit must be 1
	unsigned short dcallno;	// Destination call number -- high bit is 1 if retransmission
	unsigned int ts;		// 32-bit timestamp in milliseconds (from 1st transmission)
	unsigned char oseqno;	// Packet number (outgoing)
	unsigned char iseqno;	// Packet number (next incoming expected)
	unsigned char type;		// Frame type
	unsigned char csub;		// Compressed subclass
	unsigned char iedata[0];
} __attribute__ ((__packed__));
*/

const minFrameLen = 12

const (
	frameTypeIAX = 0x06
	subclassPoke = 0x1e // POKE message (similar to PING but doesn't require active connection)
	subclassPong = 0x03 // PONG
	subclassAck  = 0x04 // ACK
)

var frameHeaderIAX2Full = []byte{0x80, 0x0}

func GetPokeFramePacket() []byte {
	// transforming golang structure to packed in-memory struct is pretty hard
	// so we will just construct the required packet manually
	var sCallNo = frameHeaderIAX2Full
	var dCallNo = []byte{0x0, 0x0}
	var ts = []byte{0x0, 0x0, 0x0, 0x0}
	var oSeqNo byte
	var iSeqNo byte
	var frameType byte = frameTypeIAX
	var cSub byte = subclassPoke

	var packet []byte
	packet = append(packet, sCallNo...)
	packet = append(packet, dCallNo...)
	packet = append(packet, ts...)
	packet = append(packet, oSeqNo, iSeqNo, frameType, cSub)

	return packet
}

func GetAckFramePacket() []byte {
	result := GetPokeFramePacket()
	result[11] = subclassAck
	return result
}

func IsPongResponse(frameBytes []byte) bool {
	if len(frameBytes) < minFrameLen {
		return false
	}

	var frameType = frameBytes[10]
	var cSub = frameBytes[11]

	return frameType == frameTypeIAX && cSub == subclassPong
}


<|start_filename|>borrowed.go<|end_filename|>
package frontman

import (
	"bytes"
	"context"
	"net"
	"os"
	"os/exec"
	"runtime"
	"strings"
	"time"

	"github.com/shirou/gopsutil/host"
)

type Invoker interface {
	Command(string, ...string) ([]byte, error)
	CommandWithContext(context.Context, string, ...string) ([]byte, error)
}

type Invoke struct{}

func (i Invoke) Command(name string, arg ...string) ([]byte, error) {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	return i.CommandWithContext(ctx, name, arg...)
}

func (i Invoke) CommandWithContext(ctx context.Context, name string, arg ...string) ([]byte, error) {
	cmd := exec.CommandContext(ctx, name, arg...)

	var buf bytes.Buffer
	cmd.Stdout = &buf
	cmd.Stderr = &buf

	if err := cmd.Start(); err != nil {
		return buf.Bytes(), err
	}

	if err := cmd.Wait(); err != nil {
		return buf.Bytes(), err
	}

	return buf.Bytes(), nil
}

var invoke Invoker = Invoke{}

func Uname() (string, error) {
	if runtime.GOOS == "windows" {
		info, err := host.Info()

		if err != nil {
			return "", err
		}
		return info.Platform + " " + info.PlatformVersion + " " + info.PlatformFamily, nil
	}
	uname, err := exec.LookPath("uname")
	if err != nil {
		return "", err
	}
	b, err := invoke.Command(uname, "-a")
	return strings.TrimSpace(string(b)), err
}

func getFQDN() string {
	hostname, err := os.Hostname()
	if err != nil {
		return "unknown"
	}

	addrs, err := net.LookupIP(hostname)
	if err != nil {
		return hostname
	}

	for _, addr := range addrs {
		if ipv4 := addr.To4(); ipv4 != nil {
			ip, err := ipv4.MarshalText()
			if err != nil {
				return hostname
			}
			hosts, err := net.LookupAddr(string(ip))
			if err != nil || len(hosts) == 0 {
				return hostname
			}
			fqdn := hosts[0]
			return strings.TrimSuffix(fqdn, ".")
		}
	}
	return hostname
}


<|start_filename|>check_test.go<|end_filename|>
package frontman

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestInProgressChecks(t *testing.T) {

	ipc := newIPC()

	ipc.add("one")
	ipc.add("two")
	assert.Equal(t, true, ipc.isInProgress("one"))
	assert.Equal(t, true, ipc.isInProgress("two"))
	assert.Equal(t, false, ipc.isInProgress("three"))

	ipc.remove("two")
	assert.Equal(t, false, ipc.isInProgress("two"))
}


<|start_filename|>errors.go<|end_filename|>
package frontman

import "github.com/pkg/errors"

func newEmptyFieldError(name string) error {
	err := errors.Errorf("unexpected empty field %s", name)
	return errors.Wrap(err, "the field must be filled with details of your Cloudradar account")
}

func newFieldError(name string, err error) error {
	return errors.Wrapf(err, "%s field verification failed", name)
}


<|start_filename|>hostinfo.go<|end_filename|>
package frontman

import (
	"context"
	"fmt"
	"os"
	"runtime"
	"strings"
	"time"

	"github.com/pkg/errors"
	"github.com/shirou/gopsutil/cpu"
	"github.com/shirou/gopsutil/host"
	"github.com/shirou/gopsutil/mem"
	"github.com/sirupsen/logrus"
)

// HostInfoResults fetches information about the host itself which can be
// send to the hub alongside measurements.
func (fm *Frontman) HostInfoResults() (MeasurementsMap, error) {
	res := MeasurementsMap{}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()

	info, err := host.InfoWithContext(ctx)
	errs := []string{}

	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			err = fmt.Errorf("timeout exceeded")
		}

		logrus.Errorf("[SYSTEM] Failed to read host info: %s", err.Error())
		errs = append(errs, err.Error())
	}

	for _, field := range fm.Config.HostInfo {
		switch strings.ToLower(field) {
		case "os_kernel":
			if info != nil {
				res[field] = info.OS
			} else {
				res[field] = nil
			}
		case "os_family":
			if info != nil {
				res[field] = info.PlatformFamily
			} else {
				res[field] = nil
			}
		case "uname":
			uname, err := Uname()
			if err != nil {
				logrus.Errorf("[SYSTEM] Failed to read host uname: %s", err.Error())
				errs = append(errs, err.Error())
				res[field] = nil
			} else {
				res[field] = uname
			}
		case "fqdn":
			res[field] = getFQDN()
		case "cpu_model":
			cpuInfo, err := cpu.Info()
			if err != nil {
				logrus.Errorf("[SYSTEM] Failed to read cpu info: %s", err.Error())
				errs = append(errs, err.Error())
				res[field] = nil
				continue
			}
			res[field] = cpuInfo[0].ModelName
		case "os_arch":
			res[field] = runtime.GOARCH
		case "memory_total_b":
			memStat, err := mem.VirtualMemory()
			if err != nil {
				logrus.Errorf("[SYSTEM] Failed to read mem info: %s", err.Error())
				errs = append(errs, err.Error())
				res[field] = nil
				continue
			}
			res[field] = memStat.Total
		case "hostname":
			name, err := os.Hostname()
			if err != nil {
				logrus.Errorf("[SYSTEM] Failed to read hostname: %s", err.Error())
				errs = append(errs, err.Error())
				res[field] = nil
				continue
			}
			res[field] = name
		}
	}

	if len(errs) == 0 {
		return res, nil
	}

	return res, errors.New("SYSTEM: " + strings.Join(errs, "; "))
}


<|start_filename|>tcp.go<|end_filename|>
package frontman

import (
	"bytes"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"regexp"
	"strings"
	"time"

	"gopkg.in/ldap.v3"
)

var defaultPortByService = map[string]int{
	"dns":   53,
	"ftp":   21,
	"ftps":  990,
	"http":  80,
	"https": 443,
	"iax2":  4569,
	"imap":  143,
	"imaps": 993,
	"ldap":  389,
	"ldaps": 636,
	"nntp":  119,
	"pop3":  110,
	"pop3s": 995,
	"smtp":  25,
	"smtps": 465,
	"ssh":   22,
	"sip":   5060,
}

var errorFailedToVerifyService = errors.New("Failed to verify service")

func (fm *Frontman) runTCPCheck(hostname string, port int, service string) (MeasurementsMap, error) {
	service = strings.ToLower(service)

	// Check if we have to autodetect port by service name
	if port <= 0 {
		// Lookup service by default port
		p, exists := defaultPortByService[service]
		if !exists {
			return nil, fmt.Errorf("failed to auto-determine port for '%s'", service)
		}
		port = p
	}

	prefix := fmt.Sprintf("net.tcp.%s.%d.", service, port)

	// Initialize MeasurementsMap
	m := MeasurementsMap{
		prefix + "success": 0,
	}

	// Start measuring execution time
	started := time.Now()
	// Calculate execution time in the end
	defer func() {
		m[prefix+"totalTimeSpent_s"] = time.Since(started).Seconds()
	}()

	addr := fmt.Sprintf("%s:%d", hostname, port)

	// Open connection to the specified addr
	conn, err := net.DialTimeout("tcp", addr, secToDuration(fm.Config.NetTCPTimeout))
	m[prefix+"connectTime_s"] = time.Since(started).Seconds()
	if err != nil {
		return m, err
	}
	defer conn.Close()

	err = conn.SetDeadline(time.Now().Add(secToDuration(fm.Config.NetTCPTimeout)))
	if err != nil {
		return m, fmt.Errorf("can't set tcp conn timeout: %s", err.Error())
	}
	// Execute the check
	err = executeTCPServiceCheck(conn, fm.Config.NetTCPTimeout, service, hostname)
	if err != nil {
		return m, fmt.Errorf("failed to verify '%s' service on %d port: %s", service, port, err.Error())
	}

	// Mark check as successful
	m[prefix+"success"] = 1

	return m, nil
}

// executeTCPServiceCheck executes a check based on the passed protocol name on the given connection
func executeTCPServiceCheck(conn net.Conn, tcpTimeout float64, service, hostname string) error {
	var err error
	switch service {
	case "ftp":
		err = checkFTP(conn, secToDuration(tcpTimeout))
	case "ftps":
		err = checkFTPS(conn, hostname, secToDuration(tcpTimeout))
	case "imap":
		err = checkIMAP(conn, secToDuration(tcpTimeout))
	case "imaps":
		err = checkIMAPS(conn, hostname, secToDuration(tcpTimeout))
	case "smtp":
		err = checkSMTP(conn, secToDuration(tcpTimeout))
	case "smtps":
		err = checkSMTPS(conn, hostname, secToDuration(tcpTimeout))
	case "pop3":
		err = checkPOP3(conn, secToDuration(tcpTimeout))
	case "pop3s":
		err = checkPOP3S(conn, hostname, secToDuration(tcpTimeout))
	case "ssh":
		err = checkSSH(conn, secToDuration(tcpTimeout))
	case "nntp":
		err = checkNNTP(conn, secToDuration(tcpTimeout))
	case "ldap":
		err = checkLDAP(conn, secToDuration(tcpTimeout))
	case "ldaps":
		err = checkLDAPS(conn, hostname, secToDuration(tcpTimeout))
	case "http":
		err = checkHTTP(conn, hostname, secToDuration(tcpTimeout))
	case "https":
		err = checkHTTPS(conn, hostname, secToDuration(tcpTimeout))
	case "dns":
		// minimal DNS test just verifies connection is established
	case "tcp":
		// In the previous call to net.Dial the test basically already happened while establishing the connection
		// so we don't have to do anything additional here.
	default:
		err = fmt.Errorf("unknown service '%s'", service)
	}

	return err
}

func checkNNTP(conn net.Conn, timeout time.Duration) error {
	conn.SetReadDeadline(time.Now().Add(timeout))

	var b = make([]byte, 64)
	n, err := conn.Read(b)

	if err != nil {
		return err
	}

	expected := []byte("200")
	expected2 := []byte("201")

	if !bytes.HasPrefix(b, expected) && !bytes.HasPrefix(b, expected2) {
		return fmt.Errorf("invalid response: expected to start with '%s' or '%s' but got '%s'", string(expected), string(expected2), string(b[0:n]))
	}

	conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err = conn.Write([]byte("QUIT\r\n"))

	return err
}

func checkPOP3(conn net.Conn, timeout time.Duration) error {
	conn.SetReadDeadline(time.Now().Add(timeout))

	var b = make([]byte, 64)
	n, err := conn.Read(b)

	if err != nil {
		return err
	}

	expected := []byte("+OK")

	if !bytes.HasPrefix(b, expected) {
		return fmt.Errorf("invalid response: expected to start with '%s' but got '%s'", string(expected), string(b[0:n]))
	}

	conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err = conn.Write([]byte("QUIT\r\n"))

	return err
}

func checkPOP3S(conn net.Conn, hostname string, timeout time.Duration) error {
	tlsConn := tls.Client(conn, &tls.Config{ServerName: hostname, InsecureSkipVerify: true, NextProtos: []string{"pop3"}})

	err := tlsConn.Handshake()
	if err != nil {
		return err
	}

	return checkPOP3(tlsConn, timeout)
}

var sshHelloRegex = regexp.MustCompile(`^SSH-[0-9]+\.[0-9]+-.*?\r?\n?`)

func checkSSH(conn net.Conn, timeout time.Duration) error {
	conn.SetReadDeadline(time.Now().Add(timeout))

	var b = make([]byte, 512)
	n, err := conn.Read(b)

	if err != nil {
		return err
	}

	if !sshHelloRegex.Match(b) {
		return fmt.Errorf("invalid response: expected to match '%s' but got '%s'", sshHelloRegex.String(), string(b[0:n]))
	}

	conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err = conn.Write([]byte("SSH-1.0-frontman\r\n"))

	return err
}

func checkSMTP(conn net.Conn, timeout time.Duration) error {
	conn.SetReadDeadline(time.Now().Add(timeout))

	var b = make([]byte, 64)
	n, err := conn.Read(b)

	if err != nil {
		return err
	}

	expected := []byte("220")

	if !bytes.HasPrefix(b, expected) {
		return fmt.Errorf("invalid response: expected to start with '%s' but got '%s'", string(expected), string(b[0:n]))
	}

	conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err = conn.Write([]byte("QUIT\r\n"))

	return err
}

func checkSMTPS(conn net.Conn, hostname string, timeout time.Duration) error {
	tlsConn := tls.Client(conn, &tls.Config{ServerName: hostname, InsecureSkipVerify: true, NextProtos: []string{"smtp"}})

	err := tlsConn.Handshake()
	if err != nil {
		return err
	}

	return checkSMTP(tlsConn, timeout)
}

func checkIMAP(conn net.Conn, timeout time.Duration) error {
	conn.SetReadDeadline(time.Now().Add(timeout))

	var b = make([]byte, 64)
	n, err := conn.Read(b)

	if err != nil {
		return err
	}

	expected := []byte("* OK")

	if !bytes.HasPrefix(b, expected) {
		return fmt.Errorf("invalid response: expected to start with '%s' but got '%s'", string(expected), string(b[0:n]))
	}

	conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err = conn.Write([]byte("a1 LOGOUT\r\n"))

	return err
}

func checkIMAPS(conn net.Conn, hostname string, timeout time.Duration) error {
	tlsConn := tls.Client(conn, &tls.Config{ServerName: hostname, InsecureSkipVerify: true, NextProtos: []string{"imap"}})

	err := tlsConn.Handshake()
	if err != nil {
		return err
	}

	return checkIMAP(tlsConn, timeout)
}

func checkFTP(conn net.Conn, timeout time.Duration) error {
	conn.SetReadDeadline(time.Now().Add(timeout))

	var b = make([]byte, 64)
	n, err := conn.Read(b)

	if err != nil {
		return err
	}

	expected := []byte("220")

	if !bytes.HasPrefix(b, expected) {
		return fmt.Errorf("invalid response: expected to start with '%s' but got '%s'", string(expected), string(b[0:n]))
	}

	conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err = conn.Write([]byte("QUIT\r\n"))

	return err
}

func checkFTPS(conn net.Conn, hostname string, timeout time.Duration) error {
	tlsConn := tls.Client(conn, &tls.Config{ServerName: hostname, InsecureSkipVerify: true, NextProtos: []string{"ftp"}})

	err := tlsConn.Handshake()
	if err != nil {
		return err
	}

	return checkFTP(tlsConn, timeout)
}

func checkHTTP(conn net.Conn, hostname string, timeout time.Duration) error {
	conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err := conn.Write([]byte(fmt.Sprintf("HEAD / HTTP/1.1\r\nHost: %s\r\n\r\n", hostname)))
	if err != nil {
		return err
	}

	conn.SetReadDeadline(time.Now().Add(timeout))

	var b = make([]byte, 64)
	n, err := conn.Read(b)

	if err != nil {
		return err
	}

	expected := []byte("HTTP/1")

	if !bytes.HasPrefix(b, expected) {
		return fmt.Errorf("invalid response: expected to start with '%s' but got '%s'", string(expected), string(b[0:n]))
	}

	return nil
}

func checkHTTPS(conn net.Conn, hostname string, timeout time.Duration) error {
	tlsConn := tls.Client(conn, &tls.Config{ServerName: hostname, InsecureSkipVerify: true, NextProtos: []string{"http/1.1"}})

	err := tlsConn.Handshake()
	if err != nil {
		return err
	}

	return checkHTTP(tlsConn, hostname, timeout)
}

func checkLDAP(conn net.Conn, timeout time.Duration) error {
	ldapConn := ldap.NewConn(conn, false)
	ldapConn.SetTimeout(timeout)

	searchRequest := ldap.NewSearchRequest(
		"dc=example,dc=com", // The base dn to search
		ldap.ScopeSingleLevel, ldap.NeverDerefAliases, 0, int(timeout.Seconds()+0.5), false,
		"(objectClass=*)",    // The filter to apply
		[]string{"dn", "cn"}, // A list attributes to retrieve
		nil,
	)

	ldapConn.Start()
	defer ldapConn.Close()

	sr, err := ldapConn.SearchWithPaging(searchRequest, 1)
	if err != nil {
		return err
	}

	if len(sr.Entries) == 0 {
		return errors.New("no entries found")
	}

	return nil
}

func checkLDAPS(conn net.Conn, hostname string, timeout time.Duration) error {
	tlsConn := tls.Client(conn, &tls.Config{ServerName: hostname, InsecureSkipVerify: true, NextProtos: []string{"ldap"}})

	err := tlsConn.Handshake()
	if err != nil {
		return err
	}

	return checkLDAP(tlsConn, timeout)
}


<|start_filename|>servicecheck.go<|end_filename|>
package frontman

import (
	"fmt"
	"time"

	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

func (check ServiceCheck) uniqueID() string {
	return check.UUID
}

func (check ServiceCheck) run(fm *Frontman) (*Result, error) {

	res := &Result{
		Node:      fm.Config.NodeName,
		CheckType: "serviceCheck",
		CheckUUID: check.UUID,
		Check:     check.Check,
		Timestamp: time.Now().Unix(),
	}

	if check.UUID == "" {
		return res, fmt.Errorf("missing checkUuid key")
	}
	if check.Check.Connect == "" {
		return res, fmt.Errorf("missing data.connect key")
	}

	var done = make(chan struct{})
	var err error
	var results map[string]interface{}

	go func() {
		switch check.Check.Protocol {
		case ProtocolICMP:
			results, err = fm.runPing(check.Check.Connect)
			if err != nil {
				logrus.Debugf("serviceCheck: %s: %s", check.UUID, err.Error())
			}
		case ProtocolTCP:
			port, _ := check.Check.Port.Int64()

			results, err = fm.runTCPCheck(check.Check.Connect, int(port), check.Check.Service)
			if err != nil {
				logrus.Debugf("serviceCheck: %s: %s", check.UUID, err.Error())
			}
		case ProtocolUDP:
			port, _ := check.Check.Port.Int64()

			results, err = fm.runUDPCheck(check.Check.Connect, int(port), check.Check.Service)
			if err != nil {
				logrus.Debugf("serviceCheck: %s: %s", check.UUID, err.Error())
			}
		case ProtocolSSL:
			port, _ := check.Check.Port.Int64()

			results, err = fm.runSSLCheck(check.Check.Connect, int(port), check.Check.Service)
			if err != nil {
				logrus.Debugf("serviceCheck: %s: %s", check.UUID, err.Error())
			}
		case "":
			logrus.Info("serviceCheck: missing check.protocol")
			err = errors.New("Missing check.protocol")
		default:
			logrus.Errorf("serviceCheck: unknown check.protocol: '%s'", check.Check.Protocol)
			err = errors.New("Unknown check.protocol")
		}
		done <- struct{}{}
	}()

	// Warning: do not rely on serviceCheckEmergencyTimeout as it leak goroutines(until it will be finished)
	// instead use individual timeouts inside all checks
	select {
	case <-done:
		res.Measurements = results
		return res, err
	case <-time.After(serviceCheckEmergencyTimeout):
		logrus.Debugf("serviceCheck %s %s: %s got unexpected timeout after %.0fs", check.Check.Protocol, check.Check.Connect, check.UUID, serviceCheckEmergencyTimeout.Seconds())
		return res, fmt.Errorf("got unexpected timeout")
	}
}


<|start_filename|>snmp_test.go<|end_filename|>
package frontman

// In order to run tests, set up snmpd somewhere and
// $ FRONTMAN_SNMPD_IP="172.16.72.169" FRONTMAN_SNMPD_COMMUNITY=public go test -v -run TestSNMP

import (
	"os"
	"testing"
	"time"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

var snmpdIP = ""
var snmpdCommunity = ""

func skipSNMP(t *testing.T) {
	snmpdIP = os.Getenv("FRONTMAN_SNMPD_IP")
	if snmpdIP == "" {
		t.Skip("Skipping test of SNMP")
	}
	snmpdCommunity = os.Getenv("FRONTMAN_SNMPD_COMMUNITY")
}

// test SNMP v1 against snmpd
func TestSNMPv1(t *testing.T) {
	skipSNMP(t)
	// necessary snmpd.conf changes:
	// agentAddress udp:161,udp6:[::1]:161

	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v1",
			Check: SNMPCheckData{
				Connect:   snmpdIP,
				Port:      161,
				Timeout:   5.0,
				Protocol:  "v1",
				Community: snmpdCommunity,
				Preset:    "basedata",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.basedata.success"])
	require.Equal(t, true, len(res.Measurements) > 1)
	require.Equal(t, true, len(res.Measurements["system.contact"].(string)) > 1)
	require.Equal(t, true, len(res.Measurements["system.location"].(string)) > 1)
}

// test SNMP v2 against snmpd
func TestSNMPv2(t *testing.T) {
	skipSNMP(t)
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v2",
			Check: SNMPCheckData{
				Connect:   snmpdIP,
				Port:      161,
				Timeout:   5.0,
				Protocol:  "v2",
				Community: snmpdCommunity,
				Preset:    "basedata",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.basedata.success"])
	require.Equal(t, true, len(res.Measurements) > 1)
	require.Equal(t, true, len(res.Measurements["system.contact"].(string)) > 1)
	require.Equal(t, true, len(res.Measurements["system.location"].(string)) > 1)
}

// test SNMP v2 against snmpd
func TestSNMPv2PresetBandwidth(t *testing.T) {
	// necessary snmpd.conf changes:
	// view   systemonly  included   .1

	// in order to verify IF-MIB configuration:
	// $ snmpwalk -v2c -c public hostname .1.3.6.1.2.1.2.2.1.8

	skipSNMP(t)

	delaySeconds := 1.
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.Sleep = delaySeconds
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v2_bandwidth",
			Check: SNMPCheckData{
				Connect:   snmpdIP,
				Port:      161,
				Timeout:   5.0,
				Protocol:  "v2",
				Community: snmpdCommunity,
				Preset:    "bandwidth",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.bandwidth.success"])

	// should be at least 1 network interface + success key in result
	require.Equal(t, true, len(res.Measurements) >= 2)

	// NOTE: test makes some assumptions that may be hard to reproduce
	// NOTE: test must be performed vs a wired connection, as snmpd don't report interface speed on wireless connections
	iface := res.Measurements["2"].(map[string]interface{})
	require.Equal(t, uint(1000), iface["ifSpeed_mbps"])
	require.Equal(t, true, len(iface["ifName"].(string)) > 0)
	require.Equal(t, true, len(iface["ifDescr"].(string)) > 0)
	require.Equal(t, 2, iface["ifIndex"])

	// do 2nd request and check delta values
	time.Sleep(time.Duration(delaySeconds) * time.Second)

	fm.processInput(input.asChecks(), true)
	res = <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.bandwidth.success"])
	require.Equal(t, true, len(res.Measurements) >= 2)

	iface = res.Measurements["2"].(map[string]interface{})

	if _, ok := iface["ifIn_Bps"]; !ok {
		t.Errorf("ifIn_Bps key missing")
	}
	if _, ok := iface["ifInUtilization_percent"]; !ok {
		t.Errorf("ifInUtilization_percent key missing")
	}
}

func TestSNMPv2PresetOidHexValue(t *testing.T) {
	skipSNMP(t)

	delaySeconds := 1.
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.Sleep = delaySeconds
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v2_oid_hex",
			Check: SNMPCheckData{
				Connect:   snmpdIP,
				Port:      161,
				Timeout:   5.0,
				Protocol:  "v2",
				Community: snmpdCommunity,
				Preset:    "oid",
				Oid:       ".1.3.6.1.2.1.2.2.1.6.2", // IF-MIB::ifPhysAddress.2
				Name:      "interface mac",
				ValueType: "hex",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan

	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.oid.success"])

	part := res.Measurements[".1.3.6.1.2.1.2.2.1.6.2"].(map[string]interface{})
	require.Equal(t, true, len(part["value"].(string)) > 0)

	require.Equal(t, ".1.3.6.1.2.1.2.2.1.6.2", part["oid"].(string))
	require.Equal(t, "hex", part["value_type"].(string))
	require.Equal(t, "interface mac", part["name"].(string))
	require.Equal(t, "", part["unit"].(string))
}

func TestSNMPv2PresetOidDeltaPerSecValue(t *testing.T) {
	skipSNMP(t)

	delaySeconds := 5.
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.Sleep = delaySeconds
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v2_oid_delta_per_sec",
			Check: SNMPCheckData{
				Connect:   snmpdIP,
				Port:      161,
				Timeout:   5.0,
				Protocol:  "v2",
				Community: snmpdCommunity,
				Preset:    "oid",
				Oid:       ".1.3.6.1.2.1.2.2.1.16.2", //  IF-MIB::ifOutOctets.2
				ValueType: "delta_per_sec",
				Name:      "delta-per-sec-value",
				Unit:      "unit-value",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan

	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.oid.success"])

	// do 2nd request and check delta values
	time.Sleep(time.Duration(delaySeconds) * time.Second)

	fm.processInput(input.asChecks(), true)
	res = <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.oid.success"])

	part := res.Measurements[".1.3.6.1.2.1.2.2.1.16.2"].(map[string]interface{})
	require.Equal(t, true, part["value"].(float64) >= 0)

	require.Equal(t, ".1.3.6.1.2.1.2.2.1.16.2", part["oid"].(string))
	require.Equal(t, "delta_per_sec", part["value_type"].(string))
	require.Equal(t, "delta-per-sec-value", part["name"].(string))
	require.Equal(t, "unit-value", part["unit"].(string))
}

func TestSNMPv2PresetOidDeltaValue(t *testing.T) {
	skipSNMP(t)

	delaySeconds := 5.
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.Sleep = delaySeconds
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v2_oid_delta",
			Check: SNMPCheckData{
				Connect:   snmpdIP,
				Port:      161,
				Timeout:   5.0,
				Protocol:  "v2",
				Community: snmpdCommunity,
				Preset:    "oid",
				Oid:       ".1.3.6.1.2.1.2.2.1.16.2", //  IF-MIB::ifOutOctets.2
				ValueType: "delta",
				Name:      "delta-value",
				Unit:      "unit-value",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan

	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.oid.success"])

	// do 2nd request and check delta values
	time.Sleep(time.Duration(delaySeconds) * time.Second)

	fm.processInput(input.asChecks(), true)
	res = <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.oid.success"])

	part := res.Measurements[".1.3.6.1.2.1.2.2.1.16.2"].(map[string]interface{})
	require.Equal(t, true, part["value"].(float64) >= 0)

	require.Equal(t, ".1.3.6.1.2.1.2.2.1.16.2", part["oid"].(string))
	require.Equal(t, "delta", part["value_type"].(string))
	require.Equal(t, "delta-value", part["name"].(string))
	require.Equal(t, "unit-value", part["unit"].(string))
}

func TestSNMPv2PresetPorterrors(t *testing.T) {
	skipSNMP(t)

	delaySeconds := 1.
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.Sleep = delaySeconds
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v2_porterrors",
			Check: SNMPCheckData{
				Connect:   snmpdIP,
				Port:      161,
				Timeout:   5.0,
				Protocol:  "v2",
				Community: snmpdCommunity,
				Preset:    "porterrors",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.porterrors.success"])

	// should be at least 1 network interface + success key in result
	require.Equal(t, true, len(res.Measurements) >= 2)

	// NOTE: test makes some assumptions that may be hard to reproduce
	// NOTE: test must be performed vs a wired connection, as snmpd don't report interface speed on wireless connections
	iface := res.Measurements["2"].(map[string]interface{})
	require.Equal(t, true, len(iface["ifName"].(string)) > 0)
	require.Equal(t, 2, iface["ifIndex"])

	// do 2nd request and check delta values
	time.Sleep(time.Duration(delaySeconds) * time.Second)

	fm.processInput(input.asChecks(), true)
	res = <-fm.resultsChan

	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.porterrors.success"])
	require.Equal(t, true, len(res.Measurements) >= 2)

	iface = res.Measurements["2"].(map[string]interface{})

	if _, ok := iface["ifInErrors_delta"]; !ok {
		t.Errorf("ifInErrors_delta key missing")
	}
	if _, ok := iface["ifOutErrors_delta"]; !ok {
		t.Errorf("ifOutErrors_delta key missing")
	}
}

// test SNMP v2 invalid community against snmpd
func TestSNMPv2InvalidCommunity(t *testing.T) {
	skipSNMP(t)
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v2_invalid_community",
			Check: SNMPCheckData{
				Connect:   snmpdIP,
				Port:      161,
				Timeout:   2.0,
				Protocol:  "v2",
				Community: "invalidCommunityName",
				Preset:    "basedata",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	// NOTE: the only error we get on invalid community name is a timeout
	require.Equal(t, "get err: Request timeout (after 0 retries)", res.Message)
	require.Equal(t, 0, res.Measurements["snmpCheck.basedata.success"])
}

// test SNMP v3 noAuthNoPriv against snmpd
func TestSNMPv3NoAuthNoPriv(t *testing.T) {
	skipSNMP(t)
	// necessary snmpd.conf changes:
	// createUser noAuthNoPrivUser
	// rouser     noAuthNoPrivUser noauth

	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v3_noAuthNoPriv",
			Check: SNMPCheckData{
				Connect:       snmpdIP,
				Port:          161,
				Timeout:       5.0,
				Protocol:      "v3",
				Preset:        "basedata",
				SecurityLevel: "noAuthNoPriv",
				Username:      "noAuthNoPrivUser",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)

	// NOTE: if empty, snmpd is not configured correctly
	require.Equal(t, true, len(res.Measurements["system.contact"].(string)) > 1)

	require.Equal(t, 1, res.Measurements["snmpCheck.basedata.success"])
}

// test SNMP v3 noAuthNoPriv against snmpd with unknown username
func TestSNMPv3NoAuthNoPrivUnknownUser(t *testing.T) {
	skipSNMP(t)
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v3_noAuthNoPriv_unknown_user",
			Check: SNMPCheckData{
				Connect:       snmpdIP,
				Port:          161,
				Timeout:       5.0,
				Protocol:      "v3",
				Preset:        "basedata",
				SecurityLevel: "noAuthNoPriv",
				Username:      "noSuchUsername",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, "unknown user name", res.Message)
	require.Equal(t, 0, res.Measurements["snmpCheck.basedata.success"])
}

// test SNMP v3 authNoPriv against snmpd
func TestSNMPv3AuthNoPriv(t *testing.T) {
	skipSNMP(t)
	// necessary snmpd.conf changes:
	// createUser authOnlyUser  SHA "password"
	// rouser     authOnlyUser

	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v3_auth",
			Check: SNMPCheckData{
				Connect:                snmpdIP,
				Port:                   161,
				Timeout:                5.0,
				Protocol:               "v3",
				Preset:                 "basedata",
				SecurityLevel:          "authNoPriv",
				Username:               "authOnlyUser",
				AuthenticationProtocol: "sha",
				AuthenticationPassword: "password",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.basedata.success"])
	require.Equal(t, true, len(res.Measurements["system.contact"].(string)) > 1)
	require.Equal(t, true, len(res.Measurements["system.location"].(string)) > 1)
}

// test SNMP v3 authNoPriv against snmpd with wrong password
func TestSNMPv3AuthNoPrivWrongPassword(t *testing.T) {
	skipSNMP(t)
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v3_authNoPriv",
			Check: SNMPCheckData{
				Connect:                snmpdIP,
				Port:                   161,
				Timeout:                5.0,
				Protocol:               "v3",
				Preset:                 "basedata",
				SecurityLevel:          "authNoPriv",
				Username:               "authOnlyUser",
				AuthenticationProtocol: "sha",
				AuthenticationPassword: "<PASSWORD>",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	// NOTE: snmp protocol doesn't have a specific error message for wrong password
	require.Equal(t, "wrong digests, possibly wrong password", res.Message)
	require.Equal(t, 0, res.Measurements["snmpCheck.basedata.success"])
}

// test SNMP v3 authPriv against snmpd
func TestSNMPv3AuthPriv(t *testing.T) {
	skipSNMP(t)
	// necessary snmpd.conf changes:
	// createUser authPrivUser  SHA "password" DES
	// rwuser   authPrivUser   priv

	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v3_priv",
			Check: SNMPCheckData{
				Connect:                snmpdIP,
				Port:                   161,
				Timeout:                5.0,
				Protocol:               "v3",
				Preset:                 "basedata",
				SecurityLevel:          "authPriv",
				Username:               "authPrivUser",
				AuthenticationProtocol: "sha",
				AuthenticationPassword: "password",
				PrivacyProtocol:        "des",
				PrivacyPassword:        "password",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["snmpCheck.basedata.success"])
	require.Equal(t, true, len(res.Measurements["system.contact"].(string)) > 1)
	require.Equal(t, true, len(res.Measurements["system.location"].(string)) > 1)
}

func TestSNMPv3PresetBandwidthWrongCredentials(t *testing.T) {
	// this test makes sure that we error out if called with invalid credentials (SNMP v3)

	skipSNMP(t)

	delaySeconds := 1.
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.Sleep = delaySeconds
	fm := helperCreateFrontman(t, cfg)

	input := &Input{
		SNMPChecks: []SNMPCheck{{
			UUID: "snmp_basedata_v3_bandwidth_wrong_credentials",
			Check: SNMPCheckData{
				Connect:                snmpdIP,
				Port:                   161,
				Timeout:                5.0,
				Protocol:               "v3",
				Preset:                 "bandwidth",
				SecurityLevel:          "authNoPriv",
				Username:               "authOnlyUser",
				AuthenticationProtocol: "sha",
				AuthenticationPassword: "<PASSWORD>",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan

	require.Equal(t, "wrong digests, possibly wrong password", res.Message)
	require.Equal(t, 0, res.Measurements["snmpCheck.bandwidth.success"])
}

func TestOidToHumanReadable(t *testing.T) {
	v, suffix, err := oidToHumanReadable(".1.3.6.1.2.1.2.2.1.8.1")
	assert.Equal(t, nil, err)
	assert.Equal(t, 1, suffix)
	assert.Equal(t, "ifOperStatus", v)

	v, suffix, err = oidToHumanReadable(".1.3.6.1.2.1.2.2.1.8.2")
	assert.Equal(t, nil, err)
	assert.Equal(t, 2, suffix)
	assert.Equal(t, "ifOperStatus", v)
}

func TestDelta(t *testing.T) {
	assert.Equal(t, uint(1), delta(1, 2))
	assert.Equal(t, uint(1), delta(2, 1))
}


<|start_filename|>webcheck.go<|end_filename|>
package frontman

import (
	"bytes"
	"context"
	"crypto/tls"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"net/http/httptrace"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/sirupsen/logrus"
	"golang.org/x/net/html"

	"github.com/cloudradar-monitoring/frontman/pkg/utils/gzipreader"
)

const maxBodySize = 1 * 1024 * 1024

func getTextFromHTML(data []byte) (text string) {
	r := bytes.NewReader(data)
	dom := html.NewTokenizer(r)
	startToken := dom.Token()

loopDom:
	for {
		tt := dom.Next()
		switch {
		case tt == html.ErrorToken:
			break loopDom // End of the document,  done
		case tt == html.StartTagToken:
			startToken = dom.Token()
		case tt == html.TextToken:

			if startToken.Data == "script" {
				continue
			}

			if startToken.Data == "style" {
				continue
			}

			TxtContent := html.UnescapeString(string(dom.Text()))
			if len(TxtContent) > 0 {
				text += TxtContent
			}
		}
	}
	return
}

func (fm *Frontman) newHTTPTransport(ignoreSSLErrors *bool) *http.Transport {
	t := &http.Transport{
		DisableKeepAlives: true,
		Proxy:             http.ProxyFromEnvironment,
		DialContext: (&net.Dialer{
			Timeout:   15 * time.Second,
			KeepAlive: 0,
			DualStack: true,
		}).DialContext,
		MaxIdleConns:          1,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		TLSClientConfig:       &tls.Config{},
	}

	valueProvided := ignoreSSLErrors != nil
	if (valueProvided && *ignoreSSLErrors) ||
		(!valueProvided && fm.Config.IgnoreSSLErrors) {
		t.TLSClientConfig.InsecureSkipVerify = true
		t.TLSClientConfig.RootCAs = fm.rootCAs
	}

	return t
}

func checkBodyReaderMatchesPattern(data []byte, pattern string, expectedPresence string, extractTextFromHTML bool) error {
	expectedPresence = strings.ToLower(expectedPresence)
	if expectedPresence != "absent" {
		expectedPresence = "present"
	}

	var whereSuffix string
	if extractTextFromHTML {
		data = []byte(getTextFromHTML(data))
		whereSuffix = "in the extracted text"
	} else {
		whereSuffix = "in the raw HTML"
	}

	if expectedPresence == "present" && !bytes.Contains(data, []byte(pattern)) {
		return fmt.Errorf("pattern expected to be present '%s' not found %s", pattern, whereSuffix)
	}
	if expectedPresence == "absent" && bytes.Contains(data, []byte(pattern)) {
		return fmt.Errorf("pattern expected to be absent '%s' found %s", pattern, whereSuffix)
	}

	return nil
}

func (check WebCheck) uniqueID() string {
	return check.UUID
}

func (check WebCheck) run(fm *Frontman) (*Result, error) {

	res := &Result{
		Node:         fm.Config.NodeName,
		CheckType:    "webCheck",
		CheckUUID:    check.UUID,
		Check:        check.Check,
		Timestamp:    time.Now().Unix(),
		Measurements: make(map[string]interface{}),
	}

	if check.UUID == "" {
		return res, fmt.Errorf("missing checkUuid key")
	}
	if check.Check.Method == "" {
		return res, fmt.Errorf("missing check.method key")
	}
	if check.Check.URL == "" {
		return res, fmt.Errorf("missing check.url key")
	}

	prefix := fmt.Sprintf("http.%s.", check.Check.Method)
	res.Measurements[prefix+"success"] = 0

	// In case the webcheck disables redirect following we set maxRedirects to 0
	maxRedirects := 0
	if !check.Check.DontFollowRedirects {
		maxRedirects = fm.Config.HTTPCheckMaxRedirects
	}
	var httpTransport = fm.newHTTPTransport(check.Check.IgnoreSSLErrors)
	httpClient := fm.newClientWithOptions(httpTransport, maxRedirects)

	timeout := fm.Config.HTTPCheckTimeout

	// set individual timeout in case it is less than in this check
	if check.Check.Timeout > 0 && check.Check.Timeout < timeout {
		timeout = check.Check.Timeout
	}

	check.Check.Method = strings.ToUpper(check.Check.Method)

	ctx, cancel := context.WithTimeout(context.Background(), secToDuration(timeout))
	defer cancel()

	url, err := normalizeURLPort(check.Check.URL)
	if err != nil {
		return res, err
	}

	req, err := http.NewRequest(check.Check.Method, url, nil)
	if err != nil {
		return res, err
	}

	startedConnectionAt := time.Now()
	defer func() {
		res.Measurements[prefix+"totalTimeSpent_s"] = time.Since(startedConnectionAt).Seconds()
	}()

	req.Header.Set("Accept-Encoding", "gzip")
	req.Header.Set("User-Agent", fm.userAgent())

	var hostHeader string
	for key, val := range check.Check.Headers {
		req.Header.Set(key, val)
		if hostHeader == "" && strings.ToLower(key) == "host" {
			hostHeader = val
		}
	}
	if hostHeader != "" && hostHeader != req.URL.Host {
		// most probably we should enable SNI handshake
		httpTransport.TLSClientConfig.ServerName = hostHeader
		req.Host = hostHeader
	}

	if check.Check.Method == "POST" && check.Check.PostData != "" {
		req.Body = ioutil.NopCloser(strings.NewReader(check.Check.PostData))
		// close noop closer to bypass lint warnings
		_ = req.Body.Close()
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}

	var wroteRequestAt time.Time
	trace := &httptrace.ClientTrace{
		GotFirstResponseByte: func() {
			res.Measurements[prefix+"timeToFirstByte_s"] = time.Since(wroteRequestAt).Seconds()
		},
	}

	wroteRequestAt = time.Now()
	resp, err := httpClient.Do(req.WithContext(httptrace.WithClientTrace(ctx, trace)))
	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			return res, fmt.Errorf("timeout exceeded")
		}
		return res, err
	}
	defer resp.Body.Close()

	// Set the httpStatusCode in case we got a response
	res.Measurements[prefix+"httpStatusCode"] = resp.StatusCode

	if check.Check.ExpectedHTTPStatus > 0 && resp.StatusCode != check.Check.ExpectedHTTPStatus {
		return res, fmt.Errorf("bad status code. Expected %d, got %d", check.Check.ExpectedHTTPStatus, resp.StatusCode)
	}

	if check.Check.Method != "HEAD" {
		if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
			length, err := strconv.ParseInt(contentLength, 10, 64)
			if err == nil && length > maxBodySize {
				res.Message = fmt.Sprintf("Content-Length too large for checking (%d)", length)
				return res, nil
			}
		}

		contentType := resp.Header.Get("Content-Type")

		ct := strings.Split(contentType, "/")
		if len(ct) >= 2 && ct[0] != "text" {

			switch ct[0] {
			case "audio", "video", "image", "font":
				res.Message = fmt.Sprintf("Content-Type is not readable as text (%s)", contentType)
				return res, nil
			}

			switch contentType {
			case "application/octet-stream", "application/ogg", "application/pdf", "application/x-shockwave-flash", "application/zip":
				res.Message = fmt.Sprintf("Content-Type is not readable as text (%s)", contentType)
				return res, nil
			}
		}
	}

	if strings.EqualFold(resp.Header.Get("Content-Encoding"), "gzip") {
		// wrap body reader with gzip reader
		resp.Body = &gzipreader.GzipReader{Reader: resp.Body}
	}

	limitedReader := http.MaxBytesReader(nil, resp.Body, maxBodySize)
	data, err := ioutil.ReadAll(limitedReader)
	if err != nil {
		if err.Error() == "http: request body too large" {
			res.Message = fmt.Sprintf("got error while reading full response body: http: request body exceeds the maximum of %dMB", maxBodySize/1024/1024)
		} else {
			res.Message = fmt.Sprintf("got error while reading response body: %s", err.Error())
		}
	}

	if check.Check.ExpectedPattern != "" {
		err = checkBodyReaderMatchesPattern(data, check.Check.ExpectedPattern, check.Check.ExpectedPatternPresence, !check.Check.SearchHTMLSource)
		if err != nil {
			res.Message = err.Error()
		}
	}

	bytesReceived := len(data)
	res.Measurements[prefix+"bytesReceived"] = bytesReceived

	secondsSinceRequestWasSent := time.Since(wroteRequestAt).Seconds()
	if secondsSinceRequestWasSent > 0 {
		// Measure download speed since the request sent
		res.Measurements[prefix+"downloadPerformance_bps"] = int64(float64(bytesReceived) / secondsSinceRequestWasSent)
	}

	if err != nil {
		if ctx.Err() == context.DeadlineExceeded {
			return res, fmt.Errorf("timeout exceeded")
		}
		return res, nil
	}

	res.Measurements[prefix+"success"] = 1
	return res, nil
}

func (fm *Frontman) newClientWithOptions(transport *http.Transport, maxRedirects int) *http.Client {
	client := &http.Client{Transport: transport}

	client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
		// copies headers from previous request (go strips Authorization header on redirects)
		lastRequest := via[len(via)-1]
		for attr, val := range lastRequest.Header {
			if _, ok := req.Header[attr]; !ok {
				req.Header[attr] = val
			}
		}

		if len(via) > 0 && maxRedirects <= 0 {
			logrus.Debugf("request resulted in a redirect, but redirects are not allowed (max_redirects=%d)", maxRedirects)
			return http.ErrUseLastResponse
		} else if len(via) > maxRedirects {
			logrus.Debugf("request resulted in %d redirects (max_redirects=%d)", len(via), maxRedirects)
			return http.ErrUseLastResponse
		}
		return nil
	}

	return client
}

// removes ports from URL if it is the default port for given scheme
func normalizeURLPort(u string) (string, error) {
	url, err := url.Parse(u)
	if err != nil {
		return u, err
	}

	portSeparator := strings.Index(url.Host, ":")
	if portSeparator != -1 {
		port := url.Host[portSeparator+1:]
		if (url.Scheme == "http" && port == "80") || (url.Scheme == "https" && port == "443") {
			url.Host = url.Host[0:portSeparator]
		}
	}

	return url.String(), nil
}


<|start_filename|>ssl_test.go<|end_filename|>
// +build !quick_tests

package frontman

import (
	"testing"

	"github.com/stretchr/testify/assert"
)

func TestFrontman_runSSLCheck(t *testing.T) {
	badSSL := []string{
		"expired.badssl.com",
		"wrong.host.badssl.com",
		"self-signed.badssl.com",
		"untrusted-root.badssl.com",

		"sha1-intermediate.badssl.com",

		// cipher suite
		"rc4-md5.badssl.com",
		"rc4.badssl.com",
		"null.badssl.com",

		// key exchange
		"dh480.badssl.com",
		"dh512.badssl.com",
		"dh1024.badssl.com",
		"dh2048.badssl.com",
		"dh-small-subgroup.badssl.com",
		"dh-composite.badssl.com",

		// certificate transparency
		"invalid-expected-sct.badssl.com",

		// upgrade
		"subdomain.preloaded-hsts.badssl.com",

		// known bad
		"Superfish.badssl.com",
		"eDellRoot.badssl.com",
		"DSDTestProvider.badssl.com",
		"preact-cli.badssl.com",
		"webpack-dev-server.badssl.com",

		// chrome tests
		"captive-portal.badssl.com",
		"mitm-software.badssl.com",

		// defunct
		"sha1-2017.badssl.com",
	}

	goodSSL := []string{
		"badssl.com",
		"sha256.badssl.com",
		"sha384.badssl.com",
		"sha512.badssl.com",

		"1000-sans.badssl.com",
		"ecc256.badssl.com",
		"ecc384.badssl.com",
		"rsa2048.badssl.com",
		"rsa4096.badssl.com",
		"extended-validation.badssl.com",
		"client.badssl.com",
		"mozilla-modern.badssl.com",

		"hsts.badssl.com",
		"upgrade.badssl.com",
		"preloaded-hsts.badssl.com",
		"https-everywhere.badssl.com",

		"long-extended-subdomain-name-containing-many-letters-and-dashes.badssl.com",
		"longextendedsubdomainnamewithoutdashesinordertotestwordwrapping.badssl.com",
	}

	if testing.Short() {
		badSSL = badSSL[:2]
		goodSSL = goodSSL[:2]
	}

	cfg := NewConfig()
	fm := helperCreateFrontman(t, cfg)

	for _, badSSLHost := range badSSL {
		_, err := fm.runSSLCheck(badSSLHost, 443, "https")
		assert.Error(t, err, badSSLHost)
	}

	for _, goodSSLHost := range goodSSL {
		_, err := fm.runSSLCheck(goodSSLHost, 443, "https")
		assert.NoError(t, err, goodSSLHost)
	}
}


<|start_filename|>udp.go<|end_filename|>
package frontman

import (
	"bytes"
	"fmt"
	"io"
	"net"
	"strconv"
	"strings"
	"text/template"
	"time"

	"github.com/cloudradar-monitoring/frontman/pkg/iax"
	"github.com/cloudradar-monitoring/frontman/pkg/utils"
)

func (fm *Frontman) runUDPCheck(hostname string, port int, service string) (MeasurementsMap, error) {
	// Check if we have to autodetect port by service name
	if port <= 0 {
		// Lookup service by default port
		p, exists := defaultPortByService[service]
		if !exists {
			return nil, fmt.Errorf("failed to auto-determine port for '%s'", service)
		}
		port = p
	}

	prefix := fmt.Sprintf("net.udp.%s.%d.", service, port)

	// Initialize MeasurementsMap
	m := MeasurementsMap{
		prefix + "success": 0,
	}

	// Start measuring execution time
	started := time.Now()
	// Calculate execution time in the end
	defer func() {
		m[prefix+"totalTimeSpent_s"] = time.Since(started).Seconds()
	}()

	checkTimeout := secToDuration(fm.Config.NetUDPTimeout)

	addr := fmt.Sprintf("%s:%d", hostname, port)

	// Open connection to the specified addr
	conn, err := net.DialTimeout("udp", addr, checkTimeout)
	m[prefix+"connectTime_s"] = time.Since(started).Seconds()
	if err != nil {
		return m, err
	}
	defer conn.Close()

	err = conn.SetDeadline(time.Now().Add(checkTimeout))
	if err != nil {
		return m, fmt.Errorf("can't set UDP conn timeout: %s", err.Error())
	}

	// Execute the check
	err = executeUDPServiceCheck(conn.(*net.UDPConn), checkTimeout, service, hostname)
	if err != nil {
		return m, fmt.Errorf("failed to verify '%s' service on %d port: %s", service, port, err.Error())
	}

	// Mark check as successful
	m[prefix+"success"] = 1

	return m, nil
}

// executeUDPServiceCheck executes a check based on the passed protocol name on the given connection
func executeUDPServiceCheck(conn *net.UDPConn, udpTimeout time.Duration, service, hostname string) error {
	var err error
	switch service {
	case "sip":
		err = checkSIP(conn, hostname, udpTimeout)
	case "iax2":
		err = checkIAX2(conn, udpTimeout)
	case "dns":
		// minimal DNS test just verifies connection is established
	case "udp":
		// In the previous call to net.Dial the test basically already happened while establishing the connection
		// so we don't have to do anything additional here.
	default:
		err = fmt.Errorf("unknown service '%s'", service)
	}

	return err
}

func checkSIP(conn *net.UDPConn, hostname string, timeout time.Duration) error {
	const magicCookie = "z9hG4bK" // See: https://tools.ietf.org/html/rfc3261#section-8.1.1.7

	requestTemplateText := `OPTIONS sip:{{.FromUser}}@{{.Domain}} SIP/2.0
Via: SIP/2.0/UDP {{.ContactDomain}}:{{.LPort}};branch={{.Branch}}
From: {{.FromName}} <sip:{{.FromUser}}@{{.Domain}}>;tag=0c26cd11
To: {{.FromName}} <sip:{{.ToUser}}@{{.Domain}}>
Contact: <sip:{{.FromUser}}@{{.ContactDomain}}:{{.LPort}};transport=udp>
Call-ID: {{.CallId}}
CSeq: 1 OPTIONS
User-Agent: {{.UserAgent}}
Max-Forwards: 70
Allow: INVITE,ACK,CANCEL,BYE,NOTIFY,REFER,OPTIONS,INFO,SUBSCRIBE,UPDATE,PRACK,MESSAGE
Content-Length: 0

`
	requestTemplateText = strings.ReplaceAll(requestTemplateText, "\n", "\r\n")
	requestTpl, err := template.New("request").Parse(requestTemplateText)
	if err != nil {
		return err
	}

	localAddr := conn.LocalAddr().(*net.UDPAddr)

	params := map[string]string{
		"FromUser":      "100",
		"Domain":        hostname,
		"ContactDomain": "1.1.1.1",
		"LPort":         strconv.Itoa(localAddr.Port),
		"Branch":        magicCookie + utils.RandomizedStr(64),
		"FromName":      "",
		"ToUser":        "100",
		"CallId":        utils.RandomizedStr(32),
		"UserAgent":     "frontman",
	}
	requestBuf := bytes.NewBuffer([]byte{})
	err = requestTpl.Execute(requestBuf, params)
	if err != nil {
		return err
	}

	_ = conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err = io.Copy(conn, requestBuf)
	if err != nil {
		return err
	}

	var response = make([]byte, 1024)
	_ = conn.SetReadDeadline(time.Now().Add(timeout))
	n, _, err := conn.ReadFrom(response)
	if err != nil {
		return err
	}

	expected := []byte("SIP/2")
	if !bytes.HasPrefix(response, expected) {
		return fmt.Errorf("invalid response: expected to start with '%s' but got '%s'", string(expected), string(response[0:n]))
	}

	return nil
}

func checkIAX2(conn *net.UDPConn, timeout time.Duration) error {
	pokePacket := iax.GetPokeFramePacket()

	_ = conn.SetWriteDeadline(time.Now().Add(timeout))
	_, err := conn.Write(pokePacket)
	if err != nil {
		return err
	}

	_ = conn.SetReadDeadline(time.Now().Add(timeout))

	var b = make([]byte, 128)
	_, err = conn.Read(b)
	if err != nil {
		return err
	}

	if !iax.IsPongResponse(b) {
		return fmt.Errorf("invalid response to POKE request. Received bytes: %v", b)
	}

	// send ACK to close connection
	// new version of Asterisk do not require this,
	// but old will resend PONG packets
	ackPacket := iax.GetAckFramePacket()
	_ = conn.SetWriteDeadline(time.Now().Add(timeout))
	_, _ = conn.Write(ackPacket)

	return nil
}


<|start_filename|>webcheck_test.go<|end_filename|>
package frontman

import (
	"net/http"
	"net/http/httptest"
	"testing"

	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/require"
)

func TestWebCheck(t *testing.T) {
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.HTTPCheckTimeout = 10.0
	cfg.Sleep = 10
	fm := helperCreateFrontman(t, cfg)
	input := &Input{
		WebChecks: []WebCheck{{
			UUID: "webcheck1",
			Check: WebCheckData{
				Timeout:            1.0,
				URL:                "https://www.google.com",
				Method:             "get",
				ExpectedHTTPStatus: 200,
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["http.get.success"])
}

func TestWebCheckHeaders(t *testing.T) {
	// verifies that extra headers are being set in http requests
	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "text/html")
		w.WriteHeader(http.StatusOK)

		assert.Equal(t, "Basic dXNlcm5hbWU6cGFzc3dvcmQ=", r.Header.Get("Authorization"))
		assert.Equal(t, "no-cache", r.Header.Get("Cache-Control"))
	}))
	defer ts.Close()

	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.HTTPCheckTimeout = 10.0
	cfg.Sleep = 10
	fm := helperCreateFrontman(t, cfg)
	input := &Input{
		WebChecks: []WebCheck{{
			UUID: "webcheck1",
			Check: WebCheckData{
				Timeout:            1.0,
				URL:                ts.URL,
				Method:             "get",
				ExpectedHTTPStatus: 200,
				Headers: map[string]string{
					"Authorization": "Basic dXNlcm5hbWU6cGFzc3dvcmQ=",
					"cache-control": "no-cache",
				},
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["http.get.success"])
}

func TestNormalizeURLPort(t *testing.T) {
	var urls = []struct {
		input    string
		expected string
	}{
		{"http://www.google.com:80/en/", "http://www.google.com/en/"},
		{"https://www.google.com:443/en/", "https://www.google.com/en/"},
		{"https://www.google.com:5555/en/", "https://www.google.com:5555/en/"},
	}

	for _, u := range urls {
		url, err := normalizeURLPort(u.input)
		assert.Equal(t, u.expected, url)
		assert.Equal(t, nil, err)
	}
}

func TestWebCheckPresentTextSuccess(t *testing.T) {
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.HTTPCheckTimeout = 10.0
	cfg.Sleep = 10
	fm := helperCreateFrontman(t, cfg)
	input := &Input{
		WebChecks: []WebCheck{{
			UUID: "webcheck1",
			Check: WebCheckData{
				Timeout:                 2.0,
				URL:                     "https://www.google.com",
				Method:                  "get",
				ExpectedHTTPStatus:      200,
				SearchHTMLSource:        true,
				ExpectedPattern:         "<title>Google</title>",
				ExpectedPatternPresence: "present",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["http.get.success"])
}

func TestWebCheckPresentTextFail(t *testing.T) {
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.HTTPCheckTimeout = 10.0
	cfg.Sleep = 10
	fm := helperCreateFrontman(t, cfg)
	input := &Input{
		WebChecks: []WebCheck{{
			UUID: "webcheck1",
			Check: WebCheckData{
				Timeout:                 2.0,
				URL:                     "https://www.google.com",
				Method:                  "get",
				ExpectedHTTPStatus:      200,
				SearchHTMLSource:        false,
				ExpectedPattern:         "yahoo rules",
				ExpectedPatternPresence: "present",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, "pattern expected to be present 'yahoo rules' not found in the extracted text", res.Message)
}

func TestWebCheckAbsentTextSuccess(t *testing.T) {
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.HTTPCheckTimeout = 10.0
	cfg.Sleep = 10
	fm := helperCreateFrontman(t, cfg)
	input := &Input{
		WebChecks: []WebCheck{{
			UUID: "webcheck1",
			Check: WebCheckData{
				Timeout:                 2.0,
				URL:                     "https://www.google.com",
				Method:                  "get",
				ExpectedHTTPStatus:      200,
				SearchHTMLSource:        true,
				ExpectedPattern:         "<title>Yahoo</title>",
				ExpectedPatternPresence: "absent",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, nil, res.Message)
	require.Equal(t, 1, res.Measurements["http.get.success"])
}

func TestWebCheckAbsentTextFail(t *testing.T) {
	cfg, _ := HandleAllConfigSetup(DefaultCfgPath)
	cfg.HTTPCheckTimeout = 10.0
	cfg.Sleep = 10
	fm := helperCreateFrontman(t, cfg)
	input := &Input{
		WebChecks: []WebCheck{{
			UUID: "webcheck1",
			Check: WebCheckData{
				Timeout:                 2.0,
				URL:                     "https://www.google.com",
				Method:                  "get",
				ExpectedHTTPStatus:      200,
				SearchHTMLSource:        false,
				ExpectedPattern:         "Google",
				ExpectedPatternPresence: "absent",
			},
		}},
	}
	fm.processInput(input.asChecks(), true)
	res := <-fm.resultsChan
	require.Equal(t, "pattern expected to be absent 'Google' found in the extracted text", res.Message)
}


<|start_filename|>pkg/notification/toast_windows.go<|end_filename|>
// +build windows

package notification

import (
	"os"
	"path/filepath"

	"gopkg.in/toast.v1"
)

const toastErrorIcon = "resources\\error.png"
const toastSuccessIcon = "resources\\success.png"
const toastAppID = "cloudradar.frontman"

func getExecutablePath() string {
	ex, err := os.Executable()
	if err != nil {
		return ""
	}

	return filepath.Dir(ex)
}

func SendErrorNotification(title, message string) error {
	msg := toast.Notification{
		AppID:    toastAppID,
		Title:    title,
		Message:  message,
		Duration: toast.Long, // last for 25sec
		Actions: []toast.Action{{
			Type:      "protocol",
			Label:     "Open settings",
			Arguments: "frontman:settings",
		}}}

	iconPath := getExecutablePath() + "\\" + toastErrorIcon
	if _, err := os.Stat(iconPath); err == nil {
		msg.Icon = iconPath
	}
	return msg.Push()
}

func SendSuccessNotification(title, message string) error {
	msg := toast.Notification{
		AppID:    toastAppID,
		Title:    title,
		Message:  message,
		Duration: toast.Long, // last for 25sec
		Actions:  []toast.Action{},
	}

	iconPath := getExecutablePath() + "\\" + toastSuccessIcon
	if _, err := os.Stat(iconPath); err == nil {
		msg.Icon = iconPath
	}
	return msg.Push()
}


<|start_filename|>log.go<|end_filename|>
package frontman

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"os"
	"path/filepath"
	"time"

	"github.com/cloudradar-monitoring/frontman/pkg/stats"
	"github.com/sirupsen/logrus"
)

type LogLevel string

const (
	LogLevelDebug LogLevel = "debug"
	LogLevelInfo  LogLevel = "info"
	LogLevelError LogLevel = "error"
)

func (lvl LogLevel) LogrusLevel() logrus.Level {
	switch lvl {
	case LogLevelDebug:
		return logrus.DebugLevel
	case LogLevelError:
		return logrus.ErrorLevel
	default:
		return logrus.InfoLevel
	}
}

type logrusFileHook struct {
	file      *os.File
	flag      int
	chmod     os.FileMode
	formatter *logrus.TextFormatter
}

func addLogFileHook(file string, flag int, chmod os.FileMode) error {
	dir := filepath.Dir(file)
	err := os.MkdirAll(dir, 0755)
	if err != nil {
		logrus.WithError(err).Errorf("Failed to create the logs dir: '%s'", dir)
	}

	plainFormatter := &logrus.TextFormatter{FullTimestamp: true, DisableColors: true}
	logFile, err := os.OpenFile(file, flag, chmod)
	if err != nil {
		return fmt.Errorf("unable to write log file: %s", err.Error())
	}

	hook := &logrusFileHook{logFile, flag, chmod, plainFormatter}

	logrus.AddHook(hook)

	return nil
}

func (fm *Frontman) addErrorHook() {
	hook := &LogrusErrorHook{
		fm: fm,
	}

	logrus.AddHook(hook)
}

// Fire event
func (hook *logrusFileHook) Fire(entry *logrus.Entry) error {
	plainformat, err := hook.formatter.Format(entry)
	if err != nil {
		return err
	}
	line := string(plainformat)
	_, err = hook.file.WriteString(line)
	if err != nil {
		fmt.Fprintf(os.Stderr, "unable to write file on filehook(entry.String)%v", err)
		return err
	}
	return nil
}

func (hook *logrusFileHook) Levels() []logrus.Level {
	return []logrus.Level{
		logrus.PanicLevel,
		logrus.FatalLevel,
		logrus.ErrorLevel,
		logrus.WarnLevel,
		logrus.InfoLevel,
		logrus.DebugLevel,
	}
}

// startWritingStats writes fm.Stats every minute to Config.StatsFile
// This method should only be called once
func (fm *Frontman) startWritingStats() {

	// Only start writing out stats if there is a StatsFile configued
	if fm.Config.StatsFile == "" {
		return
	}

	fm.stats.StartedAt = time.Now()
	logrus.Debugf("Start writing stats file: %s", fm.Config.StatsFile)

	var buff bytes.Buffer
	var err error

	// Make the output indented
	encoder := json.NewEncoder(&buff)
	encoder.SetIndent("", "    ")

	for {
		buff.Reset()
		time.Sleep(time.Second * 30)

		stats := fm.statsSnapshot()
		stats.Uptime = uint64(time.Since(stats.StartedAt).Seconds())

		err = encoder.Encode(stats)
		if err != nil {
			logrus.Errorf("Could not encode stats file: %s", err)
			continue
		}

		err = ioutil.WriteFile(fm.Config.StatsFile, buff.Bytes(), 0755)
		if err != nil {
			logrus.Errorf("Could not write stats file: %s", err)
			return
		}
	}
}

// Get snapshot from current stats
func (fm *Frontman) statsSnapshot() stats.FrontmanStats {
	fm.statsLock.Lock()
	defer fm.statsLock.Unlock()
	return *fm.stats
}

// SetLogLevel sets Log level and corresponding logrus level
func (fm *Frontman) SetLogLevel(lvl LogLevel) {
	fm.Config.LogLevel = lvl
	logrus.SetLevel(lvl.LogrusLevel())
}

type LogrusErrorHook struct {
	fm *Frontman
}

func (h *LogrusErrorHook) Fire(entry *logrus.Entry) error {
	h.fm.statsLock.Lock()
	h.fm.stats.InternalErrorsTotal++
	h.fm.stats.InternalLastErrorMessage = entry.Message
	h.fm.stats.InternalLastErrorTimestamp = uint64(time.Now().Unix())
	h.fm.statsLock.Unlock()
	return nil
}

func (h *LogrusErrorHook) Levels() []logrus.Level {
	return []logrus.Level{
		logrus.ErrorLevel,
	}
}

func (fm *Frontman) configureLogger() {
	tfmt := logrus.TextFormatter{FullTimestamp: true, DisableColors: true}
	logrus.SetFormatter(&tfmt)

	fm.SetLogLevel(fm.Config.LogLevel)

	if fm.Config.LogFile != "" {
		err := addLogFileHook(fm.Config.LogFile, os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
		if err != nil {
			logrus.Error("Can't write logs to file: ", err.Error())
		}
	}
	if fm.Config.LogSyslog != "" {
		err := addSyslogHook(fm.Config.LogSyslog)
		if err != nil {
			logrus.Error("Can't set up syslog: ", err.Error())
		}
	}

	if fm.Config.Node.ForwardLog != "" {
		var err error
		fm.forwardLog, err = os.OpenFile(fm.Config.Node.ForwardLog, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666)
		if err != nil {
			logrus.Error("Can't set up forward_log: ", err.Error())
		}
	}

	// Add hook to logrus that updates our LastInternalError statistics
	// whenever an error log is done
	fm.addErrorHook()

	// sets standard logging to /dev/null
	devNull, err := os.OpenFile(os.DevNull, os.O_APPEND|os.O_WRONLY, os.ModeAppend)
	if err != nil {
		logrus.Error("err", err)
	}
	logrus.SetOutput(devNull)
}


<|start_filename|>handler.go<|end_filename|>
package frontman

import (
	"context"
	"crypto/tls"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net"
	"net/http"
	"net/url"
	"os"
	"sync/atomic"
	"time"

	"github.com/cloudradar-monitoring/selfupdate"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"
)

type ErrorHubTooManyRequests struct{}

func (e ErrorHubTooManyRequests) Error() string {
	return "Hub replied with a 429 error code"
}

type ErrorMissingHubOrInput struct{}

func (e ErrorMissingHubOrInput) Error() string {
	return "Missing input file flag (-i) or hub_url param in config"
}

type ErrorHubGeneral struct {
	err  int
	text string
}

type ErrorHubResetContent struct{}

func (e ErrorHubResetContent) Error() string {
	return "Hub replied with 205 RESET CONTENT"
}

func (e ErrorHubGeneral) Error() string {
	return fmt.Sprintf("Hub replied with error %s", e.text)
}

// serviceCheckEmergencyTimeout used to protect from unhandled timeouts
const serviceCheckEmergencyTimeout = time.Second * 30

func InputFromFile(filename string) (*Input, error) {
	b, err := ioutil.ReadFile(filename)
	if err != nil {
		return nil, fmt.Errorf("parseInputFromFile: '%s' failed to read the file: %s", filename, err.Error())
	}

	var i Input
	err = json.Unmarshal(b, &i)
	if err != nil {
		return nil, fmt.Errorf("parseInputFromFile: '%s' JSON unmarshal error: %s", filename, err.Error())
	}
	return &i, nil
}

func (fm *Frontman) initHubClient() {
	transport := &http.Transport{
		ResponseHeaderTimeout: 15 * time.Second,
	}
	if fm.rootCAs != nil {
		transport.TLSClientConfig = &tls.Config{
			RootCAs: fm.rootCAs,
		}
	}
	if fm.Config.HubProxy != "" {
		proxyURL, err := url.Parse(fm.Config.HubProxy)
		if err != nil {
			logrus.WithFields(logrus.Fields{
				"url": fm.Config.HubProxy,
			}).Warningln("failed to parse hub_proxy URL")
		} else {
			if fm.Config.HubProxyUser != "" {
				proxyURL.User = url.UserPassword(fm.Config.HubProxyUser, fm.Config.HubProxyPassword)
			}
			transport.Proxy = func(_ *http.Request) (*url.URL, error) {
				return proxyURL, nil
			}
		}
	}
	fm.hubClient = &http.Client{
		Timeout:   time.Duration(fm.Config.HubRequestTimeout) * time.Second,
		Transport: transport,
	}
}

// CheckHubCredentials performs credentials check for a Hub config, returning errors that reference
// field names as in source config. Since config may be filled from file or UI, the field names can be different.
// Consider also localization of UI, we want to decouple credential checking logic from their actual view in UI.
//
// Examples:
// * for TOML: CheckHubCredentials(ctx, "hub_url", "hub_user", "hub_password")
// * for WinUI: CheckHubCredentials(ctx, "URL", "User", "Password")
func (fm *Frontman) CheckHubCredentials(ctx context.Context, fieldHubURL, fieldHubUser, fieldHubPassword string) error {

	if fm.Config.HubURL == "" {
		return newEmptyFieldError(fieldHubURL)
	} else if u, err := url.Parse(fm.Config.HubURL); err != nil {
		err = errors.WithStack(err)
		return newFieldError(fieldHubURL, err)
	} else if u.Scheme != "http" && u.Scheme != "https" {
		err := errors.Errorf("wrong scheme '%s', URL must start with http:// or https://", u.Scheme)
		return newFieldError(fieldHubURL, err)
	}
	req, _ := http.NewRequest("HEAD", fm.Config.HubURL, nil)
	req.Header.Add("User-Agent", fm.userAgent())
	if fm.Config.HubUser != "" {
		req.SetBasicAuth(fm.Config.HubUser, fm.Config.HubPassword)
	}

	ctx, cancelFn := context.WithTimeout(ctx, time.Minute)
	req = req.WithContext(ctx)
	resp, err := fm.hubClient.Do(req)
	cancelFn()
	if err = fm.checkClientError(resp, err, fieldHubUser, fieldHubPassword); err != nil {
		return errors.WithStack(err)
	}

	return nil
}

func (fm *Frontman) checkClientError(resp *http.Response, err error, fieldHubUser, fieldHubPassword string) error {
	if err != nil {
		if errors.Cause(err) == context.DeadlineExceeded {
			err = errors.New("connection timeout, please check your proxy or firewall settings")
			return err
		}
		return err
	}

	var responseBody string
	responseBodyBytes, readBodyErr := ioutil.ReadAll(resp.Body)
	if readBodyErr == nil {
		responseBody = string(responseBodyBytes)
	}

	_ = resp.Body.Close()
	if resp.StatusCode == http.StatusUnauthorized {
		if fm.Config.HubUser == "" {
			return newEmptyFieldError(fieldHubUser)
		} else if fm.Config.HubPassword == "" {
			return newEmptyFieldError(fieldHubPassword)
		}
		return errors.Errorf("unable to authorize with provided Hub credentials (HTTP %d). %s", resp.StatusCode, responseBody)
	} else if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
		return errors.Errorf("got unexpected response from server (HTTP %d). %s", resp.StatusCode, responseBody)
	}
	return nil
}

func (fm *Frontman) inputFromHub() (*Input, error) {
	if fm.Config.HubURL == "" {
		return nil, newEmptyFieldError("hub_url")
	} else if u, err := url.Parse(fm.Config.HubURL); err != nil {
		err = errors.WithStack(err)
		return nil, newFieldError("hub_url", err)
	} else if u.Scheme != "http" && u.Scheme != "https" {
		err := errors.Errorf("wrong scheme '%s', URL must start with http:// or https://", u.Scheme)
		return nil, newFieldError("hub_url", err)
	}

	i := Input{}
	r, err := http.NewRequest("GET", fm.Config.HubURL, nil)
	if err != nil {
		return nil, err
	}

	r.Header.Add("User-Agent", fm.userAgent())

	if fm.Config.HubUser != "" {
		r.SetBasicAuth(fm.Config.HubUser, fm.Config.HubPassword)
	}

	resp, err := fm.hubClient.Do(r)
	if err != nil {
		if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
			err = fmt.Errorf("hub request timeout of %d seconds exceeded", fm.Config.HubRequestTimeout)
			err = errors.Wrap(err, netErr.Error())
		}

		fm.statsLock.Lock()
		fm.stats.HubLastErrorMessage = err.Error()
		fm.stats.HubLastErrorTimestamp = uint64(time.Now().Unix())
		fm.stats.HubErrorsTotal++
		fm.statsLock.Unlock()
		return nil, err
	}

	defer resp.Body.Close()

	if resp.StatusCode < 200 || resp.StatusCode >= 400 {
		logrus.Debugf("inputFromHub failed: hub replied with error %s", resp.Status)
		if resp.StatusCode == http.StatusTooManyRequests {
			return nil, ErrorHubTooManyRequests{}
		}
		if resp.StatusCode >= 400 {
			return nil, ErrorHubGeneral{resp.StatusCode, resp.Status}
		}
		return nil, errors.New(resp.Status)
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}

	err = json.Unmarshal(body, &i)
	if err != nil {
		return nil, err
	}

	// Update frontman statistics
	fm.statsLock.Lock()
	fm.stats.BytesFetchedFromHubTotal += uint64(len(body))
	fm.stats.ChecksFetchedFromHub += uint64(len(i.ServiceChecks)) + uint64(len(i.WebChecks)) + uint64(len(i.SNMPChecks))
	fm.statsLock.Unlock()

	return &i, nil
}

// Run runs all checks continuously and sends result to hub or file
func (fm *Frontman) Run(inputFilePath string, outputFile *os.File) {

	go fm.startWritingStats()

	if fm.Config.Updates.Enabled {
		fm.selfUpdater = selfupdate.StartChecking()
		defer fm.selfUpdater.Shutdown()
	}

	if !fm.hostInfoSent {
		var err error
		var hostInfo MeasurementsMap

		// Only try to collect HostInfo when defined in config
		if len(fm.Config.HostInfo) > 0 {
			hostInfo, err = fm.HostInfoResults()
			if err != nil {
				logrus.Warnf("Failed to fetch HostInfo: %s", err)
				hostInfo["error"] = err.Error()
			}
		}

		// Send hostInfo as first result
		fm.resultsChan <- Result{
			Measurements: hostInfo,
			CheckType:    "hostInfo",
			Timestamp:    time.Now().Unix(),
		}
		fm.hostInfoSent = true
	}

	if fm.Config.HTTPListener.HTTPListen != "" {
		logrus.Info("Running in node mode, no checks from hub will be processed")
	} else {
		go fm.updateInputChecksContinuous(inputFilePath)
		go fm.processInputContinuous(true)
		go fm.sendResultsChanToHubQueue()
		go fm.pollResultsChan()
	}
	go fm.writeQueueStatsContinuous()

	for {
		select {
		case <-fm.InterruptChan:
			return
		case <-time.After(1000 * time.Millisecond):
			continue
		}
	}
}

// updates list of checks to execute from input file or the hub
func (fm *Frontman) updateInputChecks(inputFilePath string) {
	checks, err := fm.fetchInputChecks(inputFilePath)
	fm.handleHubError(err)

	fm.addUniqueChecks(checks)
}

// RunOnce runs all checks once and send result to hub or file
func (fm *Frontman) RunOnce(inputFilePath string, outputFile *os.File) error {

	fm.updateInputChecks(inputFilePath)

	fm.checksLock.Lock()
	fm.processInput(fm.checks, true)
	fm.checksLock.Unlock()

	logrus.Debugf("RunOnce")

	// close it so we can iterate it in sendResultsChanToFile and sendResultsChanToHub
	close(fm.resultsChan)

	var err error
	if outputFile != nil {
		logrus.Debugf("sendResultsChanToFile")
		err = fm.sendResultsChanToFile(outputFile)
	} else {
		err = fm.sendResultsChanToHub()
	}

	return err
}

func (fm *Frontman) handleHubError(err error) {
	switch err.(type) {
	case ErrorMissingHubOrInput:
		logrus.Warnln(err)
		// This is necessary because MSI does not respect if service quits with status 0 but quickly.
		// In other cases this delay doesn't matter, but also can be useful for polling config changes in a loop.
		time.Sleep(10 * time.Second)
		os.Exit(0)
	case ErrorHubGeneral:
		logrus.Warnln("ErrorHubGeneral", err)
		// sleep until the next data submission is due
		delay := secToDuration(fm.Config.Sleep)
		logrus.Debugf("handleHubError sleeping for %v", delay)
		time.Sleep(delay)
	case ErrorHubTooManyRequests:
		logrus.Warnln(err)
		// for error code 429, wait 10 seconds and try again
		time.Sleep(10 * time.Second)
	case nil:
		return
	default:
		logrus.Error(err)
	}
}

func (input *Input) asChecks() []Check {
	checks := []Check{}
	for _, c := range input.ServiceChecks {
		checks = append(checks, c)
	}
	for _, c := range input.WebChecks {
		checks = append(checks, c)
	}
	for _, c := range input.SNMPChecks {
		checks = append(checks, c)
	}
	return checks
}

// fetchInputChecks reads checks from a json file or the hub
func (fm *Frontman) fetchInputChecks(inputFilePath string) ([]Check, error) {
	var input *Input
	var err error

	if inputFilePath != "" {
		input, err = InputFromFile(inputFilePath)
		if err != nil {
			return nil, fmt.Errorf("InputFromFile(%s) error: %s", inputFilePath, err.Error())
		}
		return input.asChecks(), nil
	}

	if fm.Config.HubURL == "" {
		return nil, ErrorMissingHubOrInput{}
	}

	// in case input file not specified this means we should request HUB instead
	input, err = fm.inputFromHub()
	if err != nil {
		switch err.(type) {
		case ErrorHubGeneral, ErrorHubTooManyRequests:
			return nil, err
		}
		if fm.Config.HubUser != "" {
			// it may be useful to log the Hub User that was used to do a HTTP Basic Auth
			// e.g. in case of '401 Unauthorized' user can see the corresponding user in the logs
			return nil, fmt.Errorf("inputFromHub(%s:***): %s", fm.Config.HubUser, err.Error())
		}
		return nil, fmt.Errorf("inputFromHub: %s", err.Error())
	}

	checks := input.asChecks()
	diag := fmt.Sprintf("fetchInputChecks: read %v checks from hub", len(checks))
	if len(checks) > 0 {
		logrus.Info(diag)
	} else {
		logrus.Debug(diag)
	}

	return checks, nil
}

// appends new checks with unique UUID to input Check queue
func (fm *Frontman) addUniqueChecks(new []Check) {
	fm.checksLock.Lock()
	defer fm.checksLock.Unlock()

	fm.resultsLock.RLock()
	defer fm.resultsLock.RUnlock()

	oldLen := len(fm.checks)
	for _, c := range new {
		uuid := c.uniqueID()
		if fm.hasCheckInQueue(uuid) {
			logrus.Infof("Skipping request for check %v. Check is in queue.", uuid)
			continue
		}
		if fm.hasCheckInResultsQueue(uuid) {
			logrus.Infof("Skipping request for check %v. Check is in results queue.", uuid)
			continue
		}
		if fm.ipc.isInProgress(uuid) {
			logrus.Infof("Skipping request for check %v. Check still in progress.", uuid)
			continue
		}
		fm.checks = append(fm.checks, c)
	}
	logrus.Debugf("addUniqueChecks: queue size %v, new %v, new queue size %v", oldLen, len(new), len(fm.checks))
}

// returns true if uuid is currently in the check queue
func (fm *Frontman) hasCheckInQueue(uuid string) bool {
	for _, v := range fm.checks {
		if v.uniqueID() == uuid {
			return true
		}
	}
	return false
}

// returns true if uuid is currently in the results queue
func (fm *Frontman) hasCheckInResultsQueue(uuid string) bool {
	for _, v := range fm.results {
		if v.CheckUUID == uuid {
			return true
		}
	}
	return false
}

// takes the oldest check from queue that is not in progress
func (fm *Frontman) takeNextCheckNotInProgress() (Check, bool) {
	if idx, ok := fm.getIndexOfFirstCheckNotInProgress(); ok {
		fm.checksLock.Lock()
		currentCheck := fm.checks[idx]
		fm.checks = append(fm.checks[:idx], fm.checks[idx+1:]...)
		fm.checksLock.Unlock()

		return currentCheck, true
	}
	return nil, false
}

func (fm *Frontman) updateInputChecksContinuous(inputFilePath string) {
	sleepTime := secToDuration(fm.Config.Sleep)
	interval := time.Duration(0)

	for {
		select {
		case <-fm.InterruptChan:
			logrus.Infof("updateInputChecksContinuous got interrupt, stopping")
			fm.TerminateQueue.Wait()
			return

		case <-time.After(interval):
			interval = sleepTime
			if err := fm.HealthCheck(); err != nil {
				fm.HealthCheckPassedPreviously = false
				logrus.WithError(err).Errorln("⚠️ Health checks are not passed. Skipping other checks. ⚠️")
				select {
				case <-fm.InterruptChan:
					return
				case <-time.After(secToDuration(fm.Config.Sleep)):
					continue
				}
			} else if !fm.HealthCheckPassedPreviously {
				fm.HealthCheckPassedPreviously = true
				logrus.Infoln("All health checks are positive. Resuming normal operation.")
			}

			logrus.Infof("updateInputChecksContinuous running updateInputChecks")
			fm.updateInputChecks(inputFilePath)
		}
	}
}

// local is false if check originated from a remote node
func (fm *Frontman) processInputContinuous(local bool) {

	sleepDurationAfterEachCheck := secToDuration(fm.Config.SleepDurationAfterCheck)
	sleepDurationForEmptyQueue := secToDuration(fm.Config.SleepDurationEmptyQueue)
	sleepDuration := sleepDurationAfterEachCheck

	for {
		if currentCheck, ok := fm.takeNextCheckNotInProgress(); ok {
			sleepDuration = sleepDurationAfterEachCheck
			fm.ipc.add(currentCheck.uniqueID())

			fm.TerminateQueue.Add(1)
			go func(check Check) {
				defer fm.TerminateQueue.Done()

				res, _ := fm.runCheck(check, local)
				fm.resultsChan <- *res

				fm.ipc.remove(check.uniqueID())

				fm.statsLock.Lock()
				fm.stats.ChecksPerformedTotal++
				fm.statsLock.Unlock()

			}(currentCheck)
		} else {
			// queue is empty
			sleepDuration = sleepDurationForEmptyQueue
		}

		select {
		case <-fm.InterruptChan:
			logrus.Infof("processInputContinuous got interrupt, stopping")
			return
		case <-time.After(sleepDuration):
			continue
		}
	}
}

// processInput processes the whole list of checks in input
// local is false if check originated from a remote node
func (fm *Frontman) processInput(checks []Check, local bool) {
	startedAt := time.Now()
	succeed := fm.runChecks(checks, local)
	totChecks := len(checks)

	fm.statsLock.Lock()
	fm.stats.ChecksPerformedTotal += uint64(totChecks)
	fm.statsLock.Unlock()

	if totChecks == 0 {
		logrus.Debugf("%d/%d checks succeed in %.1f sec", succeed, totChecks, time.Since(startedAt).Seconds())
	} else {
		logrus.Infof("%d/%d checks succeed in %.1f sec", succeed, totChecks, time.Since(startedAt).Seconds())
	}
}

// runs all checks in checkList and sends results to resultsChan
func (fm *Frontman) runChecks(checkList []Check, local bool) int {

	succeed := int32(0)
	for _, check := range checkList {
		fm.TerminateQueue.Add(1)
		go func(check Check) {
			defer fm.TerminateQueue.Done()

			res, err := fm.runCheck(check, local)
			if err == nil {
				atomic.AddInt32(&succeed, 1)
			}

			fm.resultsChan <- *res
		}(check)
	}

	fm.TerminateQueue.Wait()

	return int(succeed)
}

func (fm *Frontman) runCheck(check Check, local bool) (*Result, error) {
	res, err := check.run(fm)
	if err == nil {
		return res, nil
	}

	recovered := false
	if fm.Config.FailureConfirmation > 0 {
		logrus.Debugf("runChecks failed, retrying up to %d times: %s: %s", fm.Config.FailureConfirmation, check.uniqueID(), err.Error())

		for i := 1; i <= fm.Config.FailureConfirmation; i++ {
			time.Sleep(time.Duration(fm.Config.FailureConfirmationDelay*1000) * time.Millisecond)
			logrus.Debugf("Retry %d for failed check %s", i, check.uniqueID())
			res, err = check.run(fm)
			if err == nil {
				recovered = true
				break
			}
		}
	}
	if !recovered {
		res.Message = err.Error()
	}
	if !recovered && local {
		fm.askNodes(check, res)
	}

	if !recovered {
		logrus.Debugf("runChecks: %s: %s", check.uniqueID(), err.Error())
	}

	return res, err
}


<|start_filename|>pkg/winui/winui_windows.go<|end_filename|>
// +build windows

package winui

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strings"
	"time"

	"github.com/lxn/walk"
	decl "github.com/lxn/walk/declarative"
	"github.com/lxn/win"
	"github.com/pkg/errors"
	"github.com/shirou/w32"
	log "github.com/sirupsen/logrus"

	"github.com/cloudradar-monitoring/frontman"
)

var (
	screenDPIX int
	screenDPIY int
)

func init() {
	// Retrieve screen DPI
	hDC := win.GetDC(0)
	defer win.ReleaseDC(0, hDC)
	screenDPIX = int(win.GetDeviceCaps(hDC, win.LOGPIXELSX))
	screenDPIY = int(win.GetDeviceCaps(hDC, win.LOGPIXELSY))
}

func pt(points int) int {
	return int(win.MulDiv(int32(points), int32(screenDPIX), 72))
}

type UI struct {
	MainWindow  *MultiPageMainWindow
	DataBinder  *walk.DataBinder
	SuccessIcon *walk.Icon
	ErrorIcon   *walk.Icon
	StatusBar   *walk.StatusBarItem
	SaveButton  *walk.ToolButton

	DefaultFont decl.Font

	frontman         *frontman.Frontman
	installationMode bool
}

type setupErrors struct {
	connectionError error
	configError     error
	serviceError    error
}

func (se *setupErrors) SetConnectionError(err error) {
	se.connectionError = err
}

func (se *setupErrors) SetConfigError(err error) {
	se.configError = err
}

func (se *setupErrors) SetServiceError(err error) {
	se.serviceError = err
}

func (se *setupErrors) Describe() string {
	buf := new(bytes.Buffer)
	if se.connectionError != nil {
		fmt.Fprintf(buf, "Hub connection failed: %v", se.connectionError)
		return buf.String()
	}

	if se.configError != nil {
		fmt.Fprintf(buf, "Failed to save settings: %v", se.configError)
		return buf.String()
	}
	fmt.Fprintln(buf, "Your settings are saved.")

	if se.serviceError != nil {
		fmt.Fprintf(buf, "Failed to start Frontman service: %v", se.serviceError)
		return buf.String()
	}
	fmt.Fprint(buf, "Services restarted and you are all set up!")

	return buf.String()
}

const urlScheme = "frontman"

func HandleFeedback(fm *frontman.Frontman, cfgPath string) {
	// handle URL schema arguments on windows
	if runtime.GOOS != "windows" {
		return
	}

	if len(os.Args) < 2 {
		return
	}

	switch os.Args[1] {
	case urlScheme + ":settings":
		// hide console window
		console := w32.GetConsoleWindow()
		if console != 0 {
			w32.ShowWindow(console, w32.SW_HIDE)
		}
		WindowsShowSettingsUI(fm, false)
	case urlScheme + ":install":
		// hide console window
		console := w32.GetConsoleWindow()
		if console != 0 {
			w32.ShowWindow(console, w32.SW_HIDE)
		}
		WindowsShowSettingsUI(fm, true)
	case urlScheme + ":test":
		fm.HandleFlagTest()
	case urlScheme + ":config":
		openConfigInNotepad(cfgPath)
	}
}

func openConfigInNotepad(cfgPath string) error {
	r := strings.NewReplacer("&", "^&")
	cfgPath = r.Replace(cfgPath)
	defer os.Exit(1)
	return exec.Command("cmd", "/C", "start", "", "notepad", cfgPath).Start()
}

// saveAndReloadProxySettings saves the proxy settings to the config and reloads the service
func (ui *UI) saveAndReloadProxySettings() {
	setupStatus := &setupErrors{}
	err := frontman.SaveConfigFile(&ui.frontman.Config.MinValuableConfig, ui.frontman.ConfigLocation)
	if err != nil {
		setupStatus.SetConfigError(errors.Wrap(err, "Failed to write config file"))
		runDialog(ui.MainWindow, ui.ErrorIcon, "Error", setupStatus.Describe(), nil)
	} else {
		runDialog(ui.MainWindow, ui.SuccessIcon, "Success", "Your Proxy Settings successfully saved. To test connection, go to Hub Settings", nil)
	}
}

// saveAndReloadLogSettings saves the log settings to the config and reloads the service
func (ui *UI) saveAndReloadLogSettings() {
	setupStatus := &setupErrors{}
	err := frontman.SaveConfigFile(&ui.frontman.Config.MinValuableConfig, ui.frontman.ConfigLocation)
	if err != nil {
		setupStatus.SetConfigError(errors.Wrap(err, "Failed to write config file"))
		runDialog(ui.MainWindow, ui.ErrorIcon, "Error", setupStatus.Describe(), nil)
	}
}

// testSaveAndReloadHubSettings trying to test the Hub address and credentials from the config.
// If testOnly is true do not show alert message about the status (used to test the existing config on start).
func (ui *UI) testSaveAndReloadHubSettings(testOnly bool) {
	saveButtonText := ui.SaveButton.Text()
	defer func() {
		ui.SaveButton.SetText(saveButtonText)
		ui.SaveButton.SetEnabled(true)
	}()

	ui.SaveButton.SetEnabled(false)
	ui.SaveButton.SetText("Testing...")

	ctx := context.Background()
	setupStatus := &setupErrors{}
	err := ui.frontman.CheckHubCredentials(ctx, "Hub URL", "Username", "Password")
	if err != nil {
		if testOnly {
			return
		}
		setupStatus.SetConnectionError(err)
		ui.StatusBar.SetText("Status: failed to connect to the Hub")
		ui.StatusBar.SetIcon(ui.ErrorIcon)
		runDialog(ui.MainWindow, ui.ErrorIcon, "Error", setupStatus.Describe(), nil)
		return
	}

	// otherwise - provide a feedback for user and set the status
	ui.StatusBar.SetText("Status: successfully connected to the Hub")
	ui.StatusBar.SetIcon(ui.SuccessIcon)

	if testOnly {
		// in case we running this inside msi installer, just exit
		if ui.installationMode {
			os.Exit(0)
		}
		return
	}

	ui.SaveButton.SetText("Saving...")

	ui.frontman.Config.MinValuableConfig.IOMode = frontman.IOModeHTTP
	err = frontman.SaveConfigFile(&ui.frontman.Config.MinValuableConfig, ui.frontman.ConfigLocation)
	if err != nil {
		setupStatus.SetConfigError(errors.Wrap(err, "Failed to write config file"))
		runDialog(ui.MainWindow, ui.ErrorIcon, "Error", setupStatus.Describe(), nil)
		return
	}

	err = ui.reloadService()
	if err != nil {
		setupStatus.SetServiceError(errors.Wrap(err, err.Error()))
		runDialog(ui.MainWindow, ui.ErrorIcon, "Error", setupStatus.Describe(), nil)
	} else {
		if ui.installationMode {
			runDialog(ui.MainWindow, ui.SuccessIcon, "Success", setupStatus.Describe(), func() {
				os.Exit(0)
			})
		} else {
			runDialog(ui.MainWindow, ui.SuccessIcon, "Success", setupStatus.Describe(), nil)
		}
	}
}

// WindowsShowSettingsUI draws a window and waits until it will be closed.
// When installationMode is true, close the window after successful test&save.
func WindowsShowSettingsUI(frontman *frontman.Frontman, installationMode bool) {
	ui := &UI{
		frontman:         frontman,
		installationMode: installationMode,
	}
	ex, err := os.Executable()
	if err != nil {
		panic(err)
	}
	exPath := filepath.Dir(ex)
	walk.Resources.SetRootDirPath(filepath.Join(exPath, "resources"))

	ui.SuccessIcon, err = walk.Resources.Icon("success.ico")
	if err != nil {
		log.Fatal(err)
	}
	ui.ErrorIcon, err = walk.Resources.Icon("error.ico")
	if err != nil {
		log.Fatal(err)
	}
	ui.DefaultFont = decl.Font{
		Family:    "Segoe UI",
		PointSize: 12,
	}
	wcfg := &MultiPageMainWindowConfig{
		Title: "Frontman Settings",
		MinSize: decl.Size{
			Width:  pt(200),
			Height: pt(200),
		},
		MaxSize: decl.Size{
			Width:  pt(200),
			Height: pt(200),
		},
		PageCfgs: []PageConfig{
			{"Hub Settings", "login.png", ui.newHubLoginPage()},
			{"Proxy Setup", "proxy.png", ui.newProxyPage()},
			{"Log View", "logs.png", ui.newLogsPage()},
		},
		StatusBarItems: []decl.StatusBarItem{{
			AssignTo:  &ui.StatusBar,
			Width:     pt(40),
			OnClicked: func() {},
		}},
	}
	mpmw, err := NewMultiPageMainWindow(wcfg)
	if err != nil {
		panic(err)
	}
	ui.MainWindow = mpmw

	go func() {
		ui.testSaveAndReloadHubSettings(true)
	}()

	// disable window resize
	win.SetWindowLong(ui.MainWindow.Handle(), win.GWL_STYLE, win.WS_CAPTION|win.WS_SYSMENU)
	win.ShowWindow(ui.MainWindow.Handle(), win.SW_SHOW)
	ui.MainWindow.Run()
}

type HubLoginPage struct {
	*walk.Composite
}

func (ui *UI) newHubLoginPage() PageFactoryFunc {
	return func(parent walk.Container) (Page, error) {
		p := new(HubLoginPage)

		if err := (decl.Composite{
			AssignTo: &p.Composite,
			Name:     "Hub Settings",
			DataBinder: decl.DataBinder{
				AssignTo:       &ui.DataBinder,
				Name:           "config",
				DataSource:     ui.frontman.Config,
				ErrorPresenter: decl.ToolTipErrorPresenter{},
			},
			Layout: decl.VBox{},
			Children: []decl.Widget{
				decl.GroupBox{
					Title: "Frontman > Hub Settings > Hub Connection Credentials",
					Layout: decl.Grid{
						Columns: 2,
						Margins: decl.Margins{
							Left:   pt(10),
							Top:    pt(10),
							Right:  pt(10),
							Bottom: pt(10),
						},
					},
					Children: []decl.Widget{
						decl.Label{
							Text: "Hub URL:",
							Font: ui.DefaultFont,
						},
						decl.LineEdit{
							Text: decl.Bind("HubURL"),
							Font: ui.DefaultFont,
						},
						decl.Label{
							Text: "Username:",
							Font: ui.DefaultFont,
						},
						decl.LineEdit{
							Text: decl.Bind("HubUser"),
							Font: ui.DefaultFont,
						},
						decl.Label{
							Text: "Password:",
							Font: ui.DefaultFont,
						},
						decl.LineEdit{
							Text: decl.Bind("HubPassword"),
							Font: ui.DefaultFont,
						},
					},
				},
				decl.Composite{
					Layout: decl.HBox{},
					Children: []decl.Widget{
						decl.ToolButton{
							MinSize: decl.Size{
								Width:  pt(380),
								Height: pt(35),
							},
							AlwaysConsumeSpace: true,
							AssignTo:           &ui.SaveButton,
							Text:               "Test and Save",
							Font:               ui.DefaultFont,
							OnClicked: func() {
								ui.DataBinder.Submit()
								ui.testSaveAndReloadHubSettings(false)
							},
						},
					},
				},
			},
		}).Create(decl.NewBuilder(parent)); err != nil {
			err = errors.Wrap(err, "failed to create composite")
			return nil, err
		}

		if err := walk.InitWrapperWindow(p); err != nil {
			err = errors.Wrap(err, "failed to init wrapper window")
			return nil, err
		}

		return p, nil
	}
}

type ProxyPage struct {
	*walk.Composite
}

func (ui *UI) newProxyPage() PageFactoryFunc {
	return func(parent walk.Container) (Page, error) {
		p := new(ProxyPage)

		if err := (decl.Composite{
			AssignTo: &p.Composite,
			Name:     "Proxy Setup",
			DataBinder: decl.DataBinder{
				AssignTo:       &ui.DataBinder,
				Name:           "config",
				DataSource:     ui.frontman.Config,
				ErrorPresenter: decl.ToolTipErrorPresenter{},
			},
			Layout: decl.VBox{},
			Children: []decl.Widget{
				decl.GroupBox{
					Title: "Frontman > Hub Settings > Hub Proxy Setup",
					Layout: decl.Grid{
						Columns: 2,
						Margins: decl.Margins{
							Left:   pt(10),
							Top:    pt(10),
							Right:  pt(10),
							Bottom: pt(10),
						},
					},
					Children: []decl.Widget{
						decl.Label{
							Text: "Proxy Address:",
							Font: ui.DefaultFont,
						},
						decl.LineEdit{
							Text: decl.Bind("HubProxy"),
							Font: ui.DefaultFont,
						},
						decl.Label{
							Text: "Proxy User:",
							Font: ui.DefaultFont,
						},
						decl.LineEdit{
							Text: decl.Bind("HubProxyUser"),
							Font: ui.DefaultFont,
						},
						decl.Label{
							Text: "Proxy Password:",
							Font: ui.DefaultFont,
						},
						decl.LineEdit{
							Text: decl.Bind("HubProxyPassword"),
							Font: ui.DefaultFont,
						},
					},
				},
				decl.Composite{
					Layout: decl.HBox{},
					Children: []decl.Widget{
						decl.ToolButton{
							MinSize: decl.Size{
								Width:  pt(380),
								Height: pt(35),
							},
							AlwaysConsumeSpace: true,
							AssignTo:           &ui.SaveButton,
							Text:               "Save Settings",
							Font:               ui.DefaultFont,
							OnClicked: func() {
								ui.DataBinder.Submit()
								ui.saveAndReloadProxySettings()
							},
						},
					},
				},
			},
		}).Create(decl.NewBuilder(parent)); err != nil {
			err = errors.Wrap(err, "failed to create composite")
			return nil, err
		}

		if err := walk.InitWrapperWindow(p); err != nil {
			err = errors.Wrap(err, "failed to init wrapper window")
			return nil, err
		}

		return p, nil
	}
}

type LogsPage struct {
	*walk.Composite
}

func (ui *UI) newLogsPage() PageFactoryFunc {
	return func(parent walk.Container) (Page, error) {
		p := new(LogsPage)

		if err := (decl.Composite{
			AssignTo: &p.Composite,
			Name:     "Logs View",
			DataBinder: decl.DataBinder{
				AssignTo:       &ui.DataBinder,
				Name:           "config",
				DataSource:     ui.frontman.Config,
				ErrorPresenter: decl.ToolTipErrorPresenter{},
			},
			Layout: decl.VBox{},
			Children: []decl.Widget{
				decl.RadioButtonGroupBox{
					ColumnSpan: 1,
					Title:      "Frontman > Logs Settings",
					Layout: decl.HBox{
						Margins: decl.Margins{
							Left:   pt(10),
							Top:    pt(10),
							Right:  pt(10),
							Bottom: pt(10),
						},
					},
					DataMember: "LogLevel",
					Buttons: []decl.RadioButton{
						{Text: "Info", Value: frontman.LogLevelInfo},
						{Text: "Debug", Value: frontman.LogLevelDebug},
						{Text: "Errors only", Value: frontman.LogLevelError},
					},
				},
				decl.PushButton{
					MinSize: decl.Size{
						Width:  pt(30),
						Height: pt(15),
					},
					Text: "Save Settings",
					Font: decl.Font{
						Family:    "Segoe UI",
						PointSize: 10,
					},
					OnClicked: func() {
						ui.DataBinder.Submit()
						ui.saveAndReloadLogSettings()
					},
				},
			},
		}).Create(decl.NewBuilder(parent)); err != nil {
			err = errors.Wrap(err, "failed to create composite")
			return nil, err
		}

		if err := walk.InitWrapperWindow(p); err != nil {
			err = errors.Wrap(err, "failed to init wrapper window")
			return nil, err
		}

		lv, err := NewLogView(p)
		if err != nil {
			err = errors.Wrap(err, "failed to init new log vieww")
			return nil, err
		}
		lv.SetMinMaxSize(walk.Size{
			Width:  pt(500),
			Height: pt(250),
		}, walk.Size{
			Width:  pt(1600),
			Height: pt(800),
		})
		logFile, err := os.Open(ui.frontman.Config.LogFile)
		if err != nil {
			lv.PostAppendText("failed to open log file: " + ui.frontman.Config.LogFile)
		} else {
			go func() {
				defer logFile.Close()
				buf := make([]byte, 65536)
				for {
					if _, err := io.CopyBuffer(lv, logFile, buf); err != nil {
						lv.PostAppendText("log file read error: " + err.Error())
						return
					}
					time.Sleep(500 * time.Millisecond)
				}
			}()
		}

		return p, nil
	}
}

func runDialog(owner walk.Form, icon *walk.Icon, title, text string, callback func()) (int, error) {
	var dlg *walk.Dialog
	var acceptPB *walk.PushButton
	font := decl.Font{PointSize: 12, Family: "Segoe UI"}

	return decl.Dialog{
		FixedSize:     true,
		AssignTo:      &dlg,
		Title:         title,
		DefaultButton: &acceptPB,
		MaxSize: decl.Size{
			Width:  pt(320),
			Height: pt(180),
		},
		Font:   font,
		Layout: decl.VBox{},
		Children: []decl.Widget{
			decl.Composite{
				Layout: decl.HBox{},
				Children: []decl.Widget{
					decl.ImageView{
						Image: icon,
					},
					decl.VSpacer{},
					decl.TextLabel{
						MaxSize: decl.Size{
							Width:  pt(320),
							Height: pt(180),
						},
						Text: text,
						Font: font,
					},
				},
			},
			decl.HSpacer{},
			decl.Composite{
				Layout: decl.VBox{},
				Children: []decl.Widget{
					decl.PushButton{
						Font:     font,
						AssignTo: &acceptPB,
						Text:     "OK",
						OnClicked: func() {
							dlg.Accept()
							if callback != nil {
								callback()
							}
						},
					},
				},
			},
		},
	}.Run(owner)
}


<|start_filename|>cmd/mockhub/main.go<|end_filename|>
package main

import (
	"flag"
	"math/rand"
	"time"

	"github.com/cloudradar-monitoring/frontman"
)

func main() {

	rand.Seed(time.Now().UnixNano())

	responseCode := flag.Int("code", 0, "response code")
	flag.Parse()

	hub := frontman.NewMockHub("0.0.0.0:9100")
	if *responseCode != 0 {
		hub.ResponseStatusCode = *responseCode
	}
	hub.Serve()
}


<|start_filename|>frontman.go<|end_filename|>
package frontman

import (
	"crypto/x509"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"runtime"
	"strings"
	"sync"
	"time"

	"github.com/cloudradar-monitoring/selfupdate"
	"github.com/kardianos/service"
	"github.com/pkg/errors"
	"github.com/sirupsen/logrus"

	"github.com/cloudradar-monitoring/frontman/pkg/stats"
)

// variables set on build. Example:
// go build -o frontman -ldflags="-X main.Version=$(git --git-dir=src/github.com/cloudradar-monitoring/frontman/.git describe --always --long --dirty --tag)" github.com/cloudradar-monitoring/frontman/cmd/frontman
var (
	Version            string
	SelfUpdatesFeedURL = "https://repo.cloudradar.io/windows/frontman/feed/rolling"
)

type Frontman struct {
	Config         *Config
	ConfigLocation string

	stats     *stats.FrontmanStats
	statsLock sync.Mutex

	HealthCheckPassedPreviously bool

	selfUpdater *selfupdate.Updater

	hubClient    *http.Client
	hostInfoSent bool

	// local cached results in case the hub is temporarily offline
	offlineResultsBuffer []Result
	offlineResultsLock   sync.Mutex

	rootCAs *x509.CertPool
	version string

	failedNodeLock  sync.Mutex
	failedNodes     map[string]time.Time
	failedNodeCache map[string][]byte

	forwardLog *os.File

	serviceConfig service.Config

	// current checks queue
	checks     []Check
	checksLock sync.RWMutex

	// in-progress checks
	ipc inProgressChecks

	// completed check results to be sent to hub
	results []Result

	// in case HUB server will hang on response we will need a buffer to continue perform checks
	resultsChan chan Result

	resultsLock sync.RWMutex

	previousSNMPBandwidthMeasure  []snmpBandwidthMeasure
	previousSNMPOidDeltaMeasure   []snmpOidDeltaMeasure
	previousSNMPPorterrorsMeasure []snmpPorterrorsMeasure

	TerminateQueue sync.WaitGroup

	// current number of send result threads
	senderThreads int64

	// interrupt handler, close it to shut down frontman
	InterruptChan chan struct{}

	// send true to signal completion
	DoneChan chan bool
}

func New(cfg *Config, cfgPath, version string) (*Frontman, error) {
	if version == "" {
		version = "{undefined}"
	}
	fm := &Frontman{
		Config:                      cfg,
		ConfigLocation:              cfgPath,
		stats:                       &stats.FrontmanStats{},
		HealthCheckPassedPreviously: true,
		hostInfoSent:                false,
		version:                     version,
		failedNodes:                 make(map[string]time.Time),
		failedNodeCache:             make(map[string][]byte),
		TerminateQueue:              sync.WaitGroup{},
		ipc:                         newIPC(),
		resultsChan:                 make(chan Result, 100),
		InterruptChan:               make(chan struct{}),
		DoneChan:                    make(chan bool),
	}

	if rootCertsPath != "" {
		if _, err := os.Stat(rootCertsPath); err == nil {
			certPool := x509.NewCertPool()

			b, err := ioutil.ReadFile(rootCertsPath)
			if err != nil {
				logrus.Error("Failed to read cacert.pem: ", err.Error())
			} else {
				ok := certPool.AppendCertsFromPEM(b)
				if ok {
					fm.rootCAs = certPool
				}
			}
		}
	}

	if err := fm.Config.sanitize(); err != nil {
		logrus.Error(err)
	}

	fm.configureLogger()

	fm.initHubClient()

	err := fm.configureAutomaticSelfUpdates()
	if err != nil {
		logrus.Error(err.Error())
		return nil, err
	}

	return fm, nil
}

func (fm *Frontman) configureAutomaticSelfUpdates() error {
	if !fm.Config.Updates.Enabled {
		return nil
	}
	if Version == "" {
		logrus.Warn("skipping configureAutomaticSelfUpdates, Version is not set")
		return nil
	}

	updatesConfig := selfupdate.DefaultConfig()
	updatesConfig.AppName = "frontman"
	updatesConfig.SigningCertificatedName = "cloudradar GmbH"
	updatesConfig.CurrentVersion = Version
	updatesConfig.CheckInterval = fm.Config.Updates.GetCheckInterval()
	updatesConfig.UpdatesFeedURL = fm.Config.Updates.URL
	logrus.Debugf("using %s as self-updates feed URL", updatesConfig.UpdatesFeedURL)

	err := selfupdate.Configure(updatesConfig)
	if err != nil {
		return errors.Wrapf(err, "invalid configuration for self-update")
	}

	selfupdate.SetLogger(logrus.StandardLogger())

	return nil
}

// returns an user agent string
func (fm *Frontman) userAgent() string {
	parts := strings.Split(fm.version, "-")
	return fmt.Sprintf("Frontman v%s %s %s", parts[0], runtime.GOOS, runtime.GOARCH)
}


<|start_filename|>pkg/winui/winui_fallback.go<|end_filename|>
// +build !windows

package winui

import (
	"github.com/cloudradar-monitoring/frontman"
)

// WindowsShowSettingsUI stub exists only for cross-platform compiling on platforms that don't implement Windows UI.
func WindowsShowSettingsUI(_ *frontman.Frontman, _ bool) {

}

func HandleFeedback(fm *frontman.Frontman, cfgPath string) {
}


<|start_filename|>config_test.go<|end_filename|>
package frontman

import (
	"io/ioutil"
	"os"
	"testing"
	"time"

	"github.com/cloudradar-monitoring/toml"
	"github.com/stretchr/testify/assert"
)

func TestNewMinimumConfig(t *testing.T) {
	envURL := "http://foo.bar"
	envUser := "foo"
	envPass := "<PASSWORD>"

	// TODO: Not sure if this is really a good idea... could mess with other things
	os.Setenv("FRONTMAN_HUB_URL", envURL)
	os.Setenv("FRONTMAN_HUB_USER", envUser)
	os.Setenv("FRONTMAN_HUB_PASSWORD", envPass)

	mvc := NewMinimumConfig()

	assert.Equal(t, envURL, mvc.HubURL, "HubURL should be set from env")
	assert.Equal(t, envUser, mvc.HubUser, "HubUser should be set from env")
	assert.Equal(t, envPass, mvc.HubPassword, "HubPassword should be set from env")

	// Unset in the end for cleanup
	os.Unsetenv("FRONTMAN_HUB_URL")
	os.Unsetenv("FRONTMAN_HUB_USER")
	os.Unsetenv("FRONTMAN_HUB_PASSWORD")
}

func TestTryUpdateConfigFromFile(t *testing.T) {
	cfg := NewConfig()

	const sampleConfig = `
pid = "/pid"
sleep = 1.23
ignore_ssl_errors = true
`

	tmpFile, err := ioutil.TempFile("", "")
	assert.Nil(t, err)
	defer os.Remove(tmpFile.Name())

	err = ioutil.WriteFile(tmpFile.Name(), []byte(sampleConfig), 0600)
	assert.Nil(t, err)

	err = cfg.TryUpdateConfigFromFile(tmpFile.Name())
	assert.Nil(t, err)

	assert.Equal(t, "/pid", cfg.PidFile)
	assert.Equal(t, 1.23, cfg.Sleep)
	assert.Equal(t, true, cfg.IgnoreSSLErrors)

	// make sure default values are propagated
	assert.Equal(t, []string{"uname", "os_kernel", "os_family", "os_arch", "cpu_model", "fqdn", "memory_total_B"}, cfg.HostInfo)
}

func TestHandleAllConfigSetup(t *testing.T) {
	t.Run("config-file-does-exist", func(t *testing.T) {
		const sampleConfig = `
pid = "/pid"
sleep = 1.0
ignore_ssl_errors = true
`

		tmpFile, err := ioutil.TempFile("", "")
		assert.Nil(t, err)
		defer os.Remove(tmpFile.Name())

		err = ioutil.WriteFile(tmpFile.Name(), []byte(sampleConfig), 0755)
		assert.Nil(t, err)

		config, err := HandleAllConfigSetup(tmpFile.Name())
		assert.Nil(t, err)

		assert.Equal(t, "/pid", config.PidFile)
		assert.Equal(t, 1.0, config.Sleep)
		assert.Equal(t, true, config.IgnoreSSLErrors)
	})

	t.Run("config-file-does-not-exist", func(t *testing.T) {
		// Create a temp file to get a file path we can use for temp
		// config generation. But delete it so we can actually write our
		// config file under the path.
		tmpFile, err := ioutil.TempFile("", "")
		assert.Nil(t, err)
		configFilePath := tmpFile.Name()
		err = tmpFile.Close()
		assert.Nil(t, err)

		err = os.Remove(tmpFile.Name())
		assert.Nil(t, err)

		_, err = HandleAllConfigSetup(configFilePath)
		assert.Nil(t, err)

		_, err = os.Stat(configFilePath)
		assert.Nil(t, err)

		mvc := NewMinimumConfig()
		loadedMVC := &MinValuableConfig{}
		_, err = toml.DecodeFile(configFilePath, loadedMVC)
		assert.Nil(t, err)

		if !assert.ObjectsAreEqual(*mvc, *loadedMVC) {
			t.Errorf("expected %+v, got %+v", *mvc, *loadedMVC)
		}
	})
}

func TestSecToDuration(t *testing.T) {
	assert.Equal(t, 150*time.Millisecond, secToDuration(0.15))
}


<|start_filename|>pkg/winui/winui_multipage.go<|end_filename|>
// Copyright 2017 The Walk Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// +build windows

package winui

import (
	"github.com/lxn/walk"
	decl "github.com/lxn/walk/declarative"
)

type MultiPageMainWindowConfig struct {
	Name                 string
	Enabled              decl.Property
	Visible              decl.Property
	Font                 decl.Font
	MinSize              decl.Size
	MaxSize              decl.Size
	ContextMenuItems     []decl.MenuItem
	OnKeyDown            walk.KeyEventHandler
	OnKeyPress           walk.KeyEventHandler
	OnKeyUp              walk.KeyEventHandler
	OnMouseDown          walk.MouseEventHandler
	OnMouseMove          walk.MouseEventHandler
	OnMouseUp            walk.MouseEventHandler
	OnSizeChanged        walk.EventHandler
	OnCurrentPageChanged walk.EventHandler
	Title                string
	Size                 decl.Size
	MenuItems            []decl.MenuItem
	ToolBar              decl.ToolBar
	PageCfgs             []PageConfig
	StatusBarItems       []decl.StatusBarItem
}

type PageConfig struct {
	Title   string
	Image   string
	NewPage PageFactoryFunc
}

type PageFactoryFunc func(parent walk.Container) (Page, error)

type Page interface {
	// Provided by Walk
	walk.Container
	Parent() walk.Container
	SetParent(parent walk.Container) error
}

type MultiPageMainWindow struct {
	*walk.MainWindow
	navTB                       *walk.ToolBar
	pageCom                     *walk.Composite
	action2NewPage              map[*walk.Action]PageFactoryFunc
	pageActions                 []*walk.Action
	currentAction               *walk.Action
	currentPage                 Page
	currentPageChangedPublisher walk.EventPublisher
}

func NewMultiPageMainWindow(cfg *MultiPageMainWindowConfig) (*MultiPageMainWindow, error) {
	mpmw := &MultiPageMainWindow{
		action2NewPage: make(map[*walk.Action]PageFactoryFunc),
	}

	if err := (decl.MainWindow{
		AssignTo:         &mpmw.MainWindow,
		Name:             cfg.Name,
		Title:            cfg.Title,
		Enabled:          cfg.Enabled,
		Visible:          cfg.Visible,
		Font:             cfg.Font,
		MinSize:          cfg.MinSize,
		MaxSize:          cfg.MaxSize,
		MenuItems:        cfg.MenuItems,
		ToolBar:          cfg.ToolBar,
		ContextMenuItems: cfg.ContextMenuItems,
		OnKeyDown:        cfg.OnKeyDown,
		OnKeyPress:       cfg.OnKeyPress,
		OnKeyUp:          cfg.OnKeyUp,
		OnMouseDown:      cfg.OnMouseDown,
		OnMouseMove:      cfg.OnMouseMove,
		OnMouseUp:        cfg.OnMouseUp,
		OnSizeChanged:    cfg.OnSizeChanged,
		Layout:           decl.HBox{MarginsZero: true, SpacingZero: true},
		Children: []decl.Widget{
			decl.ScrollView{
				HorizontalFixed: true,
				Layout:          decl.VBox{MarginsZero: true},
				Children: []decl.Widget{
					decl.Composite{
						Layout: decl.VBox{MarginsZero: true},
						Children: []decl.Widget{
							PagesToolBar{
								ToolBar: decl.ToolBar{
									AssignTo:    &mpmw.navTB,
									Orientation: decl.Vertical,
									ButtonStyle: decl.ToolBarButtonImageAboveText,
									MaxTextRows: 2,
								},
								IconSize: walk.Size{Width: 40, Height: 40},
							},
						},
					},
				},
			},
			decl.Composite{
				AssignTo: &mpmw.pageCom,
				Name:     "pageCom",
				Layout:   decl.HBox{MarginsZero: true, SpacingZero: true},
			},
		},
		StatusBarItems: cfg.StatusBarItems,
	}).Create(); err != nil {
		return nil, err
	}

	succeeded := false
	defer func() {
		if !succeeded {
			mpmw.Dispose()
		}
	}()

	for _, pc := range cfg.PageCfgs {
		action, err := mpmw.newPageAction(pc.Title, pc.Image, pc.NewPage)
		if err != nil {
			return nil, err
		}

		mpmw.pageActions = append(mpmw.pageActions, action)
	}

	if err := mpmw.updateNavigationToolBar(); err != nil {
		return nil, err
	}

	if len(mpmw.pageActions) > 0 {
		if err := mpmw.setCurrentAction(mpmw.pageActions[0]); err != nil {
			return nil, err
		}
	}

	if cfg.OnCurrentPageChanged != nil {
		mpmw.CurrentPageChanged().Attach(cfg.OnCurrentPageChanged)
	}

	succeeded = true

	return mpmw, nil
}

func (mpmw *MultiPageMainWindow) CurrentPage() Page {
	return mpmw.currentPage
}

func (mpmw *MultiPageMainWindow) CurrentPageTitle() string {
	if mpmw.currentAction == nil {
		return ""
	}

	return mpmw.currentAction.Text()
}

func (mpmw *MultiPageMainWindow) CurrentPageChanged() *walk.Event {
	return mpmw.currentPageChangedPublisher.Event()
}

func (mpmw *MultiPageMainWindow) newPageAction(title, image string, newPage PageFactoryFunc) (*walk.Action, error) {
	img, err := walk.Resources.Bitmap(image)
	if err != nil {
		return nil, err
	}

	action := walk.NewAction()
	action.SetCheckable(true)
	action.SetExclusive(true)
	action.SetImage(img)
	action.SetText(title)

	mpmw.action2NewPage[action] = newPage

	action.Triggered().Attach(func() {
		mpmw.setCurrentAction(action)
	})

	return action, nil
}

func (mpmw *MultiPageMainWindow) setCurrentAction(action *walk.Action) error {
	defer func() {
		if !mpmw.pageCom.IsDisposed() {
			mpmw.pageCom.RestoreState()
			mpmw.pageCom.Layout().Update(false)
		}
	}()

	mpmw.SetFocus()

	if prevPage := mpmw.currentPage; prevPage != nil {
		mpmw.pageCom.SaveState()
		prevPage.SetVisible(false)
		prevPage.(walk.Widget).SetParent(nil)
		prevPage.Dispose()
	}

	newPage := mpmw.action2NewPage[action]

	page, err := newPage(mpmw.pageCom)
	if err != nil {
		return err
	}

	action.SetChecked(true)

	mpmw.currentPage = page
	mpmw.currentAction = action

	mpmw.currentPageChangedPublisher.Publish()

	return nil
}

func (mpmw *MultiPageMainWindow) updateNavigationToolBar() error {
	mpmw.navTB.SetSuspended(true)
	defer mpmw.navTB.SetSuspended(false)

	actions := mpmw.navTB.Actions()

	if err := actions.Clear(); err != nil {
		return err
	}

	for _, action := range mpmw.pageActions {
		if err := actions.Add(action); err != nil {
			return err
		}
	}

	if mpmw.currentAction != nil {
		if !actions.Contains(mpmw.currentAction) {
			for _, action := range mpmw.pageActions {
				if action != mpmw.currentAction {
					if err := mpmw.setCurrentAction(action); err != nil {
						return err
					}

					break
				}
			}
		}
	}

	return nil
}

type PagesToolBar struct {
	decl.ToolBar
	IconSize walk.Size
}

func (tb PagesToolBar) Create(builder *decl.Builder) error {
	w, err := walk.NewToolBarWithOrientationAndButtonStyle(
		builder.Parent(),
		walk.Orientation(tb.Orientation),
		walk.ToolBarButtonStyle(tb.ButtonStyle),
	)
	if err != nil {
		return err
	}

	if tb.ToolBar.AssignTo != nil {
		*tb.ToolBar.AssignTo = w
	}
	if tb.IconSize.Height == 0 {
		tb.IconSize.Height = 16
	}
	if tb.IconSize.Width == 0 {
		tb.IconSize.Width = 16
	}

	return builder.InitWidget(tb, w, func() error {
		imageList, err := walk.NewImageList(tb.IconSize, 0)
		if err != nil {
			return err
		}
		w.SetImageList(imageList)

		mtr := tb.MaxTextRows
		if mtr < 1 {
			mtr = 1
		}
		if err := w.SetMaxTextRows(mtr); err != nil {
			return err
		}
		if err := addToActionList(w.Actions(), tb.Actions); err != nil {
			return err
		}

		return nil
	})
}

func addToActionList(list *walk.ActionList, actions []*walk.Action) error {
	for _, a := range actions {
		if err := list.Add(a); err != nil {
			return err
		}
	}

	return nil
}
