Restrict image/gif media host instead of hashing

This commit is contained in:
Zed 2019-09-13 12:27:04 +02:00
parent ec43987363
commit 9c91688497
7 changed files with 65 additions and 25 deletions

View file

@ -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 =

View file

@ -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")

View file

@ -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

View file

@ -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)

View file

@ -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;

View file

@ -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

View file

@ -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"):