github.com/keybase/client/go@v0.0.0-20241007131713-f10651d043c8/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  		TempCreds: true,
   619  	}
   620  	return r.ri().S3Sign(context.Background(), arg)
   621  }
   622  
   623  type RemoteAttachmentFetcher struct {
   624  	globals.Contextified
   625  	utils.DebugLabeler
   626  	store attachments.Store
   627  }
   628  
   629  var _ types.AttachmentFetcher = (*RemoteAttachmentFetcher)(nil)
   630  
   631  func NewRemoteAttachmentFetcher(g *globals.Context, store attachments.Store) *RemoteAttachmentFetcher {
   632  	return &RemoteAttachmentFetcher{
   633  		Contextified: globals.NewContextified(g),
   634  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "RemoteAttachmentFetcher", false),
   635  		store:        store,
   636  	}
   637  }
   638  
   639  func (r *RemoteAttachmentFetcher) StreamAttachment(ctx context.Context, convID chat1.ConversationID,
   640  	asset chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer) (res io.ReadSeeker, err error) {
   641  	defer r.Trace(ctx, &err, "StreamAttachment")()
   642  	// Grab S3 params for the conversation
   643  	s3params, err := ri().GetS3Params(ctx, chat1.GetS3ParamsArg{
   644  		ConversationID: convID,
   645  		TempCreds:      true,
   646  	})
   647  	if err != nil {
   648  		return nil, err
   649  	}
   650  	return r.store.StreamAsset(ctx, s3params, asset, signer)
   651  }
   652  
   653  func (r *RemoteAttachmentFetcher) FetchAttachment(ctx context.Context, w io.Writer,
   654  	convID chat1.ConversationID, asset chat1.Asset,
   655  	ri func() chat1.RemoteInterface, signer s3.Signer, progress types.ProgressReporter) (err error) {
   656  	defer r.Trace(ctx, &err, "FetchAttachment")()
   657  	// Grab S3 params for the conversation
   658  	s3params, err := ri().GetS3Params(ctx, chat1.GetS3ParamsArg{
   659  		ConversationID: convID,
   660  		TempCreds:      true,
   661  	})
   662  	if err != nil {
   663  		return err
   664  	}
   665  	return r.store.DownloadAsset(ctx, s3params, asset, w, signer, progress)
   666  }
   667  
   668  func (r *RemoteAttachmentFetcher) DeleteAssets(ctx context.Context,
   669  	convID chat1.ConversationID, assets []chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer) (err error) {
   670  	defer r.Trace(ctx, &err, "DeleteAssets")()
   671  
   672  	if len(assets) == 0 {
   673  		return nil
   674  	}
   675  
   676  	// get s3 params from server
   677  	s3params, err := ri().GetS3Params(ctx, chat1.GetS3ParamsArg{
   678  		ConversationID: convID,
   679  		TempCreds:      true,
   680  	})
   681  	if err != nil {
   682  		r.Debug(ctx, "error getting s3params: %s", err)
   683  		return err
   684  	}
   685  
   686  	// Try to delete the assets remotely
   687  	if err := r.store.DeleteAssets(ctx, s3params, signer, assets); err != nil {
   688  		// there's no way to get asset information after this point.
   689  		// any assets not deleted will be stranded on s3.
   690  		r.Debug(ctx, "error deleting assets: %s", err)
   691  	}
   692  
   693  	r.Debug(ctx, "deleted %d assets", len(assets))
   694  	return nil
   695  }
   696  
   697  func (r *RemoteAttachmentFetcher) PutUploadedAsset(ctx context.Context, filename string, asset chat1.Asset) error {
   698  	return nil
   699  }
   700  
   701  func (r *RemoteAttachmentFetcher) IsAssetLocal(ctx context.Context, asset chat1.Asset) (bool, error) {
   702  	return false, nil
   703  }
   704  
   705  func (r *RemoteAttachmentFetcher) OnDbNuke(mctx libkb.MetaContext) error { return nil }
   706  func (r *RemoteAttachmentFetcher) OnStart(mctx libkb.MetaContext)        {}
   707  
   708  type CachingAttachmentFetcher struct {
   709  	globals.Contextified
   710  	utils.DebugLabeler
   711  
   712  	store   attachments.Store
   713  	diskLRU *disklru.DiskLRU
   714  
   715  	// testing
   716  	tempDir string
   717  }
   718  
   719  var _ types.AttachmentFetcher = (*CachingAttachmentFetcher)(nil)
   720  
   721  func NewCachingAttachmentFetcher(g *globals.Context, store attachments.Store, size int) *CachingAttachmentFetcher {
   722  	return &CachingAttachmentFetcher{
   723  		Contextified: globals.NewContextified(g),
   724  		DebugLabeler: utils.NewDebugLabeler(g.ExternalG(), "CachingAttachmentFetcher", false),
   725  		store:        store,
   726  		diskLRU:      disklru.NewDiskLRU("attachments", 2, size),
   727  	}
   728  }
   729  
   730  func (c *CachingAttachmentFetcher) getBaseDir() string {
   731  	baseDir := c.G().GetCacheDir()
   732  	if len(c.tempDir) > 0 {
   733  		baseDir = c.tempDir
   734  	}
   735  	return baseDir
   736  }
   737  
   738  func (c *CachingAttachmentFetcher) getCacheDir() string {
   739  	return filepath.Join(c.getBaseDir(), "attachments")
   740  }
   741  
   742  func (c *CachingAttachmentFetcher) getFullFilename(name string) string {
   743  	return name + ".attachment"
   744  }
   745  
   746  func (c *CachingAttachmentFetcher) closeFile(f io.Closer) {
   747  	if f != nil {
   748  		f.Close()
   749  	}
   750  }
   751  
   752  func (c *CachingAttachmentFetcher) cacheKey(asset chat1.Asset) string {
   753  	return asset.Path
   754  }
   755  
   756  func (c *CachingAttachmentFetcher) createAttachmentFile(ctx context.Context) (*os.File, error) {
   757  	err := os.MkdirAll(c.getCacheDir(), os.ModePerm)
   758  	if err != nil {
   759  		return nil, err
   760  	}
   761  	file, err := os.CreateTemp(c.getCacheDir(), "att")
   762  	file.Close()
   763  	if err != nil {
   764  		return nil, err
   765  	}
   766  	path := c.getFullFilename(file.Name())
   767  	if err := os.Rename(file.Name(), path); err != nil {
   768  		return nil, err
   769  	}
   770  	return os.OpenFile(path, os.O_RDWR, os.ModeAppend)
   771  }
   772  
   773  // normalizeFilenameFromCache substitutes the existing cache dir value into the
   774  // file path since it's possible for the path to the cache dir to change,
   775  // especially on mobile.
   776  func (c *CachingAttachmentFetcher) normalizeFilenameFromCache(file string) string {
   777  	dir := filepath.Base(filepath.Dir(file))
   778  	file = filepath.Base(file)
   779  	// some attachments may be in the "uploadedpreviews"/"uploadedfulls" dirs,
   780  	// so we preserve the parent directory here.
   781  	return filepath.Join(c.getBaseDir(), dir, file)
   782  }
   783  
   784  func (c *CachingAttachmentFetcher) localAssetPath(ctx context.Context, asset chat1.Asset) (found bool, path string, err error) {
   785  	found, entry, err := c.diskLRU.Get(ctx, c.G(), c.cacheKey(asset))
   786  	if err != nil {
   787  		return found, path, err
   788  	}
   789  	if found {
   790  		path = c.normalizeFilenameFromCache(entry.Value.(string))
   791  	}
   792  	return found, path, nil
   793  }
   794  
   795  func (c *CachingAttachmentFetcher) StreamAttachment(ctx context.Context, convID chat1.ConversationID,
   796  	asset chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer) (res io.ReadSeeker, err error) {
   797  	defer c.Trace(ctx, &err, "StreamAttachment")()
   798  	return NewRemoteAttachmentFetcher(c.G(), c.store).StreamAttachment(ctx, convID, asset, ri, signer)
   799  }
   800  
   801  func (c *CachingAttachmentFetcher) FetchAttachment(ctx context.Context, w io.Writer,
   802  	convID chat1.ConversationID, asset chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer,
   803  	progress types.ProgressReporter) (err error) {
   804  
   805  	defer c.Trace(ctx, &err, "FetchAttachment")()
   806  
   807  	// Check for a disk cache hit, and decrypt that onto the response stream
   808  	found, path, err := c.localAssetPath(ctx, asset)
   809  	if err != nil {
   810  		return err
   811  	}
   812  	if found {
   813  		c.Debug(ctx, "FetchAttachment: cache hit for: %s filepath: %s", asset.Path, path)
   814  		fileReader, err := os.Open(path)
   815  		defer c.closeFile(fileReader)
   816  		if err != nil {
   817  			c.Debug(ctx, "FetchAttachment: failed to read cached file, removing: %s", err)
   818  			os.Remove(path)
   819  			_ = c.diskLRU.Remove(ctx, c.G(), c.cacheKey(asset))
   820  			found = false
   821  		}
   822  		if found {
   823  			return c.store.DecryptAsset(ctx, w, fileReader, asset, progress)
   824  		}
   825  	}
   826  
   827  	// Grab S3 params for the conversation
   828  	s3params, err := ri().GetS3Params(ctx, chat1.GetS3ParamsArg{
   829  		ConversationID: convID,
   830  		TempCreds:      true,
   831  	})
   832  	if err != nil {
   833  		return err
   834  	}
   835  
   836  	// Create a reader to the remote ciphertext
   837  	remoteReader, err := c.store.GetAssetReader(ctx, s3params, asset, signer)
   838  	defer c.closeFile(remoteReader)
   839  	if err != nil {
   840  		return err
   841  	}
   842  
   843  	// Create a file we can write the ciphertext into
   844  	fileWriter, err := c.createAttachmentFile(ctx)
   845  	defer c.closeFile(fileWriter)
   846  	if err != nil {
   847  		return err
   848  	}
   849  
   850  	// Read out the ciphertext into the decryption copier, and simultaneously write
   851  	// into the cached file (the ciphertext)
   852  	teeReader := io.TeeReader(remoteReader, fileWriter)
   853  	if err := c.store.DecryptAsset(ctx, w, teeReader, asset, progress); err != nil {
   854  		c.Debug(ctx, "FetchAttachment: error reading asset: %s", err)
   855  		c.closeFile(fileWriter)
   856  		os.Remove(fileWriter.Name())
   857  		return err
   858  	}
   859  
   860  	// commit to the on disk LRU
   861  	return c.putFileInLRU(ctx, fileWriter.Name(), asset)
   862  }
   863  
   864  func (c *CachingAttachmentFetcher) putFileInLRU(ctx context.Context, filename string, asset chat1.Asset) error {
   865  	// Add an entry to the disk LRU mapping the asset path to the local path, and remove
   866  	// the remnants of any evicted attachments.
   867  	evicted, err := c.diskLRU.Put(ctx, c.G(), c.cacheKey(asset), filename)
   868  	if err != nil {
   869  		return err
   870  	}
   871  	if evicted != nil {
   872  		path := c.normalizeFilenameFromCache(evicted.Value.(string))
   873  		os.Remove(path)
   874  	}
   875  	return nil
   876  }
   877  
   878  func (c *CachingAttachmentFetcher) IsAssetLocal(ctx context.Context, asset chat1.Asset) (found bool, err error) {
   879  	defer c.Trace(ctx, &err, "IsAssetLocal")()
   880  	found, path, err := c.localAssetPath(ctx, asset)
   881  	if err != nil {
   882  		return false, err
   883  	}
   884  	if !found {
   885  		return false, nil
   886  	}
   887  	fileReader, err := os.Open(path)
   888  	defer c.closeFile(fileReader)
   889  	if err != nil {
   890  		return false, nil
   891  	}
   892  	return true, nil
   893  }
   894  
   895  func (c *CachingAttachmentFetcher) PutUploadedAsset(ctx context.Context, filename string, asset chat1.Asset) (err error) {
   896  	defer c.Trace(ctx, &err, "PutUploadedAsset")()
   897  	return c.putFileInLRU(ctx, filename, asset)
   898  }
   899  
   900  func (c *CachingAttachmentFetcher) DeleteAssets(ctx context.Context,
   901  	convID chat1.ConversationID, assets []chat1.Asset, ri func() chat1.RemoteInterface, signer s3.Signer) (err error) {
   902  	defer c.Trace(ctx, &err, "DeleteAssets")()
   903  
   904  	if len(assets) == 0 {
   905  		return nil
   906  	}
   907  
   908  	// Delete the assets locally
   909  	for _, asset := range assets {
   910  		found, path, err := c.localAssetPath(ctx, asset)
   911  		if err != nil {
   912  			c.Debug(ctx, "error getting asset: %s", err)
   913  			continue
   914  		}
   915  		if found {
   916  			os.Remove(path)
   917  			_ = c.diskLRU.Remove(ctx, c.G(), c.cacheKey(asset))
   918  		}
   919  	}
   920  
   921  	// get s3 params from server
   922  	s3params, err := ri().GetS3Params(ctx, chat1.GetS3ParamsArg{
   923  		ConversationID: convID,
   924  		TempCreds:      true,
   925  	})
   926  	if err != nil {
   927  		c.Debug(ctx, "error getting s3params: %s", err)
   928  		return err
   929  	}
   930  
   931  	// Try to delete the assets remotely
   932  	if err := c.store.DeleteAssets(ctx, s3params, signer, assets); err != nil {
   933  		// there's no way to get asset information after this point.
   934  		// any assets not deleted will be stranded on s3.
   935  		c.Debug(ctx, "error deleting assets: %s", err)
   936  	}
   937  
   938  	c.Debug(ctx, "deleted %d assets", len(assets))
   939  	return nil
   940  }
   941  
   942  func (c *CachingAttachmentFetcher) OnStart(mctx libkb.MetaContext) {
   943  	mctx, cancel := mctx.WithContextCancel()
   944  	mctx.G().PushShutdownHook(func(libkb.MetaContext) error {
   945  		cancel()
   946  		return nil
   947  	})
   948  	go disklru.CleanOutOfSyncWithDelay(mctx, c.diskLRU, c.getCacheDir(), 10*time.Second)
   949  }
   950  
   951  func (c *CachingAttachmentFetcher) OnDbNuke(mctx libkb.MetaContext) error {
   952  	if c.diskLRU != nil {
   953  		if err := c.diskLRU.CleanOutOfSync(mctx, c.getCacheDir()); err != nil {
   954  			c.Debug(mctx.Ctx(), "unable to run clean: %v", err)
   955  		}
   956  	}
   957  	return nil
   958  }