Restrict image/gif media host instead of hashing
This commit is contained in:
parent
ec43987363
commit
9c91688497
7 changed files with 65 additions and 25 deletions
|
@ -77,7 +77,7 @@ proc stripTwitterUrls*(text: string): string =
|
||||||
proc proxifyVideo*(manifest: string; proxy: bool): string =
|
proc proxifyVideo*(manifest: string; proxy: bool): string =
|
||||||
proc cb(m: RegexMatch; s: string): string =
|
proc cb(m: RegexMatch; s: string): string =
|
||||||
result = "https://video.twimg.com" & s[m.group(0)[0]]
|
result = "https://video.twimg.com" & s[m.group(0)[0]]
|
||||||
if proxy: result = result.getSigUrl("video")
|
if proxy: result = getVidUrl(result)
|
||||||
result = manifest.replace(re"(.+(.ts|.m3u8|.vmap))", cb)
|
result = manifest.replace(re"(.+(.ts|.m3u8|.vmap))", cb)
|
||||||
|
|
||||||
proc getUserpic*(userpic: string; style=""): string =
|
proc getUserpic*(userpic: string; style=""): string =
|
||||||
|
|
|
@ -12,24 +12,26 @@ export utils
|
||||||
|
|
||||||
proc createMediaRouter*(cfg: Config) =
|
proc createMediaRouter*(cfg: Config) =
|
||||||
router media:
|
router media:
|
||||||
get "/pic/@sig/@url":
|
get "/pic/@url":
|
||||||
cond "http" in @"url"
|
cond "http" in @"url"
|
||||||
cond "twimg" in @"url"
|
cond "twimg" in @"url"
|
||||||
let
|
|
||||||
uri = parseUri(decodeUrl(@"url"))
|
|
||||||
path = uri.path.split("/")[2 .. ^1].join("/")
|
|
||||||
filename = cfg.cacheDir / cleanFilename(path & uri.query)
|
|
||||||
|
|
||||||
if getHmac($uri) != @"sig":
|
let uri = parseUri(decodeUrl(@"url"))
|
||||||
resp showError("Failed to verify signature", cfg.title)
|
cond isTwitterUrl($uri) == true
|
||||||
|
|
||||||
|
let path = uri.path.split("/")[2 .. ^1].join("/")
|
||||||
|
let filename = cfg.cacheDir / cleanFilename(path & uri.query)
|
||||||
|
|
||||||
if not existsDir(cfg.cacheDir):
|
if not existsDir(cfg.cacheDir):
|
||||||
createDir(cfg.cacheDir)
|
createDir(cfg.cacheDir)
|
||||||
|
|
||||||
if not existsFile(filename):
|
if not existsFile(filename):
|
||||||
let client = newAsyncHttpClient()
|
let client = newAsyncHttpClient()
|
||||||
|
try:
|
||||||
await client.downloadFile($uri, filename)
|
await client.downloadFile($uri, filename)
|
||||||
client.close()
|
client.close()
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
if not existsFile(filename):
|
if not existsFile(filename):
|
||||||
resp Http404
|
resp Http404
|
||||||
|
@ -40,6 +42,27 @@ proc createMediaRouter*(cfg: Config) =
|
||||||
|
|
||||||
resp buf, mimetype(filename)
|
resp buf, mimetype(filename)
|
||||||
|
|
||||||
|
get "/gif/@url":
|
||||||
|
cond "http" in @"url"
|
||||||
|
cond "twimg" in @"url"
|
||||||
|
cond "mp4" in @"url" or "gif" in @"url"
|
||||||
|
|
||||||
|
let url = decodeUrl(@"url")
|
||||||
|
cond isTwitterUrl(url) == true
|
||||||
|
|
||||||
|
let client = newAsyncHttpClient()
|
||||||
|
var content: string
|
||||||
|
try:
|
||||||
|
content = await client.getContent(url)
|
||||||
|
client.close
|
||||||
|
except:
|
||||||
|
discard
|
||||||
|
|
||||||
|
if content.len == 0:
|
||||||
|
resp Http404
|
||||||
|
|
||||||
|
resp content, mimetype(url)
|
||||||
|
|
||||||
get "/video/@sig/@url":
|
get "/video/@sig/@url":
|
||||||
cond "http" in @"url"
|
cond "http" in @"url"
|
||||||
var url = decodeUrl(@"url")
|
var url = decodeUrl(@"url")
|
||||||
|
|
|
@ -1,7 +1,15 @@
|
||||||
import strutils, strformat, sequtils, uri, tables
|
import strutils, strformat, sequtils, uri, tables
|
||||||
import nimcrypto, regex
|
import nimcrypto, regex
|
||||||
|
|
||||||
const key = "supersecretkey"
|
const
|
||||||
|
key = "supersecretkey"
|
||||||
|
twitterDomains = @[
|
||||||
|
"twitter.com",
|
||||||
|
"twimg.com",
|
||||||
|
"abs.twimg.com",
|
||||||
|
"pbs.twimg.com",
|
||||||
|
"video.twimg.com"
|
||||||
|
]
|
||||||
|
|
||||||
proc mimetype*(filename: string): string =
|
proc mimetype*(filename: string): string =
|
||||||
if ".png" in filename:
|
if ".png" in filename:
|
||||||
|
@ -16,11 +24,17 @@ proc mimetype*(filename: string): string =
|
||||||
proc getHmac*(data: string): string =
|
proc getHmac*(data: string): string =
|
||||||
($hmac(sha256, key, data))[0 .. 12]
|
($hmac(sha256, key, data))[0 .. 12]
|
||||||
|
|
||||||
proc getSigUrl*(link: string; path: string): string =
|
proc getVidUrl*(link: string): string =
|
||||||
let
|
let
|
||||||
sig = getHmac(link)
|
sig = getHmac(link)
|
||||||
url = encodeUrl(link)
|
url = encodeUrl(link)
|
||||||
&"/{path}/{sig}/{url}"
|
&"/video/{sig}/{url}"
|
||||||
|
|
||||||
|
proc getGifUrl*(link: string): string =
|
||||||
|
&"/gif/{encodeUrl(link)}"
|
||||||
|
|
||||||
|
proc getPicUrl*(link: string): string =
|
||||||
|
&"/pic/{encodeUrl(link)}"
|
||||||
|
|
||||||
proc cleanFilename*(filename: string): string =
|
proc cleanFilename*(filename: string): string =
|
||||||
const reg = re"[^A-Za-z0-9._-]"
|
const reg = re"[^A-Za-z0-9._-]"
|
||||||
|
@ -29,3 +43,6 @@ proc cleanFilename*(filename: string): string =
|
||||||
proc filterParams*(params: Table): seq[(string, string)] =
|
proc filterParams*(params: Table): seq[(string, string)] =
|
||||||
let filter = ["name", "id"]
|
let filter = ["name", "id"]
|
||||||
toSeq(params.pairs()).filterIt(it[0] notin filter)
|
toSeq(params.pairs()).filterIt(it[0] notin filter)
|
||||||
|
|
||||||
|
proc isTwitterUrl*(url: string): bool =
|
||||||
|
parseUri(url).hostname in twitterDomains
|
||||||
|
|
|
@ -41,7 +41,7 @@ proc renderMain*(body: VNode; prefs: Prefs; title="Nitter"; titleText=""; desc="
|
||||||
meta(property="og:site_name", content="Twitter")
|
meta(property="og:site_name", content="Twitter")
|
||||||
|
|
||||||
for url in images:
|
for url in images:
|
||||||
meta(property="og:image", content=getSigUrl(url, "pic"))
|
meta(property="og:image", content=getPicUrl(url))
|
||||||
|
|
||||||
if video.len > 0:
|
if video.len > 0:
|
||||||
meta(property="og:video:url", content=video)
|
meta(property="og:video:url", content=video)
|
||||||
|
|
|
@ -14,7 +14,7 @@ proc renderStat(num, class: string; text=""): VNode =
|
||||||
proc renderProfileCard*(profile: Profile; prefs: Prefs): VNode =
|
proc renderProfileCard*(profile: Profile; prefs: Prefs): VNode =
|
||||||
buildHtml(tdiv(class="profile-card")):
|
buildHtml(tdiv(class="profile-card")):
|
||||||
tdiv(class="profile-card-info"):
|
tdiv(class="profile-card-info"):
|
||||||
let url = profile.getUserPic().getSigUrl("pic")
|
let url = getPicUrl(profile.getUserPic())
|
||||||
a(class="profile-card-avatar", href=url, target="_blank"):
|
a(class="profile-card-avatar", href=url, target="_blank"):
|
||||||
genImg(profile.getUserpic("_200x200"))
|
genImg(profile.getUserpic("_200x200"))
|
||||||
|
|
||||||
|
@ -72,7 +72,7 @@ proc renderBanner(profile: Profile): VNode =
|
||||||
if "#" in profile.banner:
|
if "#" in profile.banner:
|
||||||
tdiv(class="profile-banner-color", style={backgroundColor: profile.banner})
|
tdiv(class="profile-banner-color", style={backgroundColor: profile.banner})
|
||||||
else:
|
else:
|
||||||
a(href=getSigUrl(profile.banner, "pic"), target="_blank"):
|
a(href=getPicUrl(profile.banner), target="_blank"):
|
||||||
genImg(profile.banner)
|
genImg(profile.banner)
|
||||||
|
|
||||||
proc renderProfile*(profile: Profile; timeline: Timeline;
|
proc renderProfile*(profile: Profile; timeline: Timeline;
|
||||||
|
|
|
@ -32,7 +32,7 @@ proc linkUser*(profile: Profile, class=""): VNode =
|
||||||
|
|
||||||
proc genImg*(url: string; class=""): VNode =
|
proc genImg*(url: string; class=""): VNode =
|
||||||
buildHtml():
|
buildHtml():
|
||||||
img(src=url.getSigUrl("pic"), class=class, alt="Image")
|
img(src=getPicUrl(url), class=class, alt="Image")
|
||||||
|
|
||||||
proc linkText*(text: string; class=""): VNode =
|
proc linkText*(text: string; class=""): VNode =
|
||||||
let url = if "http" notin text: "http://" & text else: text
|
let url = if "http" notin text: "http://" & text else: text
|
||||||
|
|
|
@ -41,7 +41,7 @@ proc renderAlbum(tweet: Tweet): VNode =
|
||||||
tdiv(class="gallery-row", style={marginTop: margin}):
|
tdiv(class="gallery-row", style={marginTop: margin}):
|
||||||
for photo in photos:
|
for photo in photos:
|
||||||
tdiv(class="attachment image"):
|
tdiv(class="attachment image"):
|
||||||
a(href=getSigUrl(photo & "?name=orig", "pic"), class="still-image",
|
a(href=getPicUrl(photo & "?name=orig"), class="still-image",
|
||||||
target="_blank", style={display: flex}):
|
target="_blank", style={display: flex}):
|
||||||
genImg(photo)
|
genImg(photo)
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ proc isPlaybackEnabled(prefs: Prefs; video: Video): bool =
|
||||||
|
|
||||||
proc renderVideoDisabled(video: Video; path: string): VNode =
|
proc renderVideoDisabled(video: Video; path: string): VNode =
|
||||||
buildHtml(tdiv):
|
buildHtml(tdiv):
|
||||||
img(src=video.thumb.getSigUrl("pic"))
|
img(src=getPicUrl(video.thumb))
|
||||||
tdiv(class="video-overlay"):
|
tdiv(class="video-overlay"):
|
||||||
case video.playbackType
|
case video.playbackType
|
||||||
of mp4:
|
of mp4:
|
||||||
|
@ -62,7 +62,7 @@ proc renderVideoDisabled(video: Video; path: string): VNode =
|
||||||
|
|
||||||
proc renderVideoUnavailable(video: Video): VNode =
|
proc renderVideoUnavailable(video: Video): VNode =
|
||||||
buildHtml(tdiv):
|
buildHtml(tdiv):
|
||||||
img(src=video.thumb.getSigUrl("pic"))
|
img(src=getPicUrl(video.thumb))
|
||||||
tdiv(class="video-overlay"):
|
tdiv(class="video-overlay"):
|
||||||
case video.reason
|
case video.reason
|
||||||
of "dmcaed":
|
of "dmcaed":
|
||||||
|
@ -74,13 +74,13 @@ proc renderVideo(video: Video; prefs: Prefs; path: string): VNode =
|
||||||
buildHtml(tdiv(class="attachments")):
|
buildHtml(tdiv(class="attachments")):
|
||||||
tdiv(class="gallery-video"):
|
tdiv(class="gallery-video"):
|
||||||
tdiv(class="attachment video-container"):
|
tdiv(class="attachment video-container"):
|
||||||
let thumb = video.thumb.getSigUrl("pic")
|
let thumb = getPicUrl(video.thumb)
|
||||||
if not video.available:
|
if not video.available:
|
||||||
renderVideoUnavailable(video)
|
renderVideoUnavailable(video)
|
||||||
elif not prefs.isPlaybackEnabled(video):
|
elif not prefs.isPlaybackEnabled(video):
|
||||||
renderVideoDisabled(video, path)
|
renderVideoDisabled(video, path)
|
||||||
else:
|
else:
|
||||||
let source = video.url.getSigUrl("video")
|
let source = getVidUrl(video.url)
|
||||||
case video.playbackType
|
case video.playbackType
|
||||||
of mp4:
|
of mp4:
|
||||||
if prefs.muteVideos:
|
if prefs.muteVideos:
|
||||||
|
@ -99,8 +99,8 @@ proc renderGif(gif: Gif; prefs: Prefs): VNode =
|
||||||
buildHtml(tdiv(class="attachments media-gif")):
|
buildHtml(tdiv(class="attachments media-gif")):
|
||||||
tdiv(class="gallery-gif", style=style(maxHeight, "unset")):
|
tdiv(class="gallery-gif", style=style(maxHeight, "unset")):
|
||||||
tdiv(class="attachment"):
|
tdiv(class="attachment"):
|
||||||
let thumb = gif.thumb.getSigUrl("pic")
|
let thumb = getPicUrl(gif.thumb)
|
||||||
let url = gif.url.getSigUrl("video")
|
let url = getGifUrl(gif.url)
|
||||||
if prefs.autoplayGifs:
|
if prefs.autoplayGifs:
|
||||||
video(class="gif", poster=thumb, autoplay="", muted="", loop=""):
|
video(class="gif", poster=thumb, autoplay="", muted="", loop=""):
|
||||||
source(src=url, `type`="video/mp4")
|
source(src=url, `type`="video/mp4")
|
||||||
|
@ -123,7 +123,7 @@ proc renderPoll(poll: Poll): VNode =
|
||||||
proc renderCardImage(card: Card): VNode =
|
proc renderCardImage(card: Card): VNode =
|
||||||
buildHtml(tdiv(class="card-image-container")):
|
buildHtml(tdiv(class="card-image-container")):
|
||||||
tdiv(class="card-image"):
|
tdiv(class="card-image"):
|
||||||
img(src=getSigUrl(get(card.image), "pic"))
|
img(src=getPicUrl(get(card.image)))
|
||||||
if card.kind == player:
|
if card.kind == player:
|
||||||
tdiv(class="card-overlay"):
|
tdiv(class="card-overlay"):
|
||||||
tdiv(class="overlay-circle"):
|
tdiv(class="overlay-circle"):
|
||||||
|
|
Loading…
Reference in a new issue