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

     1  package chat
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/hmac"
     6  	"crypto/sha256"
     7  	"encoding/hex"
     8  	"fmt"
     9  	"hash"
    10  	"io"
    11  	"net/http"
    12  	"net/url"
    13  	"os"
    14  	"path/filepath"
    15  	"strings"
    16  	"sync"
    17  	"time"
    18  
    19  	"github.com/keybase/client/go/chat/giphy"
    20  	"github.com/keybase/client/go/chat/storage"
    21  	"github.com/keybase/client/go/kbhttp/manager"
    22  	"github.com/keybase/go-codec/codec"
    23  
    24  	lru "github.com/hashicorp/golang-lru"
    25  	"github.com/keybase/client/go/chat/attachments"
    26  	"github.com/keybase/client/go/chat/globals"
    27  	"github.com/keybase/client/go/chat/s3"
    28  	"github.com/keybase/client/go/chat/types"
    29  	"github.com/keybase/client/go/chat/utils"
    30  	"github.com/keybase/client/go/libkb"
    31  	disklru "github.com/keybase/client/go/lru"
    32  	"github.com/keybase/client/go/protocol/chat1"
    33  	"github.com/keybase/client/go/protocol/gregor1"
    34  	"github.com/keybase/client/go/protocol/keybase1"
    35  	"golang.org/x/net/context"
    36  )
    37  
    38  const keyPrefixLen = 2
    39  
    40  var blankProgress = func(bytesComplete, bytesTotal int64) {}
    41  
    42  type AttachmentHTTPSrv struct {
    43  	sync.Mutex
    44  	globals.Contextified
    45  	utils.DebugLabeler
    46  
    47  	endpoint           string
    48  	attachmentPrefix   string
    49  	pendingPrefix      string
    50  	unfurlPrefix       string
    51  	giphyPrefix        string
    52  	giphyGalleryPrefix string
    53  	giphySelectPrefix  string
    54  	urlMap             *lru.Cache
    55  	fetcher            types.AttachmentFetcher
    56  	ri                 func() chat1.RemoteInterface
    57  	httpSrv            *manager.Srv
    58  	hmacPool           sync.Pool
    59  }
    60  
    61  var _ types.AttachmentURLSrv = (*AttachmentHTTPSrv)(nil)
    62  
    63  func NewAttachmentHTTPSrv(g *globals.Context, httpSrv *manager.Srv, fetcher types.AttachmentFetcher,
    64  	ri func() chat1.RemoteInterface) *AttachmentHTTPSrv {
    65  	l, err := lru.New(2000)
    66  	if err != nil {
    67  		panic(err)
    68  	}
    69  
    70  	token, err := libkb.RandBytes(32)
    71  	if err != nil {
    72  		panic(err)
    73  	}
    74  	r := &AttachmentHTTPSrv{
    75  		Contextified:       globals.NewContextified(g),
    76  		DebugLabeler:       utils.NewDebugLabeler(g.ExternalG(), "AttachmentHTTPSrv", false),
    77  		endpoint:           "at",
    78  		attachmentPrefix:   "at",
    79  		pendingPrefix:      "pe",
    80  		unfurlPrefix:       "uf",
    81  		giphyPrefix:        "gf",
    82  		giphyGalleryPrefix: "gg",
    83  		giphySelectPrefix:  "gs",
    84  		ri:                 ri,
    85  		urlMap:             l,
    86  		fetcher:            fetcher,
    87  		httpSrv:            httpSrv,
    88  		hmacPool: sync.Pool{
    89  			New: func() interface{} {
    90  				return hmac.New(sha256.New, token)
    91  			},
    92  		},
    93  	}
    94  	r.httpSrv.HandleFunc(r.endpoint, manager.SrvTokenModeUnchecked, r.serve)
    95  	r.fetcher.OnStart(libkb.NewMetaContextTODO(g.ExternalG()))
    96  	return r
    97  }
    98  
    99  func (r *AttachmentHTTPSrv) OnDbNuke(mctx libkb.MetaContext) error {
   100  	return r.fetcher.OnDbNuke(mctx)
   101  }
   102  
   103  func (r *AttachmentHTTPSrv) GetAttachmentFetcher() types.AttachmentFetcher {
   104  	return r.fetcher
   105  }
   106  
   107  func (r *AttachmentHTTPSrv) genURLKey(prefix string, payload interface{}) (string, error) {
   108  	h := r.hmacPool.Get().(hash.Hash)
   109  	defer r.hmacPool.Put(h)
   110  	h.Reset()
   111  	_, _ = h.Write([]byte(prefix))
   112  	var data []byte
   113  	mh := codec.MsgpackHandle{WriteExt: true}
   114  	if err := codec.NewEncoderBytes(&data, &mh).Encode(payload); err != nil {
   115  		return "", err
   116  	}
   117  	_, _ = h.Write(data)
   118  	return prefix + hex.EncodeToString(h.Sum(nil)), nil
   119  }
   120  
   121  func (r *AttachmentHTTPSrv) getURL(ctx context.Context, prefix string, payload interface{}) string {
   122  	if !r.httpSrv.Active() {
   123  		r.Debug(ctx, "getURL: http server failed to start earlier")
   124  		return ""
   125  	}
   126  	addr, err := r.httpSrv.Addr()
   127  	if err != nil {
   128  		r.Debug(ctx, "getURL: failed to get HTTP server address: %s", err)
   129  		return ""
   130  	}
   131  	key, err := r.genURLKey(prefix, payload)
   132  	if err != nil {
   133  		r.Debug(ctx, "getURL: failed to generate URL key: %s", err)
   134  		return ""
   135  	}
   136  	r.urlMap.Add(key, payload)
   137  	return fmt.Sprintf("http://%s/%s?key=%s", addr, r.endpoint, key)
   138  }
   139  
   140  func (r *AttachmentHTTPSrv) GetURL(ctx context.Context, convID chat1.ConversationID, msgID chat1.MessageID,
   141  	preview, noAnim, isEmoji bool) string {
   142  	r.Lock()
   143  	defer r.Unlock()
   144  	defer r.Trace(ctx, nil, "GetURL(%s,%d)", convID, msgID)()
   145  	url := r.getURL(ctx, r.attachmentPrefix, chat1.ConversationIDMessageIDPair{
   146  		ConvID: convID,
   147  		MsgID:  msgID,
   148  	})
   149  	url += fmt.Sprintf("&prev=%v&noanim=%v&isemoji=%v", preview, noAnim, isEmoji)
   150  	r.Debug(ctx, "GetURL: handler URL: convID: %s msgID: %d %s", convID, msgID, url)
   151  	return url
   152  }
   153  
   154  func (r *AttachmentHTTPSrv) GetPendingPreviewURL(ctx context.Context, outboxID chat1.OutboxID) string {
   155  	defer r.Trace(ctx, nil, "GetPendingPreviewURL(%s)", outboxID)()
   156  	url := r.getURL(ctx, r.pendingPrefix, outboxID)
   157  	r.Debug(ctx, "GetPendingPreviewURL: handler URL: outboxID: %s %s", outboxID, url)
   158  	return url
   159  }
   160  
   161  type unfurlAsset struct {
   162  	Asset  chat1.Asset
   163  	ConvID chat1.ConversationID
   164  }
   165  
   166  func (r *AttachmentHTTPSrv) GetUnfurlAssetURL(ctx context.Context, convID chat1.ConversationID,
   167  	asset chat1.Asset) string {
   168  	defer r.Trace(ctx, nil, "GetUnfurlAssetURL")()
   169  	url := r.getURL(ctx, r.unfurlPrefix, unfurlAsset{
   170  		Asset:  asset,
   171  		ConvID: convID,
   172  	})
   173  	r.Debug(ctx, "GetUnfurlAssetURL: handler URL: %s", url)
   174  	return url
   175  }
   176  
   177  func (r *AttachmentHTTPSrv) GetGiphyURL(ctx context.Context, giphyURL string) string {
   178  	defer r.Trace(ctx, nil, "GetGiphyURL")()
   179  	url := r.getURL(ctx, r.giphyPrefix, giphyURL)
   180  	r.Debug(ctx, "GetGiphyURL: handler URL: %s", url)
   181  	return url
   182  }
   183  
   184  func (r *AttachmentHTTPSrv) GetGiphyGalleryURL(ctx context.Context, convID chat1.ConversationID,
   185  	tlfName string, results []chat1.GiphySearchResult) string {
   186  	defer r.Trace(ctx, nil, "GetGiphyGalleryURL")()
   187  	url := r.getURL(ctx, r.giphyGalleryPrefix, giphyGalleryInfo{
   188  		Results: results,
   189  		ConvID:  convID,
   190  		TlfName: tlfName,
   191  	})
   192  	r.Debug(ctx, "GetGiphyGalleryURL: handler URL: %s", url)
   193  	return url
   194  }
   195  
   196  func (r *AttachmentHTTPSrv) servePendingPreview(ctx context.Context, w http.ResponseWriter, req *http.Request) {
   197  	defer r.Trace(ctx, nil, "servePendingPreview")()
   198  	key := req.URL.Query().Get("key")
   199  	intOutboxID, ok := r.urlMap.Get(key)
   200  	if !ok {
   201  		r.makeError(ctx, w, http.StatusNotFound, "missing key: %s", key)
   202  		return
   203  	}
   204  	outboxID, ok := intOutboxID.(chat1.OutboxID)
   205  	if !ok {
   206  		r.makeError(ctx, w, http.StatusBadRequest, "invalid outboxID")
   207  		return
   208  	}
   209  	pre, err := attachments.NewPendingPreviews(r.G()).Get(ctx, outboxID)
   210  	if err != nil {
   211  		r.makeError(ctx, w, http.StatusInternalServerError, "error reading preview: %s", err)
   212  		return
   213  	}
   214  	if _, err := io.Copy(w, bytes.NewReader(pre.Preview)); err != nil {
   215  		r.makeError(ctx, w, http.StatusInternalServerError, "failed to write resposne: %s", err)
   216  		return
   217  	}
   218  }
   219  
   220  func (r *AttachmentHTTPSrv) serveUnfurlAsset(ctx context.Context, w http.ResponseWriter, req *http.Request) {
   221  	defer r.Trace(ctx, nil, "serveUnfurlAsset")()
   222  	key := req.URL.Query().Get("key")
   223  	val, ok := r.urlMap.Get(key)
   224  	if !ok {
   225  		r.makeError(ctx, w, http.StatusNotFound, "invalid key: %s", key)
   226  		return
   227  	}
   228  	ua := val.(unfurlAsset)
   229  	if r.shouldServeContent(ctx, ua.Asset, req) {
   230  		if r.serveUnfurlVideoHostPage(ctx, w, req) {
   231  			// if we served the host page, just bail out
   232  			return
   233  		}
   234  		r.Debug(ctx, "serveUnfurlAsset: streaming: req: method: %s range: %s", req.Method,
   235  			req.Header.Get("Range"))
   236  		rs, err := r.fetcher.StreamAttachment(ctx, ua.ConvID, ua.Asset, r.ri, r)
   237  		if err != nil {
   238  			r.makeError(ctx, w, http.StatusInternalServerError, "failed to get streamer: %s", err)
   239  			return
   240  		}
   241  		http.ServeContent(w, req, ua.Asset.Filename, time.Time{}, rs)
   242  	} else {
   243  		if err := r.fetcher.FetchAttachment(ctx, w, ua.ConvID, ua.Asset, r.ri, r, blankProgress); err != nil {
   244  			r.makeError(ctx, w, http.StatusInternalServerError, "failed to fetch attachment: %s", err)
   245  			return
   246  		}
   247  	}
   248  }
   249  
   250  type giphyGalleryInfo struct {
   251  	Results []chat1.GiphySearchResult
   252  	ConvID  chat1.ConversationID
   253  	TlfName string
   254  }
   255  
   256  func (r *AttachmentHTTPSrv) getGiphyGallerySelectURL(ctx context.Context, convID chat1.ConversationID,
   257  	tlfName string, result chat1.GiphySearchResult) string {
   258  	addr, err := r.httpSrv.Addr()
   259  	if err != nil {
   260  		r.Debug(ctx, "getGiphySelectURL: failed to get HTTP server address: %s", err)
   261  		return ""
   262  	}
   263  	key, err := r.genURLKey(r.giphySelectPrefix, result)
   264  	if err != nil {
   265  		r.Debug(ctx, "getGiphySelectURL: failed to generate URL key: %s", err)
   266  		return ""
   267  	}
   268  	r.urlMap.Add(key, result)
   269  	return fmt.Sprintf("http://%s/%s?url=%s&convID=%s&tlfName=%s&key=%s", addr, r.endpoint,
   270  		url.QueryEscape(result.TargetUrl), convID, tlfName, key)
   271  }
   272  
   273  func (r *AttachmentHTTPSrv) serveGiphyGallerySelect(ctx context.Context, w http.ResponseWriter,
   274  	req *http.Request) {
   275  	defer r.Trace(ctx, nil, "serveGiphyGallerySelect")()
   276  	url := req.URL.Query().Get("url")
   277  	strConvID := req.URL.Query().Get("convID")
   278  	tlfName := req.URL.Query().Get("tlfName")
   279  	key := req.URL.Query().Get("key")
   280  
   281  	infoInt, ok := r.urlMap.Get(key)
   282  	if !ok {
   283  		r.makeError(ctx, w, http.StatusNotFound, "invalid key: %s", key)
   284  		return
   285  	}
   286  	result := infoInt.(chat1.GiphySearchResult)
   287  	if result.TargetUrl != url {
   288  		r.makeError(ctx, w, http.StatusNotFound, "invalid key: %s", key)
   289  		return
   290  	}
   291  	convID, err := chat1.MakeConvID(strConvID)
   292  	if err != nil {
   293  		r.makeError(context.TODO(), w, http.StatusInternalServerError, "failed to decode convID: %s",
   294  			err)
   295  		return
   296  	}
   297  	uid := gregor1.UID(r.G().Env.GetUID().ToBytes())
   298  	if err := r.G().InboxSource.Draft(ctx, uid, convID, nil); err != nil {
   299  		r.Debug(ctx, "serveGiphyGallerySelect: failed to clear draft: %s", err)
   300  	}
   301  	if err := r.G().ChatHelper.SendTextByID(ctx, convID, tlfName, url, keybase1.TLFVisibility_PRIVATE); err != nil {
   302  		r.makeError(context.TODO(), w, http.StatusInternalServerError, "failed to send giphy url: %s",
   303  			err)
   304  	}
   305  	ui, err := r.G().UIRouter.GetChatUI()
   306  	if err == nil && ui != nil {
   307  		err := ui.ChatGiphyToggleResultWindow(ctx, convID, false, true)
   308  		if err != nil {
   309  			r.Debug(ctx, "serveGiphyGallerySelect: failed to toggle giphy: %s", err)
   310  		}
   311  	} else {
   312  		r.Debug(ctx, "serveGiphyGallerySelect: failed to get chat UI: %s", err)
   313  	}
   314  
   315  	err = storage.NewGiphyStore(r.G()).Put(ctx, uid, result)
   316  	if err != nil {
   317  		r.Debug(ctx, "serveGiphyGallerySelect: failed to track giphy select: %s", err)
   318  	}
   319  }
   320  
   321  func (r *AttachmentHTTPSrv) serveGiphyGallery(ctx context.Context, w http.ResponseWriter, req *http.Request) {
   322  	defer r.Trace(ctx, nil, "serveGiphyGallery")()
   323  	key := req.URL.Query().Get("key")
   324  	infoInt, ok := r.urlMap.Get(key)
   325  	if !ok {
   326  		r.makeError(ctx, w, http.StatusNotFound, "invalid key: %s", key)
   327  		return
   328  	}
   329  	galleryInfo := infoInt.(giphyGalleryInfo)
   330  	var videoStr string
   331  	for _, res := range galleryInfo.Results {
   332  		videoStr += fmt.Sprintf(`
   333  			<img style="height: 100%%" src="%s" onclick="sendMessage('%s')" />
   334  		`, res.PreviewUrl, r.getGiphyGallerySelectURL(ctx, galleryInfo.ConvID, galleryInfo.TlfName,
   335  			res))
   336  	}
   337  	res := fmt.Sprintf(`
   338  	<html>
   339  		<head>
   340  			<title>Keybase Giphy Gallery</title>
   341  			<script>
   342  				window.sendMessage = function(url) {
   343  					var req = new XMLHttpRequest();
   344  					req.open("GET", url);
   345  					req.send();
   346  				}
   347  			</script>
   348  		</head>
   349  		<body style="margin: 0px;">
   350  			<div style="display: flex; flex-direction: row; height: 100%%; overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap;  -webkit-overflow-scrolling: touch; border-top: 1px solid rgba(0, 0, 0, 0.20); align-items: flex-end;">
   351  				%s
   352  			</div>
   353  		</body>
   354  	</html>`, videoStr)
   355  	if _, err := io.WriteString(w, res); err != nil {
   356  		r.makeError(context.TODO(), w, http.StatusInternalServerError, "failed to write giphy gallery: %s",
   357  			err)
   358  	}
   359  }
   360  
   361  func (r *AttachmentHTTPSrv) serveGiphyLink(ctx context.Context, w http.ResponseWriter, req *http.Request) {
   362  	defer r.Trace(ctx, nil, "serveGiphyLink")()
   363  	key := req.URL.Query().Get("key")
   364  	val, ok := r.urlMap.Get(key)
   365  	if !ok {
   366  		r.makeError(ctx, w, http.StatusNotFound, "invalid key: %s", key)
   367  		return
   368  	}
   369  	// Grab range headers
   370  	rangeHeader := req.Header.Get("Range")
   371  	client := giphy.AssetClient(libkb.NewMetaContext(ctx, r.G().GlobalContext))
   372  	url, err := giphy.ProxyURL(val.(string))
   373  	if err != nil {
   374  		r.makeError(ctx, w, http.StatusInternalServerError, "url creation: %s", err)
   375  		return
   376  	}
   377  	giphyReq, err := http.NewRequest("GET", url, nil)
   378  	if err != nil {
   379  		r.makeError(ctx, w, http.StatusInternalServerError, "request creation: %s", err)
   380  		return
   381  	}
   382  	if len(rangeHeader) > 0 {
   383  		giphyReq.Header.Add("Range", rangeHeader)
   384  	}
   385  	giphyReq.Host = giphy.MediaHost
   386  	resp, err := client.Do(giphyReq)
   387  	if err != nil {
   388  		status := http.StatusInternalServerError
   389  		if resp != nil {
   390  			status = resp.StatusCode
   391  		}
   392  		r.makeError(ctx, w, status, "failed to get read giphy link: %s", err)
   393  		return
   394  	}
   395  	defer resp.Body.Close()
   396  	for k := range resp.Header {
   397  		w.Header().Add(k, resp.Header.Get(k))
   398  	}
   399  	if _, err := io.Copy(w, resp.Body); err != nil {
   400  		r.makeError(ctx, w, resp.StatusCode, "failed to write giphy data: %s", err)
   401  		return
   402  	}
   403  }
   404  
   405  func (r *AttachmentHTTPSrv) makeError(ctx context.Context, w http.ResponseWriter, code int, msg string,
   406  	args ...interface{}) {
   407  	r.Debug(ctx, "serve: error code: %d msg %s", code, fmt.Sprintf(msg, args...))
   408  	w.WriteHeader(code)
   409  }
   410  
   411  func (r *AttachmentHTTPSrv) shouldServeContent(ctx context.Context, asset chat1.Asset, req *http.Request) bool {
   412  	noStream := "true" == req.URL.Query().Get("nostream")
   413  	if noStream {
   414  		// If we just want the bits without streaming
   415  		return false
   416  	}
   417  	return strings.HasPrefix(asset.MimeType, "video")
   418  }
   419  
   420  func (r *AttachmentHTTPSrv) serveUnfurlVideoHostPage(ctx context.Context, w http.ResponseWriter, req *http.Request) bool {
   421  	contentForce := "true" == req.URL.Query().Get("contentforce")
   422  	if r.G().IsMobileAppType() && !contentForce {
   423  		r.Debug(ctx, "serveUnfurlVideoHostPage: mobile client detected, showing the HTML video viewer")
   424  		w.Header().Set("Content-Type", "text/html")
   425  		autoplay := ""
   426  		if req.URL.Query().Get("autoplay") != "true" {
   427  			autoplay = `onloadeddata="togglePlay('pause')"`
   428  		}
   429  		if _, err := w.Write([]byte(fmt.Sprintf(`
   430  			<html>
   431  				<head>
   432  					<meta name="viewport" content="initial-scale=1, viewport-fit=cover">
   433  					<title>Keybase Video Viewer</title>
   434  					<script>
   435  						window.playVideo = function(data) {
   436  							var vid = document.getElementById("vid");
   437  							vid.play()
   438  						}
   439  						window.togglePlay = function(data) {
   440  							var vid = document.getElementById("vid");
   441  							if (data === "play") {
   442  								vid.play();
   443  							} else {
   444  								vid.pause();
   445  							}
   446  						}
   447  					</script>
   448  				</head>
   449  				<body style="margin: 0px; background-color: rgba(0,0,0,0.05)">
   450  					<video id="vid" %s preload="auto" style="width: 100%%; height: 100%%; border-radius: 4px; object-fit:fill" src="%s" playsinline webkit-playsinline loop autoplay muted />
   451  				</body>
   452  			</html>
   453  		`, autoplay, req.URL.String()+"&contentforce=true"))); err != nil {
   454  			r.Debug(ctx, "serveUnfurlVideoHostPage: failed to write HTML video player: %s", err)
   455  		}
   456  		return true
   457  	}
   458  	return false
   459  }
   460  
   461  func (r *AttachmentHTTPSrv) serveVideoHostPage(ctx context.Context, w http.ResponseWriter, req *http.Request) bool {
   462  	contentForce := "true" == req.URL.Query().Get("contentforce")
   463  	if r.G().IsMobileAppType() && !contentForce {
   464  		r.Debug(ctx, "serve: mobile client detected, showing the HTML video viewer")
   465  		w.Header().Set("Content-Type", "text/html")
   466  		if _, err := w.Write([]byte(fmt.Sprintf(`
   467  			<html>
   468  				<head>
   469  					<meta name="viewport" content="initial-scale=1, viewport-fit=cover">
   470  					<title>Keybase Video Viewer</title>
   471  					<script>
   472  						window.togglePlay = function(data) {
   473  							var vid = document.getElementById("vid");
   474  							if (data === "play") {
   475  								vid.play();
   476  								vid.setAttribute('controls', 'controls');
   477  							} else {
   478  								vid.pause();
   479  								vid.removeAttribute('controls');
   480  							}
   481  						  }
   482  					</script>
   483  				</head>
   484  				<body style="margin: 0px;">
   485  					<video id="vid" style="width: 100%%; height: 100%%; object-fit:fill; border-radius: 4px" poster="%s" src="%s" preload="none" playsinline webkit-playsinline />
   486  				</body>
   487  			</html>
   488  		`, req.URL.Query().Get("poster"), req.URL.String()+"&contentforce=true"))); err != nil {
   489  			r.Debug(ctx, "serve: failed to write HTML video player: %s", err)
   490  		}
   491  		return true
   492  	}
   493  	return false
   494  }
   495  
   496  func (r *AttachmentHTTPSrv) serveAttachment(ctx context.Context, w http.ResponseWriter, req *http.Request) {
   497  	defer r.Trace(ctx, nil, "serveAttachment")()
   498  
   499  	preview := "true" == req.URL.Query().Get("prev")
   500  	noAnim := "true" == req.URL.Query().Get("noanim")
   501  	isEmoji := "true" == req.URL.Query().Get("isemoji")
   502  	key := req.URL.Query().Get("key")
   503  	r.Lock()
   504  	pairInt, ok := r.urlMap.Get(key)
   505  	r.Unlock()
   506  	if !ok {
   507  		r.makeError(ctx, w, http.StatusNotFound, "key not found in URL map")
   508  		return
   509  	}
   510  
   511  	pair := pairInt.(chat1.ConversationIDMessageIDPair)
   512  	uid := gregor1.UID(r.G().Env.GetUID().ToBytes())
   513  	r.Debug(ctx, "serveAttachment: convID: %s msgID: %d", pair.ConvID, pair.MsgID)
   514  
   515  	asset, err := attachments.AssetFromMessage(ctx, r.G(), uid, pair.ConvID, pair.MsgID, preview)
   516  	if err != nil {
   517  		r.makeError(ctx, w, http.StatusInternalServerError, "failed to get asset: %s", err)
   518  		return
   519  	}
   520  	if len(asset.Path) == 0 {
   521  		r.makeError(ctx, w, http.StatusNotFound, "attachment not uploaded yet, no path")
   522  		return
   523  	}
   524  	if isEmoji && !r.G().EmojiSource.IsValidSize(asset.Size) {
   525  		r.makeError(ctx, w, http.StatusBadRequest, "%v", fmt.Errorf("emoji incorrectly sized: %d", asset.Size))
   526  		return
   527  	}
   528  
   529  	r.Debug(ctx, "serveAttachment: setting content-type: %s sz: %d", asset.MimeType, asset.Size)
   530  	w.Header().Set("Content-Type", asset.MimeType)
   531  	if r.shouldServeContent(ctx, asset, req) {
   532  		if r.serveVideoHostPage(ctx, w, req) {
   533  			// if we served the host page, just bail out
   534  			return
   535  		}
   536  		r.Debug(ctx, "serveAttachment: streaming: req: method: %s range: %s", req.Method,
   537  			req.Header.Get("Range"))
   538  		rs, err := r.fetcher.StreamAttachment(ctx, pair.ConvID, asset, r.ri, r)
   539  		if err != nil {
   540  			r.makeError(ctx, w, http.StatusInternalServerError, "failed to get streamer: %s", err)
   541  			return
   542  		}
   543  		http.ServeContent(w, req, asset.Filename, time.Time{}, rs)
   544  	} else {
   545  		// no animation mode is intended to transform GIF images into single frame versions
   546  		if noAnim {
   547  			var buf bytes.Buffer
   548  			if err := r.fetcher.FetchAttachment(ctx, &buf, pair.ConvID, asset, r.ri, r, blankProgress); err != nil {
   549  				r.makeError(ctx, w, http.StatusInternalServerError, "failed to fetch attachment: %s", err)
   550  				return
   551  			}
   552  			bufReader := attachments.NewBufReadResetter(buf.Bytes())
   553  			if err := attachments.GIFToPNG(ctx, bufReader, w); err != nil {
   554  				r.Debug(ctx, "serveAttachment: not a gif in no animation mode: %s", err)
   555  				_ = bufReader.Reset()
   556  				if _, err := io.Copy(w, bufReader); err != nil {
   557  					r.makeError(ctx, w, http.StatusInternalServerError, "failed to write attachment: %s", err)
   558  					return
   559  				}
   560  			}
   561  		} else {
   562  			if err := r.fetcher.FetchAttachment(ctx, w, pair.ConvID, asset, r.ri, r, blankProgress); err != nil {
   563  				r.makeError(ctx, w, http.StatusInternalServerError, "failed to fetch attachment: %s", err)
   564  				return
   565  			}
   566  		}
   567  	}
   568  }
   569  
   570  func (r *AttachmentHTTPSrv) serve(w http.ResponseWriter, req *http.Request) {
   571  	ctx := globals.ChatCtx(context.Background(), r.G(), keybase1.TLFIdentifyBehavior_CHAT_GUI, nil,
   572  		NewSimpleIdentifyNotifier(r.G()))
   573  	defer r.Trace(ctx, nil, "serve")()
   574  	addr, err := r.httpSrv.Addr()
   575  	if err != nil {
   576  		r.Debug(ctx, "serve: failed to get HTTP server address: %s", err)
   577  		r.makeError(ctx, w, http.StatusInternalServerError, "unable to determine addr")
   578  		return
   579  	}
   580  	if req.Host != addr {
   581  		r.Debug(ctx, "Host %s didn't match addr %s, failing request to protect against DNS rebinding", req.Host, addr)
   582  		r.makeError(ctx, w, http.StatusBadRequest, "invalid host")
   583  		return
   584  	}
   585  	key := req.URL.Query().Get("key")
   586  	if len(key) < keyPrefixLen {
   587  		r.makeError(ctx, w, http.StatusNotFound, "invalid key")
   588  		return
   589  	}
   590  	if _, ok := r.urlMap.Get(key); !ok {
   591  		r.makeError(ctx, w, http.StatusNotFound, "invalid key")
   592  		return
   593  	}
   594  	prefix := key[:keyPrefixLen]
   595  	switch prefix {
   596  	case r.unfurlPrefix:
   597  		r.serveUnfurlAsset(ctx, w, req)
   598  	case r.giphyPrefix:
   599  		r.serveGiphyLink(ctx, w, req)
   600  	case r.giphyGalleryPrefix:
   601  		r.serveGiphyGallery(ctx, w, req)
   602  	case r.giphySelectPrefix:
   603  		r.serveGiphyGallerySelect(ctx, w, req)
   604  	case r.pendingPrefix:
   605  		r.servePendingPreview(ctx, w, req)
   606  	case r.attachmentPrefix:
   607  		r.serveAttachment(ctx, w, req)
   608  	default:
   609  		r.makeError(ctx, w, http.StatusBadRequest, "invalid key prefix")
   610  	}
   611  }
   612  
   613  // Sign implements github.com/keybase/go/chat/s3.Signer interface.
   614  func (r *AttachmentHTTPSrv) Sign(payload []byte) ([]byte, error) {
   615  	arg := chat1.S3SignArg{
   616  		Payload: payload,
   617  		Version: 1,
   618  	}
   619  	return r.ri().S3Sign(context.Background(), arg)
   620  }
   621  
   622  type RemoteAttachmentFetcher struct {
   623  	globals.Contextified
   624  	utils.DebugLabeler
   625  	store attachments.Store
   626  }
   627  
   628  var _ types.AttachmentFetcher = (*RemoteAttachmentFetcher)(nil)
   629  
   630  func NewRemoteAttachmentFetcher(g *globals.Context, store attachments.Store) *RemoteAttachmentFetcher {
   631  	return &RemoteAttachmentFetcher{
   632  		Contextified: globals.NewContextified(g),
   633  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "RemoteAttachmentFetcher", false),
   634  		store:        store,
   635  	}
   636  }
   637  
   638  func (r *RemoteAttachmentFetcher) StreamAttachment(ctx context.Context, convID chat1.ConversationID,
   639  	asset chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer) (res io.ReadSeeker, err error) {
   640  	defer r.Trace(ctx, &err, "StreamAttachment")()
   641  	// Grab S3 params for the conversation
   642  	s3params, err := ri().GetS3Params(ctx, convID)
   643  	if err != nil {
   644  		return nil, err
   645  	}
   646  	return r.store.StreamAsset(ctx, s3params, asset, signer)
   647  }
   648  
   649  func (r *RemoteAttachmentFetcher) FetchAttachment(ctx context.Context, w io.Writer,
   650  	convID chat1.ConversationID, asset chat1.Asset,
   651  	ri func() chat1.RemoteInterface, signer s3.Signer, progress types.ProgressReporter) (err error) {
   652  	defer r.Trace(ctx, &err, "FetchAttachment")()
   653  	// Grab S3 params for the conversation
   654  	s3params, err := ri().GetS3Params(ctx, convID)
   655  	if err != nil {
   656  		return err
   657  	}
   658  	return r.store.DownloadAsset(ctx, s3params, asset, w, signer, progress)
   659  }
   660  
   661  func (r *RemoteAttachmentFetcher) DeleteAssets(ctx context.Context,
   662  	convID chat1.ConversationID, assets []chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer) (err error) {
   663  	defer r.Trace(ctx, &err, "DeleteAssets")()
   664  
   665  	if len(assets) == 0 {
   666  		return nil
   667  	}
   668  
   669  	// get s3 params from server
   670  	s3params, err := ri().GetS3Params(ctx, convID)
   671  	if err != nil {
   672  		r.Debug(ctx, "error getting s3params: %s", err)
   673  		return err
   674  	}
   675  
   676  	// Try to delete the assets remotely
   677  	if err := r.store.DeleteAssets(ctx, s3params, signer, assets); err != nil {
   678  		// there's no way to get asset information after this point.
   679  		// any assets not deleted will be stranded on s3.
   680  		r.Debug(ctx, "error deleting assets: %s", err)
   681  	}
   682  
   683  	r.Debug(ctx, "deleted %d assets", len(assets))
   684  	return nil
   685  }
   686  
   687  func (r *RemoteAttachmentFetcher) PutUploadedAsset(ctx context.Context, filename string, asset chat1.Asset) error {
   688  	return nil
   689  }
   690  
   691  func (r *RemoteAttachmentFetcher) IsAssetLocal(ctx context.Context, asset chat1.Asset) (bool, error) {
   692  	return false, nil
   693  }
   694  
   695  func (r *RemoteAttachmentFetcher) OnDbNuke(mctx libkb.MetaContext) error { return nil }
   696  func (r *RemoteAttachmentFetcher) OnStart(mctx libkb.MetaContext)        {}
   697  
   698  type CachingAttachmentFetcher struct {
   699  	globals.Contextified
   700  	utils.DebugLabeler
   701  
   702  	store   attachments.Store
   703  	diskLRU *disklru.DiskLRU
   704  
   705  	// testing
   706  	tempDir string
   707  }
   708  
   709  var _ types.AttachmentFetcher = (*CachingAttachmentFetcher)(nil)
   710  
   711  func NewCachingAttachmentFetcher(g *globals.Context, store attachments.Store, size int) *CachingAttachmentFetcher {
   712  	return &CachingAttachmentFetcher{
   713  		Contextified: globals.NewContextified(g),
   714  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "CachingAttachmentFetcher", false),
   715  		store:        store,
   716  		diskLRU:      disklru.NewDiskLRU("attachments", 2, size),
   717  	}
   718  }
   719  
   720  func (c *CachingAttachmentFetcher) getBaseDir() string {
   721  	baseDir := c.G().GetCacheDir()
   722  	if len(c.tempDir) > 0 {
   723  		baseDir = c.tempDir
   724  	}
   725  	return baseDir
   726  }
   727  
   728  func (c *CachingAttachmentFetcher) getCacheDir() string {
   729  	return filepath.Join(c.getBaseDir(), "attachments")
   730  }
   731  
   732  func (c *CachingAttachmentFetcher) getFullFilename(name string) string {
   733  	return name + ".attachment"
   734  }
   735  
   736  func (c *CachingAttachmentFetcher) closeFile(f io.Closer) {
   737  	if f != nil {
   738  		f.Close()
   739  	}
   740  }
   741  
   742  func (c *CachingAttachmentFetcher) cacheKey(asset chat1.Asset) string {
   743  	return asset.Path
   744  }
   745  
   746  func (c *CachingAttachmentFetcher) createAttachmentFile(ctx context.Context) (*os.File, error) {
   747  	err := os.MkdirAll(c.getCacheDir(), os.ModePerm)
   748  	if err != nil {
   749  		return nil, err
   750  	}
   751  	file, err := os.CreateTemp(c.getCacheDir(), "att")
   752  	file.Close()
   753  	if err != nil {
   754  		return nil, err
   755  	}
   756  	path := c.getFullFilename(file.Name())
   757  	if err := os.Rename(file.Name(), path); err != nil {
   758  		return nil, err
   759  	}
   760  	return os.OpenFile(path, os.O_RDWR, os.ModeAppend)
   761  }
   762  
   763  // normalizeFilenameFromCache substitutes the existing cache dir value into the
   764  // file path since it's possible for the path to the cache dir to change,
   765  // especially on mobile.
   766  func (c *CachingAttachmentFetcher) normalizeFilenameFromCache(file string) string {
   767  	dir := filepath.Base(filepath.Dir(file))
   768  	file = filepath.Base(file)
   769  	// some attachments may be in the "uploadedpreviews"/"uploadedfulls" dirs,
   770  	// so we preserve the parent directory here.
   771  	return filepath.Join(c.getBaseDir(), dir, file)
   772  }
   773  
   774  func (c *CachingAttachmentFetcher) localAssetPath(ctx context.Context, asset chat1.Asset) (found bool, path string, err error) {
   775  	found, entry, err := c.diskLRU.Get(ctx, c.G(), c.cacheKey(asset))
   776  	if err != nil {
   777  		return found, path, err
   778  	}
   779  	if found {
   780  		path = c.normalizeFilenameFromCache(entry.Value.(string))
   781  	}
   782  	return found, path, nil
   783  }
   784  
   785  func (c *CachingAttachmentFetcher) StreamAttachment(ctx context.Context, convID chat1.ConversationID,
   786  	asset chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer) (res io.ReadSeeker, err error) {
   787  	defer c.Trace(ctx, &err, "StreamAttachment")()
   788  	return NewRemoteAttachmentFetcher(c.G(), c.store).StreamAttachment(ctx, convID, asset, ri, signer)
   789  }
   790  
   791  func (c *CachingAttachmentFetcher) FetchAttachment(ctx context.Context, w io.Writer,
   792  	convID chat1.ConversationID, asset chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer,
   793  	progress types.ProgressReporter) (err error) {
   794  
   795  	defer c.Trace(ctx, &err, "FetchAttachment")()
   796  
   797  	// Check for a disk cache hit, and decrypt that onto the response stream
   798  	found, path, err := c.localAssetPath(ctx, asset)
   799  	if err != nil {
   800  		return err
   801  	}
   802  	if found {
   803  		c.Debug(ctx, "FetchAttachment: cache hit for: %s filepath: %s", asset.Path, path)
   804  		fileReader, err := os.Open(path)
   805  		defer c.closeFile(fileReader)
   806  		if err != nil {
   807  			c.Debug(ctx, "FetchAttachment: failed to read cached file, removing: %s", err)
   808  			os.Remove(path)
   809  			_ = c.diskLRU.Remove(ctx, c.G(), c.cacheKey(asset))
   810  			found = false
   811  		}
   812  		if found {
   813  			return c.store.DecryptAsset(ctx, w, fileReader, asset, progress)
   814  		}
   815  	}
   816  
   817  	// Grab S3 params for the conversation
   818  	s3params, err := ri().GetS3Params(ctx, convID)
   819  	if err != nil {
   820  		return err
   821  	}
   822  
   823  	// Create a reader to the remote ciphertext
   824  	remoteReader, err := c.store.GetAssetReader(ctx, s3params, asset, signer)
   825  	defer c.closeFile(remoteReader)
   826  	if err != nil {
   827  		return err
   828  	}
   829  
   830  	// Create a file we can write the ciphertext into
   831  	fileWriter, err := c.createAttachmentFile(ctx)
   832  	defer c.closeFile(fileWriter)
   833  	if err != nil {
   834  		return err
   835  	}
   836  
   837  	// Read out the ciphertext into the decryption copier, and simultaneously write
   838  	// into the cached file (the ciphertext)
   839  	teeReader := io.TeeReader(remoteReader, fileWriter)
   840  	if err := c.store.DecryptAsset(ctx, w, teeReader, asset, progress); err != nil {
   841  		c.Debug(ctx, "FetchAttachment: error reading asset: %s", err)
   842  		c.closeFile(fileWriter)
   843  		os.Remove(fileWriter.Name())
   844  		return err
   845  	}
   846  
   847  	// commit to the on disk LRU
   848  	return c.putFileInLRU(ctx, fileWriter.Name(), asset)
   849  }
   850  
   851  func (c *CachingAttachmentFetcher) putFileInLRU(ctx context.Context, filename string, asset chat1.Asset) error {
   852  	// Add an entry to the disk LRU mapping the asset path to the local path, and remove
   853  	// the remnants of any evicted attachments.
   854  	evicted, err := c.diskLRU.Put(ctx, c.G(), c.cacheKey(asset), filename)
   855  	if err != nil {
   856  		return err
   857  	}
   858  	if evicted != nil {
   859  		path := c.normalizeFilenameFromCache(evicted.Value.(string))
   860  		os.Remove(path)
   861  	}
   862  	return nil
   863  }
   864  
   865  func (c *CachingAttachmentFetcher) IsAssetLocal(ctx context.Context, asset chat1.Asset) (found bool, err error) {
   866  	defer c.Trace(ctx, &err, "IsAssetLocal")()
   867  	found, path, err := c.localAssetPath(ctx, asset)
   868  	if err != nil {
   869  		return false, err
   870  	}
   871  	if !found {
   872  		return false, nil
   873  	}
   874  	fileReader, err := os.Open(path)
   875  	defer c.closeFile(fileReader)
   876  	if err != nil {
   877  		return false, nil
   878  	}
   879  	return true, nil
   880  }
   881  
   882  func (c *CachingAttachmentFetcher) PutUploadedAsset(ctx context.Context, filename string, asset chat1.Asset) (err error) {
   883  	defer c.Trace(ctx, &err, "PutUploadedAsset")()
   884  	return c.putFileInLRU(ctx, filename, asset)
   885  }
   886  
   887  func (c *CachingAttachmentFetcher) DeleteAssets(ctx context.Context,
   888  	convID chat1.ConversationID, assets []chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer) (err error) {
   889  	defer c.Trace(ctx, &err, "DeleteAssets")()
   890  
   891  	if len(assets) == 0 {
   892  		return nil
   893  	}
   894  
   895  	// Delete the assets locally
   896  	for _, asset := range assets {
   897  		found, path, err := c.localAssetPath(ctx, asset)
   898  		if err != nil {
   899  			c.Debug(ctx, "error getting asset: %s", err)
   900  			continue
   901  		}
   902  		if found {
   903  			os.Remove(path)
   904  			_ = c.diskLRU.Remove(ctx, c.G(), c.cacheKey(asset))
   905  		}
   906  	}
   907  
   908  	// get s3 params from server
   909  	s3params, err := ri().GetS3Params(ctx, convID)
   910  	if err != nil {
   911  		c.Debug(ctx, "error getting s3params: %s", err)
   912  		return err
   913  	}
   914  
   915  	// Try to delete the assets remotely
   916  	if err := c.store.DeleteAssets(ctx, s3params, signer, assets); err != nil {
   917  		// there's no way to get asset information after this point.
   918  		// any assets not deleted will be stranded on s3.
   919  		c.Debug(ctx, "error deleting assets: %s", err)
   920  	}
   921  
   922  	c.Debug(ctx, "deleted %d assets", len(assets))
   923  	return nil
   924  }
   925  
   926  func (c *CachingAttachmentFetcher) OnStart(mctx libkb.MetaContext) {
   927  	mctx, cancel := mctx.WithContextCancel()
   928  	mctx.G().PushShutdownHook(func(libkb.MetaContext) error {
   929  		cancel()
   930  		return nil
   931  	})
   932  	go disklru.CleanOutOfSyncWithDelay(mctx, c.diskLRU, c.getCacheDir(), 10*time.Second)
   933  }
   934  
   935  func (c *CachingAttachmentFetcher) OnDbNuke(mctx libkb.MetaContext) error {
   936  	if c.diskLRU != nil {
   937  		if err := c.diskLRU.CleanOutOfSync(mctx, c.getCacheDir()); err != nil {
   938  			c.Debug(mctx.Ctx(), "unable to run clean: %v", err)
   939  		}
   940  	}
   941  	return nil
   942  }