func (c *Client) Stream(path string, headers map[string]string, in io.Reader, out io.WriteCloser) error { origin := fmt.Sprintf("https://%s", c.Host) endpoint := fmt.Sprintf("wss://%s%s", c.Host, path) config, err := websocket.NewConfig(endpoint, origin) if err != nil { return err } config.TlsConfig = &tls.Config{ InsecureSkipVerify: true, } config.Header.Set("Version", c.Version) userpass := fmt.Sprintf("convox:%s", c.Password) userpass_encoded := base64.StdEncoding.EncodeToString([]byte(userpass)) config.Header.Add("Authorization", fmt.Sprintf("Basic %s", userpass_encoded)) for k, v := range headers { config.Header.Add(k, v) } config.TlsConfig = &tls.Config{ InsecureSkipVerify: true, } var ws *websocket.Conn if proxy := os.Getenv("HTTPS_PROXY"); proxy != "" { ws, err = c.proxyWebsocket(config, proxy) } else { ws, err = websocket.DialConfig(config) } if err != nil { return err } defer ws.Close() var wg sync.WaitGroup if in != nil { go io.Copy(ws, in) } if out != nil { wg.Add(1) go copyAsync(out, ws, &wg) } wg.Wait() out.Close() return nil }
func AppLogs(ws *websocket.Conn) *httperr.Error { app := mux.Vars(ws.Request())["app"] a, err := models.GetApp(app) if awsError(err) == "ValidationError" { return httperr.Errorf(404, "no such app: %s", app) } if err != nil { return httperr.Server(err) } logs := make(chan []byte) done := make(chan bool) a.SubscribeLogs(logs, done) go signalWsClose(ws, done) for data := range logs { ws.Write(data) } return nil }
// Sends "true" to the done channel when either // the websocket is closed or after a timeout func signalWsClose(ws *websocket.Conn, done chan bool) { buf := make([]byte, 0) expires := time.Now().Add(RequestTimeout) for { _, err := ws.Read(buf) expired := time.Now().After(expires) if err == io.EOF || expired { done <- true return } } }
func keepAlive(ws *websocket.Conn, quit chan bool) { c := time.Tick(5 * time.Second) b := []byte{} for { select { case <-c: ws.Write(b) case <-quit: return } } }
func BuildLogs(ws *websocket.Conn) *httperr.Error { vars := mux.Vars(ws.Request()) app := vars["app"] build := vars["build"] _, err := models.GetApp(app) if awsError(err) == "ValidationError" { return httperr.Errorf(404, "no such app: %s", app) } _, err = models.GetBuild(app, build) if err != nil { return httperr.Server(err) } // proxy to docker container logs // https://docs.docker.com/reference/api/docker_remote_api_v1.19/#get-container-logs client, err := docker.NewClient("unix:///var/run/docker.sock") if err != nil { return httperr.Server(err) } r, w := io.Pipe() quit := make(chan bool) go scanLines(r, ws) go keepAlive(ws, quit) err = client.Logs(docker.LogsOptions{ Container: fmt.Sprintf("build-%s", build), Follow: true, Stdout: true, Stderr: true, Tail: "all", RawTerminal: false, OutputStream: w, ErrorStream: w, }) quit <- true return httperr.Server(err) }
func ProcessExecAttached(ws *websocket.Conn) *httperr.Error { vars := mux.Vars(ws.Request()) app := vars["app"] pid := vars["pid"] command := ws.Request().Header.Get("Command") a, err := models.GetApp(app) if awsError(err) == "ValidationError" { return httperr.Errorf(404, "no such app: %s", app) } if err != nil { return httperr.Server(err) } return httperr.Server(a.ExecAttached(pid, command, ws)) }
func ServiceLogs(ws *websocket.Conn) *httperr.Error { service := mux.Vars(ws.Request())["service"] s, err := models.GetService(service) if err != nil { return httperr.Server(err) } logs := make(chan []byte) done := make(chan bool) s.SubscribeLogs(logs, done) go signalWsClose(ws, done) for data := range logs { ws.Write(data) } return nil }
func ProcessRunAttached(ws *websocket.Conn) *httperr.Error { vars := mux.Vars(ws.Request()) header := ws.Request().Header app := vars["app"] process := vars["process"] command := header.Get("Command") height, _ := strconv.Atoi(header.Get("Height")) width, _ := strconv.Atoi(header.Get("Width")) a, err := models.GetApp(app) if awsError(err) == "ValidationError" { return httperr.Errorf(404, "no such app: %s", app) } if err != nil { return httperr.Server(err) } return httperr.Server(a.RunAttached(process, command, height, width, ws)) }
func InstanceSSH(ws *websocket.Conn) *httperr.Error { vars := mux.Vars(ws.Request()) id := vars["id"] cmd := ws.Request().Header.Get("Command") term := ws.Request().Header.Get("Terminal") var height, width int var err error if term != "" { height, err = strconv.Atoi(ws.Request().Header.Get("Height")) if err != nil { return httperr.Server(err) } width, err = strconv.Atoi(ws.Request().Header.Get("Width")) if err != nil { return httperr.Server(err) } } return httperr.Server(models.InstanceSSH(id, cmd, term, height, width, ws)) }
func Proxy(ws *websocket.Conn) *httperr.Error { vars := mux.Vars(ws.Request()) host := vars["host"] port := vars["port"] conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%s", host, port), 3*time.Second) if neterr, ok := err.(net.Error); ok && neterr.Timeout() { return httperr.Errorf(403, "timeout") } if err != nil { return httperr.Server(err) } var wg sync.WaitGroup wg.Add(2) go copyAsync(ws, conn, &wg) go copyAsync(conn, ws, &wg) wg.Wait() return nil }
func scanLines(r io.Reader, ws *websocket.Conn) { scanner := bufio.NewScanner(r) for scanner.Scan() { parts := strings.SplitN(scanner.Text(), "|", 2) if len(parts) < 2 { ws.Write([]byte(parts[0] + "\n")) continue } switch parts[0] { case "manifest": case "error": ws.Write([]byte(parts[1] + "\n")) default: ws.Write([]byte(parts[1] + "\n")) } } }
func BuildLogs(ws *websocket.Conn) *httperr.Error { vars := mux.Vars(ws.Request()) app := vars["app"] build := vars["build"] _, err := models.GetApp(app) if awsError(err) == "ValidationError" { return httperr.Errorf(404, "no such app: %s", app) } _, err = models.GetBuild(app, build) if err != nil { return httperr.Server(err) } // default to local docker socket host := "unix:///var/run/docker.sock" // in production loop through docker hosts that the rack is running on // to find the build if os.Getenv("DEVELOPMENT") != "true" { pss, err := models.ListProcesses(os.Getenv("RACK")) if err != nil { return httperr.Server(err) } for _, ps := range pss { client, err := ps.Docker() if err != nil { return httperr.Server(err) } res, err := client.ListContainers(docker.ListContainersOptions{ All: true, Filters: map[string][]string{ "name": []string{fmt.Sprintf("build-%s", build)}, }, }) if len(res) > 0 { host = fmt.Sprintf("http://%s:2376", ps.Host) break } } } fmt.Printf("host %+v\n", host) // proxy to docker container logs // https://docs.docker.com/reference/api/docker_remote_api_v1.19/#get-container-logs client, err := docker.NewClient(host) if err != nil { return httperr.Server(err) } r, w := io.Pipe() quit := make(chan bool) go scanLines(r, ws) go keepAlive(ws, quit) err = client.Logs(docker.LogsOptions{ Container: fmt.Sprintf("build-%s", build), Follow: true, Stdout: true, Stderr: true, Tail: "all", RawTerminal: false, OutputStream: w, ErrorStream: w, }) quit <- true return httperr.Server(err) }