mirror of
https://github.com/zedeus/nitter
synced 2026-09-05 22:59:31 +00:00
181 lines
5.3 KiB
Nim
181 lines
5.3 KiB
Nim
# SPDX-License-Identifier: AGPL-3.0-only
|
|
import uri, strutils, httpclient, os, hashes, base64, re
|
|
import asynchttpserver, asyncstreams, asyncfile, asyncnet
|
|
import asyncdispatch
|
|
|
|
import jester
|
|
|
|
import router_utils
|
|
import ".."/[types, formatters, utils]
|
|
|
|
export asynchttpserver, asyncstreams, asyncfile, asyncnet
|
|
export httpclient, os, strutils, asyncstreams, base64, re
|
|
|
|
const
|
|
m3u8Mime* = "application/vnd.apple.mpegurl"
|
|
maxAge* = "max-age=604800"
|
|
|
|
proc safeFetch*(url: string): Future[string] {.async.} =
|
|
# maxRedirects=0: the caller already validated the host, so never follow a
|
|
# redirect off the allowlisted host (would re-open the #1411 SSRF).
|
|
let client = newAsyncHttpClient(maxRedirects = 0)
|
|
try: result = await client.getContent(url)
|
|
except: discard
|
|
finally: client.close()
|
|
|
|
template respond*(req: asynchttpserver.Request; headers) =
|
|
var msg = "HTTP/1.1 200 OK\c\L"
|
|
for k, v in headers:
|
|
msg.add(k & ": " & v & "\c\L")
|
|
|
|
msg.add "\c\L"
|
|
yield req.client.send(msg)
|
|
|
|
proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
|
|
result = Http200
|
|
let request = req.getNativeReq()
|
|
var fetchUrl = url
|
|
|
|
for attempt in 0 .. 2:
|
|
let client = newAsyncHttpClient(maxRedirects = 0)
|
|
var shouldRetry = false
|
|
try:
|
|
let resFut = client.get(fetchUrl)
|
|
let completed = await withTimeout(resFut, 5000)
|
|
if not completed:
|
|
if attempt < 2:
|
|
echo "[media] Retry $1/2, timeout after 5s, url: $2" % [$(attempt + 1), fetchUrl]
|
|
shouldRetry = true
|
|
else:
|
|
echo "[media] Proxying timeout after 5s, url: $1" % [fetchUrl]
|
|
return Http504
|
|
else:
|
|
let res = resFut.read()
|
|
if res.status != "200 OK":
|
|
if res.status == "404 Not Found":
|
|
return Http404
|
|
if res.status.startsWith("30") and res.headers.hasKey("location"):
|
|
let location = res.headers["location", 0]
|
|
if isTwitterUrl(location):
|
|
fetchUrl = location
|
|
shouldRetry = true
|
|
continue
|
|
else:
|
|
return Http403
|
|
if attempt < 2:
|
|
echo "[media] Retry $1/2, status: $2, url: $3" % [$(attempt + 1), res.status, fetchUrl]
|
|
shouldRetry = true
|
|
else:
|
|
echo "[media] Proxying failed, status: $1, url: $2" % [res.status, fetchUrl]
|
|
return Http404
|
|
else:
|
|
let hashed = $hash(url)
|
|
if request.headers.getOrDefault("If-None-Match") == hashed:
|
|
return Http304
|
|
|
|
let contentLength =
|
|
if res.headers.hasKey("content-length"):
|
|
res.headers["content-length", 0]
|
|
else:
|
|
""
|
|
|
|
let headers = newHttpHeaders({
|
|
"content-type": res.headers["content-type", 0],
|
|
"content-length": contentLength,
|
|
"cache-control": maxAge,
|
|
"etag": hashed
|
|
})
|
|
|
|
respond(request, headers)
|
|
|
|
var (hasValue, data) = (true, "")
|
|
while hasValue:
|
|
(hasValue, data) = await res.bodyStream.read()
|
|
if hasValue:
|
|
await request.client.send(data)
|
|
data.setLen 0
|
|
return Http200
|
|
except CatchableError:
|
|
if attempt < 2:
|
|
echo "[media] Retry $1/2, error: $2, url: $3" % [$(attempt + 1), getCurrentExceptionMsg(), fetchUrl]
|
|
shouldRetry = true
|
|
else:
|
|
echo "[media] Proxying exception, error: $1, url: $2" % [getCurrentExceptionMsg(), fetchUrl]
|
|
result = Http404
|
|
finally:
|
|
client.close()
|
|
if not shouldRetry:
|
|
break
|
|
|
|
template check*(code): untyped =
|
|
if code != Http200:
|
|
resp code
|
|
else:
|
|
enableRawMode()
|
|
break route
|
|
|
|
proc decoded*(req: jester.Request; index: int): string =
|
|
let
|
|
based = req.matches[0].len > 1
|
|
encoded = req.matches[index]
|
|
if based: decode(encoded)
|
|
else: decodeUrl(encoded)
|
|
|
|
proc normalizeImgUrl*(url: var string) =
|
|
if not url.startsWith("http"):
|
|
if "twimg.com" notin url:
|
|
url.insert(twimg)
|
|
url.insert(https)
|
|
|
|
proc createMediaRouter*(cfg: Config) =
|
|
router media:
|
|
get "/pic/?":
|
|
resp Http404
|
|
|
|
get re"^\/pic\/orig\/(enc)?\/?(.+)":
|
|
var url = decoded(request, 1)
|
|
cond "/amplify_video/" notin url
|
|
normalizeImgUrl(url)
|
|
url.add("?name=orig")
|
|
|
|
let uri = parseUri(url)
|
|
cond isTwitterUrl(uri) == true
|
|
|
|
let code = await proxyMedia(request, url)
|
|
check code
|
|
|
|
get re"^\/pic\/(enc)?\/?(.+)":
|
|
var url = decoded(request, 1)
|
|
cond "/amplify_video/" notin url
|
|
normalizeImgUrl(url)
|
|
|
|
let uri = parseUri(url)
|
|
cond isTwitterUrl(uri) == true
|
|
|
|
let code = await proxyMedia(request, url)
|
|
check code
|
|
|
|
get re"^\/video\/(enc)?\/?(.+)\/(.+)$":
|
|
let url = decoded(request, 2)
|
|
cond isTwitterUrl(url)
|
|
|
|
if getHmac(url) != request.matches[1]:
|
|
resp Http403, showError("Failed to verify signature", cfg)
|
|
|
|
if ".mp4" in url or ".ts" in url or ".m4s" in url or ".aac" in url:
|
|
let code = await proxyMedia(request, url)
|
|
check code
|
|
|
|
var content: string
|
|
if ".vmap" in url:
|
|
let m3u8 = getM3u8Url(await safeFetch(url))
|
|
if m3u8.len > 0:
|
|
content = await safeFetch(url)
|
|
else:
|
|
resp Http404
|
|
|
|
if ".m3u8" in url:
|
|
let vid = await safeFetch(url)
|
|
content = proxifyVideo(vid, requestPrefs().proxyVideos, url)
|
|
|
|
resp content, m3u8Mime
|