diff options
author | Sebastien Douheret <sebastien.douheret@iot.bzh> | 2017-05-11 19:42:00 +0200 |
---|---|---|
committer | Sebastien Douheret <sebastien.douheret@iot.bzh> | 2017-05-11 19:42:22 +0200 |
commit | ec7051e1da665206f594c7616ad381bfeaea333a (patch) | |
tree | ecc01ee358794c9d8c5fbb87d2f5b6ce3f60f431 /lib/common | |
parent | ca3e1762832b27dc25cf90125b376c56e24e2db2 (diff) |
Initial main commit.
Signed-off-by: Sebastien Douheret <sebastien.douheret@iot.bzh>
Diffstat (limited to 'lib/common')
-rw-r--r-- | lib/common/error.go | 13 | ||||
-rw-r--r-- | lib/common/execPipeWs.go | 148 | ||||
-rw-r--r-- | lib/common/httpclient.go | 221 |
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 +} |