summaryrefslogtreecommitdiffstats
path: root/lib/common
diff options
context:
space:
mode:
authorSebastien Douheret <sebastien.douheret@iot.bzh>2017-05-11 19:42:00 +0200
committerSebastien Douheret <sebastien.douheret@iot.bzh>2017-05-11 19:42:22 +0200
commitec7051e1da665206f594c7616ad381bfeaea333a (patch)
treeecc01ee358794c9d8c5fbb87d2f5b6ce3f60f431 /lib/common
parentca3e1762832b27dc25cf90125b376c56e24e2db2 (diff)
Initial main commit.
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
Diffstat (limited to 'lib/common')
-rw-r--r--lib/common/error.go13
-rw-r--r--lib/common/execPipeWs.go148
-rw-r--r--lib/common/httpclient.go221
3 files changed, 382 insertions, 0 deletions
diff --git a/lib/common/error.go b/lib/common/error.go
new file mode 100644
index 0000000..d03c176
--- /dev/null
+++ b/lib/common/error.go
@@ -0,0 +1,13 @@
+package common
+
+import (
+ "github.com/gin-gonic/gin"
+)
+
+// APIError returns an uniform json formatted error
+func APIError(c *gin.Context, err string) {
+ c.JSON(500, gin.H{
+ "status": "error",
+ "error": err,
+ })
+}
diff --git a/lib/common/execPipeWs.go b/lib/common/execPipeWs.go
new file mode 100644
index 0000000..3b63cdc
--- /dev/null
+++ b/lib/common/execPipeWs.go
@@ -0,0 +1,148 @@
+package common
+
+import (
+ "bufio"
+ "fmt"
+ "io"
+ "os"
+ "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)
+
+// EmitExitCB is the function callback used to emit exit proc code
+type EmitExitCB func(sid string, cmdID int, code int, err error)
+
+// 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, so *socketio.Socket, sid string, cmdID int,
+ cmdExecTimeout int, log *logrus.Logger, eoCB EmitOutputCB, eeCB EmitExitCB) 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", cmd}
+ proc, err := os.StartProcess("/bin/bash", bashArgs, &os.ProcAttr{
+ Files: []*os.File{inr, outw, outw},
+ })
+ 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)
+
+ // Blocking function that poll input or wait for end of process
+ cmdPumpStdin(so, inw, proc, sid, cmdID, cmdExecTimeout, log, eeCB)
+
+ // 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) {
+ /* 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)
+ case <-time.After(time.Duration(tmo) * time.Second):
+ exitFuncCB(sid, cmdID, -99,
+ fmt.Errorf("Exit Timeout for command ID %v", cmdID))
+ }
+}
+
+func cmdPumpStdout(so *socketio.Socket, r io.Reader, done chan struct{},
+ sid string, cmdID int, log *logrus.Logger, emitFuncCB EmitOutputCB) {
+ defer func() {
+ }()
+
+ sc := bufio.NewScanner(r)
+ for sc.Scan() {
+ emitFuncCB(sid, cmdID, string(sc.Bytes()), "")
+ }
+ if sc.Err() != nil {
+ log.Errorln("scan:", sc.Err())
+ }
+ close(done)
+}
diff --git a/lib/common/httpclient.go b/lib/common/httpclient.go
new file mode 100644
index 0000000..40d7bc2
--- /dev/null
+++ b/lib/common/httpclient.go
@@ -0,0 +1,221 @@
+package common
+
+import (
+ "bytes"
+ "crypto/tls"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "strings"
+)
+
+type HTTPClient struct {
+ httpClient http.Client
+ endpoint string
+ apikey string
+ username string
+ password string
+ id string
+ csrf string
+ conf HTTPClientConfig
+}
+
+type HTTPClientConfig struct {
+ URLPrefix string
+ HeaderAPIKeyName string
+ Apikey string
+ HeaderClientKeyName string
+ CsrfDisable bool
+}
+
+// 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
+}
+
+// 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)
+ }
+
+ 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
+}