github.com/keybase/client/go@v0.0.0-20240309051027-028f7c731f8b/avatars/srv.go (about)

     1  package avatars
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/base64"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"net"
    10  	"net/http"
    11  	"net/url"
    12  	"os"
    13  	"runtime"
    14  	"time"
    15  
    16  	"github.com/keybase/client/go/kbhttp/manager"
    17  	"github.com/keybase/client/go/protocol/keybase1"
    18  
    19  	"github.com/keybase/client/go/libkb"
    20  )
    21  
    22  var avatarTransport = &http.Transport{
    23  	DialContext: (&net.Dialer{
    24  		Timeout:   30 * time.Second,
    25  		KeepAlive: 30 * time.Second,
    26  		DualStack: true,
    27  	}).DialContext,
    28  	MaxConnsPerHost:       10,
    29  	MaxIdleConns:          100,
    30  	IdleConnTimeout:       90 * time.Second,
    31  	TLSHandshakeTimeout:   10 * time.Second,
    32  	ExpectContinueTimeout: 1 * time.Second,
    33  }
    34  
    35  type Srv struct {
    36  	libkb.Contextified
    37  
    38  	httpSrv *manager.Srv
    39  	source  libkb.AvatarLoaderSource
    40  }
    41  
    42  func NewSrv(g *libkb.GlobalContext, httpSrv *manager.Srv, source libkb.AvatarLoaderSource) *Srv {
    43  	s := &Srv{
    44  		Contextified: libkb.NewContextified(g),
    45  		httpSrv:      httpSrv,
    46  		source:       source,
    47  	}
    48  	s.httpSrv.HandleFunc("av", manager.SrvTokenModeDefault, s.serve)
    49  	return s
    50  }
    51  
    52  func (s *Srv) GetUserAvatar(username string) (string, error) {
    53  	if s.httpSrv == nil {
    54  		return "", fmt.Errorf("HttpSrv is not ready")
    55  	}
    56  
    57  	addr, err := s.httpSrv.Addr()
    58  	if err != nil {
    59  		return "", err
    60  	}
    61  
    62  	token := s.httpSrv.Token()
    63  
    64  	return fmt.Sprintf("http://%v/av?typ=user&name=%v&format=square_192&token=%v", addr, username, token), nil
    65  }
    66  
    67  func (s *Srv) debug(msg string, args ...interface{}) {
    68  	s.G().GetLog().Debug("Avatars.Srv: %s", fmt.Sprintf(msg, args...))
    69  }
    70  
    71  func (s *Srv) makeError(w http.ResponseWriter, code int, msg string,
    72  	args ...interface{}) {
    73  	s.debug("serve: error code: %d msg %s", code, fmt.Sprintf(msg, args...))
    74  	w.WriteHeader(code)
    75  }
    76  
    77  func (s *Srv) loadFromURL(raw string) (io.ReadCloser, error) {
    78  	parsed, err := url.Parse(raw)
    79  	if err != nil {
    80  		return nil, err
    81  	}
    82  	switch parsed.Scheme {
    83  	case "http", "https":
    84  		avatarTransport.Proxy = libkb.MakeProxy(s.G().GetEnv())
    85  		xprt := libkb.NewInstrumentedRoundTripper(s.G(), func(*http.Request) string { return "AvatarSrv" },
    86  			libkb.NewClosingRoundTripper(avatarTransport))
    87  		cli := &http.Client{
    88  			Transport: xprt,
    89  		}
    90  		resp, err := cli.Get(raw)
    91  		if err != nil {
    92  			return nil, err
    93  		}
    94  		return resp.Body, nil
    95  	case "file":
    96  		filePath := parsed.Path
    97  		if runtime.GOOS == "windows" && len(filePath) > 0 {
    98  			filePath = filePath[1:]
    99  		}
   100  		return os.Open(filePath)
   101  	default:
   102  		return nil, fmt.Errorf("unknown URL scheme: %s raw: %s", parsed.Scheme, raw)
   103  	}
   104  }
   105  
   106  func (s *Srv) loadPlaceholder(format keybase1.AvatarFormat, placeholderMap map[keybase1.AvatarFormat]string) ([]byte, error) {
   107  	encoded, ok := placeholderMap[format]
   108  	if !ok {
   109  		return nil, errors.New("no placeholder for format")
   110  	}
   111  	return base64.StdEncoding.DecodeString(encoded)
   112  }
   113  
   114  func (s *Srv) serve(w http.ResponseWriter, req *http.Request) {
   115  	typ := req.URL.Query().Get("typ")
   116  	name := req.URL.Query().Get("name")
   117  	format := keybase1.AvatarFormat(req.URL.Query().Get("format"))
   118  	mode := req.URL.Query().Get("mode")
   119  	mctx := libkb.NewMetaContextBackground(s.G())
   120  
   121  	var loadFn func(libkb.MetaContext, []string, []keybase1.AvatarFormat) (keybase1.LoadAvatarsRes, error)
   122  	var placeholderMap map[keybase1.AvatarFormat]string
   123  	switch typ {
   124  	case "user":
   125  		loadFn = s.source.LoadUsers
   126  		placeholderMap = userPlaceholders
   127  		if mode == "dark" {
   128  			placeholderMap = userPlaceholdersDark
   129  		}
   130  	case "team":
   131  		loadFn = s.source.LoadTeams
   132  		placeholderMap = teamPlaceholders
   133  		if mode == "dark" {
   134  			placeholderMap = teamPlaceholdersDark
   135  		}
   136  	default:
   137  		s.makeError(w, http.StatusBadRequest, "unknown avatar type: %s", typ)
   138  		return
   139  	}
   140  	res, err := loadFn(mctx, []string{name}, []keybase1.AvatarFormat{format})
   141  	if err != nil {
   142  		s.makeError(w, http.StatusInternalServerError, "failed to load: %s", err)
   143  		return
   144  	}
   145  	nameRes := res.Picmap[name]
   146  	if nameRes == nil {
   147  		s.makeError(w, http.StatusInternalServerError, "avatar not loaded")
   148  		return
   149  	}
   150  	var reader io.ReadCloser
   151  	url, ok := nameRes[format]
   152  	if !ok || len(url.String()) == 0 {
   153  		placeholder, err := s.loadPlaceholder(format, placeholderMap)
   154  		if err != nil {
   155  			s.makeError(w, http.StatusInternalServerError, "failed to load placeholder: %s", err)
   156  			return
   157  		}
   158  		reader = io.NopCloser(bytes.NewReader(placeholder))
   159  	} else {
   160  		if reader, err = s.loadFromURL(url.String()); err != nil {
   161  			s.makeError(w, http.StatusInternalServerError, "failed to get URL reader: %s", err)
   162  			return
   163  		}
   164  	}
   165  	defer reader.Close()
   166  	if _, err := io.Copy(w, reader); err != nil {
   167  		s.makeError(w, http.StatusInternalServerError, "failed to write response: %s", err)
   168  		return
   169  	}
   170  }