main.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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.wsgi import WSGIResource
  23. from twisted.web import proxy, server
  24. from twisted.internet.protocol import ClientFactory
  25. from twisted.internet import reactor, utils, ssl, tcp
  26. plain_cookies = {}
  27. ################################################################################
  28. # Modified Dynamic Proxy (from twisted)
  29. ################################################################################
  30. class ProxyClient(HTTPClient):
  31. _finished = False
  32. def __init__(self, command, rest, version, headers, data, father):
  33. self.father = father
  34. self.command = command
  35. self.rest = rest
  36. if b"proxy-connection" in headers:
  37. del headers[b"proxy-connection"]
  38. headers[b"connection"] = b"close"
  39. headers.pop(b"keep-alive", None)
  40. self.headers = headers
  41. self.data = data
  42. def connectionMade(self):
  43. self.sendCommand(self.command, self.rest)
  44. for header, value in self.headers.items():
  45. self.sendHeader(header, value)
  46. self.endHeaders()
  47. self.transport.write(self.data)
  48. def handleStatus(self, version, code, message):
  49. self.father.setResponseCode(int(code), message)
  50. def handleHeader(self, key, value):
  51. if key.lower() in [b"server", b"date", b"content-type"]:
  52. self.father.responseHeaders.setRawHeaders(key, [value])
  53. else:
  54. self.father.responseHeaders.addRawHeader(key, value)
  55. def handleResponsePart(self, buffer):
  56. self.father.write(buffer)
  57. def handleResponseEnd(self):
  58. if not self._finished:
  59. self._finished = True
  60. self.father.notifyFinish().addErrback(lambda x: None)
  61. self.transport.loseConnection()
  62. class ProxyClientFactory(ClientFactory):
  63. protocol = ProxyClient
  64. def __init__(self, command, rest, version, headers, data, father):
  65. self.father = father
  66. self.command = command
  67. self.rest = rest
  68. self.headers = headers
  69. self.data = data
  70. self.version = version
  71. def buildProtocol(self, addr):
  72. return self.protocol(
  73. self.command, self.rest, self.version, self.headers, self.data, self.father
  74. )
  75. def clientConnectionFailed(self, connector, reason):
  76. self.father.setResponseCode(501, b"Gateway error")
  77. self.father.responseHeaders.addRawHeader(b"Content-Type", b"text/html")
  78. self.father.write(b"<H1>Could not connect</H1>")
  79. self.father.finish()
  80. class ReverseProxyResource(Resource):
  81. def __init__(self, path, reactor=reactor):
  82. Resource.__init__(self)
  83. self.path = path
  84. self.reactor = reactor
  85. def getChild(self, path, request):
  86. return ReverseProxyResource(
  87. self.path + b'/' + urlquote(path, safe=b'').encode("utf-8"),
  88. self.reactor
  89. )
  90. def render_proxy_pic(self, request, req_path):
  91. req_path = req_path[11:]
  92. domain = req_path.split('/')[0]
  93. if not domain.endswith('.hdslb.com'):
  94. request.setResponseCode(403)
  95. return
  96. request.requestHeaders.setRawHeaders(b'host', [domain.encode("ascii")])
  97. request.requestHeaders.setRawHeaders(b'user-agent', [b'Mozilla/5.0'
  98. b'BiliDroid/10.10.10 (bbcallen@gmail.com)'])
  99. request.content.seek(0, 0)
  100. clientFactory = ProxyClientFactory(
  101. b'GET', ('https://' + req_path).encode('utf-8'),
  102. request.clientproto,
  103. request.getAllHeaders(),
  104. request.content.read(),
  105. request,
  106. )
  107. self.reactor.connectSSL(domain, 443, clientFactory, ssl.ClientContextFactory())
  108. return server.NOT_DONE_YET
  109. def render(self, request):
  110. # Justify the request path.
  111. req_path = self.path.decode('utf-8')
  112. if req_path.startswith('/proxy/video/'):
  113. pass
  114. elif req_path.startswith('/proxy/pic/'):
  115. return self.render_proxy_pic(request, req_path)
  116. else:
  117. request.setResponseCode(418, b'I\'m a teapot')
  118. return
  119. # Parse and retrive the URL info.
  120. vid, vidx, vqn = req_path.lstrip('/proxy/video/').split('_')
  121. url = appredis.get(f'mikuinv_{vid}_{vidx}_{vqn}')
  122. if not url:
  123. request.setResponseCode(404, b'Not found')
  124. return
  125. url = url.decode()
  126. urlp = urlparse(url)
  127. if not appconf['proxy']['video']:
  128. if urlp.netloc.endswith('-mirrorakam.akamaized.net'):
  129. request.setResponseCode(302)
  130. request.setHeader('Location', url)
  131. return b'oops'
  132. else:
  133. request.setResponseCode(401)
  134. return
  135. request.requestHeaders.setRawHeaders(b'host', [urlp.netloc.encode("ascii")])
  136. if plain_cookies:
  137. request.requestHeaders.setRawHeaders('cookie', [plain_cookies])
  138. request.requestHeaders.setRawHeaders(b'referer', [b'https://www.bilibili.com'])
  139. request.requestHeaders.setRawHeaders(b'user-agent', [b'Mozilla/5.0'
  140. b'BiliDroid/10.10.10 (bbcallen@gmail.com)'])
  141. request.content.seek(0, 0)
  142. clientFactory = ProxyClientFactory(
  143. b'GET', url.encode('utf-8'),
  144. request.clientproto,
  145. request.getAllHeaders(),
  146. request.content.read(),
  147. request,
  148. )
  149. request.notifyFinish().addErrback(lambda x: clientFactory.doStop())
  150. nethost = urlp.netloc.split(':')[0] if ':' in urlp.netloc else urlp.netloc
  151. netport = int(urlp.netloc.split(':')[1]) if ':' in urlp.netloc else (80 if urlp.scheme == 'http' else 443)
  152. if urlp.scheme == 'http':
  153. self.reactor.connectTCP(nethost, netport, clientFactory)
  154. elif urlp.scheme == 'https':
  155. self.reactor.connectSSL(nethost, netport, clientFactory, ssl.ClientContextFactory())
  156. return server.NOT_DONE_YET
  157. ################################################################################
  158. class MikuInvidiousResource(Resource):
  159. isLeaf = True
  160. def __init__(self):
  161. super().__init__()
  162. self.wsgi = WSGIResource(reactor, reactor.getThreadPool(), app)
  163. def render(self, request):
  164. if request.uri.startswith(b'/proxy'):
  165. return ReverseProxyResource(request.uri).render(request)
  166. return self.wsgi.render(request)
  167. def main():
  168. # Intialize cookies.
  169. plain_cookies = appconf['credential']
  170. if plain_cookies['use_cred']:
  171. del plain_cookies['use_cred']
  172. cookiejar = ''
  173. for k, v in plain_cookies.items():
  174. cookiejar += f'{k}={v}; '
  175. plain_cookies = cookiejar[:-2]
  176. else:
  177. plain_cookies = False
  178. site = server.Site(MikuInvidiousResource())
  179. # reactor.listenTCP(appconf['twisted']['port'], site) # only listens on ipv4
  180. port = tcp.Port(appconf['twisted']['port'], site, 50, appconf['twisted']['host'], reactor)
  181. port.startListening()
  182. reactor.run()
  183. if __name__ == '__main__':
  184. main()