media.nim 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. # SPDX-License-Identifier: AGPL-3.0-only
  2. import uri, strutils, httpclient, os, hashes, base64, re
  3. import asynchttpserver, asyncstreams, asyncfile, asyncnet
  4. import jester
  5. import router_utils
  6. import ".."/[types, formatters, utils]
  7. export asynchttpserver, asyncstreams, asyncfile, asyncnet
  8. export httpclient, os, strutils, asyncstreams, base64, re
  9. const
  10. m3u8Mime* = "application/vnd.apple.mpegurl"
  11. maxAge* = "max-age=604800"
  12. proc safeFetch*(url: string): Future[string] {.async.} =
  13. let client = newAsyncHttpClient()
  14. try: result = await client.getContent(url)
  15. except: discard
  16. finally: client.close()
  17. template respond*(req: asynchttpserver.Request; headers) =
  18. var msg = "HTTP/1.1 200 OK\c\L"
  19. for k, v in headers:
  20. msg.add(k & ": " & v & "\c\L")
  21. msg.add "\c\L"
  22. yield req.client.send(msg)
  23. proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
  24. result = Http200
  25. let
  26. request = req.getNativeReq()
  27. client = newAsyncHttpClient()
  28. try:
  29. let res = await client.get(url)
  30. if res.status != "200 OK":
  31. if res.status != "404 Not Found":
  32. echo "[media] Proxying failed, status: $1, url: $2" % [res.status, url]
  33. return Http404
  34. let hashed = $hash(url)
  35. if request.headers.getOrDefault("If-None-Match") == hashed:
  36. return Http304
  37. let contentLength =
  38. if res.headers.hasKey("content-length"):
  39. res.headers["content-length", 0]
  40. else:
  41. ""
  42. let headers = newHttpHeaders({
  43. "Content-Type": res.headers["content-type", 0],
  44. "Content-Length": contentLength,
  45. "Cache-Control": maxAge,
  46. "ETag": hashed
  47. })
  48. respond(request, headers)
  49. var (hasValue, data) = (true, "")
  50. while hasValue:
  51. (hasValue, data) = await res.bodyStream.read()
  52. if hasValue:
  53. await request.client.send(data)
  54. data.setLen 0
  55. except HttpRequestError, ProtocolError, OSError:
  56. echo "[media] Proxying exception, error: $1, url: $2" % [getCurrentExceptionMsg(), url]
  57. result = Http404
  58. finally:
  59. client.close()
  60. template check*(code): untyped =
  61. if code != Http200:
  62. resp code
  63. else:
  64. enableRawMode()
  65. break route
  66. proc decoded*(req: jester.Request; index: int): string =
  67. let
  68. based = req.matches[0].len > 1
  69. encoded = req.matches[index]
  70. if based: decode(encoded)
  71. else: decodeUrl(encoded)
  72. proc createMediaRouter*(cfg: Config) =
  73. router media:
  74. get "/pic/?":
  75. resp Http404
  76. get re"^\/pic\/orig\/(enc)?\/?(.+)":
  77. var url = decoded(request, 1)
  78. if "twimg.com" notin url:
  79. url.insert(twimg)
  80. if not url.startsWith(https):
  81. url.insert(https)
  82. url.add("?name=orig")
  83. let uri = parseUri(url)
  84. cond isTwitterUrl(uri) == true
  85. let code = await proxyMedia(request, url)
  86. check code
  87. get re"^\/pic\/(enc)?\/?(.+)":
  88. var url = decoded(request, 1)
  89. if "twimg.com" notin url:
  90. url.insert(twimg)
  91. if not url.startsWith(https):
  92. url.insert(https)
  93. let uri = parseUri(url)
  94. cond isTwitterUrl(uri) == true
  95. let code = await proxyMedia(request, url)
  96. check code
  97. get re"^\/video\/(enc)?\/?(.+)\/(.+)$":
  98. let url = decoded(request, 2)
  99. cond "http" in url
  100. if getHmac(url) != request.matches[1]:
  101. resp Http403, showError("Failed to verify signature", cfg)
  102. if ".mp4" in url or ".ts" in url or ".m4s" in url:
  103. let code = await proxyMedia(request, url)
  104. check code
  105. var content: string
  106. if ".vmap" in url:
  107. let m3u8 = getM3u8Url(await safeFetch(url))
  108. if m3u8.len > 0:
  109. content = await safeFetch(url)
  110. else:
  111. resp Http404
  112. if ".m3u8" in url:
  113. let vid = await safeFetch(url)
  114. content = proxifyVideo(vid, cookiePref(proxyVideos))
  115. resp content, m3u8Mime