main.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. # Copyright (C) 2023 MikuInvidious Team
  2. #
  3. # MikuInvidious is free software; you can redistribute it and/or
  4. # modify it under the terms of the GNU General Public License as
  5. # published by the Free Software Foundation; either version 3 of
  6. # the License, or (at your option) any later version.
  7. #
  8. # MikuInvidious is distributed in the hope that it will be useful,
  9. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  11. # General Public License for more details.
  12. #
  13. # You should have received a copy of the GNU General Public License
  14. # along with MikuInvidious. If not, see <http://www.gnu.org/licenses/>.
  15. import multiprocessing
  16. from shared import *
  17. from app import app
  18. from http.cookies import BaseCookie
  19. from urllib.parse import quote as urlquote, urlparse, urlunparse
  20. from twisted.web.http import _QUEUED_SENTINEL, HTTPChannel, HTTPClient, Request
  21. from twisted.web.resource import Resource
  22. from twisted.web import proxy, server
  23. from twisted.internet.protocol import ClientFactory
  24. from twisted.internet import reactor, utils, ssl
  25. plain_cookies = {}
  26. ################################################################################
  27. # Modified Dynamic Proxy (from twisted)
  28. ################################################################################
  29. class ProxyClient(HTTPClient):
  30. _finished = False
  31. def __init__(self, command, rest, version, headers, data, father):
  32. self.father = father
  33. self.command = command
  34. self.rest = rest
  35. if b"proxy-connection" in headers:
  36. del headers[b"proxy-connection"]
  37. headers[b"connection"] = b"close"
  38. headers.pop(b"keep-alive", None)
  39. self.headers = headers
  40. self.data = data
  41. def connectionMade(self):
  42. self.sendCommand(self.command, self.rest)
  43. for header, value in self.headers.items():
  44. self.sendHeader(header, value)
  45. self.endHeaders()
  46. self.transport.write(self.data)
  47. def handleStatus(self, version, code, message):
  48. self.father.setResponseCode(int(code), message)
  49. def handleHeader(self, key, value):
  50. if key.lower() in [b"server", b"date", b"content-type"]:
  51. self.father.responseHeaders.setRawHeaders(key, [value])
  52. else:
  53. self.father.responseHeaders.addRawHeader(key, value)
  54. def handleResponsePart(self, buffer):
  55. self.father.write(buffer)
  56. def handleResponseEnd(self):
  57. if not self._finished:
  58. self._finished = True
  59. self.father.notifyFinish().addErrback(lambda x: None)
  60. self.transport.loseConnection()
  61. class ProxyClientFactory(ClientFactory):
  62. protocol = ProxyClient
  63. def __init__(self, command, rest, version, headers, data, father):
  64. self.father = father
  65. self.command = command
  66. self.rest = rest
  67. self.headers = headers
  68. self.data = data
  69. self.version = version
  70. def buildProtocol(self, addr):
  71. return self.protocol(
  72. self.command, self.rest, self.version, self.headers, self.data, self.father
  73. )
  74. def clientConnectionFailed(self, connector, reason):
  75. self.father.setResponseCode(501, b"Gateway error")
  76. self.father.responseHeaders.addRawHeader(b"Content-Type", b"text/html")
  77. self.father.write(b"<H1>Could not connect</H1>")
  78. self.father.finish()
  79. class ReverseProxyResource(Resource):
  80. def __init__(self, path, reactor=reactor):
  81. Resource.__init__(self)
  82. self.path = path
  83. self.reactor = reactor
  84. def getChild(self, path, request):
  85. return ReverseProxyResource(
  86. self.path + b'/' + urlquote(path, safe=b'').encode("utf-8"),
  87. self.reactor
  88. )
  89. def render_proxy_pic(self, request, req_path):
  90. req_path = req_path[11:]
  91. domain = req_path.split('/')[0]
  92. if not domain.endswith('.hdslb.com'):
  93. request.setResponseCode(403)
  94. return
  95. request.requestHeaders.setRawHeaders(b'host', [domain.encode("ascii")])
  96. request.requestHeaders.setRawHeaders(b'user-agent', [b'Mozilla/5.0'
  97. b'BiliDroid/10.10.10 (bbcallen@gmail.com)'])
  98. request.content.seek(0, 0)
  99. clientFactory = ProxyClientFactory(
  100. b'GET', ('https://' + req_path).encode('utf-8'),
  101. request.clientproto,
  102. request.getAllHeaders(),
  103. request.content.read(),
  104. request,
  105. )
  106. self.reactor.connectSSL(domain, 443, clientFactory, ssl.ClientContextFactory())
  107. return server.NOT_DONE_YET
  108. def render(self, request):
  109. # Justify the request path.
  110. req_path = self.path.decode('utf-8')
  111. if req_path.startswith('/proxy/video/'):
  112. pass
  113. elif req_path.startswith('/proxy/pic/'):
  114. return self.render_proxy_pic(request, req_path)
  115. else:
  116. request.setResponseCode(418, b'I\'m a teapot')
  117. return
  118. # Parse and retrive the URL info.
  119. vid, vidx, vqn = req_path.lstrip('/proxy/video/').split('_')
  120. url = appredis.get(f'mikuinv_{vid}_{vidx}_{vqn}')
  121. if not url:
  122. request.setResponseCode(404, b'Not found')
  123. return
  124. url = url.decode()
  125. urlp = urlparse(url)
  126. if not appconf['proxy']['video']:
  127. if urlp.netloc.endswith('-mirrorakam.akamaized.net'):
  128. request.setResponseCode(302)
  129. request.setHeader('Location', url)
  130. return b'oops'
  131. else:
  132. request.setResponseCode(401)
  133. return
  134. request.requestHeaders.setRawHeaders(b'host', [urlp.netloc.encode("ascii")])
  135. if plain_cookies:
  136. request.requestHeaders.setRawHeaders('cookie', [plain_cookies])
  137. request.requestHeaders.setRawHeaders(b'referer', [b'https://www.bilibili.com'])
  138. request.requestHeaders.setRawHeaders(b'user-agent', [b'Mozilla/5.0'
  139. b'BiliDroid/10.10.10 (bbcallen@gmail.com)'])
  140. request.content.seek(0, 0)
  141. clientFactory = ProxyClientFactory(
  142. b'GET', url.encode('utf-8'),
  143. request.clientproto,
  144. request.getAllHeaders(),
  145. request.content.read(),
  146. request,
  147. )
  148. request.notifyFinish().addErrback(lambda x: clientFactory.doStop())
  149. nethost = urlp.netloc.split(':')[0] if ':' in urlp.netloc else urlp.netloc
  150. netport = int(urlp.netloc.split(':')[1]) if ':' in urlp.netloc else (80 if urlp.scheme == 'http' else 443)
  151. if urlp.scheme == 'http':
  152. self.reactor.connectTCP(nethost, netport, clientFactory)
  153. elif urlp.scheme == 'https':
  154. self.reactor.connectSSL(nethost, netport, clientFactory, ssl.ClientContextFactory())
  155. return server.NOT_DONE_YET
  156. ################################################################################
  157. # To start this function for testing: python -c 'import main; main.twisted_start()'
  158. def twisted_start():
  159. # Intialize cookies.
  160. plain_cookies = appconf['credential']
  161. if plain_cookies['use_cred']:
  162. del plain_cookies['use_cred']
  163. cookiejar = ''
  164. for k, v in plain_cookies.items():
  165. cookiejar += f'{k}={v}; '
  166. plain_cookies = cookiejar[:-2]
  167. else:
  168. plain_cookies = False
  169. flask_res = proxy.ReverseProxyResource('127.0.0.1', appconf['flask']['port']+1, b'')
  170. flask_res.putChild(b'proxy', ReverseProxyResource(b'/proxy'))
  171. site = server.Site(flask_res)
  172. reactor.listenTCP(appconf['flask']['port'], site)
  173. reactor.run()
  174. # To start this function for testing: python -c 'import main; main.flask_start()'
  175. def flask_start():
  176. appconf['flask']['port'] += 1
  177. app.run(**appconf['flask'])
  178. # If we're executed directly, also start the flask daemon.
  179. if __name__ == '__main__':
  180. flask_task = multiprocessing.Process(target=flask_start)
  181. flask_task.daemon = True # Exit the child if the parent was killed :-(
  182. flask_task.start()
  183. twisted_start()