upload.go 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. // Copyright (c) 2024 Tulir Asokan
  2. //
  3. // This Source Code Form is subject to the terms of the Mozilla Public
  4. // License, v. 2.0. If a copy of the MPL was not distributed with this
  5. // file, You can obtain one at http://mozilla.org/MPL/2.0/.
  6. package whatsmeow
  7. import (
  8. "bytes"
  9. "context"
  10. "crypto/hmac"
  11. "crypto/sha256"
  12. "encoding/base64"
  13. "encoding/json"
  14. "fmt"
  15. "io"
  16. "net/http"
  17. "net/url"
  18. "os"
  19. "go.mau.fi/util/random"
  20. "go.mau.fi/whatsmeow/socket"
  21. "go.mau.fi/whatsmeow/util/cbcutil"
  22. )
  23. // UploadResponse contains the data from the attachment upload, which can be put into a message to send the attachment.
  24. type UploadResponse struct {
  25. URL string `json:"url"`
  26. DirectPath string `json:"direct_path"`
  27. Handle string `json:"handle"`
  28. ObjectID string `json:"object_id"`
  29. MediaKey []byte `json:"-"`
  30. FileEncSHA256 []byte `json:"-"`
  31. FileSHA256 []byte `json:"-"`
  32. FileLength uint64 `json:"-"`
  33. }
  34. // Upload uploads the given attachment to WhatsApp servers.
  35. //
  36. // You should copy the fields in the response to the corresponding fields in a protobuf message.
  37. //
  38. // For example, to send an image:
  39. //
  40. // resp, err := cli.Upload(context.Background(), yourImageBytes, whatsmeow.MediaImage)
  41. // // handle error
  42. //
  43. // imageMsg := &waE2E.ImageMessage{
  44. // Caption: proto.String("Hello, world!"),
  45. // Mimetype: proto.String("image/png"), // replace this with the actual mime type
  46. // // you can also optionally add other fields like ContextInfo and JpegThumbnail here
  47. //
  48. // URL: &resp.URL,
  49. // DirectPath: &resp.DirectPath,
  50. // MediaKey: resp.MediaKey,
  51. // FileEncSHA256: resp.FileEncSHA256,
  52. // FileSHA256: resp.FileSHA256,
  53. // FileLength: &resp.FileLength,
  54. // }
  55. // _, err = cli.SendMessage(context.Background(), targetJID, &waE2E.Message{
  56. // ImageMessage: imageMsg,
  57. // })
  58. // // handle error again
  59. //
  60. // The same applies to the other message types like DocumentMessage, just replace the struct type and Message field name.
  61. func (cli *Client) Upload(ctx context.Context, plaintext []byte, appInfo MediaType) (resp UploadResponse, err error) {
  62. resp.FileLength = uint64(len(plaintext))
  63. resp.MediaKey = random.Bytes(32)
  64. plaintextSHA256 := sha256.Sum256(plaintext)
  65. resp.FileSHA256 = plaintextSHA256[:]
  66. iv, cipherKey, macKey, _ := getMediaKeys(resp.MediaKey, appInfo)
  67. var ciphertext []byte
  68. ciphertext, err = cbcutil.Encrypt(cipherKey, iv, plaintext)
  69. if err != nil {
  70. err = fmt.Errorf("failed to encrypt file: %w", err)
  71. return
  72. }
  73. h := hmac.New(sha256.New, macKey)
  74. h.Write(iv)
  75. h.Write(ciphertext)
  76. dataToUpload := append(ciphertext, h.Sum(nil)[:10]...)
  77. dataHash := sha256.Sum256(dataToUpload)
  78. resp.FileEncSHA256 = dataHash[:]
  79. err = cli.rawUpload(ctx, bytes.NewReader(dataToUpload), uint64(len(dataToUpload)), resp.FileEncSHA256, appInfo, false, &resp)
  80. return
  81. }
  82. // UploadReader uploads the given attachment to WhatsApp servers.
  83. //
  84. // This is otherwise identical to [Upload], but it reads the plaintext from an [io.Reader] instead of a byte slice.
  85. // A temporary file is required for the encryption process. If tempFile is nil, a temporary file will be created
  86. // and deleted after the upload.
  87. //
  88. // To use only one file, pass the same file as both plaintext and tempFile. This will cause the file to be overwritten with encrypted data.
  89. func (cli *Client) UploadReader(ctx context.Context, plaintext io.Reader, tempFile io.ReadWriteSeeker, appInfo MediaType) (resp UploadResponse, err error) {
  90. resp.MediaKey = random.Bytes(32)
  91. iv, cipherKey, macKey, _ := getMediaKeys(resp.MediaKey, appInfo)
  92. if tempFile == nil {
  93. tempFile, err = os.CreateTemp("", "whatsmeow-upload-*")
  94. if err != nil {
  95. err = fmt.Errorf("failed to create temporary file: %w", err)
  96. return
  97. }
  98. defer func() {
  99. tempFileFile := tempFile.(*os.File)
  100. _ = tempFileFile.Close()
  101. _ = os.Remove(tempFileFile.Name())
  102. }()
  103. }
  104. var uploadSize uint64
  105. resp.FileSHA256, resp.FileEncSHA256, resp.FileLength, uploadSize, err = cbcutil.EncryptStream(cipherKey, iv, macKey, plaintext, tempFile)
  106. if err != nil {
  107. err = fmt.Errorf("failed to encrypt file: %w", err)
  108. return
  109. }
  110. _, err = tempFile.Seek(0, io.SeekStart)
  111. if err != nil {
  112. err = fmt.Errorf("failed to seek to start of temporary file: %w", err)
  113. return
  114. }
  115. err = cli.rawUpload(ctx, tempFile, uploadSize, resp.FileEncSHA256, appInfo, false, &resp)
  116. return
  117. }
  118. // UploadNewsletter uploads the given attachment to WhatsApp servers without encrypting it first.
  119. //
  120. // Newsletter media works mostly the same way as normal media, with a few differences:
  121. // * Since it's unencrypted, there's no MediaKey or FileEncSHA256 fields.
  122. // * There's a "media handle" that needs to be passed in SendRequestExtra.
  123. //
  124. // Example:
  125. //
  126. // resp, err := cli.UploadNewsletter(context.Background(), yourImageBytes, whatsmeow.MediaImage)
  127. // // handle error
  128. //
  129. // imageMsg := &waE2E.ImageMessage{
  130. // // Caption, mime type and other such fields work like normal
  131. // Caption: proto.String("Hello, world!"),
  132. // Mimetype: proto.String("image/png"),
  133. //
  134. // // URL and direct path are also there like normal media
  135. // URL: &resp.URL,
  136. // DirectPath: &resp.DirectPath,
  137. // FileSHA256: resp.FileSHA256,
  138. // FileLength: &resp.FileLength,
  139. // // Newsletter media isn't encrypted, so the media key and file enc sha fields are not applicable
  140. // }
  141. // _, err = cli.SendMessage(context.Background(), newsletterJID, &waE2E.Message{
  142. // ImageMessage: imageMsg,
  143. // }, whatsmeow.SendRequestExtra{
  144. // // Unlike normal media, newsletters also include a "media handle" in the send request.
  145. // MediaHandle: resp.Handle,
  146. // })
  147. // // handle error again
  148. func (cli *Client) UploadNewsletter(ctx context.Context, data []byte, appInfo MediaType) (resp UploadResponse, err error) {
  149. resp.FileLength = uint64(len(data))
  150. hash := sha256.Sum256(data)
  151. resp.FileSHA256 = hash[:]
  152. err = cli.rawUpload(ctx, bytes.NewReader(data), resp.FileLength, resp.FileSHA256, appInfo, true, &resp)
  153. return
  154. }
  155. // UploadNewsletterReader uploads the given attachment to WhatsApp servers without encrypting it first.
  156. //
  157. // This is otherwise identical to [UploadNewsletter], but it reads the plaintext from an [io.Reader] instead of a byte slice.
  158. // Unlike [UploadReader], this does not require a temporary file. However, the data needs to be hashed first,
  159. // so an [io.ReadSeeker] is required to be able to read the data twice.
  160. func (cli *Client) UploadNewsletterReader(ctx context.Context, data io.ReadSeeker, appInfo MediaType) (resp UploadResponse, err error) {
  161. hasher := sha256.New()
  162. var fileLength int64
  163. fileLength, err = io.Copy(hasher, data)
  164. resp.FileLength = uint64(fileLength)
  165. resp.FileSHA256 = hasher.Sum(nil)
  166. _, err = data.Seek(0, io.SeekStart)
  167. if err != nil {
  168. err = fmt.Errorf("failed to seek to start of data: %w", err)
  169. return
  170. }
  171. err = cli.rawUpload(ctx, data, resp.FileLength, resp.FileSHA256, appInfo, true, &resp)
  172. return
  173. }
  174. func (cli *Client) rawUpload(ctx context.Context, dataToUpload io.Reader, uploadSize uint64, fileHash []byte, appInfo MediaType, newsletter bool, resp *UploadResponse) error {
  175. mediaConn, err := cli.refreshMediaConn(ctx, false)
  176. if err != nil {
  177. return fmt.Errorf("failed to refresh media connections: %w", err)
  178. }
  179. token := base64.URLEncoding.EncodeToString(fileHash)
  180. q := url.Values{
  181. "auth": []string{mediaConn.Auth},
  182. "token": []string{token},
  183. }
  184. mmsType := mediaTypeToMMSType[appInfo]
  185. uploadPrefix := "mms"
  186. if cli.MessengerConfig != nil {
  187. uploadPrefix = "wa-msgr/mms"
  188. // Messenger upload only allows voice messages, not audio files
  189. if mmsType == "audio" {
  190. mmsType = "ptt"
  191. }
  192. }
  193. if newsletter {
  194. mmsType = fmt.Sprintf("newsletter-%s", mmsType)
  195. uploadPrefix = "newsletter"
  196. }
  197. var host string
  198. // Hacky hack to prefer last option (rupload.facebook.com) for messenger uploads.
  199. // For some reason, the primary host doesn't work, even though it has the <upload/> tag.
  200. if cli.MessengerConfig != nil {
  201. host = mediaConn.Hosts[len(mediaConn.Hosts)-1].Hostname
  202. } else {
  203. host = mediaConn.Hosts[0].Hostname
  204. }
  205. uploadURL := url.URL{
  206. Scheme: "https",
  207. Host: host,
  208. Path: fmt.Sprintf("/%s/%s/%s", uploadPrefix, mmsType, token),
  209. RawQuery: q.Encode(),
  210. }
  211. req, err := http.NewRequestWithContext(ctx, http.MethodPost, uploadURL.String(), dataToUpload)
  212. if err != nil {
  213. return fmt.Errorf("failed to prepare request: %w", err)
  214. }
  215. req.ContentLength = int64(uploadSize)
  216. req.Header.Set("Origin", socket.Origin)
  217. req.Header.Set("Referer", socket.Origin+"/")
  218. httpResp, err := cli.mediaHTTP.Do(req)
  219. if err != nil {
  220. err = fmt.Errorf("failed to execute request: %w", err)
  221. } else if httpResp.StatusCode != http.StatusOK {
  222. err = fmt.Errorf("upload failed with status code %d", httpResp.StatusCode)
  223. } else if err = json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
  224. err = fmt.Errorf("failed to parse upload response: %w", err)
  225. }
  226. if httpResp != nil {
  227. _ = httpResp.Body.Close()
  228. }
  229. return err
  230. }