diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a38df26 --- /dev/null +++ b/.gitignore @@ -0,0 +1,131 @@ +## Core latex/pdflatex auxiliary files: +*.aux +*.lof +*.log +*.lot +*.fls +*.out +*.toc + +## Intermediate documents: +*.dvi +*-converted-to.* +# these rules might exclude image files for figures etc. +*.ps +*.eps +*.pdf + +## Bibliography auxiliary files (bibtex/biblatex/biber): +*.bbl +*.bcf +*.blg +*-blx.aux +*-blx.bib +*.brf +*.run.xml + +## Build tool auxiliary files: +*.fdb_latexmk +*.synctex +*.synctex.gz +*.synctex.gz(busy) +*.pdfsync + +## Auxiliary and intermediate files from other packages: + +# algorithms +*.alg +*.loa + +# achemso +acs-*.bib + +# amsthm +*.thm + +# beamer +*.nav +*.snm +*.vrb + +#(e)ledmac/(e)ledpar +*.end +*.[1-9] +*.[1-9][0-9] +*.[1-9][0-9][0-9] +*.[1-9]R +*.[1-9][0-9]R +*.[1-9][0-9][0-9]R +*.eledsec[1-9] +*.eledsec[1-9]R +*.eledsec[1-9][0-9] +*.eledsec[1-9][0-9]R +*.eledsec[1-9][0-9][0-9] +*.eledsec[1-9][0-9][0-9]R + +# glossaries +*.acn +*.acr +*.glg +*.glo +*.gls + +# hyperref +*.brf + +# knitr +*-concordance.tex +*.tikz +*-tikzDictionary + +# listings +*.lol + +# makeidx +*.idx +*.ilg +*.ind +*.ist + +# minitoc +*.maf +*.mtc +*.mtc0 + +# minted +_minted* +*.pyg + +# morewrites +*.mw + +# nomencl +*.nlo + +# sagetex +*.sagetex.sage +*.sagetex.py +*.sagetex.scmd + +# sympy +*.sout +*.sympy +sympy-plots-for-*.tex/ + +# todonotes +*.tdo + +# xindy +*.xdy + + +__pycache__ + +# IDE's go +.idea/ + + +# go project +profile.cov +config.conf +cmd/stock/config.conf diff --git a/.test-coverage b/.test-coverage new file mode 100755 index 0000000..045ec05 --- /dev/null +++ b/.test-coverage @@ -0,0 +1,26 @@ +#!/bin/bash +# Issue: https://github.com/mattn/goveralls/issues/20 +# Source: https://github.com/uber/go-torch/blob/63da5d33a225c195fea84610e2456d5f722f3963/.test-cover.sh + +echo "mode: count" > profile.cov +FAIL=0 + +# Standard go tooling behavior is to ignore dirs with leading underscors +for dir in $(find . -maxdepth 10 -not -path './.git*' -not -path '*/_*' -type d); +do + if ls $dir/*.go &> /dev/null; then + go test -p 1 -v -covermode=count -coverprofile=profile.tmp $dir || FAIL=$? + if [ -f profile.tmp ] + then + tail -n +2 < profile.tmp >> profile.cov + rm profile.tmp + fi + fi +done + +# Failures have incomplete results, so don't send +if [ "$FAIL" -eq 0 ]; then + goveralls -service=travis-ci -v -coverprofile=profile.cov +fi + +exit $FAIL diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1559e8c --- /dev/null +++ b/.travis.yml @@ -0,0 +1,10 @@ +language: go +go: + - tip +install: + - go get -t github.com/FreifunkBremen/freifunkmanager/... + - go get github.com/mattn/goveralls + - go get "golang.org/x/tools/cmd/cover" +script: + - ./.test-coverage + - go install github.com/FreifunkBremen/freifunkmanager/cmd/freifunkmanager diff --git a/cmd/freifunkmanager/main.go b/cmd/freifunkmanager/main.go new file mode 100644 index 0000000..d1d64f5 --- /dev/null +++ b/cmd/freifunkmanager/main.go @@ -0,0 +1,61 @@ +package main + +import ( + "flag" + "net/http" + "os" + "os/signal" + "syscall" + + "github.com/NYTimes/gziphandler" + goji "goji.io" + "goji.io/pat" + + configPackage "github.com/FreifunkBremen/freifunkmanager/config" + "github.com/FreifunkBremen/freifunkmanager/lib/log" + "github.com/FreifunkBremen/freifunkmanager/ssh" +) + +var ( + configFile string + config *configPackage.Config +) + +func main() { + flag.StringVar(&configFile, "config", "config.conf", "path of configuration file (default:config.conf)") + flag.Parse() + + config = configPackage.ReadConfigFile(configFile) + + log.Log.Info("starting...") + + sshmanager := ssh.NewManager(config.SSHPrivateKey) + + // Startwebserver + router := goji.NewMux() + + router.Handle(pat.New("/*"), gziphandler.GzipHandler(http.FileServer(http.Dir(config.Webroot)))) + + srv := &http.Server{ + Addr: config.WebserverBind, + Handler: router, + } + go func() { + if err := srv.ListenAndServe(); err != nil { + panic(err) + } + }() + + log.Log.Info("started") + + // Wait for system signal + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + sig := <-sigs + + // Stop services + srv.Close() + sshmanager.Close() + + log.Log.Info("stop recieve:", sig) +} diff --git a/config/main.go b/config/main.go new file mode 100644 index 0000000..8af9fb4 --- /dev/null +++ b/config/main.go @@ -0,0 +1,38 @@ +package config + +import ( + "io/ioutil" + + "github.com/BurntSushi/toml" + + "github.com/FreifunkBremen/freifunkmanager/lib/log" +) + +//config file of this daemon (for more the config_example.conf in git repository) +type Config struct { + // address on which the api and static content webserver runs + WebserverBind string `toml:"webserver_bind"` + + // path to deliver static content + Webroot string `toml:"webroot"` + // yanic socket + YanicSocket string `toml:"yanic_socket"` + + // SSH private key + SSHPrivateKey string `toml:"ssh_key"` +} + +//reads a config model from path of a yml file +func ReadConfigFile(path string) *Config { + config := &Config{} + file, err := ioutil.ReadFile(path) + if err != nil { + log.Log.Panic(err) + } + + if err := toml.Unmarshal(file, config); err != nil { + log.Log.Panic(err) + } + + return config +} diff --git a/config/main_test.go b/config/main_test.go new file mode 100644 index 0000000..6712e4d --- /dev/null +++ b/config/main_test.go @@ -0,0 +1,24 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReadConfig(t *testing.T) { + assert := assert.New(t) + + config := ReadConfigFile("../config_example.conf") + assert.NotNil(config) + + assert.Equal(":8080", config.WebserverBind) + + assert.Panics(func() { + ReadConfigFile("../config_example.co") + }, "wrong file") + + assert.Panics(func() { + ReadConfigFile("testdata/config_panic.conf") + }, "wrong toml") +} diff --git a/config/testdata/config_panic.conf b/config/testdata/config_panic.conf new file mode 100644 index 0000000..3d07101 --- /dev/null +++ b/config/testdata/config_panic.conf @@ -0,0 +1 @@ +not unmarshalable diff --git a/config_example.conf b/config_example.conf new file mode 100644 index 0000000..bcedecb --- /dev/null +++ b/config_example.conf @@ -0,0 +1,4 @@ +webserver_bind = ":8080" +webroot = "webroot" +yanic_socket = "" +ssh_key = "/etc/a" diff --git a/lib/log/log.go b/lib/log/log.go new file mode 100644 index 0000000..0d73258 --- /dev/null +++ b/lib/log/log.go @@ -0,0 +1,33 @@ +// Package log provides the +// functionality to start und initialize to logger +package log + +import ( + "log" + "net/http" + + logger "github.com/Sirupsen/logrus" +) + +// current logger with configuration +var Log *logger.Logger + +// Function to initiate a new logger +func init() { + Log = logger.New() + log.SetOutput(Log.Writer()) // Enable fallback if core logger +} + +// Function to add the information of a http request to the log +// Input: pointer to the http request r +func HTTP(r *http.Request) *logger.Entry { + ip := r.Header.Get("X-Forwarded-For") + if len(ip) <= 1 { + ip = r.RemoteAddr + } + return Log.WithFields(logger.Fields{ + "remote": ip, + "method": r.Method, + "url": r.URL.RequestURI(), + }) +} diff --git a/lib/log/log_test.go b/lib/log/log_test.go new file mode 100644 index 0000000..fc7cdc9 --- /dev/null +++ b/lib/log/log_test.go @@ -0,0 +1,24 @@ +// Package log provides the +// functionality to start und initialize to logger +package log + +import ( + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +// Function to test the logging +// Input: pointer to teh testing object +func TestLog(t *testing.T) { + assertion := assert.New(t) + + req, _ := http.NewRequest("GET", "https://google.com/lola/duda?q=wasd", nil) + log := HTTP(req) + _, ok := log.Data["remote"] + + assertion.NotNil(ok, "remote address not set in logger") + assertion.Equal("GET", log.Data["method"], "method not set in logger") + assertion.Equal("/lola/duda?q=wasd", log.Data["url"], "path not set in logger") +} diff --git a/ssh/auth.go b/ssh/auth.go new file mode 100644 index 0000000..6769eb7 --- /dev/null +++ b/ssh/auth.go @@ -0,0 +1,30 @@ +package ssh + +import ( + "io/ioutil" + "net" + "os" + + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" +) + +func SSHAgent() ssh.AuthMethod { + if sshAgent, err := net.Dial("unix", os.Getenv("SSH_AUTH_SOCK")); err == nil { + return ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers) + } + return nil +} + +func PublicKeyFile(file string) ssh.AuthMethod { + buffer, err := ioutil.ReadFile(file) + if err != nil { + return nil + } + + key, err := ssh.ParsePrivateKey(buffer) + if err != nil { + return nil + } + return ssh.PublicKeys(key) +} diff --git a/ssh/execute.go b/ssh/execute.go new file mode 100644 index 0000000..ba6df0d --- /dev/null +++ b/ssh/execute.go @@ -0,0 +1,36 @@ +package ssh + +import ( + "net" + + "golang.org/x/crypto/ssh" + + "github.com/FreifunkBremen/freifunkmanager/lib/log" +) + +func (m *Manager) ExecuteEverywhere(cmd string) { + for host, client := range m.clients { + m.execute(host, client, cmd) + } +} + +func (m *Manager) ExecuteOn(host net.IP, cmd string) { + client := m.ConnectTo(host) + m.execute(host.String(), client, cmd) +} + +func (m *Manager) execute(host string, client *ssh.Client, cmd string) { + session, err := client.NewSession() + defer session.Close() + + if err != nil { + log.Log.Warnf("can not create session on %s: %s", host, err) + delete(m.clients, host) + return + } + err = session.Run(cmd) + if err != nil { + log.Log.Warnf("could not run %s on %s: %s", cmd, host, err) + delete(m.clients, host) + } +} diff --git a/ssh/execute_test.go b/ssh/execute_test.go new file mode 100644 index 0000000..73784c2 --- /dev/null +++ b/ssh/execute_test.go @@ -0,0 +1,23 @@ +package ssh + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExecute(t *testing.T) { + assert := assert.New(t) + + mgmt := NewManager("~/.ssh/id_rsa") + assert.NotNil(mgmt, "no new manager created") + + mgmt.ConnectTo(net.ParseIP("2a06:8782:ffbb:1337::127")) + + mgmt.ExecuteEverywhere("echo $HOSTNAME") + mgmt.ExecuteOn(net.ParseIP("2a06:8782:ffbb:1337::127"), "uptime") + mgmt.ExecuteOn(net.ParseIP("2a06:8782:ffbb:1337::127"), "echo $HOSTNAME") + + mgmt.Close() +} diff --git a/ssh/manager.go b/ssh/manager.go new file mode 100644 index 0000000..de6f158 --- /dev/null +++ b/ssh/manager.go @@ -0,0 +1,66 @@ +package ssh + +import ( + "net" + "sync" + + "golang.org/x/crypto/ssh" + + "github.com/FreifunkBremen/freifunkmanager/lib/log" +) + +// the SSH Connection Manager for multiple connections +type Manager struct { + config *ssh.ClientConfig + clients map[string]*ssh.Client + clientsMUX sync.Mutex +} + +// create a new SSH Connection Manager by ssh file +func NewManager(file string) *Manager { + var auths []ssh.AuthMethod + if auth := SSHAgent(); auth != nil { + auths = append(auths, auth) + } + if auth := PublicKeyFile(file); auth != nil { + auths = append(auths, auth) + } + + sshConfig := &ssh.ClientConfig{ + User: "root", + Auth: auths, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } + return &Manager{ + config: sshConfig, + clients: make(map[string]*ssh.Client), + } +} + +func (m *Manager) ConnectTo(host net.IP) *ssh.Client { + m.clientsMUX.Lock() + defer m.clientsMUX.Unlock() + + if client, ok := m.clients[host.String()]; ok { + return client + } + addr := net.TCPAddr{ + IP: host, + Port: 22, + } + client, err := ssh.Dial("tcp", addr.String(), m.config) + if err != nil { + log.Log.Error(err) + return nil + } + + m.clients[host.String()] = client + return client +} + +func (m *Manager) Close() { + for host, client := range m.clients { + client.Close() + delete(m.clients, host) + } +} diff --git a/ssh/manager_test.go b/ssh/manager_test.go new file mode 100644 index 0000000..38fe7b0 --- /dev/null +++ b/ssh/manager_test.go @@ -0,0 +1,19 @@ +package ssh + +import ( + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestManager(t *testing.T) { + assert := assert.New(t) + + mgmt := NewManager("~/.ssh/id_rsa") + assert.NotNil(mgmt, "no new manager created") + + mgmt.ConnectTo(net.ParseIP("2a06:8782:ffbb:1337::127")) + + mgmt.Close() +} diff --git a/ssh/run.go b/ssh/run.go new file mode 100644 index 0000000..9662f31 --- /dev/null +++ b/ssh/run.go @@ -0,0 +1,59 @@ +package ssh + +import ( + "bytes" + "io" + "net" + + "golang.org/x/crypto/ssh" + + "github.com/FreifunkBremen/freifunkmanager/lib/log" +) + +type SSHRunResultHandler func([]byte, error) + +func (m *Manager) RunEverywhere(cmd string, handler SSHRunResultHandler) { + for host, client := range m.clients { + result, err := m.run(host, client, cmd) + handler(result, err) + } +} + +func (m *Manager) RunOn(host net.IP, cmd string) ([]byte, error) { + client := m.ConnectTo(host) + return m.run(host.String(), client, cmd) +} + +func (m *Manager) run(host string, client *ssh.Client, cmd string) ([]byte, error) { + session, err := client.NewSession() + defer session.Close() + + if err != nil { + log.Log.Warnf("can not create session on %s: %s", host, err) + delete(m.clients, host) + return nil, err + } + stdout, err := session.StdoutPipe() + buffer := &bytes.Buffer{} + go io.Copy(buffer, stdout) + if err != nil { + log.Log.Warnf("can not create pipe for run on %s: %s", host, err) + delete(m.clients, host) + return nil, err + } + err = session.Run(cmd) + if err != nil { + log.Log.Warnf("could not run %s on %s: %s", cmd, host, err) + delete(m.clients, host) + return nil, err + } + var result []byte + for { + b, err := buffer.ReadByte() + if err != nil { + break + } + result = append(result, b) + } + return result, nil +} diff --git a/ssh/run_test.go b/ssh/run_test.go new file mode 100644 index 0000000..4d75c71 --- /dev/null +++ b/ssh/run_test.go @@ -0,0 +1,35 @@ +package ssh + +import ( + "net" + "strconv" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRun(t *testing.T) { + assert := assert.New(t) + + mgmt := NewManager("~/.ssh/id_rsa") + assert.NotNil(mgmt, "no new manager created") + + mgmt.ConnectTo(net.ParseIP("2a06:8782:ffbb:1337::127")) + + mgmt.RunEverywhere("echo 13", func(result []byte, err error) { + assert.NoError(err) + + result = result[:len(result)-1] + + assert.Equal([]byte{'1', '3'}, result) + }) + result, err := mgmt.RunOn(net.ParseIP("2a06:8782:ffbb:1337::127"), "echo 16") + assert.NoError(err) + + result = result[:len(result)-1] + resultInt, _ := strconv.Atoi(string(result)) + + assert.Equal(16, resultInt) + + mgmt.Close() +}