aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSebastien Douheret <sebastien.douheret@iot.bzh>2017-06-26 17:58:36 +0200
committerSebastien Douheret <sebastien.douheret@iot.bzh>2017-06-26 17:58:36 +0200
commit1e628d0aaaa9137efa52d05351083abf05f97106 (patch)
tree5ef5ea99f3686f40129aadfe697f97ec7a093d0d
parent3348095d2b00947f23f20237377a52d4c5949b6b (diff)
Initial commit to add golib
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
-rw-r--r--golib/error.go16
-rw-r--r--golib/execPipeWs.go151
-rw-r--r--golib/filepath.go81
-rw-r--r--golib/httpclient.go257
4 files changed, 505 insertions, 0 deletions
diff --git a/golib/error.go b/golib/error.go
new file mode 100644
index 0000000..6873d82
--- /dev/null
+++ b/golib/error.go
@@ -0,0 +1,16 @@
+package common
+
+import (
+ "fmt"
+
+ "github.com/gin-gonic/gin"
+)
+
+// APIError returns an uniform json formatted error
+func APIError(c *gin.Context, format string, args ...interface{}) {
+ errMsg := fmt.Sprintf(format, args...)
+ c.JSON(500, gin.H{
+ "status": "error",
+ "error": errMsg,
+ })
+}
diff --git a/golib/execPipeWs.go b/golib/execPipeWs.go
new file mode 100644
index 0000000..9bb4517
--- /dev/null
+++ b/golib/execPipeWs.go
@@ -0,0 +1,151 @@
+package common
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "strings"
+ "time"
+
+ "syscall"
+
+ "github.com/Sirupsen/logrus"
+ "github.com/googollee/go-socket.io"
+)
+
+// EmitOutputCB is the function callback used to emit data
+type EmitOutputCB func(sid string, cmdID int, stdout, stderr string, data *map[string]interface{})
+
+// EmitExitCB is the function callback used to emit exit proc code
+type EmitExitCB func(sid string, cmdID int, code int, err error, data *map[string]interface{})
+
+// Inspired by :
+// https://github.com/gorilla/websocket/blob/master/examples/command/main.go
+
+// ExecPipeWs executes a command and redirect stdout/stderr into a WebSocket
+func ExecPipeWs(cmd []string, env []string, so *socketio.Socket, sid string, cmdID int,
+ cmdExecTimeout int, log *logrus.Logger, eoCB EmitOutputCB, eeCB EmitExitCB, data *map[string]interface{}) error {
+
+ outr, outw, err := os.Pipe()
+ if err != nil {
+ return fmt.Errorf("Pipe stdout error: " + err.Error())
+ }
+
+ // XXX - do we need to pipe stdin one day ?
+ inr, inw, err := os.Pipe()
+ if err != nil {
+ outr.Close()
+ outw.Close()
+ return fmt.Errorf("Pipe stdin error: " + err.Error())
+ }
+
+ bashArgs := []string{"/bin/bash", "-c", strings.Join(cmd, " ")}
+ proc, err := os.StartProcess("/bin/bash", bashArgs, &os.ProcAttr{
+ Files: []*os.File{inr, outw, outw},
+ Env: append(os.Environ(), env...),
+ })
+ if err != nil {
+ outr.Close()
+ outw.Close()
+ inr.Close()
+ inw.Close()
+ return fmt.Errorf("Process start error: " + err.Error())
+ }
+
+ go func() {
+ defer outr.Close()
+ defer outw.Close()
+ defer inr.Close()
+ defer inw.Close()
+
+ stdoutDone := make(chan struct{})
+ go cmdPumpStdout(so, outr, stdoutDone, sid, cmdID, log, eoCB, data)
+
+ // Blocking function that poll input or wait for end of process
+ cmdPumpStdin(so, inw, proc, sid, cmdID, cmdExecTimeout, log, eeCB, data)
+
+ // Some commands will exit when stdin is closed.
+ inw.Close()
+
+ defer outr.Close()
+
+ if status, err := proc.Wait(); err == nil {
+ // Other commands need a bonk on the head.
+ if !status.Exited() {
+ if err := proc.Signal(os.Interrupt); err != nil {
+ log.Errorln("Proc interrupt:", err)
+ }
+
+ select {
+ case <-stdoutDone:
+ case <-time.After(time.Second):
+ // A bigger bonk on the head.
+ if err := proc.Signal(os.Kill); err != nil {
+ log.Errorln("Proc term:", err)
+ }
+ <-stdoutDone
+ }
+ }
+ }
+ }()
+
+ return nil
+}
+
+func cmdPumpStdin(so *socketio.Socket, w io.Writer, proc *os.Process,
+ sid string, cmdID int, tmo int, log *logrus.Logger, exitFuncCB EmitExitCB,
+ data *map[string]interface{}) {
+ /* XXX - code to add to support stdin through WS
+ for {
+ _, message, err := so. ?? ReadMessage()
+ if err != nil {
+ break
+ }
+ message = append(message, '\n')
+ if _, err := w.Write(message); err != nil {
+ break
+ }
+ }
+ */
+
+ // Monitor process exit
+ type DoneChan struct {
+ status int
+ err error
+ }
+ done := make(chan DoneChan, 1)
+ go func() {
+ status := 0
+ sts, err := proc.Wait()
+ if !sts.Success() {
+ s := sts.Sys().(syscall.WaitStatus)
+ status = s.ExitStatus()
+ }
+ done <- DoneChan{status, err}
+ }()
+
+ // Wait cmd complete
+ select {
+ case dC := <-done:
+ exitFuncCB(sid, cmdID, dC.status, dC.err, data)
+ case <-time.After(time.Duration(tmo) * time.Second):
+ exitFuncCB(sid, cmdID, -99,
+ fmt.Errorf("Exit Timeout for command ID %v", cmdID), data)
+ }
+}
+
+func cmdPumpStdout(so *socketio.Socket, r io.Reader, done chan struct{},
+ sid string, cmdID int, log *logrus.Logger, emitFuncCB EmitOutputCB, data *map[string]interface{}) {
+ defer func() {
+ }()
+
+ sc := bufio.NewScanner(r)
+ for sc.Scan() {
+ emitFuncCB(sid, cmdID, string(sc.Bytes()), "", data)
+ }
+ if sc.Err() != nil {
+ log.Errorln("scan:", sc.Err())
+ }
+ close(done)
+}
diff --git a/golib/filepath.go b/golib/filepath.go
new file mode 100644
index 0000000..5a923b4
--- /dev/null
+++ b/golib/filepath.go
@@ -0,0 +1,81 @@
+package common
+
+import (
+ "fmt"
+ "os"
+ "os/user"
+ "path"
+ "path/filepath"
+ "regexp"
+ "runtime"
+ "strings"
+)
+
+// Exists returns whether the given file or directory exists or not
+func Exists(path string) bool {
+ _, err := os.Stat(path)
+ if err == nil {
+ return true
+ }
+ if os.IsNotExist(err) {
+ return false
+ }
+ return true
+}
+
+// ResolveEnvVar Resolved environment variable regarding the syntax ${MYVAR}
+// or $MYVAR following by a slash or a backslash
+func ResolveEnvVar(s string) (string, error) {
+ if s == "" {
+ return s, nil
+ }
+
+ // Resolved tilde : ~/
+ if len(s) > 2 && s[:2] == "~/" {
+ if usr, err := user.Current(); err == nil {
+ s = filepath.Join(usr.HomeDir, s[2:])
+ }
+ }
+
+ // Resolved ${MYVAR}
+ re := regexp.MustCompile("\\${([^}]+)}")
+ vars := re.FindAllStringSubmatch(s, -1)
+ res := s
+ for _, v := range vars {
+ val := os.Getenv(v[1])
+ if val == "" {
+ // Specific case to resolved $HOME or ${HOME} on Windows host
+ if runtime.GOOS == "windows" && v[1] == "HOME" {
+ if usr, err := user.Current(); err == nil {
+ val = usr.HomeDir
+ }
+ } else {
+ return res, fmt.Errorf("ERROR: %s env variable not defined", v[1])
+ }
+ }
+
+ rer := regexp.MustCompile("\\${" + v[1] + "}")
+ res = rer.ReplaceAllString(res, val)
+ }
+
+ // Resolved $MYVAR following by a slash (or a backslash for Windows)
+ // TODO
+ //re := regexp.MustCompile("\\$([^\\/])+/")
+
+ return path.Clean(res), nil
+}
+
+// PathNormalize
+func PathNormalize(p string) string {
+ sep := string(filepath.Separator)
+ if sep != "/" {
+ return p
+ }
+ // Replace drive like C: by C/
+ res := p
+ if p[1:2] == ":" {
+ res = p[0:1] + sep + p[2:]
+ }
+ res = strings.Replace(res, "\\", "/", -1)
+ return filepath.Clean(res)
+}
diff --git a/golib/httpclient.go b/golib/httpclient.go
new file mode 100644
index 0000000..72132bf
--- /dev/null
+++ b/golib/httpclient.go
@@ -0,0 +1,257 @@
+package common
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "github.com/Sirupsen/logrus"
+)
+
+type HTTPClient struct {
+ httpClient http.Client
+ endpoint string
+ apikey string
+ username string
+ password string
+ id string
+ csrf string
+ conf HTTPClientConfig
+ logger *logrus.Logger
+}
+
+type HTTPClientConfig struct {
+ URLPrefix string
+ HeaderAPIKeyName string
+ Apikey string
+ HeaderClientKeyName string
+ CsrfDisable bool
+}
+
+const (
+ logError = 1
+ logWarning = 2
+ logInfo = 3
+ logDebug = 4
+)
+
+// Inspired by syncthing/cmd/cli
+
+const insecure = false
+
+// HTTPNewClient creates a new HTTP client to deal with Syncthing
+func HTTPNewClient(baseURL string, cfg HTTPClientConfig) (*HTTPClient, error) {
+
+ // Create w new Http client
+ httpClient := http.Client{
+ Transport: &http.Transport{
+ TLSClientConfig: &tls.Config{
+ InsecureSkipVerify: insecure,
+ },
+ },
+ }
+ client := HTTPClient{
+ httpClient: httpClient,
+ endpoint: baseURL,
+ apikey: cfg.Apikey,
+ conf: cfg,
+ /* TODO - add user + pwd support
+ username: c.GlobalString("username"),
+ password: c.GlobalString("password"),
+ */
+ }
+
+ if client.apikey == "" {
+ if err := client.getCidAndCsrf(); err != nil {
+ return nil, err
+ }
+ }
+ return &client, nil
+}
+
+// SetLogger Define the logger to use
+func (c *HTTPClient) SetLogger(log *logrus.Logger) {
+ c.logger = log
+}
+
+func (c *HTTPClient) log(level int, format string, args ...interface{}) {
+ if c.logger != nil {
+ switch level {
+ case logError:
+ c.logger.Errorf(format, args...)
+ break
+ case logWarning:
+ c.logger.Warningf(format, args...)
+ break
+ case logInfo:
+ c.logger.Infof(format, args...)
+ break
+ default:
+ c.logger.Debugf(format, args...)
+ break
+ }
+ }
+}
+
+// Send request to retrieve Client id and/or CSRF token
+func (c *HTTPClient) getCidAndCsrf() error {
+ request, err := http.NewRequest("GET", c.endpoint, nil)
+ if err != nil {
+ return err
+ }
+ if _, err := c.handleRequest(request); err != nil {
+ return err
+ }
+ if c.id == "" {
+ return errors.New("Failed to get device ID")
+ }
+ if !c.conf.CsrfDisable && c.csrf == "" {
+ return errors.New("Failed to get CSRF token")
+ }
+ return nil
+}
+
+// GetClientID returns the id
+func (c *HTTPClient) GetClientID() string {
+ return c.id
+}
+
+// formatURL Build full url by concatenating all parts
+func (c *HTTPClient) formatURL(endURL string) string {
+ url := c.endpoint
+ if !strings.HasSuffix(url, "/") {
+ url += "/"
+ }
+ url += strings.TrimLeft(c.conf.URLPrefix, "/")
+ if !strings.HasSuffix(url, "/") {
+ url += "/"
+ }
+ return url + strings.TrimLeft(endURL, "/")
+}
+
+// HTTPGet Send a Get request to client and return an error object
+func (c *HTTPClient) HTTPGet(url string, data *[]byte) error {
+ _, err := c.HTTPGetWithRes(url, data)
+ return err
+}
+
+// HTTPGetWithRes Send a Get request to client and return both response and error
+func (c *HTTPClient) HTTPGetWithRes(url string, data *[]byte) (*http.Response, error) {
+ request, err := http.NewRequest("GET", c.formatURL(url), nil)
+ if err != nil {
+ return nil, err
+ }
+ res, err := c.handleRequest(request)
+ if err != nil {
+ return res, err
+ }
+ if res.StatusCode != 200 {
+ return res, errors.New(res.Status)
+ }
+
+ *data = c.responseToBArray(res)
+
+ return res, nil
+}
+
+// HTTPPost Send a POST request to client and return an error object
+func (c *HTTPClient) HTTPPost(url string, body string) error {
+ _, err := c.HTTPPostWithRes(url, body)
+ return err
+}
+
+// HTTPPostWithRes Send a POST request to client and return both response and error
+func (c *HTTPClient) HTTPPostWithRes(url string, body string) (*http.Response, error) {
+ request, err := http.NewRequest("POST", c.formatURL(url), bytes.NewBufferString(body))
+ if err != nil {
+ return nil, err
+ }
+ res, err := c.handleRequest(request)
+ if err != nil {
+ return res, err
+ }
+ if res.StatusCode != 200 {
+ return res, errors.New(res.Status)
+ }
+ return res, nil
+}
+
+func (c *HTTPClient) responseToBArray(response *http.Response) []byte {
+ defer response.Body.Close()
+ bytes, err := ioutil.ReadAll(response.Body)
+ if err != nil {
+ // TODO improved error reporting
+ fmt.Println("ERROR: " + err.Error())
+ }
+ return bytes
+}
+
+func (c *HTTPClient) handleRequest(request *http.Request) (*http.Response, error) {
+ if c.conf.HeaderAPIKeyName != "" && c.apikey != "" {
+ request.Header.Set(c.conf.HeaderAPIKeyName, c.apikey)
+ }
+ if c.conf.HeaderClientKeyName != "" && c.id != "" {
+ request.Header.Set(c.conf.HeaderClientKeyName, c.id)
+ }
+ if c.username != "" || c.password != "" {
+ request.SetBasicAuth(c.username, c.password)
+ }
+ if c.csrf != "" {
+ request.Header.Set("X-CSRF-Token-"+c.id[:5], c.csrf)
+ }
+
+ c.log(logDebug, "HTTP %s %v", request.Method, request.URL)
+
+ response, err := c.httpClient.Do(request)
+ if err != nil {
+ return nil, err
+ }
+
+ // Detect client ID change
+ cid := response.Header.Get(c.conf.HeaderClientKeyName)
+ if cid != "" && c.id != cid {
+ c.id = cid
+ }
+
+ // Detect CSR token change
+ for _, item := range response.Cookies() {
+ if item.Name == "CSRF-Token-"+c.id[:5] {
+ c.csrf = item.Value
+ goto csrffound
+ }
+ }
+ // OK CSRF found
+csrffound:
+
+ if response.StatusCode == 404 {
+ return nil, errors.New("Invalid endpoint or API call")
+ } else if response.StatusCode == 401 {
+ return nil, errors.New("Invalid username or password")
+ } else if response.StatusCode == 403 {
+ if c.apikey == "" {
+ // Request a new Csrf for next requests
+ c.getCidAndCsrf()
+ return nil, errors.New("Invalid CSRF token")
+ }
+ return nil, errors.New("Invalid API key")
+ } else if response.StatusCode != 200 {
+ data := make(map[string]interface{})
+ // Try to decode error field of APIError struct
+ json.Unmarshal(c.responseToBArray(response), &data)
+ if err, found := data["error"]; found {
+ return nil, fmt.Errorf(err.(string))
+ } else {
+ body := strings.TrimSpace(string(c.responseToBArray(response)))
+ if body != "" {
+ return nil, fmt.Errorf(body)
+ }
+ }
+ return nil, errors.New("Unknown HTTP status returned: " + response.Status)
+ }
+ return response, nil
+}