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 }