from twisted.web import proxy, http
from twisted.internet import reactor, protocol
from twisted.python import log
from twisted.python.compat import urllib_parse
from io import StringIO
import sys, gzip, os, time
from subprocess import Popen, PIPE, STDOUT
import requests
from pyquery.pyquery import PyQuery

log.startLogging(sys.stdout)

def kill_existed():
	pgrep = Popen(['pgrep', '-f', 'NeteaseMusicProxy'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
	existed = pgrep.communicate()[0].rstrip().split(b'\n')
	for pid in existed:
		if pid and int(pid) != os.getpid():
			kill = Popen(['kill', '-9', pid], stderr=STDOUT)
			kill.communicate()
			time.sleep(3)

def py_gzip_compress(plain_content):
	temp_file = StringIO.StringIO()
	with gzip.GzipFile(fileobj=temp_file, mode="w") as f:
		f.write(plain_content)
		return temp_file.getvalue()

def py_gzip_decompress(compressed_content):
	temp_file = StringIO.StringIO()
	temp_file.write(compressed_content)
	temp_file.seek(0)
	with gzip.GzipFile(fileobj=temp_file, mode="rb") as f:
		return f.read()

def sh_gzip_compress(plain_content):
	p = Popen(['gzip', '-c'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
	return p.communicate(input=plain_content)[0]

def sh_gzip_decompress(compressed_content):
	p = Popen(['gzip', '-dc'], stdout=PIPE, stdin=PIPE, stderr=STDOUT)
	res = p.communicate(input=compressed_content)[0]
	if res.startswith(b'gzip'):
		return None
	return res

def modify_response(response):
	response = response.replace(b'"st":-100', b'"st":0')
	response = response.replace(b'"st":-200', b'"st":0')
	response = response.replace(b'"pl":0', b'"pl":320000')
	response = response.replace(b'"dl":0', b'"dl":320000')
	response = response.replace(b'"fl":0', b'"fl":320000')
	response = response.replace(b'"sp":0', b'"sp":7')
	response = response.replace(b'"cp":0', b'"cp":1')
	response = response.replace(b'"subp":0', b'"subp":1')
	return response

class MainlandProxy():
	def __init__(self):
		self.default_ip = '123.57.215.44'
		self.default_port = 32796
		self.ip = ''
		self.port = -1
		self.failed_times = 0
		self.set_proxy()
		self.status = 0
		self.url_request_length = b'264'

	def set_proxy(self):
		r = requests.get("http://cn-proxy.com/")
		q = PyQuery(r.content)
		trs = q("tbody tr")
		if (len(trs) == 0):
			self.ip = self.default_ip
			self.port = self.default_port
			return
		tr = trs[min(self.failed_times,len(trs)-1)]
		trq = PyQuery(tr)
		tds = trq.children()
		ip = tds.eq(0).text()
		port = int(tds.eq(1).text())
		if self.ip == ip and self.port == port:
			self.set_to_default()
		else:
			self.ip = ip
			self.port = port

	def set_to_default(self):
		self.ip, self.port = self.default_ip, self.default_port

	def change(self):
		self.failed_times += 1
		print('bad proxy, ip: %s port: %d' % (self.ip, self.port))
		if (self.failed_times > 5 and self.ip != self.default_ip):
			self.failed_times = 0
			self.ip = self.default_ip
			self.port = self.default_port
		else:
			self.set_proxy()
		print("using ip %s and port %d" % (self.ip, self.port))

	def check(self, buffer_str):
		for msg in ['HTTP Status 404', 'ERROR', 'ERR_INVALID_URL', 'Internal Server Error']:
			if msg in buffer_str:
				self.change()
				break


class NeteaseMusicProxyClient(proxy.ProxyClient):
		def __init__(self, *args, **kwargs):
			self.intercept = {'song': b'/eapi/v3/song/detail/', 'search': b'/eapi/cloudsearch/pc', 'url': b'/eapi/song/enhance/player/url', 'album': b'/eapi/v1/album', 'artist': b'/eapi/v1/artist', 'playlist': b'/eapi/v3/playlist/detail', 'discovery': b'/eapi/v1/discovery/new/songs', 'linux': b'/api/linux/forward'}
			self.interval = {self.intercept['song']: 10, self.intercept['search']: 100, 'default': 10}
			self.temp_buffer = {self.intercept['song']: None, self.intercept['search']: None}
			self.timestamp = {self.intercept['song']: time.time(), self.intercept['search']: time.time()}
			proxy.ProxyClient.__init__(self, *args, **kwargs)

		def check_buffer(self, buffer):
			if len(buffer) != 414:
				print('length of buffer:', len(buffer))
				if len(buffer) == 238:
					print('cannot play this song due to copyright')
				else:
					mainland_proxy.change()
			else:
				mainland_proxy.status = 0

		def handleResponsePart(self, buffer):
			if self.rest in [self.intercept['song'], self.intercept['search'], self.intercept['playlist'], self.intercept['discovery'], self.intercept['linux']] or self.intercept['album'] in self.rest or self.intercept['artist'] in self.rest:
				print(self.headers)
				if self.headers[b'content-length'] != mainland_proxy.url_request_length:
					print('response intercepted: ', self.rest)
					if self.rest not in self.timestamp:
						self.timestamp[self.rest] = time.time()
						self.temp_buffer[self.rest] = None
					if time.time() - self.timestamp[self.rest] > self.interval.get(self.rest, self.interval['default']):
						self.temp_buffer[self.rest] = 0
						self.timestamp[self.rest] = time.time()
					if self.temp_buffer[self.rest] != None:
						buffer = self.temp_buffer[self.rest] + buffer
					buffer_str = sh_gzip_decompress(buffer)
					if buffer_str == None or b'unexpected end of file' in buffer_str:
						self.temp_buffer[self.rest] = buffer
						self.timestamp[self.rest] = time.time()
						return
					else:
						del self.temp_buffer[self.rest]
						del self.timestamp[self.rest]
					buffer_str = modify_response(buffer_str)
					#print buffer_str
					buffer = sh_gzip_compress(buffer_str)
			if self.rest == self.intercept['url']:
				self.check_buffer(buffer)
			proxy.ProxyClient.handleResponsePart(self, buffer)

class NeteaseMusicProxyClientFactory(proxy.ProxyClientFactory):
	protocol = NeteaseMusicProxyClient
	def clientConnectionFailed(self, connector, reason):
		print(reason, 'client connection failed, changing proxy')
		mainland_proxy.change()
	def clientConnectionLost(self, connector, reason):
		print(reason)
		if self.headers[b'connection'] != b'close' and mainland_proxy.status == -1:
			mainland_proxy.change()
			mainland_proxy.status = 0

class NeteaseMusicProxyRequest(proxy.ProxyRequest):
	protocols = {b'http': NeteaseMusicProxyClientFactory}

	def process_prepare(self):
		# print self.method, self.uri, self.path, self.args, self.requestHeaders, self.responseHeaders, self.received_cookies, self.protocols, self.host, self.channel, self.content, self.cookies
		parsed = urllib_parse.urlparse(self.uri)
		protocol = parsed[0] or 'http'
		host = parsed[1].decode('ascii')
		port = self.ports[protocol]
		if ':' in host:
			host, port = host.split(':')
			port = int(port)
		rest = urllib_parse.urlunparse((b'', b'') + parsed[2:])
		if not rest:
			rest = rest + b'/'
		class_ = self.protocols[protocol]
		headers = self.getAllHeaders().copy()
		if b'host' not in headers:
			headers[b'host'] = host.encode('ascii')
		self.content.seek(0, 0)
		s = self.content.read()
		clientFactory = class_(self.method, rest, self.clientproto, headers, s, self)
		return host, port, clientFactory

	def process(self):
		if self.uri == b'music.163.com:443':
			if self.getHeader(b'host') == self.uri:
				host, port = self.uri.split(b':')
				port = int(port)
				clientFactory = ConnectProxyClientFactory(host, port, self)
				self.reactor.connectTCP(host, port, clientFactory)
				return
			print('DEBUG: Abort on request:', self.uri)
			self.channel._respondToBadRequestAndDisconnect()
			return
		host, port, clientFactory = self.process_prepare()
		print(self.uri)
		if self.uri == b'http://music.163.com/eapi/song/enhance/player/url' or self.uri == b'http://music.163.com/api/linux/forward' and self.getHeader(b'content-length') == mainland_proxy.url_request_length:
			print('request intercepted:', self.uri, self.getHeader(b'content-length'))
			mainland_proxy.set_to_default()
			mainland_proxy.status = -1
			self.reactor.connectTCP(mainland_proxy.ip, mainland_proxy.port, clientFactory)
			return
		self.reactor.connectTCP(host, port, clientFactory)

class NeteaseMusicProxy(proxy.Proxy):
	requestFactory = NeteaseMusicProxyRequest
	connectedRemote = None

	def requestDone(self, request):
		if request.method == b'CONNECT' and self.connectedRemote is not None:
			self.connectedRemote.connectedClient = self
			self._handlingRequest = False
			if self._savedTimeOut:
				self.setTimeout(self._savedTimeOut)
			data = b''.join(self._dataBuffer)
			self._dataBuffer = []
			self.setLineMode(data)
		else:
			proxy.Proxy.requestDone(self, request)

	def dataReceived(self, data):
		if self.connectedRemote is None:
			proxy.Proxy.dataReceived(self, data)
		else:
			self.connectedRemote.transport.write(data)

class NeteaseMusicProxyFactory(http.HTTPFactory):
	protocol = NeteaseMusicProxy

class ConnectProxyClient(protocol.Protocol):
	connectedClient = None

	def connectionMade(self):
		self.factory.request.channel.connectedRemote = self
		self.factory.request.setResponseCode(200, b'CONNECT OK')
		self.factory.request.setHeader(b'X-Connected-IP', self.transport.realAddress[0])
		self.factory.request.setHeader(b'Content-Length', b'0')
		self.factory.request.finish()

	def connectionLost(self, reason):
		if self.connectedClient is not None:
			self.connectedClient.transport.loseConnection()

	def dataReceived(self, data):
		if self.connectedClient is not None:
			self.connectedClient.transport.write(data)
		else:
			print('Unexpected data received:', data)

class ConnectProxyClientFactory(protocol.ClientFactory):
	protocol = ConnectProxyClient

	def __init__(self, host, port, request):
		self.request = request
		self.host = host
		self.port = port

	def clientConnectionFailed(self, connector, reason):
		self.request.fail(b'Gateway Error', str(reason))

mainland_proxy = MainlandProxy()
kill_existed()
reactor.listenTCP(32794, NeteaseMusicProxyFactory())
reactor.run()