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 }