github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/kbnm/handler.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"bytes"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"os"
    10  	"os/exec"
    11  	"regexp"
    12  	"strings"
    13  )
    14  
    15  var errInvalidMethod = errors.New("invalid method")
    16  
    17  var errInvalidInput = errors.New("invalid input")
    18  
    19  var errMissingField = errors.New("missing field")
    20  
    21  var errUserNotFound = errors.New("user not found")
    22  
    23  var errKeybaseNotRunning = errors.New("keybase is not running")
    24  
    25  var errKeybaseNotLoggedIn = errors.New("keybase is not logged in")
    26  
    27  type errUnexpected struct {
    28  	value string
    29  }
    30  
    31  func (err *errUnexpected) Error() string {
    32  	return fmt.Sprintf("unexpected error: %s", err.value)
    33  }
    34  
    35  func execRunner(cmd *exec.Cmd) error {
    36  	return cmd.Run()
    37  }
    38  
    39  // reUsernameQuery matches valid username queries
    40  var reUsernameQuery = regexp.MustCompile(`^[a-zA-Z0-9_\-.:@]{1,256}$`)
    41  
    42  // checkUsernameQuery returns the query if it's valid to use
    43  // We return the valid string so that it can be used as a separate
    44  // pre-validated variable which makes it less likely that the validation will
    45  // be accidentally removed in the future, as opposed to if we just continued
    46  // using the input variable after validating it.
    47  func checkUsernameQuery(s string) (string, error) {
    48  	if s == "" {
    49  		return "", errMissingField
    50  	}
    51  	if !reUsernameQuery.MatchString(s) {
    52  		return "", errInvalidInput
    53  	}
    54  	return s, nil
    55  }
    56  
    57  // newHandler returns a request handler.
    58  func newHandler() *handler {
    59  	return &handler{
    60  		Run: execRunner,
    61  		FindKeybaseBinary: func() (string, error) {
    62  			return findKeybaseBinary(keybaseBinary)
    63  		},
    64  	}
    65  }
    66  
    67  type handler struct {
    68  	// Run wraps the equivalent of cmd.Run(), allowing for mocking
    69  	Run func(cmd *exec.Cmd) error
    70  	// FindCmd returns the path of the keybase binary if it can find it
    71  	FindKeybaseBinary func() (string, error)
    72  }
    73  
    74  // Handle accepts a request, handles it, and returns an optional result if there was no error
    75  func (h *handler) Handle(req *Request) (interface{}, error) {
    76  	switch req.Method {
    77  	case "chat":
    78  		return nil, h.handleChat(req)
    79  	case "query":
    80  		return h.handleQuery(req)
    81  	}
    82  	return nil, errInvalidMethod
    83  }
    84  
    85  // handleChat sends a chat message to a user.
    86  func (h *handler) handleChat(req *Request) error {
    87  	if req.Body == "" {
    88  		return errMissingField
    89  	}
    90  	idQuery, err := checkUsernameQuery(req.To)
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	binPath, err := h.FindKeybaseBinary()
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	var out bytes.Buffer
   101  	cmd := exec.Command(binPath, "chat", "send", "--private", idQuery)
   102  	cmd.Env = append(os.Environ(), "KEYBASE_LOG_FORMAT=plain")
   103  	cmd.Stdin = strings.NewReader(req.Body)
   104  	cmd.Stdout = &out
   105  	cmd.Stderr = &out
   106  
   107  	if err := h.Run(cmd); err != nil {
   108  		return parseError(&out, err)
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  type resultQuery struct {
   115  	Username string `json:"username"`
   116  }
   117  
   118  // parseQuery reads the stderr from a keybase query command and returns a result
   119  func parseQuery(r io.Reader) (*resultQuery, error) {
   120  	scanner := bufio.NewScanner(r)
   121  
   122  	var lastErrLine string
   123  	for scanner.Scan() {
   124  		// Find a line that looks like... "[INFO] 001 Identifying someuser"
   125  		line := strings.TrimSpace(scanner.Text())
   126  		parts := strings.Split(line, " ")
   127  		if len(parts) < 4 {
   128  			continue
   129  		}
   130  
   131  		// Short circuit errors
   132  		if parts[0] == "[ERRO]" {
   133  			lastErrLine = strings.Join(parts[2:], " ")
   134  			if lastErrLine == "Not found" {
   135  				return nil, errUserNotFound
   136  			}
   137  			continue
   138  		}
   139  
   140  		if parts[2] != "Identifying" {
   141  			continue
   142  		}
   143  
   144  		resp := &resultQuery{
   145  			Username: parts[3],
   146  		}
   147  		return resp, nil
   148  	}
   149  
   150  	if err := scanner.Err(); err != nil {
   151  		return nil, scanner.Err()
   152  	}
   153  
   154  	// This could happen if the keybase service is broken
   155  	return nil, &errUnexpected{lastErrLine}
   156  }
   157  
   158  // parseError reads stderr output and returns an error made from it. If it
   159  // fails to parse an error, it returns the fallback error.
   160  func parseError(r io.Reader, fallback error) error {
   161  	scanner := bufio.NewScanner(r)
   162  
   163  	// Find the final error
   164  	var lastErr error
   165  	for scanner.Scan() {
   166  		// Should be of the form "[ERRO] 001 Not found" or "...: No resolution found"
   167  		line := strings.TrimSpace(scanner.Text())
   168  		if line == "" {
   169  			continue
   170  		}
   171  		// Check some error states we know about:
   172  		if strings.Contains(line, "Keybase isn't running.") {
   173  			return errKeybaseNotRunning
   174  		}
   175  		if strings.Contains(line, "You are not logged into Keybase.") {
   176  			return errKeybaseNotLoggedIn
   177  		}
   178  		parts := strings.SplitN(line, " ", 3)
   179  		if len(parts) < 3 {
   180  			continue
   181  		}
   182  		if parts[0] != "[ERRO]" {
   183  			continue
   184  		}
   185  		if strings.HasSuffix(parts[2], "No resolution found") {
   186  			return errUserNotFound
   187  		}
   188  		if strings.HasPrefix(parts[2], "Not found") {
   189  			return errUserNotFound
   190  		}
   191  		lastErr = fmt.Errorf(parts[2])
   192  	}
   193  
   194  	if lastErr != nil {
   195  		return lastErr
   196  	}
   197  
   198  	return fallback
   199  }
   200  
   201  // handleQuery searches whether a user is present in Keybase.
   202  func (h *handler) handleQuery(req *Request) (*resultQuery, error) {
   203  	idQuery, err := checkUsernameQuery(req.To)
   204  	if err != nil {
   205  		return nil, err
   206  	}
   207  
   208  	binPath, err := h.FindKeybaseBinary()
   209  	if err != nil {
   210  		return nil, err
   211  	}
   212  
   213  	// Unfortunately `keybase id ...` does not support JSON output, so we parse the output
   214  	var out bytes.Buffer
   215  	cmd := exec.Command(binPath, "id", idQuery)
   216  	cmd.Env = append(os.Environ(), "KEYBASE_LOG_FORMAT=plain")
   217  	cmd.Stdout = &out
   218  	cmd.Stderr = &out
   219  
   220  	if err := h.Run(cmd); err != nil {
   221  		return nil, parseError(&out, err)
   222  	}
   223  
   224  	return parseQuery(&out)
   225  }