mirror of
https://github.com/zedeus/nitter.git
synced 2024-05-21 10:38:10 +00:00
160 lines
4.4 KiB
Nim
160 lines
4.4 KiB
Nim
# SPDX-License-Identifier: AGPL-3.0-only
|
|
import uri, strutils, httpclient, os, hashes, base64, re
|
|
import asynchttpserver, asyncstreams, asyncfile, asyncnet
|
|
|
|
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"
|
|
mp4Mime* = "video/mp4"
|
|
maxAge* = "public, max-age=604800, must-revalidate"
|
|
|
|
proc safeFetch*(url: string): Future[string] {.async.} =
|
|
let client = newAsyncHttpClient()
|
|
try: result = await client.getContent(url)
|
|
except: discard
|
|
finally: client.close()
|
|
|
|
template respond*(req: asynchttpserver.Request; code: HttpCode;
|
|
headers: seq[(string, string)]) =
|
|
var msg = "HTTP/1.1 " & $code & "\c\L"
|
|
for (k, v) in headers:
|
|
msg.add(k & ": " & v & "\c\L")
|
|
|
|
msg.add "\c\L"
|
|
yield req.client.send(msg, flags={})
|
|
|
|
proc getContentLength(res: AsyncResponse): string =
|
|
result = "0"
|
|
if res.headers.hasKey("content-length"):
|
|
result = $res.contentLength
|
|
elif res.headers.hasKey("content-range"):
|
|
result = res.headers["content-range"]
|
|
result = result[result.find('/') + 1 .. ^1]
|
|
if result == "*":
|
|
result.setLen(0)
|
|
|
|
proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
|
|
result = Http200
|
|
|
|
let
|
|
request = req.getNativeReq()
|
|
hashed = $hash(url)
|
|
|
|
if request.headers.getOrDefault("If-None-Match") == hashed:
|
|
return Http304
|
|
|
|
let c = newAsyncHttpClient(headers=newHttpHeaders({
|
|
"accept": "*/*",
|
|
"range": $req.headers.getOrDefault("range")
|
|
}))
|
|
|
|
try:
|
|
var res = await c.get(url)
|
|
if not res.status.startsWith("20"):
|
|
return Http404
|
|
|
|
var headers = @{
|
|
"accept-ranges": "bytes",
|
|
"content-type": $res.headers.getOrDefault("content-type"),
|
|
"cache-control": maxAge,
|
|
"age": $res.headers.getOrDefault("age"),
|
|
"date": $res.headers.getOrDefault("date"),
|
|
"last-modified": $res.headers.getOrDefault("last-modified")
|
|
}
|
|
|
|
var tries = 0
|
|
while tries <= 10 and res.headers.hasKey("transfer-encoding"):
|
|
await sleepAsync(100 + tries * 200)
|
|
res = await c.get(url)
|
|
tries.inc
|
|
|
|
let contentLength = res.getContentLength
|
|
if contentLength.len > 0:
|
|
headers.add ("content-length", contentLength)
|
|
|
|
if res.headers.hasKey("content-range"):
|
|
headers.add ("content-range", $res.headers.getOrDefault("content-range"))
|
|
respond(request, Http206, headers)
|
|
else:
|
|
respond(request, Http200, headers)
|
|
|
|
var (hasValue, data) = (true, "")
|
|
while hasValue:
|
|
(hasValue, data) = await res.bodyStream.read()
|
|
if hasValue:
|
|
await request.client.send(data, flags={})
|
|
data.setLen 0
|
|
except OSError: discard
|
|
except ProtocolError, HttpRequestError:
|
|
result = Http404
|
|
finally:
|
|
c.close()
|
|
|
|
template check*(c): untyped =
|
|
let code = c
|
|
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 getPicUrl*(req: jester.Request): string =
|
|
result = decoded(req, 1)
|
|
if "twimg.com" notin result:
|
|
result.insert(twimg)
|
|
if not result.startsWith(https):
|
|
result.insert(https)
|
|
|
|
proc createMediaRouter*(cfg: Config) =
|
|
router media:
|
|
get "/pic/?":
|
|
resp Http404
|
|
|
|
get re"^\/pic\/orig\/(enc)?\/?(.+)":
|
|
let url = getPicUrl(request)
|
|
cond isTwitterUrl(parseUri(url)) == true
|
|
check await proxyMedia(request, url & "?name=orig")
|
|
|
|
get re"^\/pic\/(enc)?\/?(.+)":
|
|
let url = getPicUrl(request)
|
|
cond isTwitterUrl(parseUri(url)) == true
|
|
check await proxyMedia(request, url)
|
|
|
|
get re"^\/video\/(enc)?\/?(.+)\/(.+)$":
|
|
let url = decoded(request, 2)
|
|
cond "http" in url
|
|
|
|
if getHmac(url) != request.matches[1]:
|
|
resp showError("Failed to verify signature", cfg)
|
|
|
|
if ".mp4" in url or ".ts" in url or ".m4s" in url:
|
|
check await proxyMedia(request, url)
|
|
|
|
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, cookiePref(proxyVideos))
|
|
|
|
resp content, m3u8Mime
|