diff --git a/cmd/launcher/control.go b/cmd/launcher/control.go index bf3e12aa8..dd5c58063 100644 --- a/cmd/launcher/control.go +++ b/cmd/launcher/control.go @@ -19,7 +19,6 @@ func createControl(ctx context.Context, db *bolt.DB, logger log.Logger, opts *la controlOpts := []control.Option{ control.WithLogger(logger), - control.WithGetShellsInterval(opts.GetShellsInterval), } if opts.InsecureTLS { controlOpts = append(controlOpts, control.WithInsecureSkipVerify()) diff --git a/cmd/launcher/options.go b/cmd/launcher/options.go index de28aee66..350c2f653 100644 --- a/cmd/launcher/options.go +++ b/cmd/launcher/options.go @@ -45,22 +45,21 @@ func parseOptions(args []string) (*launcher.Options, error) { var ( // Primary options - flCertPins = flagset.String("cert_pins", "", "Comma separated, hex encoded SHA256 hashes of pinned subject public key info") - flControl = flagset.Bool("control", false, "Whether or not the control server is enabled (default: false)") - flControlServerURL = flagset.String("control_hostname", "", "The hostname of the control server") - flEnrollSecret = flagset.String("enroll_secret", "", "The enroll secret that is used in your environment") - flEnrollSecretPath = flagset.String("enroll_secret_path", "", "Optionally, the path to your enrollment secret") - flGetShellsInterval = flagset.Duration("control_get_shells_interval", 60*time.Second, "The interval at which the 'get shells' request will be made") - flInitialRunner = flagset.Bool("with_initial_runner", false, "Run differential queries from config ahead of scheduled interval.") - flKolideServerURL = flagset.String("hostname", "", "The hostname of the gRPC server") - flTransport = flagset.String("transport", "grpc", "The transport protocol that should be used to communicate with remote (default: grpc)") - flLoggingInterval = flagset.Duration("logging_interval", 60*time.Second, "The interval at which logs should be flushed to the server") - flOsquerydPath = flagset.String("osqueryd_path", "", "Path to the osqueryd binary to use (Default: find osqueryd in $PATH)") - flRootDirectory = flagset.String("root_directory", "", "The location of the local database, pidfiles, etc.") - flRootPEM = flagset.String("root_pem", "", "Path to PEM file including root certificates to verify against") - flVersion = flagset.Bool("version", false, "Print Launcher version and exit") - flOsqueryFlags arrayFlags // set below with flagset.Var - _ = flagset.String("config", "", "config file to parse options from (optional)") + flCertPins = flagset.String("cert_pins", "", "Comma separated, hex encoded SHA256 hashes of pinned subject public key info") + flControl = flagset.Bool("control", false, "Whether or not the control server is enabled (default: false)") + flControlServerURL = flagset.String("control_hostname", "", "The hostname of the control server") + flEnrollSecret = flagset.String("enroll_secret", "", "The enroll secret that is used in your environment") + flEnrollSecretPath = flagset.String("enroll_secret_path", "", "Optionally, the path to your enrollment secret") + flInitialRunner = flagset.Bool("with_initial_runner", false, "Run differential queries from config ahead of scheduled interval.") + flKolideServerURL = flagset.String("hostname", "", "The hostname of the gRPC server") + flTransport = flagset.String("transport", "grpc", "The transport protocol that should be used to communicate with remote (default: grpc)") + flLoggingInterval = flagset.Duration("logging_interval", 60*time.Second, "The interval at which logs should be flushed to the server") + flOsquerydPath = flagset.String("osqueryd_path", "", "Path to the osqueryd binary to use (Default: find osqueryd in $PATH)") + flRootDirectory = flagset.String("root_directory", "", "The location of the local database, pidfiles, etc.") + flRootPEM = flagset.String("root_pem", "", "Path to PEM file including root certificates to verify against") + flVersion = flagset.Bool("version", false, "Print Launcher version and exit") + flOsqueryFlags arrayFlags // set below with flagset.Var + _ = flagset.String("config", "", "config file to parse options from (optional)") // Autoupdate options flAutoupdate = flagset.Bool("autoupdate", false, "Whether or not the osquery autoupdater is enabled (default: false)") @@ -159,7 +158,6 @@ func parseOptions(args []string) (*launcher.Options, error) { EnableInitialRunner: *flInitialRunner, EnrollSecret: *flEnrollSecret, EnrollSecretPath: *flEnrollSecretPath, - GetShellsInterval: *flGetShellsInterval, InsecureTLS: *flInsecureTLS, InsecureTransport: *flInsecureTransport, KolideServerURL: *flKolideServerURL, diff --git a/cmd/launcher/options_test.go b/cmd/launcher/options_test.go index 523a4a9b0..895acbcc7 100644 --- a/cmd/launcher/options_test.go +++ b/cmd/launcher/options_test.go @@ -105,7 +105,6 @@ func getArgsAndResponse() (map[string]string, *launcher.Options) { Control: true, OsquerydPath: windowsAddExe("/dev/null"), KolideServerURL: randomHostname, - GetShellsInterval: 60 * time.Second, LoggingInterval: time.Duration(randomInt) * time.Second, AutoupdateInterval: 48 * time.Hour, NotaryServerURL: "https://notary.kolide.co", diff --git a/pkg/control/control.go b/pkg/control/control.go index 2d83f48a1..c6e5d08f5 100644 --- a/pkg/control/control.go +++ b/pkg/control/control.go @@ -6,7 +6,6 @@ import ( "encoding/json" "net/http" "net/url" - "time" "github.com/boltdb/bolt" "github.com/go-kit/kit/log" @@ -14,15 +13,14 @@ import ( ) type Client struct { - addr string - baseURL *url.URL - cancel context.CancelFunc - client *http.Client - db *bolt.DB - getShellsInterval time.Duration - insecure bool - disableTLS bool - logger log.Logger + addr string + baseURL *url.URL + cancel context.CancelFunc + client *http.Client + db *bolt.DB + insecure bool + disableTLS bool + logger log.Logger } func NewControlClient(db *bolt.DB, addr string, opts ...Option) (*Client, error) { @@ -31,12 +29,11 @@ func NewControlClient(db *bolt.DB, addr string, opts ...Option) (*Client, error) return nil, errors.Wrap(err, "parsing URL") } c := &Client{ - logger: log.NewNopLogger(), - baseURL: baseURL, - client: http.DefaultClient, - db: db, - addr: addr, - getShellsInterval: 5 * time.Second, + logger: log.NewNopLogger(), + baseURL: baseURL, + client: http.DefaultClient, + db: db, + addr: addr, } for _, opt := range opts { @@ -52,13 +49,10 @@ func NewControlClient(db *bolt.DB, addr string, opts ...Option) (*Client, error) func (c *Client) Start(ctx context.Context) { ctx, c.cancel = context.WithCancel(ctx) - getShellsTicker := time.NewTicker(c.getShellsInterval) for { select { case <-ctx.Done(): return - case <-getShellsTicker.C: - c.getShells(ctx) } } } diff --git a/pkg/control/option.go b/pkg/control/option.go index b5c22d1db..df06d86be 100644 --- a/pkg/control/option.go +++ b/pkg/control/option.go @@ -3,7 +3,6 @@ package control import ( "crypto/tls" "net/http" - "time" "github.com/go-kit/kit/log" ) @@ -27,12 +26,6 @@ func WithInsecureSkipVerify() Option { } } -func WithGetShellsInterval(i time.Duration) Option { - return func(c *Client) { - c.getShellsInterval = i - } -} - func WithDisableTLS() Option { return func(c *Client) { c.disableTLS = true diff --git a/pkg/control/shells.go b/pkg/control/shells.go deleted file mode 100644 index 326a68b2f..000000000 --- a/pkg/control/shells.go +++ /dev/null @@ -1,154 +0,0 @@ -package control - -import ( - "context" - "encoding/json" - "net/http" - - "github.com/go-kit/kit/log/level" - "github.com/kolide/launcher/pkg/osquery" - "github.com/kolide/launcher/pkg/ptycmd" - "github.com/kolide/launcher/pkg/webtty" - "github.com/kolide/launcher/pkg/wsrelay" -) - -type getShellsRequest struct { - NodeKey string `json:"node_key"` -} - -type getShellsResponse struct { - Sessions []map[string]string `json:"sessions"` - Err string `json:"error,omitempty"` - NodeInvalid bool `json:"node_invalid,omitempty"` -} - -func (c *Client) getShells(ctx context.Context) { - nodeKey, err := osquery.NodeKeyFromDB(c.db) - if err != nil { - level.Debug(c.logger).Log( - "msg", "error getting node key from db to request shells", - "err", err, - ) - return - } - - verb, path := "POST", "/api/v1/shells" - params := &getShellsRequest{ - NodeKey: nodeKey, - } - response, err := c.do(verb, path, params) - if err != nil { - level.Debug(c.logger).Log( - "msg", "error making request to get shells endpoint", - "err", err, - ) - return - } - defer response.Body.Close() - - switch response.StatusCode { - case http.StatusNotFound: - level.Debug(c.logger).Log( - "msg", "got 404 making get shells request", - "err", err, - ) - return - } - - if response.StatusCode != http.StatusOK { - level.Debug(c.logger).Log( - "msg", "got not-ok status code getting shells", - "response_code", response.StatusCode, - ) - return - } - - var responseBody getShellsResponse - if err := json.NewDecoder(response.Body).Decode(&responseBody); err != nil { - level.Debug(c.logger).Log( - "msg", "error decoding get shells json", - "err", err, - ) - return - } - - if responseBody.Err != "" { - level.Debug(c.logger).Log( - "msg", "response body contained error", - "err", responseBody.Err, - ) - return - } - - if len(responseBody.Sessions) > 0 { - level.Debug(c.logger).Log( - "msg", "found shell session requests", - "count", len(responseBody.Sessions), - ) - - // for every shell, handle the shell in a goroutine - for _, session := range responseBody.Sessions { - go c.connectToShell(ctx, path, session) - } - } -} - -func (c *Client) connectToShell(ctx context.Context, path string, session map[string]string) { - room, ok := session["session_id"] - if !ok { - level.Debug(c.logger).Log( - "msg", "session didn't contain id", - ) - return - } - - secret, ok := session["secret"] - if !ok { - level.Debug(c.logger).Log( - "msg", "session didn't contain secret", - ) - return - } - - wsPath := path + "/" + room - client, err := wsrelay.NewClient(c.addr, wsPath, c.disableTLS, c.insecure) - if err != nil { - level.Debug(c.logger).Log( - "msg", "error creating client", - "err", err, - ) - return - } - defer client.Close() - - pty, err := ptycmd.NewCmd("/bin/bash", []string{"--login"}) - if err != nil { - level.Debug(c.logger).Log( - "msg", "error creating PTY command", - "err", err, - ) - return - } - - TTY, err := webtty.New( - client, - pty, - secret, - webtty.WithPermitWrite(), - webtty.WithLogger(c.logger), - webtty.WithKeepAliveDeadline(), - ) - if err != nil { - level.Debug(c.logger).Log( - "msg", "error creating TTY", - "err", err, - ) - } - if err := TTY.Run(ctx); err != nil { - level.Debug(c.logger).Log( - "msg", "error running TTY", - "err", err, - ) - return - } -} diff --git a/pkg/launcher/options.go b/pkg/launcher/options.go index a73999003..1d3aa1295 100644 --- a/pkg/launcher/options.go +++ b/pkg/launcher/options.go @@ -40,9 +40,6 @@ type Options struct { Control bool // ControlServerURL URL for control server. ControlServerURL string - // GetShellsInterval is the interval at which the control server should - // be checked for shells. - GetShellsInterval time.Duration // Autoupdate enables the autoupdate functionality. Autoupdate bool diff --git a/pkg/webtty/option.go b/pkg/webtty/option.go deleted file mode 100644 index ae99bbd4f..000000000 --- a/pkg/webtty/option.go +++ /dev/null @@ -1,86 +0,0 @@ -package webtty - -import ( - "encoding/json" - "time" - - "github.com/go-kit/kit/log" - "github.com/pkg/errors" -) - -// Option is an option for WebTTY. -type Option func(*WebTTY) error - -// WithPermitWrite sets a WebTTY to accept input from the TTY. -func WithPermitWrite() Option { - return func(wt *WebTTY) error { - wt.permitWrite = true - return nil - } -} - -// WithFixedColumns sets a fixed width to the TTY. -func WithFixedColumns(columns int) Option { - return func(wt *WebTTY) error { - wt.columns = columns - return nil - } -} - -// WithFixedRows sets a fixed height to the TTY. -func WithFixedRows(rows int) Option { - return func(wt *WebTTY) error { - wt.rows = rows - return nil - } -} - -// WithTitle sets the default window title of the session -func WithTitle(title []byte) Option { - return func(wt *WebTTY) error { - wt.title = title - return nil - } -} - -// WithReconnect enables reconnection on the TTY side. -func WithReconnect(timeInSeconds int) Option { - return func(wt *WebTTY) error { - wt.reconnect = timeInSeconds - return nil - } -} - -// WithTTYPreferences sets an optional configuration of TTY. -func WithTTYPreferences(preferences interface{}) Option { - return func(wt *WebTTY) error { - prefs, err := json.Marshal(preferences) - if err != nil { - return errors.Wrapf(err, "failed to marshal preferences as JSON") - } - wt.ttyPreferences = prefs - return nil - } -} - -// WithKeepAliveDeadline specifies the duration to time out -// after the last message was received -func WithKeepAliveDeadline() Option { - // TODO: make this a flag and not a magic number - deadline := time.Second * 30 - return func(wt *WebTTY) error { - wt.deadliner = deadliner{ - period: deadline, - ticker: *time.NewTicker(deadline), - } - return nil - } -} - -// WithLogger sets the logger to use -func WithLogger(logger log.Logger) Option { - return func(wt *WebTTY) error { - wt.logger = logger - return nil - } -} diff --git a/pkg/webtty/webtty.go b/pkg/webtty/webtty.go deleted file mode 100644 index 94c145c89..000000000 --- a/pkg/webtty/webtty.go +++ /dev/null @@ -1,350 +0,0 @@ -package webtty - -import ( - "context" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "sync" - "time" - - "github.com/go-kit/kit/log" - "github.com/go-kit/kit/log/level" - "github.com/pkg/errors" -) - -var ( - // ErrPTYClosed indicates the function has exited by the pty - ErrPTYClosed = errors.New("PTY closed") - - // ErrTTYClosed is returned when the tty connection is closed. - ErrTTYClosed = errors.New("TTY closed") -) - -const ( - // Protocol defines the name of this protocol, - // which is supposed to be used to the subprotocol of Websocket streams. - Protocol = "webtty" - - // INCOMING MESSAGE TYPES - - // AuthenticateTTY is sent by the TTY to negotiate authentication - AuthenticateTTY = '0' - // Input is user input typically from a keyboard - Input = '1' - // Ping to the server - Ping = '2' - // Resize is to notify that the TTY size has been changed - Resize = '3' - - // OUTGOING MESSAGE TYPES - - // AuthenticatePTY is sent by the PTY to negotiate authentication - AuthenticatePTY = '0' - // Output is normal output to the terminal - Output = '1' - // Pong to the browser - Pong = '2' - // SetTitle of the terminal - SetTitle = '3' - // SetPreferences of the terminal - SetPreferences = '4' - // SetReconnect is to signal the terminal to reconnect - SetReconnect = '5' -) - -// WebTTY bridges a PTY pty and its PTY tty. -// To support text-based streams and side channel commands such as -// terminal resizing, WebTTY uses an original protocol. -type WebTTY struct { - // the attached TTY - tty TTY - - // the PTY that the TTY is proxied to - pty PTY - - // the secret that the TTY uses to authenticate - secret string - - // the title of the TTY - title []byte - - // allow writes to this TTY - permitWrite bool - - // TTY width - columns int - - // TTY height - rows int - - keepAlive chan bool - - // the deadliner - deadliner deadliner - - // how many seconds to reconnect after - reconnect int - - // JSON preferences for the TTY - ttyPreferences []byte - - // size of buffer for messages - bufferSize int - - // lock to ensure threadsafe - writeMutex sync.Mutex - - // injectable, structured logger - logger log.Logger -} - -// deadliner is a ticker that ticks after the keep alive duration, -// and gets reset when we get a ping from the client -type deadliner struct { - period time.Duration - ticker time.Ticker -} - -// TTY represents a TTY connection, typically a websocket -type TTY io.ReadWriteCloser - -// PTY represents a PTY pty, typically it's a local command. -type PTY interface { - io.ReadWriteCloser - - // Get the title for the TTY - Title() string - - // Resize the attached PTY - Resize(columns int, rows int) error -} - -// New creates a new instance of WebTTY, -// given a connection to the TTY and a PTY to connect it to. -func New(tty TTY, pty PTY, secret string, options ...Option) (*WebTTY, error) { - wt := &WebTTY{ - tty: tty, - pty: pty, - secret: secret, - title: []byte(pty.Title()), - permitWrite: false, - columns: 0, - rows: 0, - bufferSize: 2048, - keepAlive: make(chan bool), - logger: log.NewNopLogger(), - } - - for _, option := range options { - option(wt) - } - - return wt, nil -} - -// Run starts the main process of the WebTTY. -// This method blocks until the context is canceled. -// Note that the tty and pty are left intact even -// after the context is canceled. Closing them is caller's -// responsibility. -// If the connection to one end gets closed, returns ErrPTYClosed or ErrTTYCLosed. -func (wt *WebTTY) Run(ctx context.Context) error { - // send message to TTY to initialize - // a title, reconnect time, and preferences - err := wt.sendInitializeMessage() - if err != nil { - return errors.Wrapf(err, "failed to send initializing message") - } - - // make a channel to return errors over - errs := make(chan error, 2) - - // spawn goroutine to relay PTY messages to the TTY - go func() { - errs <- func() error { - buffer := make([]byte, wt.bufferSize) - for { - n, err := wt.pty.Read(buffer) - if err != nil { - return ErrPTYClosed - } - - err = wt.relayToTTY(buffer[:n]) - if err != nil { - return err - } - } - }() - }() - - // spawn goroutine to relay TTY messages to the PTY - go func() { - errs <- func() error { - buffer := make([]byte, wt.bufferSize) - for { - n, err := wt.tty.Read(buffer) - if err != nil { - return ErrTTYClosed - } - - // we received a message, so reset the deadliner - wt.keepAlive <- true - - err = wt.relayToPTY(buffer[:n]) - if err != nil { - return err - } - } - }() - }() - - // wait for the context to be closed, returning any errors - for { - select { - - case <-ctx.Done(): - err = ctx.Err() - return err - case <-wt.keepAlive: - wt.deadliner.Reset() - case <-wt.deadliner.ticker.C: - level.Debug(wt.logger).Log( - "msg", "tty deadline exceeded", - "title", wt.title, - ) - wt.tty.Close() - wt.pty.Close() - return nil - case err = <-errs: - return err - } - } -} - -// sendInitializeMessage sends the title, reconnect time, -// and preferences to the TTY on startup -func (wt *WebTTY) sendInitializeMessage() error { - // send the authticate message with the secret - if err := wt.writeTTY(append([]byte{AuthenticatePTY}, wt.secret...)); err != nil { - return errors.Wrapf(err, "failed to authenticate") - } - - // send the settitle message - if err := wt.writeTTY(append([]byte{SetTitle}, wt.title...)); err != nil { - return errors.Wrapf(err, "failed to send window title") - } - - if wt.reconnect > 0 { - reconnect, _ := json.Marshal(wt.reconnect) - err := wt.writeTTY(append([]byte{SetReconnect}, reconnect...)) - if err != nil { - return errors.Wrapf(err, "failed to set reconnect") - } - } - - if wt.ttyPreferences != nil { - err := wt.writeTTY(append([]byte{SetPreferences}, wt.ttyPreferences...)) - if err != nil { - return errors.Wrapf(err, "failed to set preferences") - } - } - - return nil -} - -// relayToTTY encodes the message and writes to the TTY -func (wt *WebTTY) relayToTTY(data []byte) error { - safeMessage := base64.StdEncoding.EncodeToString(data) - err := wt.writeTTY(append([]byte{Output}, []byte(safeMessage)...)) - if err != nil { - return errors.Wrapf(err, "failed to send message to tty") - } - - return nil -} - -// writeTTY writes to the TTY in a threadsafe way -func (wt *WebTTY) writeTTY(data []byte) error { - // lock when writing to not clobber messages - wt.writeMutex.Lock() - defer wt.writeMutex.Unlock() - - // write the data to the TTY - _, err := wt.tty.Write(data) - if err != nil { - return errors.Wrapf(err, "failed to write to tty") - } - - return nil -} - -// relayToPTY handles writing different message types from the TTY -// and writing any input to the PTY -func (wt *WebTTY) relayToPTY(data []byte) error { - // make sure the read yielded data - if len(data) == 0 { - return errors.New("unexpected zero length read from tty") - } - - switch data[0] { - // handle input - case Input: - // check if we can write to the webTTY - if !wt.permitWrite { - return nil - } - - // make sure there's data to send and not an empty input message - if len(data) < 2 { - return nil - } - - // write the data to the pty - _, err := wt.pty.Write(data[1:]) - if err != nil { - return errors.Wrapf(err, "failed to write received data to pty") - } - - // handle a ping by returning a pong - case Ping: - err := wt.writeTTY([]byte{Pong}) - if err != nil { - return errors.Wrapf(err, "failed to return Pong message to tty") - } - - // handle a resize message - case Resize: - // don't set if the payload for resize is empty - if len(data) < 2 { - return errors.New("received malformed remote command for terminal resize: empty payload") - } - - // read the json payload to resize - var args struct { - Columns float64 - Rows float64 - } - err := json.Unmarshal(data[1:], &args) - if err != nil { - return errors.Wrapf(err, "received malformed data for terminal resize") - } - - wt.pty.Resize(int(args.Columns), int(args.Rows)) - - // catch all to handle unknown messages - default: - level.Info(wt.logger).Log( - "msg", "unknown message type", - "type", fmt.Sprintf("%c", data[0]), - ) - } - - return nil -} - -func (d *deadliner) Reset() { - d.ticker.Stop() - d.ticker = *time.NewTicker(d.period) -}