# Copyright (C) 2023 MikuInvidious Team
#
# MikuInvidious is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 3 of
# the License, or (at your option) any later version.
#
# MikuInvidious is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with MikuInvidious. If not, see .
import multiprocessing
from shared import *
from app import app
from http.cookies import BaseCookie
from urllib.parse import quote as urlquote, urlparse, urlunparse
from twisted.web.http import _QUEUED_SENTINEL, HTTPChannel, HTTPClient, Request
from twisted.web.resource import Resource
from twisted.web import proxy, server
from twisted.internet.protocol import ClientFactory
from twisted.internet import reactor, utils, ssl
plain_cookies = {}
################################################################################
# Modified Dynamic Proxy (from twisted)
################################################################################
class ProxyClient(HTTPClient):
_finished = False
def __init__(self, command, rest, version, headers, data, father):
self.father = father
self.command = command
self.rest = rest
if b"proxy-connection" in headers:
del headers[b"proxy-connection"]
headers[b"connection"] = b"close"
headers.pop(b"keep-alive", None)
self.headers = headers
self.data = data
def connectionMade(self):
self.sendCommand(self.command, self.rest)
for header, value in self.headers.items():
self.sendHeader(header, value)
self.endHeaders()
self.transport.write(self.data)
def handleStatus(self, version, code, message):
self.father.setResponseCode(int(code), message)
def handleHeader(self, key, value):
if key.lower() in [b"server", b"date", b"content-type"]:
self.father.responseHeaders.setRawHeaders(key, [value])
else:
self.father.responseHeaders.addRawHeader(key, value)
def handleResponsePart(self, buffer):
self.father.write(buffer)
def handleResponseEnd(self):
if not self._finished:
self._finished = True
self.father.notifyFinish().addErrback(lambda x: None)
self.transport.loseConnection()
class ProxyClientFactory(ClientFactory):
protocol = ProxyClient
def __init__(self, command, rest, version, headers, data, father):
self.father = father
self.command = command
self.rest = rest
self.headers = headers
self.data = data
self.version = version
def buildProtocol(self, addr):
return self.protocol(
self.command, self.rest, self.version, self.headers, self.data, self.father
)
def clientConnectionFailed(self, connector, reason):
self.father.setResponseCode(501, b"Gateway error")
self.father.responseHeaders.addRawHeader(b"Content-Type", b"text/html")
self.father.write(b"
Could not connect
")
self.father.finish()
class ReverseProxyResource(Resource):
def __init__(self, path, reactor=reactor):
Resource.__init__(self)
self.path = path
self.reactor = reactor
def getChild(self, path, request):
return ReverseProxyResource(
self.path + b'/' + urlquote(path, safe=b'').encode("utf-8"),
self.reactor
)
def render_proxy_pic(self, request, req_path):
req_path = req_path[11:]
domain = req_path.split('/')[0]
if not domain.endswith('.hdslb.com'):
request.setResponseCode(403)
return
request.requestHeaders.setRawHeaders(b'host', [domain.encode("ascii")])
request.requestHeaders.setRawHeaders(b'user-agent', [b'Mozilla/5.0'
b'BiliDroid/10.10.10 (bbcallen@gmail.com)'])
request.content.seek(0, 0)
clientFactory = ProxyClientFactory(
b'GET', ('https://' + req_path).encode('utf-8'),
request.clientproto,
request.getAllHeaders(),
request.content.read(),
request,
)
self.reactor.connectSSL(domain, 443, clientFactory, ssl.ClientContextFactory())
return server.NOT_DONE_YET
def render(self, request):
# Justify the request path.
req_path = self.path.decode('utf-8')
if req_path.startswith('/proxy/video/'):
pass
elif req_path.startswith('/proxy/pic/'):
return self.render_proxy_pic(request, req_path)
else:
request.setResponseCode(418, b'I\'m a teapot')
return
# Parse and retrive the URL info.
vid, vidx, vqn = req_path.lstrip('/proxy/video/').split('_')
url = appredis.get(f'mikuinv_{vid}_{vidx}_{vqn}')
if not url:
request.setResponseCode(404, b'Not found')
return
url = url.decode()
urlp = urlparse(url)
if not appconf['proxy']['video']:
if urlp.netloc.endswith('-mirrorakam.akamaized.net'):
request.setResponseCode(302)
request.setHeader('Location', url)
return b'oops'
else:
request.setResponseCode(401)
return
request.requestHeaders.setRawHeaders(b'host', [urlp.netloc.encode("ascii")])
if plain_cookies:
request.requestHeaders.setRawHeaders('cookie', [plain_cookies])
request.requestHeaders.setRawHeaders(b'referer', [b'https://www.bilibili.com'])
request.requestHeaders.setRawHeaders(b'user-agent', [b'Mozilla/5.0'
b'BiliDroid/10.10.10 (bbcallen@gmail.com)'])
request.content.seek(0, 0)
clientFactory = ProxyClientFactory(
b'GET', url.encode('utf-8'),
request.clientproto,
request.getAllHeaders(),
request.content.read(),
request,
)
request.notifyFinish().addErrback(lambda x: clientFactory.doStop())
nethost = urlp.netloc.split(':')[0] if ':' in urlp.netloc else urlp.netloc
netport = int(urlp.netloc.split(':')[1]) if ':' in urlp.netloc else (80 if urlp.scheme == 'http' else 443)
if urlp.scheme == 'http':
self.reactor.connectTCP(nethost, netport, clientFactory)
elif urlp.scheme == 'https':
self.reactor.connectSSL(nethost, netport, clientFactory, ssl.ClientContextFactory())
return server.NOT_DONE_YET
################################################################################
# To start this function for testing: python -c 'import main; main.twisted_start()'
def twisted_start():
# Intialize cookies.
plain_cookies = appconf['credential']
if plain_cookies['use_cred']:
del plain_cookies['use_cred']
cookiejar = ''
for k, v in plain_cookies.items():
cookiejar += f'{k}={v}; '
plain_cookies = cookiejar[:-2]
else:
plain_cookies = False
flask_res = proxy.ReverseProxyResource('127.0.0.1', appconf['flask']['port']+1, b'')
flask_res.putChild(b'proxy', ReverseProxyResource(b'/proxy'))
site = server.Site(flask_res)
reactor.listenTCP(appconf['flask']['port'], site)
reactor.run()
# To start this function for testing: python -c 'import main; main.flask_start()'
def flask_start():
appconf['flask']['port'] += 1
app.run(**appconf['flask'])
# If we're executed directly, also start the flask daemon.
if __name__ == '__main__':
flask_task = multiprocessing.Process(target=flask_start)
flask_task.daemon = True # Exit the child if the parent was killed :-(
flask_task.start()
twisted_start()