media.nim 3.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  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, agents, 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. let mediaAgent* = getAgent()
  13. proc safeFetch*(url, agent: string): Future[string] {.async.} =
  14. let client = newAsyncHttpClient(userAgent=agent)
  15. try: result = await client.getContent(url)
  16. except: discard
  17. finally: client.close()
  18. template respond*(req: asynchttpserver.Request; headers) =
  19. var msg = "HTTP/1.1 200 OK\c\L"
  20. for k, v in headers:
  21. msg.add(k & ": " & v & "\c\L")
  22. msg.add "\c\L"
  23. yield req.client.send(msg)
  24. proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
  25. result = Http200
  26. let
  27. request = req.getNativeReq()
  28. client = newAsyncHttpClient(userAgent=mediaAgent)
  29. try:
  30. let res = await client.get(url)
  31. if res.status != "200 OK":
  32. return Http404
  33. let hashed = $hash(url)
  34. if request.headers.getOrDefault("If-None-Match") == hashed:
  35. return Http304
  36. let contentLength =
  37. if res.headers.hasKey("content-length"):
  38. res.headers["content-length", 0]
  39. else:
  40. ""
  41. let headers = newHttpHeaders({
  42. "Content-Type": res.headers["content-type", 0],
  43. "Content-Length": contentLength,
  44. "Cache-Control": maxAge,
  45. "ETag": hashed
  46. })
  47. respond(request, headers)
  48. var (hasValue, data) = (true, "")
  49. while hasValue:
  50. (hasValue, data) = await res.bodyStream.read()
  51. if hasValue:
  52. await request.client.send(data)
  53. data.setLen 0
  54. except HttpRequestError, ProtocolError, OSError:
  55. result = Http404
  56. finally:
  57. client.close()
  58. template check*(code): untyped =
  59. if code != Http200:
  60. resp code
  61. else:
  62. enableRawMode()
  63. break route
  64. proc decoded*(req: jester.Request; index: int): string =
  65. let
  66. based = req.matches[0].len > 1
  67. encoded = req.matches[index]
  68. if based: decode(encoded)
  69. else: decodeUrl(encoded)
  70. proc createMediaRouter*(cfg: Config) =
  71. router media:
  72. get "/pic/?":
  73. resp Http404
  74. get re"^\/pic\/(enc)?\/?(.+)":
  75. var url = decoded(request, 1)
  76. if "twimg.com" notin url:
  77. url.insert(twimg)
  78. if not url.startsWith(https):
  79. url.insert(https)
  80. let uri = parseUri(url)
  81. cond isTwitterUrl(uri) == true
  82. let code = await proxyMedia(request, url)
  83. check code
  84. get re"^\/video\/(enc)?\/?(.+)\/(.+)$":
  85. let url = decoded(request, 2)
  86. cond "http" in url
  87. if getHmac(url) != request.matches[1]:
  88. resp showError("Failed to verify signature", cfg)
  89. if ".mp4" in url or ".ts" in url or ".m4s" in url:
  90. let code = await proxyMedia(request, url)
  91. check code
  92. var content: string
  93. if ".vmap" in url:
  94. let m3u8 = getM3u8Url(await safeFetch(url, mediaAgent))
  95. if m3u8.len > 0:
  96. content = await safeFetch(url, mediaAgent)
  97. else:
  98. resp Http404
  99. if ".m3u8" in url:
  100. let vid = await safeFetch(url, mediaAgent)
  101. content = proxifyVideo(vid, cookiePref(proxyVideos))
  102. resp content, m3u8Mime