From 89ea6ebd3671e6ebbf6101525a5416427806f318 Mon Sep 17 00:00:00 2001 From: Sebastien Douheret Date: Thu, 4 Apr 2019 23:45:56 +0200 Subject: Fixed package tree and go mod file Change-Id: I1047094d5b80d0622e2c2ce674979f18207b8c0f Signed-off-by: Sebastien Douheret --- eows/eows-in.go | 59 ++++++ eows/eows-out.go | 122 +++++++++++ eows/eows-signal.go | 48 +++++ eows/eows-signal_windows.go | 43 ++++ eows/eows.go | 287 ++++++++++++++++++++++++++ error.go | 16 ++ filepath.go | 129 ++++++++++++ go.mod | 13 +- go.sum | 46 +---- golib/eows/eows-in.go | 59 ------ golib/eows/eows-out.go | 122 ----------- golib/eows/eows-signal.go | 48 ----- golib/eows/eows-signal_windows.go | 43 ---- golib/eows/eows.go | 287 -------------------------- golib/error.go | 16 -- golib/filepath.go | 129 ------------ golib/httpclient.go | 411 -------------------------------------- httpclient.go | 411 ++++++++++++++++++++++++++++++++++++++ 18 files changed, 1125 insertions(+), 1164 deletions(-) create mode 100644 eows/eows-in.go create mode 100644 eows/eows-out.go create mode 100644 eows/eows-signal.go create mode 100644 eows/eows-signal_windows.go create mode 100644 eows/eows.go create mode 100644 error.go create mode 100644 filepath.go delete mode 100644 golib/eows/eows-in.go delete mode 100644 golib/eows/eows-out.go delete mode 100644 golib/eows/eows-signal.go delete mode 100644 golib/eows/eows-signal_windows.go delete mode 100644 golib/eows/eows.go delete mode 100644 golib/error.go delete mode 100644 golib/filepath.go delete mode 100644 golib/httpclient.go create mode 100644 httpclient.go diff --git a/eows/eows-in.go b/eows/eows-in.go new file mode 100644 index 0000000..5e74c76 --- /dev/null +++ b/eows/eows-in.go @@ -0,0 +1,59 @@ +package eows + +import ( + "fmt" + "os" + "syscall" + "time" +) + +// DoneChan Channel used to propagate status+error on command exit +type DoneChan struct { + status int + err error +} + +// pumpStdin is in charge of receive characters and send them to stdin +func (e *ExecOverWS) pumpStdin(inw *os.File) { + + done := make(chan DoneChan, 1) + + if e.InputEvent != "" && e.InputCB != nil { + + err := (*e.SocketIO).On(e.InputEvent, func(stdin []byte) { + in, err := e.InputCB(e, stdin) + if err != nil { + e.logDebug("Error stdin: %s", err.Error()) + inw.Close() + return + } + if _, err := inw.Write(in); err != nil { + e.logError("Error while writing to stdin: %s", err.Error()) + } + }) + if err != nil { + e.logError("Error stdin on event: %s", err.Error()) + } + } + + // Monitor process exit + go func() { + status := 0 + sts, err := e.proc.Wait() + if !sts.Success() { + s := sts.Sys().(syscall.WaitStatus) + status = s.ExitStatus() + } + e.procExited = true + + done <- DoneChan{status, err} + }() + + // Wait cmd complete + select { + case dC := <-done: + e.ExitCB(e, dC.status, dC.err) + case <-time.After(time.Duration(e.CmdExecTimeout) * time.Second): + e.ExitCB(e, -999, fmt.Errorf("Exit Timeout for command ID %v", e.CmdID)) + } +} diff --git a/eows/eows-out.go b/eows/eows-out.go new file mode 100644 index 0000000..3163b2f --- /dev/null +++ b/eows/eows-out.go @@ -0,0 +1,122 @@ +package eows + +import ( + "bufio" + "io" + "strings" + "time" +) + +// scanChars - gain character by character (or as soon as one or more characters are available) +func scanChars(data []byte, atEOF bool) (advance int, token []byte, err error) { + if atEOF && len(data) == 0 { + return 0, nil, nil + } + return len(data), data, nil +} + +// _pumper is in charge to collect +func (e *ExecOverWS) _pumper(sc *bufio.Scanner, fctCB func(s string)) { + + // Select split function (default sc.ScanLines) + if e.OutSplit == SplitChar || e.OutSplit == SplitLineTime || e.OutSplit == SplitTime { + sc.Split(scanChars) + } + + // Scan method according to split type + if e.OutSplit == SplitLineTime || e.OutSplit == SplitTime { + t0 := time.Now() + buf := "" + for sc.Scan() { + buf += sc.Text() + if time.Since(t0).Nanoseconds() > e.LineTimeSpan || + (e.OutSplit == SplitLineTime && strings.Contains(buf, "\n")) { + fctCB(buf) + buf = "" + t0 = time.Now() + } + if e.procExited { + break + } + } + // Send remaining characters + if len(buf) > 0 { + fctCB(buf) + } + + } else { + + for sc.Scan() { + fctCB(sc.Text()) + if e.procExited { + break + } + } + } + +} + +// pipePumpStdout is in charge to forward stdout in websocket +func (e *ExecOverWS) pipePumpStdout(r io.Reader, done chan struct{}) { + + sc := bufio.NewScanner(r) + + e._pumper(sc, func(b string) { + if e.OutputCB != nil { + e.OutputCB(e, []byte(b), []byte{}) + } + }) + + e.logDebug("STDOUT pump exit") + + if sc.Err() != nil && !strings.Contains(sc.Err().Error(), "file already closed") { + e.logError("stdout scan: %v", sc.Err()) + } + + close(done) +} + +// pipePumpStderr is in charge to forward stderr in websocket +func (e *ExecOverWS) pipePumpStderr(r io.Reader) { + + sc := bufio.NewScanner(r) + + e._pumper(sc, func(b string) { + if e.OutputCB != nil { + e.OutputCB(e, []byte{}, []byte(b)) + } + }) + + e.logDebug("STDERR pump exit") + + if sc.Err() != nil && !strings.Contains(sc.Err().Error(), "file already closed") { + e.logError("stderr scan: %v", sc.Err()) + } +} + +// ptsPumpStdout is in charge to forward stdout in websocket +// (only used when PtyMode is set) +func (e *ExecOverWS) ptsPumpStdout(r io.Reader, done chan struct{}) { + + buffer := make([]byte, 1024) + for { + n, err := r.Read(buffer) + if err != nil { + if err != io.EOF && + !strings.Contains(err.Error(), "file already closed") { + e.logError("Error stdout read: %v", err) + } + break + } + if n == 0 { + continue + } + if e.OutputCB != nil { + e.OutputCB(e, buffer[:n], []byte{}) + } + } + + close(done) + + e.logDebug("Eows stdout pump exited") +} diff --git a/eows/eows-signal.go b/eows/eows-signal.go new file mode 100644 index 0000000..f48279a --- /dev/null +++ b/eows/eows-signal.go @@ -0,0 +1,48 @@ +// +build !windows + +// Package eows is used to Execute commands Over WebSocket +package eows + +import ( + "fmt" + "os" + "syscall" +) + +// Signal sends a signal to the running command / process +func (e *ExecOverWS) Signal(signal string) error { + var sig os.Signal + switch signal { + case "quit", "SIGQUIT": + sig = syscall.SIGQUIT + case "terminated", "SIGTERM": + sig = syscall.SIGTERM + case "interrupt", "SIGINT": + sig = syscall.SIGINT + case "aborted", "SIGABRT": + sig = syscall.SIGABRT + case "continued", "SIGCONT": + sig = syscall.SIGCONT + case "hangup", "SIGHUP": + sig = syscall.SIGHUP + case "killed", "SIGKILL": + sig = syscall.SIGKILL + case "stopped (signal)", "SIGSTOP": + sig = syscall.SIGSTOP + case "stopped", "SIGTSTP": + sig = syscall.SIGTSTP + case "user defined signal 1", "SIGUSR1": + sig = syscall.SIGUSR1 + case "user defined signal 2", "SIGUSR2": + sig = syscall.SIGUSR2 + default: + return fmt.Errorf("Unsupported signal") + } + + if e.proc == nil { + return fmt.Errorf("Cannot retrieve process") + } + + e.logDebug("SEND signal %v to proc %v", sig, e.proc.Pid) + return e.proc.Signal(sig) +} diff --git a/eows/eows-signal_windows.go b/eows/eows-signal_windows.go new file mode 100644 index 0000000..23ad924 --- /dev/null +++ b/eows/eows-signal_windows.go @@ -0,0 +1,43 @@ +package eows + +// +build windows + +// Signal sends a signal to the running command / process +func (e *ExecOverWS) Signal(signal string) error { + panic("FIXME: Not implemented") + /* + var sig os.Signal + switch signal { + case "quit", "SIGQUIT": + sig = syscall.SIGQUIT + case "terminated", "SIGTERM": + sig = syscall.SIGTERM + case "interrupt", "SIGINT": + sig = syscall.SIGINT + case "aborted", "SIGABRT": + sig = syscall.SIGABRT + case "continued", "SIGCONT": + sig = syscall.SIGCONT + case "hangup", "SIGHUP": + sig = syscall.SIGHUP + case "killed", "SIGKILL": + sig = syscall.SIGKILL + case "stopped (signal)", "SIGSTOP": + sig = syscall.SIGSTOP + case "stopped", "SIGTSTP": + sig = syscall.SIGTSTP + case "user defined signal 1", "SIGUSR1": + sig = syscall.SIGUSR1 + case "user defined signal 2", "SIGUSR2": + sig = syscall.SIGUSR2 + default: + return fmt.Errorf("Unsupported signal") + } + + if e.proc == nil { + return fmt.Errorf("Cannot retrieve process") + } + fmt.Printf("SEND signal %v to proc %v\n", sig, e.proc.Pid) + return e.proc.Signal(sig) + */ +} diff --git a/eows/eows.go b/eows/eows.go new file mode 100644 index 0000000..9d0b520 --- /dev/null +++ b/eows/eows.go @@ -0,0 +1,287 @@ +// Package eows is used to Execute commands Over WebSocket +package eows + +import ( + "fmt" + "os" + "os/exec" + "strings" + "syscall" + "time" + "unsafe" + + "github.com/Sirupsen/logrus" + "github.com/googollee/go-socket.io" + "github.com/kr/pty" +) + +// OnInputCB is the function callback used to receive data +type OnInputCB func(e *ExecOverWS, stdin []byte) ([]byte, error) + +// EmitOutputCB is the function callback used to emit data +type EmitOutputCB func(e *ExecOverWS, stdout, stderr []byte) + +// EmitExitCB is the function callback used to emit exit proc code +type EmitExitCB func(e *ExecOverWS, code int, err error) + +// SplitType Type of spliting method to tokenize stdout/stderr +type SplitType uint8 + +const ( + // SplitLine Split line by line + SplitLine SplitType = iota + // SplitChar Split character by character + SplitChar + // SplitLineTime Split by line or until a timeout has passed + SplitLineTime + // SplitTime Split until a timeout has passed + SplitTime +) + +// Inspired by : +// https://github.com/gorilla/websocket/blob/master/examples/command/main.go + +// ExecOverWS . +type ExecOverWS struct { + Cmd string // command name to execute + Args []string // command arguments + SocketIO *socketio.Socket // websocket + Sid string // websocket ID + CmdID string // command ID + + // Optional fields + Env []string // command environment variables + CmdExecTimeout int // command execution time timeout + Log *logrus.Logger // logger (nil if disabled) + InputEvent string // websocket input event name + InputCB OnInputCB // stdin callback + OutputCB EmitOutputCB // stdout/stderr callback + ExitCB EmitExitCB // exit proc callback + UserData *map[string]interface{} // user data passed to callbacks + OutSplit SplitType // split method to tokenize stdout/stderr + LineTimeSpan int64 // time span (only used with SplitTime or SplitLineTime) + PtyMode bool // Allocate a pseudo-terminal (allow to execute screen-based program) + PtyTermEcho bool // Turn on/off terminal echo + + // Private fields + + proc *os.Process + command *exec.Cmd + ptmx *os.File + procExited bool +} + +var cmdIDMap = make(map[string]*ExecOverWS) + +// New creates a new instace of eows +func New(cmd string, args []string, so *socketio.Socket, soID, cmdID string) *ExecOverWS { + + e := &ExecOverWS{ + Cmd: cmd, + Args: args, + SocketIO: so, + Sid: soID, + CmdID: cmdID, + CmdExecTimeout: -1, // default no timeout + OutSplit: SplitLineTime, // default split by line with time + LineTimeSpan: 500 * time.Millisecond.Nanoseconds(), + PtyMode: false, + PtyTermEcho: true, + } + + cmdIDMap[cmdID] = e + + return e +} + +// GetEows gets ExecOverWS object from command ID +func GetEows(cmdID string) *ExecOverWS { + if _, ok := cmdIDMap[cmdID]; !ok { + return nil + } + return cmdIDMap[cmdID] +} + +// Start executes the command and redirect stdout/stderr into a WebSocket +func (e *ExecOverWS) Start() error { + var err error + var outr, outw, errr, errw, inr, inw *os.File + + bashArgs := []string{"/bin/bash", "-c", e.Cmd + " " + strings.Join(e.Args, " ")} + + // no timeout == 1 year + if e.CmdExecTimeout == -1 { + e.CmdExecTimeout = 365 * 24 * 60 * 60 + } + + e.procExited = false + + if e.PtyMode { + + e.command = exec.Command(bashArgs[0], bashArgs[1:]...) + e.command.Env = append(os.Environ(), e.Env...) + e.ptmx, err = pty.Start(e.command) + if err != nil { + err = fmt.Errorf("Process start error: " + err.Error()) + goto exitErr + } + e.proc = e.command.Process + + // Turn off terminal echo + if !e.PtyTermEcho { + e.terminalEcho(e.ptmx, false) + } + + } else { + + // Create pipes + outr, outw, err = os.Pipe() + if err != nil { + err = fmt.Errorf("Pipe stdout error: " + err.Error()) + goto exitErr + } + + errr, errw, err = os.Pipe() + if err != nil { + err = fmt.Errorf("Pipe stderr error: " + err.Error()) + goto exitErr + } + + inr, inw, err = os.Pipe() + if err != nil { + err = fmt.Errorf("Pipe stdin error: " + err.Error()) + goto exitErr + } + + e.proc, err = os.StartProcess(bashArgs[0], bashArgs, &os.ProcAttr{ + Files: []*os.File{inr, outw, errw}, + Env: append(os.Environ(), e.Env...), + }) + if err != nil { + err = fmt.Errorf("Process start error: " + err.Error()) + goto exitErr + } + } + + go func() { + stdoutDone := make(chan struct{}) + + if e.PtyMode { + // Make sure to close the pty at the end. + defer e.ptmx.Close() + + // Handle both stdout mixed with stderr + go e.ptsPumpStdout(e.ptmx, stdoutDone) + + // Blocking function that poll input or wait for end of process + e.pumpStdin(e.ptmx) + + } else { + // Make sure to close all pipes + defer outr.Close() + defer outw.Close() + defer errr.Close() + defer errw.Close() + defer inr.Close() + defer inw.Close() + + // Handle stdout + stderr + go e.pipePumpStdout(outr, stdoutDone) + go e.pipePumpStderr(errr) + + // Blocking function that poll input or wait for end of process + e.pumpStdin(inw) + } + + if status, err := e.proc.Wait(); err == nil { + // Other commands need a bonk on the head. + if !status.Exited() { + if err := e.proc.Signal(os.Interrupt); err != nil { + e.logError("Proc interrupt:", err) + } + + select { + case <-stdoutDone: + case <-time.After(time.Second): + // A bigger bonk on the head. + if err := e.proc.Signal(os.Kill); err != nil { + e.logError("Proc term:", err) + } + <-stdoutDone + } + } + } + + delete(cmdIDMap, e.CmdID) + }() + + return nil + +exitErr: + for _, pf := range []*os.File{outr, outw, errr, errw, inr, inw} { + pf.Close() + } + return err +} + +// TerminalSetSize Set terminal size +func (e *ExecOverWS) TerminalSetSize(rows, cols uint16) error { + if !e.PtyMode || e.ptmx == nil { + return fmt.Errorf("PtyMode not set") + } + w, err := pty.GetsizeFull(e.ptmx) + if err != nil { + return err + } + return e.TerminalSetSizePos(rows, cols, w.X, w.Y) +} + +// TerminalSetSizePos Set terminal size and position +func (e *ExecOverWS) TerminalSetSizePos(rows, cols, x, y uint16) error { + if !e.PtyMode || e.ptmx == nil { + return fmt.Errorf("PtyMode not set") + } + winSz := pty.Winsize{Rows: rows, Cols: cols, X: x, Y: y} + return pty.Setsize(e.ptmx, &winSz) +} + +/** + * Private functions + **/ + +// terminalEcho Enable or disable echoing terminal input. +// This is useful specifically for when users enter passwords. +func (e *ExecOverWS) terminalEcho(ff *os.File, show bool) { + var termios = &syscall.Termios{} + + fd := ff.Fd() + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, + syscall.TCGETS, uintptr(unsafe.Pointer(termios))); err != 0 { + return + } + + if show { + termios.Lflag |= syscall.ECHO + } else { + termios.Lflag &^= syscall.ECHO + } + + if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, + uintptr(syscall.TCSETS), + uintptr(unsafe.Pointer(termios))); err != 0 { + return + } +} + +func (e *ExecOverWS) logDebug(format string, a ...interface{}) { + if e.Log != nil { + e.Log.Debugf(format, a...) + } +} + +func (e *ExecOverWS) logError(format string, a ...interface{}) { + if e.Log != nil { + e.Log.Errorf(format, a...) + } +} diff --git a/error.go b/error.go new file mode 100644 index 0000000..6873d82 --- /dev/null +++ b/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/filepath.go b/filepath.go new file mode 100644 index 0000000..1817f58 --- /dev/null +++ b/filepath.go @@ -0,0 +1,129 @@ +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 +} + +// IsDir returns true when the given path is a directory +func IsDir(path string) bool { + fi, err := os.Stat(path) + if err != nil { + return false + } + + return fi.Mode().IsDir() +} + +// 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 := "" + if v[1] == "EXEPATH" { + // Specific case to resolve $EXEPATH or ${EXEPATH} used as current executable path + val = GetExePath() + + } else { + // Get env var value + 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 normalizes a linux or windows like path +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) +} + +// GetUserHome returns the user's home directory or empty string on error +func GetUserHome() string { + if usr, err := user.Current(); err == nil && usr != nil && usr.HomeDir != "" { + return usr.HomeDir + } + for _, p := range []string{"HOME", "HomePath"} { + if h := os.Getenv(p); h != "" { + return h + } + } + + return "" +} + +// GetExePath returns the full path of the current executable +func GetExePath() string { + exePath := os.Args[0] // set fallback value + ee, _ := os.Executable() + exeAbsPath, err := filepath.Abs(ee) + if err == nil { + exePath, err = filepath.EvalSymlinks(exeAbsPath) + if err == nil { + exePath = filepath.Dir(ee) + } else { + exePath = filepath.Dir(exeAbsPath) + } + } + return exePath +} diff --git a/go.mod b/go.mod index 53d58c9..b8a0c29 100644 --- a/go.mod +++ b/go.mod @@ -6,18 +6,11 @@ require ( github.com/Sirupsen/logrus v0.11.5 github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 // indirect github.com/gin-gonic/gin v1.3.0 - github.com/golang/protobuf v1.3.0 // indirect + github.com/golang/protobuf v1.3.1 // indirect github.com/googollee/go-socket.io v1.0.1 - github.com/json-iterator/go v1.1.5 // indirect github.com/kr/pty v1.1.3 - github.com/mattn/go-isatty v0.0.6 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.1 // indirect - github.com/stretchr/testify v1.3.0 // indirect - github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 // indirect - golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 // indirect - golang.org/x/sys v0.0.0-20190308023053-584f3b12f43e // indirect - gopkg.in/go-playground/assert.v1 v1.2.1 // indirect + github.com/mattn/go-isatty v0.0.7 // indirect + github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 // indirect gopkg.in/go-playground/validator.v8 v8.18.2 // indirect gopkg.in/yaml.v2 v2.2.2 // indirect ) diff --git a/go.sum b/go.sum index 2b053da..98303a8 100644 --- a/go.sum +++ b/go.sum @@ -1,59 +1,27 @@ -github.com/Sirupsen/logrus v0.11.5 h1:aIMrrsnipdTlAieMe7FC/iiuJ0+ELiXCT4YiVQiK9j8= github.com/Sirupsen/logrus v0.11.5/go.mod h1:rmk17hk6i8ZSAJkSDa7nOxamrG+SP4P0mm+DAvExv4U= -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3 h1:t8FVkw33L+wilf2QiWkw0UV77qRpcH/JHPKGpKa2E8g= github.com/gin-contrib/sse v0.0.0-20190301062529-5545eab6dad3/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s= github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs= github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y= -github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= -github.com/googollee/go-engine.io v1.0.1 h1:Q0H6NyghLSleyzQa5pN7N0ZZw15MLcgd+kqgXM2eAcA= +github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/googollee/go-engine.io v1.0.1/go.mod h1:ZcJSV0EqRvvcCXN7h7d8/EncnShfx85kv0SUsTIKTsg= -github.com/googollee/go-socket.io v1.0.1 h1:uWBxm1BBV7XSFHOr0vZMYC6TMHMPsI3YcQPXMWzwjUw= github.com/googollee/go-socket.io v1.0.1/go.mod h1:I46rLznx5OmtL5sPHp9GQJK/z0+lkLOBIx1NO8Mp5io= -github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e h1:JKmoR8x90Iww1ks85zJ1lfDGgIiMDuIptTOhJq+zKyg= github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q= github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= -github.com/json-iterator/go v1.1.5 h1:gL2yXlmiIo4+t+y32d4WGwOjKGYcGOuyrg46vadswDE= -github.com/json-iterator/go v1.1.5/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/jtolds/gls v4.2.1+incompatible h1:fSuqC+Gmlu6l/ZYAoZzx2pyucC8Xza35fpRVWLVmUEE= github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= -github.com/kr/pty v1.1.3 h1:/Um6a/ZmD5tF7peoOJ5oN5KMQ0DrGVQSXLNwyckutPk= github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/mattn/go-isatty v0.0.6 h1:SrwhHcpV4nWrMGdNcC2kXpMfcBVYGDuTArqyhocJgvA= -github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac h1:wbW+Bybf9pXxnCFAOWZTqkRjAc7rAIwo2e1ArUhiHxg= +github.com/mattn/go-isatty v0.0.7 h1:UvyT9uN+3r7yLEYSlJsbQGdsaB/a0DlgWP3pql6iwOc= +github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/smartystreets/assertions v0.0.0-20190215210624-980c5ac6f3ac/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c h1:Ho+uVpkel/udgjbwB5Lktg9BtvJSh2DT0Hi6LPSyI2w= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/ugorji/go v1.1.2 h1:JON3E2/GPW2iDNGoSAusl1KDf5TRQ8k8q7Tp097pZGs= github.com/ugorji/go v1.1.2/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ= -github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43 h1:BasDe+IErOQKrMVXab7UayvSlIpiyGwRvuX3EKYY7UA= -github.com/ugorji/go/codec v0.0.0-20190204201341-e444a5086c43/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95 h1:fY7Dsw114eJN4boqzVSbpVHO6rTdhq6/GnXeu+PKnzU= -golang.org/x/net v0.0.0-20190301231341-16b79f2e4e95/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780 h1:vG/gY/PxA3v3l04qxe3tDjXyu3bozii8ulSlIPOYKhI= +github.com/ugorji/go/codec v0.0.0-20190320090025-2dc34c0b8780/go.mod h1:iT03XoTwV7xq/+UGwKO3UbC1nNNlopQiY61beSdrtOA= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190308023053-584f3b12f43e h1:K7CV15oJ823+HLXQ+M7MSMrUg8LjfqY7O3naO+8Pp/I= -golang.org/x/sys v0.0.0-20190308023053-584f3b12f43e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= -gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= diff --git a/golib/eows/eows-in.go b/golib/eows/eows-in.go deleted file mode 100644 index 5e74c76..0000000 --- a/golib/eows/eows-in.go +++ /dev/null @@ -1,59 +0,0 @@ -package eows - -import ( - "fmt" - "os" - "syscall" - "time" -) - -// DoneChan Channel used to propagate status+error on command exit -type DoneChan struct { - status int - err error -} - -// pumpStdin is in charge of receive characters and send them to stdin -func (e *ExecOverWS) pumpStdin(inw *os.File) { - - done := make(chan DoneChan, 1) - - if e.InputEvent != "" && e.InputCB != nil { - - err := (*e.SocketIO).On(e.InputEvent, func(stdin []byte) { - in, err := e.InputCB(e, stdin) - if err != nil { - e.logDebug("Error stdin: %s", err.Error()) - inw.Close() - return - } - if _, err := inw.Write(in); err != nil { - e.logError("Error while writing to stdin: %s", err.Error()) - } - }) - if err != nil { - e.logError("Error stdin on event: %s", err.Error()) - } - } - - // Monitor process exit - go func() { - status := 0 - sts, err := e.proc.Wait() - if !sts.Success() { - s := sts.Sys().(syscall.WaitStatus) - status = s.ExitStatus() - } - e.procExited = true - - done <- DoneChan{status, err} - }() - - // Wait cmd complete - select { - case dC := <-done: - e.ExitCB(e, dC.status, dC.err) - case <-time.After(time.Duration(e.CmdExecTimeout) * time.Second): - e.ExitCB(e, -999, fmt.Errorf("Exit Timeout for command ID %v", e.CmdID)) - } -} diff --git a/golib/eows/eows-out.go b/golib/eows/eows-out.go deleted file mode 100644 index 3163b2f..0000000 --- a/golib/eows/eows-out.go +++ /dev/null @@ -1,122 +0,0 @@ -package eows - -import ( - "bufio" - "io" - "strings" - "time" -) - -// scanChars - gain character by character (or as soon as one or more characters are available) -func scanChars(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - return len(data), data, nil -} - -// _pumper is in charge to collect -func (e *ExecOverWS) _pumper(sc *bufio.Scanner, fctCB func(s string)) { - - // Select split function (default sc.ScanLines) - if e.OutSplit == SplitChar || e.OutSplit == SplitLineTime || e.OutSplit == SplitTime { - sc.Split(scanChars) - } - - // Scan method according to split type - if e.OutSplit == SplitLineTime || e.OutSplit == SplitTime { - t0 := time.Now() - buf := "" - for sc.Scan() { - buf += sc.Text() - if time.Since(t0).Nanoseconds() > e.LineTimeSpan || - (e.OutSplit == SplitLineTime && strings.Contains(buf, "\n")) { - fctCB(buf) - buf = "" - t0 = time.Now() - } - if e.procExited { - break - } - } - // Send remaining characters - if len(buf) > 0 { - fctCB(buf) - } - - } else { - - for sc.Scan() { - fctCB(sc.Text()) - if e.procExited { - break - } - } - } - -} - -// pipePumpStdout is in charge to forward stdout in websocket -func (e *ExecOverWS) pipePumpStdout(r io.Reader, done chan struct{}) { - - sc := bufio.NewScanner(r) - - e._pumper(sc, func(b string) { - if e.OutputCB != nil { - e.OutputCB(e, []byte(b), []byte{}) - } - }) - - e.logDebug("STDOUT pump exit") - - if sc.Err() != nil && !strings.Contains(sc.Err().Error(), "file already closed") { - e.logError("stdout scan: %v", sc.Err()) - } - - close(done) -} - -// pipePumpStderr is in charge to forward stderr in websocket -func (e *ExecOverWS) pipePumpStderr(r io.Reader) { - - sc := bufio.NewScanner(r) - - e._pumper(sc, func(b string) { - if e.OutputCB != nil { - e.OutputCB(e, []byte{}, []byte(b)) - } - }) - - e.logDebug("STDERR pump exit") - - if sc.Err() != nil && !strings.Contains(sc.Err().Error(), "file already closed") { - e.logError("stderr scan: %v", sc.Err()) - } -} - -// ptsPumpStdout is in charge to forward stdout in websocket -// (only used when PtyMode is set) -func (e *ExecOverWS) ptsPumpStdout(r io.Reader, done chan struct{}) { - - buffer := make([]byte, 1024) - for { - n, err := r.Read(buffer) - if err != nil { - if err != io.EOF && - !strings.Contains(err.Error(), "file already closed") { - e.logError("Error stdout read: %v", err) - } - break - } - if n == 0 { - continue - } - if e.OutputCB != nil { - e.OutputCB(e, buffer[:n], []byte{}) - } - } - - close(done) - - e.logDebug("Eows stdout pump exited") -} diff --git a/golib/eows/eows-signal.go b/golib/eows/eows-signal.go deleted file mode 100644 index f48279a..0000000 --- a/golib/eows/eows-signal.go +++ /dev/null @@ -1,48 +0,0 @@ -// +build !windows - -// Package eows is used to Execute commands Over WebSocket -package eows - -import ( - "fmt" - "os" - "syscall" -) - -// Signal sends a signal to the running command / process -func (e *ExecOverWS) Signal(signal string) error { - var sig os.Signal - switch signal { - case "quit", "SIGQUIT": - sig = syscall.SIGQUIT - case "terminated", "SIGTERM": - sig = syscall.SIGTERM - case "interrupt", "SIGINT": - sig = syscall.SIGINT - case "aborted", "SIGABRT": - sig = syscall.SIGABRT - case "continued", "SIGCONT": - sig = syscall.SIGCONT - case "hangup", "SIGHUP": - sig = syscall.SIGHUP - case "killed", "SIGKILL": - sig = syscall.SIGKILL - case "stopped (signal)", "SIGSTOP": - sig = syscall.SIGSTOP - case "stopped", "SIGTSTP": - sig = syscall.SIGTSTP - case "user defined signal 1", "SIGUSR1": - sig = syscall.SIGUSR1 - case "user defined signal 2", "SIGUSR2": - sig = syscall.SIGUSR2 - default: - return fmt.Errorf("Unsupported signal") - } - - if e.proc == nil { - return fmt.Errorf("Cannot retrieve process") - } - - e.logDebug("SEND signal %v to proc %v", sig, e.proc.Pid) - return e.proc.Signal(sig) -} diff --git a/golib/eows/eows-signal_windows.go b/golib/eows/eows-signal_windows.go deleted file mode 100644 index 23ad924..0000000 --- a/golib/eows/eows-signal_windows.go +++ /dev/null @@ -1,43 +0,0 @@ -package eows - -// +build windows - -// Signal sends a signal to the running command / process -func (e *ExecOverWS) Signal(signal string) error { - panic("FIXME: Not implemented") - /* - var sig os.Signal - switch signal { - case "quit", "SIGQUIT": - sig = syscall.SIGQUIT - case "terminated", "SIGTERM": - sig = syscall.SIGTERM - case "interrupt", "SIGINT": - sig = syscall.SIGINT - case "aborted", "SIGABRT": - sig = syscall.SIGABRT - case "continued", "SIGCONT": - sig = syscall.SIGCONT - case "hangup", "SIGHUP": - sig = syscall.SIGHUP - case "killed", "SIGKILL": - sig = syscall.SIGKILL - case "stopped (signal)", "SIGSTOP": - sig = syscall.SIGSTOP - case "stopped", "SIGTSTP": - sig = syscall.SIGTSTP - case "user defined signal 1", "SIGUSR1": - sig = syscall.SIGUSR1 - case "user defined signal 2", "SIGUSR2": - sig = syscall.SIGUSR2 - default: - return fmt.Errorf("Unsupported signal") - } - - if e.proc == nil { - return fmt.Errorf("Cannot retrieve process") - } - fmt.Printf("SEND signal %v to proc %v\n", sig, e.proc.Pid) - return e.proc.Signal(sig) - */ -} diff --git a/golib/eows/eows.go b/golib/eows/eows.go deleted file mode 100644 index 9d0b520..0000000 --- a/golib/eows/eows.go +++ /dev/null @@ -1,287 +0,0 @@ -// Package eows is used to Execute commands Over WebSocket -package eows - -import ( - "fmt" - "os" - "os/exec" - "strings" - "syscall" - "time" - "unsafe" - - "github.com/Sirupsen/logrus" - "github.com/googollee/go-socket.io" - "github.com/kr/pty" -) - -// OnInputCB is the function callback used to receive data -type OnInputCB func(e *ExecOverWS, stdin []byte) ([]byte, error) - -// EmitOutputCB is the function callback used to emit data -type EmitOutputCB func(e *ExecOverWS, stdout, stderr []byte) - -// EmitExitCB is the function callback used to emit exit proc code -type EmitExitCB func(e *ExecOverWS, code int, err error) - -// SplitType Type of spliting method to tokenize stdout/stderr -type SplitType uint8 - -const ( - // SplitLine Split line by line - SplitLine SplitType = iota - // SplitChar Split character by character - SplitChar - // SplitLineTime Split by line or until a timeout has passed - SplitLineTime - // SplitTime Split until a timeout has passed - SplitTime -) - -// Inspired by : -// https://github.com/gorilla/websocket/blob/master/examples/command/main.go - -// ExecOverWS . -type ExecOverWS struct { - Cmd string // command name to execute - Args []string // command arguments - SocketIO *socketio.Socket // websocket - Sid string // websocket ID - CmdID string // command ID - - // Optional fields - Env []string // command environment variables - CmdExecTimeout int // command execution time timeout - Log *logrus.Logger // logger (nil if disabled) - InputEvent string // websocket input event name - InputCB OnInputCB // stdin callback - OutputCB EmitOutputCB // stdout/stderr callback - ExitCB EmitExitCB // exit proc callback - UserData *map[string]interface{} // user data passed to callbacks - OutSplit SplitType // split method to tokenize stdout/stderr - LineTimeSpan int64 // time span (only used with SplitTime or SplitLineTime) - PtyMode bool // Allocate a pseudo-terminal (allow to execute screen-based program) - PtyTermEcho bool // Turn on/off terminal echo - - // Private fields - - proc *os.Process - command *exec.Cmd - ptmx *os.File - procExited bool -} - -var cmdIDMap = make(map[string]*ExecOverWS) - -// New creates a new instace of eows -func New(cmd string, args []string, so *socketio.Socket, soID, cmdID string) *ExecOverWS { - - e := &ExecOverWS{ - Cmd: cmd, - Args: args, - SocketIO: so, - Sid: soID, - CmdID: cmdID, - CmdExecTimeout: -1, // default no timeout - OutSplit: SplitLineTime, // default split by line with time - LineTimeSpan: 500 * time.Millisecond.Nanoseconds(), - PtyMode: false, - PtyTermEcho: true, - } - - cmdIDMap[cmdID] = e - - return e -} - -// GetEows gets ExecOverWS object from command ID -func GetEows(cmdID string) *ExecOverWS { - if _, ok := cmdIDMap[cmdID]; !ok { - return nil - } - return cmdIDMap[cmdID] -} - -// Start executes the command and redirect stdout/stderr into a WebSocket -func (e *ExecOverWS) Start() error { - var err error - var outr, outw, errr, errw, inr, inw *os.File - - bashArgs := []string{"/bin/bash", "-c", e.Cmd + " " + strings.Join(e.Args, " ")} - - // no timeout == 1 year - if e.CmdExecTimeout == -1 { - e.CmdExecTimeout = 365 * 24 * 60 * 60 - } - - e.procExited = false - - if e.PtyMode { - - e.command = exec.Command(bashArgs[0], bashArgs[1:]...) - e.command.Env = append(os.Environ(), e.Env...) - e.ptmx, err = pty.Start(e.command) - if err != nil { - err = fmt.Errorf("Process start error: " + err.Error()) - goto exitErr - } - e.proc = e.command.Process - - // Turn off terminal echo - if !e.PtyTermEcho { - e.terminalEcho(e.ptmx, false) - } - - } else { - - // Create pipes - outr, outw, err = os.Pipe() - if err != nil { - err = fmt.Errorf("Pipe stdout error: " + err.Error()) - goto exitErr - } - - errr, errw, err = os.Pipe() - if err != nil { - err = fmt.Errorf("Pipe stderr error: " + err.Error()) - goto exitErr - } - - inr, inw, err = os.Pipe() - if err != nil { - err = fmt.Errorf("Pipe stdin error: " + err.Error()) - goto exitErr - } - - e.proc, err = os.StartProcess(bashArgs[0], bashArgs, &os.ProcAttr{ - Files: []*os.File{inr, outw, errw}, - Env: append(os.Environ(), e.Env...), - }) - if err != nil { - err = fmt.Errorf("Process start error: " + err.Error()) - goto exitErr - } - } - - go func() { - stdoutDone := make(chan struct{}) - - if e.PtyMode { - // Make sure to close the pty at the end. - defer e.ptmx.Close() - - // Handle both stdout mixed with stderr - go e.ptsPumpStdout(e.ptmx, stdoutDone) - - // Blocking function that poll input or wait for end of process - e.pumpStdin(e.ptmx) - - } else { - // Make sure to close all pipes - defer outr.Close() - defer outw.Close() - defer errr.Close() - defer errw.Close() - defer inr.Close() - defer inw.Close() - - // Handle stdout + stderr - go e.pipePumpStdout(outr, stdoutDone) - go e.pipePumpStderr(errr) - - // Blocking function that poll input or wait for end of process - e.pumpStdin(inw) - } - - if status, err := e.proc.Wait(); err == nil { - // Other commands need a bonk on the head. - if !status.Exited() { - if err := e.proc.Signal(os.Interrupt); err != nil { - e.logError("Proc interrupt:", err) - } - - select { - case <-stdoutDone: - case <-time.After(time.Second): - // A bigger bonk on the head. - if err := e.proc.Signal(os.Kill); err != nil { - e.logError("Proc term:", err) - } - <-stdoutDone - } - } - } - - delete(cmdIDMap, e.CmdID) - }() - - return nil - -exitErr: - for _, pf := range []*os.File{outr, outw, errr, errw, inr, inw} { - pf.Close() - } - return err -} - -// TerminalSetSize Set terminal size -func (e *ExecOverWS) TerminalSetSize(rows, cols uint16) error { - if !e.PtyMode || e.ptmx == nil { - return fmt.Errorf("PtyMode not set") - } - w, err := pty.GetsizeFull(e.ptmx) - if err != nil { - return err - } - return e.TerminalSetSizePos(rows, cols, w.X, w.Y) -} - -// TerminalSetSizePos Set terminal size and position -func (e *ExecOverWS) TerminalSetSizePos(rows, cols, x, y uint16) error { - if !e.PtyMode || e.ptmx == nil { - return fmt.Errorf("PtyMode not set") - } - winSz := pty.Winsize{Rows: rows, Cols: cols, X: x, Y: y} - return pty.Setsize(e.ptmx, &winSz) -} - -/** - * Private functions - **/ - -// terminalEcho Enable or disable echoing terminal input. -// This is useful specifically for when users enter passwords. -func (e *ExecOverWS) terminalEcho(ff *os.File, show bool) { - var termios = &syscall.Termios{} - - fd := ff.Fd() - - if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, - syscall.TCGETS, uintptr(unsafe.Pointer(termios))); err != 0 { - return - } - - if show { - termios.Lflag |= syscall.ECHO - } else { - termios.Lflag &^= syscall.ECHO - } - - if _, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, - uintptr(syscall.TCSETS), - uintptr(unsafe.Pointer(termios))); err != 0 { - return - } -} - -func (e *ExecOverWS) logDebug(format string, a ...interface{}) { - if e.Log != nil { - e.Log.Debugf(format, a...) - } -} - -func (e *ExecOverWS) logError(format string, a ...interface{}) { - if e.Log != nil { - e.Log.Errorf(format, a...) - } -} diff --git a/golib/error.go b/golib/error.go deleted file mode 100644 index 6873d82..0000000 --- a/golib/error.go +++ /dev/null @@ -1,16 +0,0 @@ -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/filepath.go b/golib/filepath.go deleted file mode 100644 index 1817f58..0000000 --- a/golib/filepath.go +++ /dev/null @@ -1,129 +0,0 @@ -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 -} - -// IsDir returns true when the given path is a directory -func IsDir(path string) bool { - fi, err := os.Stat(path) - if err != nil { - return false - } - - return fi.Mode().IsDir() -} - -// 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 := "" - if v[1] == "EXEPATH" { - // Specific case to resolve $EXEPATH or ${EXEPATH} used as current executable path - val = GetExePath() - - } else { - // Get env var value - 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 normalizes a linux or windows like path -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) -} - -// GetUserHome returns the user's home directory or empty string on error -func GetUserHome() string { - if usr, err := user.Current(); err == nil && usr != nil && usr.HomeDir != "" { - return usr.HomeDir - } - for _, p := range []string{"HOME", "HomePath"} { - if h := os.Getenv(p); h != "" { - return h - } - } - - return "" -} - -// GetExePath returns the full path of the current executable -func GetExePath() string { - exePath := os.Args[0] // set fallback value - ee, _ := os.Executable() - exeAbsPath, err := filepath.Abs(ee) - if err == nil { - exePath, err = filepath.EvalSymlinks(exeAbsPath) - if err == nil { - exePath = filepath.Dir(ee) - } else { - exePath = filepath.Dir(exeAbsPath) - } - } - return exePath -} diff --git a/golib/httpclient.go b/golib/httpclient.go deleted file mode 100644 index f4880e6..0000000 --- a/golib/httpclient.go +++ /dev/null @@ -1,411 +0,0 @@ -package common - -import ( - "bytes" - "crypto/tls" - "encoding/json" - "errors" - "fmt" - "io" - "io/ioutil" - "net/http" - "os" - "strings" -) - -// HTTPClient . -type HTTPClient struct { - LoggerOut io.Writer - LoggerLevel int - LoggerPrefix string - - httpClient http.Client - initDone bool - endpoint string - apikey string - username string - password string - id string - csrf string - conf HTTPClientConfig -} - -// HTTPClientConfig is used to config HTTPClient -type HTTPClientConfig struct { - URLPrefix string - ContentType string - HeaderAPIKeyName string - Apikey string - HeaderClientKeyName string - CsrfDisable bool - LogOut io.Writer - LogLevel int - LogPrefix string -} - -// Logger levels constants -const ( - HTTPLogLevelPanic = 0 - HTTPLogLevelError = 1 - HTTPLogLevelWarning = 2 - HTTPLogLevelInfo = 3 - HTTPLogLevelDebug = 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, - }, - }, - } - - lOut := cfg.LogOut - if cfg.LogOut == nil { - lOut = os.Stdout - } - client := HTTPClient{ - LoggerOut: lOut, - LoggerLevel: cfg.LogLevel, - LoggerPrefix: cfg.LogPrefix, - - httpClient: httpClient, - initDone: false, - endpoint: baseURL, - apikey: cfg.Apikey, - conf: cfg, - /* TODO - add user + pwd support - username: c.GlobalString("username"), - password: c.GlobalString("password"), - */ - } - - // Default set Content-Type to json - if client.conf.ContentType == "" { - client.conf.ContentType = "application/json" - } - - if err := client.getCidAndCsrf(); err != nil { - client.log(HTTPLogLevelError, "Cannot retrieve Client ID and/or CSRF: %v", err) - return &client, err - } - - client.log(HTTPLogLevelDebug, "HTTP client url %s init Done", client.endpoint) - client.initDone = true - return &client, nil -} - -// GetLogLevel Get a readable string representing the log level -func (c *HTTPClient) GetLogLevel() string { - return c.LogLevelToString(c.LoggerLevel) -} - -// LogLevelToString Convert an integer log level to string -func (c *HTTPClient) LogLevelToString(lvl int) string { - switch lvl { - case HTTPLogLevelPanic: - return "panic" - case HTTPLogLevelError: - return "error" - case HTTPLogLevelWarning: - return "warning" - case HTTPLogLevelInfo: - return "info" - case HTTPLogLevelDebug: - return "debug" - } - return "Unknown" -} - -// SetLogLevel set the log level from a readable string -func (c *HTTPClient) SetLogLevel(lvl string) error { - switch strings.ToLower(lvl) { - case "panic": - c.LoggerLevel = HTTPLogLevelPanic - case "error": - c.LoggerLevel = HTTPLogLevelError - case "warn", "warning": - c.LoggerLevel = HTTPLogLevelWarning - case "info": - c.LoggerLevel = HTTPLogLevelInfo - case "debug": - c.LoggerLevel = HTTPLogLevelDebug - default: - return fmt.Errorf("Unknown level") - } - return nil -} - -// GetClientID returns the id -func (c *HTTPClient) GetClientID() string { - return c.id -} - -/*** -** High level functions -***/ - -// Get Send a Get request to client and return directly data of body response -func (c *HTTPClient) Get(url string, out interface{}) error { - return c._Request("GET", url, nil, out) -} - -// Post Send a Post request to client and return directly data of body response -func (c *HTTPClient) Post(url string, in interface{}, out interface{}) error { - return c._Request("POST", url, in, out) -} - -// Put Send a Put request to client and return directly data of body response -func (c *HTTPClient) Put(url string, in interface{}, out interface{}) error { - return c._Request("PUT", url, in, out) -} - -// Delete Send a Delete request to client and return directly data of body response -func (c *HTTPClient) Delete(url string, out interface{}) error { - return c._Request("DELETE", url, nil, out) -} - -/*** -** Low level functions -***/ - -// HTTPGet Send a Get request to client and return an error object -func (c *HTTPClient) HTTPGet(url string, data *[]byte) error { - _, err := c._HTTPRequest("GET", url, nil, 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) { - return c._HTTPRequest("GET", url, nil, data) -} - -// HTTPPost Send a POST request to client and return an error object -func (c *HTTPClient) HTTPPost(url string, body string) error { - _, err := c._HTTPRequest("POST", url, &body, nil) - 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) { - return c._HTTPRequest("POST", url, &body, nil) -} - -// HTTPPut Send a PUT request to client and return an error object -func (c *HTTPClient) HTTPPut(url string, body string) error { - _, err := c._HTTPRequest("PUT", url, &body, nil) - return err -} - -// HTTPPutWithRes Send a PUT request to client and return both response and error -func (c *HTTPClient) HTTPPutWithRes(url string, body string) (*http.Response, error) { - return c._HTTPRequest("PUT", url, &body, nil) -} - -// HTTPDelete Send a DELETE request to client and return an error object -func (c *HTTPClient) HTTPDelete(url string) error { - _, err := c._HTTPRequest("DELETE", url, nil, nil) - return err -} - -// HTTPDeleteWithRes Send a DELETE request to client and return both response and error -func (c *HTTPClient) HTTPDeleteWithRes(url string) (*http.Response, error) { - return c._HTTPRequest("DELETE", url, nil, nil) -} - -// ResponseToBArray converts an Http response to a byte array -func (c *HTTPClient) ResponseToBArray(response *http.Response) []byte { - defer response.Body.Close() - bytes, err := ioutil.ReadAll(response.Body) - if err != nil { - c.log(HTTPLogLevelError, "ResponseToBArray failure: %v", err.Error()) - } - return bytes -} - -/*** -** Private functions -***/ - -// _HTTPRequest Generic function used by high level function to send requests -func (c *HTTPClient) _Request(method string, url string, in interface{}, out interface{}) error { - var err error - var res *http.Response - var body []byte - if in != nil { - body, err = json.Marshal(in) - if err != nil { - return err - } - sb := string(body) - res, err = c._HTTPRequest(method, url, &sb, nil) - } else { - res, err = c._HTTPRequest(method, url, nil, nil) - } - if err != nil { - return err - } - if res.StatusCode != 200 { - return fmt.Errorf("HTTP status %s", res.Status) - } - - // Don't decode response if no out data pointer is nil - if out == nil { - return nil - } - return json.Unmarshal(c.ResponseToBArray(res), out) -} - -// _HTTPRequest Generic function that returns a new Request given a method, URL, and optional body and data. -func (c *HTTPClient) _HTTPRequest(method, url string, body *string, data *[]byte) (*http.Response, error) { - if !c.initDone { - if err := c.getCidAndCsrf(); err == nil { - c.initDone = true - } - } - - var err error - var request *http.Request - if body != nil { - request, err = http.NewRequest(method, c.formatURL(url), bytes.NewBufferString(*body)) - } else { - request, err = http.NewRequest(method, 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) - } - - if data != nil { - *data = c.ResponseToBArray(res) - } - - return res, nil -} - -func (c *HTTPClient) handleRequest(request *http.Request) (*http.Response, error) { - if c.conf.ContentType != "" { - request.Header.Set("Content-Type", c.conf.ContentType) - } - 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(HTTPLogLevelDebug, "HTTP %s %v", request.Method, request.URL) - response, err := c.httpClient.Do(request) - c.log(HTTPLogLevelDebug, "HTTP RESPONSE: %v\n", response) - if err != nil { - c.log(HTTPLogLevelInfo, "%v", err) - 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 c.id != "" && 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)) - } - 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 -} - -// 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, "/") -} - -// Send request to retrieve Client id and/or CSRF token -func (c *HTTPClient) getCidAndCsrf() error { - // Don't use cid + csrf when apikey is set - if c.apikey != "" { - return nil - } - 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 -} - -// log Internal logger function -func (c *HTTPClient) log(level int, format string, args ...interface{}) { - if level > c.LoggerLevel { - return - } - sLvl := strings.ToUpper(c.LogLevelToString(level)) - fmt.Fprintf(c.LoggerOut, sLvl+": "+c.LoggerPrefix+format+"\n", args...) -} diff --git a/httpclient.go b/httpclient.go new file mode 100644 index 0000000..f4880e6 --- /dev/null +++ b/httpclient.go @@ -0,0 +1,411 @@ +package common + +import ( + "bytes" + "crypto/tls" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + "strings" +) + +// HTTPClient . +type HTTPClient struct { + LoggerOut io.Writer + LoggerLevel int + LoggerPrefix string + + httpClient http.Client + initDone bool + endpoint string + apikey string + username string + password string + id string + csrf string + conf HTTPClientConfig +} + +// HTTPClientConfig is used to config HTTPClient +type HTTPClientConfig struct { + URLPrefix string + ContentType string + HeaderAPIKeyName string + Apikey string + HeaderClientKeyName string + CsrfDisable bool + LogOut io.Writer + LogLevel int + LogPrefix string +} + +// Logger levels constants +const ( + HTTPLogLevelPanic = 0 + HTTPLogLevelError = 1 + HTTPLogLevelWarning = 2 + HTTPLogLevelInfo = 3 + HTTPLogLevelDebug = 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, + }, + }, + } + + lOut := cfg.LogOut + if cfg.LogOut == nil { + lOut = os.Stdout + } + client := HTTPClient{ + LoggerOut: lOut, + LoggerLevel: cfg.LogLevel, + LoggerPrefix: cfg.LogPrefix, + + httpClient: httpClient, + initDone: false, + endpoint: baseURL, + apikey: cfg.Apikey, + conf: cfg, + /* TODO - add user + pwd support + username: c.GlobalString("username"), + password: c.GlobalString("password"), + */ + } + + // Default set Content-Type to json + if client.conf.ContentType == "" { + client.conf.ContentType = "application/json" + } + + if err := client.getCidAndCsrf(); err != nil { + client.log(HTTPLogLevelError, "Cannot retrieve Client ID and/or CSRF: %v", err) + return &client, err + } + + client.log(HTTPLogLevelDebug, "HTTP client url %s init Done", client.endpoint) + client.initDone = true + return &client, nil +} + +// GetLogLevel Get a readable string representing the log level +func (c *HTTPClient) GetLogLevel() string { + return c.LogLevelToString(c.LoggerLevel) +} + +// LogLevelToString Convert an integer log level to string +func (c *HTTPClient) LogLevelToString(lvl int) string { + switch lvl { + case HTTPLogLevelPanic: + return "panic" + case HTTPLogLevelError: + return "error" + case HTTPLogLevelWarning: + return "warning" + case HTTPLogLevelInfo: + return "info" + case HTTPLogLevelDebug: + return "debug" + } + return "Unknown" +} + +// SetLogLevel set the log level from a readable string +func (c *HTTPClient) SetLogLevel(lvl string) error { + switch strings.ToLower(lvl) { + case "panic": + c.LoggerLevel = HTTPLogLevelPanic + case "error": + c.LoggerLevel = HTTPLogLevelError + case "warn", "warning": + c.LoggerLevel = HTTPLogLevelWarning + case "info": + c.LoggerLevel = HTTPLogLevelInfo + case "debug": + c.LoggerLevel = HTTPLogLevelDebug + default: + return fmt.Errorf("Unknown level") + } + return nil +} + +// GetClientID returns the id +func (c *HTTPClient) GetClientID() string { + return c.id +} + +/*** +** High level functions +***/ + +// Get Send a Get request to client and return directly data of body response +func (c *HTTPClient) Get(url string, out interface{}) error { + return c._Request("GET", url, nil, out) +} + +// Post Send a Post request to client and return directly data of body response +func (c *HTTPClient) Post(url string, in interface{}, out interface{}) error { + return c._Request("POST", url, in, out) +} + +// Put Send a Put request to client and return directly data of body response +func (c *HTTPClient) Put(url string, in interface{}, out interface{}) error { + return c._Request("PUT", url, in, out) +} + +// Delete Send a Delete request to client and return directly data of body response +func (c *HTTPClient) Delete(url string, out interface{}) error { + return c._Request("DELETE", url, nil, out) +} + +/*** +** Low level functions +***/ + +// HTTPGet Send a Get request to client and return an error object +func (c *HTTPClient) HTTPGet(url string, data *[]byte) error { + _, err := c._HTTPRequest("GET", url, nil, 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) { + return c._HTTPRequest("GET", url, nil, data) +} + +// HTTPPost Send a POST request to client and return an error object +func (c *HTTPClient) HTTPPost(url string, body string) error { + _, err := c._HTTPRequest("POST", url, &body, nil) + 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) { + return c._HTTPRequest("POST", url, &body, nil) +} + +// HTTPPut Send a PUT request to client and return an error object +func (c *HTTPClient) HTTPPut(url string, body string) error { + _, err := c._HTTPRequest("PUT", url, &body, nil) + return err +} + +// HTTPPutWithRes Send a PUT request to client and return both response and error +func (c *HTTPClient) HTTPPutWithRes(url string, body string) (*http.Response, error) { + return c._HTTPRequest("PUT", url, &body, nil) +} + +// HTTPDelete Send a DELETE request to client and return an error object +func (c *HTTPClient) HTTPDelete(url string) error { + _, err := c._HTTPRequest("DELETE", url, nil, nil) + return err +} + +// HTTPDeleteWithRes Send a DELETE request to client and return both response and error +func (c *HTTPClient) HTTPDeleteWithRes(url string) (*http.Response, error) { + return c._HTTPRequest("DELETE", url, nil, nil) +} + +// ResponseToBArray converts an Http response to a byte array +func (c *HTTPClient) ResponseToBArray(response *http.Response) []byte { + defer response.Body.Close() + bytes, err := ioutil.ReadAll(response.Body) + if err != nil { + c.log(HTTPLogLevelError, "ResponseToBArray failure: %v", err.Error()) + } + return bytes +} + +/*** +** Private functions +***/ + +// _HTTPRequest Generic function used by high level function to send requests +func (c *HTTPClient) _Request(method string, url string, in interface{}, out interface{}) error { + var err error + var res *http.Response + var body []byte + if in != nil { + body, err = json.Marshal(in) + if err != nil { + return err + } + sb := string(body) + res, err = c._HTTPRequest(method, url, &sb, nil) + } else { + res, err = c._HTTPRequest(method, url, nil, nil) + } + if err != nil { + return err + } + if res.StatusCode != 200 { + return fmt.Errorf("HTTP status %s", res.Status) + } + + // Don't decode response if no out data pointer is nil + if out == nil { + return nil + } + return json.Unmarshal(c.ResponseToBArray(res), out) +} + +// _HTTPRequest Generic function that returns a new Request given a method, URL, and optional body and data. +func (c *HTTPClient) _HTTPRequest(method, url string, body *string, data *[]byte) (*http.Response, error) { + if !c.initDone { + if err := c.getCidAndCsrf(); err == nil { + c.initDone = true + } + } + + var err error + var request *http.Request + if body != nil { + request, err = http.NewRequest(method, c.formatURL(url), bytes.NewBufferString(*body)) + } else { + request, err = http.NewRequest(method, 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) + } + + if data != nil { + *data = c.ResponseToBArray(res) + } + + return res, nil +} + +func (c *HTTPClient) handleRequest(request *http.Request) (*http.Response, error) { + if c.conf.ContentType != "" { + request.Header.Set("Content-Type", c.conf.ContentType) + } + 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(HTTPLogLevelDebug, "HTTP %s %v", request.Method, request.URL) + response, err := c.httpClient.Do(request) + c.log(HTTPLogLevelDebug, "HTTP RESPONSE: %v\n", response) + if err != nil { + c.log(HTTPLogLevelInfo, "%v", err) + 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 c.id != "" && 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)) + } + 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 +} + +// 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, "/") +} + +// Send request to retrieve Client id and/or CSRF token +func (c *HTTPClient) getCidAndCsrf() error { + // Don't use cid + csrf when apikey is set + if c.apikey != "" { + return nil + } + 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 +} + +// log Internal logger function +func (c *HTTPClient) log(level int, format string, args ...interface{}) { + if level > c.LoggerLevel { + return + } + sLvl := strings.ToUpper(c.LogLevelToString(level)) + fmt.Fprintf(c.LoggerOut, sLvl+": "+c.LoggerPrefix+format+"\n", args...) +} -- cgit 1.2.3-korg