Skip to content

Commit

Permalink
Proxy support (#325)
Browse files Browse the repository at this point in the history
* add http and socks proxy support
---------
Co-authored-by: Dmitry Misharov <[email protected]>
  • Loading branch information
nikolaev-rd authored Mar 9, 2024
1 parent 69a70ed commit d451b52
Show file tree
Hide file tree
Showing 16 changed files with 1,713 additions and 194 deletions.
359 changes: 186 additions & 173 deletions requirements_dev.txt

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/addon.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<requires>
<import addon="xbmc.python" version="3.0.0"/>
<import addon="script.module.inputstreamhelper" version="0.5.7" optional="true"/>
<import addon="script.module.pysocks" version="1.7.0"/>
</requires>
<extension point="xbmc.python.pluginsource" library="addon.py">
<provides>video</provides>
Expand Down
28 changes: 19 additions & 9 deletions src/resources/lib/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@
import xbmc
import xbmcgui

from resources.lib.client import KinoApiRequestProcessor

if TYPE_CHECKING:
from resources.lib.plugin import Plugin
from resources.lib.utils import cached_property
from resources.lib.utils import localize
from resources.lib.utils import popup_error


TIMEOUT = 60


class AuthException(Exception):
pass

Expand Down Expand Up @@ -74,19 +79,24 @@ class Auth:
def __init__(self, plugin: "Plugin") -> None:
self._auth_dialog = AuthDialog(plugin)
self.plugin = plugin
self.opener = urllib.request.build_opener(
KinoApiRequestProcessor(self.plugin),
)

def _make_request(self, payload):
self.plugin.logger.debug(f"Sending payload {payload} to oauth api")
try:
headers = {
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36"
}
response = urllib.request.urlopen(
urllib.request.Request(self.plugin.settings.oauth_api_url, headers=headers),
urllib.parse.urlencode(payload).encode("utf-8"),
).read()
return json.loads(response)
request = urllib.request.Request(
self.plugin.settings.oauth_api_url,
data=urllib.parse.urlencode(payload or {}).encode("utf-8"),
)
request.add_header(
"user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
)
response = self.opener.open(request, timeout=TIMEOUT)
return json.loads(response.read())
except urllib.error.HTTPError as e:
if e.code == 400:
response = json.loads(e.read())
Expand Down
48 changes: 43 additions & 5 deletions src/resources/lib/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import base64
import http
import json
import socket
import sys
import urllib.error
import urllib.parse
Expand All @@ -13,8 +15,10 @@
from typing import TYPE_CHECKING
from typing import Union

import socks
import xbmc


if TYPE_CHECKING:
from resources.lib.plugin import Plugin
from resources.lib.utils import localize
Expand All @@ -39,8 +43,47 @@ def https_request(self, request: urllib.request.Request) -> urllib.request.Reque
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
)
self.plugin.logger.debug(
f"Get system proxy settings: type={self.plugin.proxy_settings.type}, "
f"host={self.plugin.proxy_settings.host}, port={self.plugin.proxy_settings.port}"
)
if self.plugin.proxy_settings.is_enabled:
if not self.plugin.proxy_settings.is_correct:
self.plugin.logger.error("http proxy settings are not correct")
return request
self.plugin.logger.debug(
f"Set {self.plugin.proxy_settings.type} proxy from system settings, "
f"auth: {self.plugin.proxy_settings.with_auth}"
)
if self.plugin.proxy_settings.is_http:
self.set_http_proxy(request=request)
if self.plugin.proxy_settings.is_socks:
self.set_socks_proxy()
return request

def set_http_proxy(self, request: urllib.request.Request) -> None:
proxy_settings = self.plugin.proxy_settings
request.set_proxy(f"{proxy_settings.host}:{proxy_settings.port}", proxy_settings.type)
if proxy_settings.with_auth:
self.plugin.logger.debug(f"Use username and password for {proxy_settings.type} proxy")
user_pass = f"{proxy_settings.username}:{proxy_settings.password}"
creds = base64.b64encode(user_pass.encode()).decode("ascii")
request.add_header("Proxy-authorization", f"Basic {creds}")
return None

def set_socks_proxy(self) -> None:
proxy_settings = self.plugin.proxy_settings
socks.set_default_proxy(
proxy_type=socks.SOCKS4 if proxy_settings.is_socks4 else socks.SOCKS5,
addr=proxy_settings.host,
port=proxy_settings.port,
rdns=proxy_settings.type == "socks5r",
username=proxy_settings.username,
password=proxy_settings.password,
)
socket.socket = socks.socksocket # type: ignore[misc]
return None

http_request = https_request


Expand Down Expand Up @@ -143,11 +186,6 @@ def _handle_response(
def _make_request(self, request: urllib.request.Request) -> Dict[str, Any]:
request.recursion_counter_401 = 0 # type: ignore[attr-defined]
request.recursion_counter_429 = 0 # type: ignore[attr-defined]
request.add_header(
"user-agent",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/119.0.0.0 Safari/537.36",
)
try:
response = self.opener.open(request, timeout=TIMEOUT)
except Exception:
Expand Down
2 changes: 2 additions & 0 deletions src/resources/lib/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from resources.lib.search_history import SearchHistory
from resources.lib.settings import Settings
from resources.lib.utils import localize
from resources.lib.xbmc_settings import XbmcProxySettings


try:
Expand Down Expand Up @@ -53,6 +54,7 @@ def __init__(self) -> None:
self.main_menu_items = self._main_menu_items()
self.items = ItemsCollection(self)
self.client = KinoPubClient(self)
self.proxy_settings = XbmcProxySettings(self)

def list_item(
self,
Expand Down
100 changes: 100 additions & 0 deletions src/resources/lib/xbmc_settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import json
from typing import TYPE_CHECKING

import xbmc

if TYPE_CHECKING:
from resources.lib.plugin import Plugin


class XbmcSettings:
def __init__(self, plugin: "Plugin") -> None:
self.plugin = plugin

def get_setting(self, setting_id: str):
# https://kodi.wiki/view/JSON-RPC_API/v13#Settings.GetSettingValue
try:
self.plugin.logger.debug(f"Try to get system setting: {setting_id}")
response = xbmc.executeJSONRPC(
"{"
'"jsonrpc": "2.0",'
'"method": "Settings.GetSettingValue",'
'"params": '
f'{{ "setting": "{setting_id}" }},'
'"id": 1'
"}"
)
# Response example:
# { "id": 1, "jsonrpc": "2.0", "result": { "value": "some data" } }
self.plugin.logger.debug(f"JSON RPC Response: {response}")
setting = json.loads(str(response))
return setting["result"]["value"]
except Exception as exception:
self.plugin.logger.error(f"JSON RPC Exception: {exception}")
return None


class XbmcProxySettings(XbmcSettings):
def proxy_type(self, type: int) -> str:
proxy_types = {
0: "http",
1: "socks4",
2: "socks4a",
3: "socks5",
4: "socks5h", # SOCKS5 with remote DNS resolving
5: "https",
}
try:
self.plugin.logger.debug(f"Parsing system proxy type: {type} -> {proxy_types[type]}")
return proxy_types[type]
except KeyError:
self.plugin.logger.warning(f"Proxy type '{type}' is unknown")
return ""

@property
def is_enabled(self) -> bool:
return self.get_setting("network.usehttpproxy") or False

@property
def type(self) -> str:
return self.proxy_type(int(self.get_setting("network.httpproxytype"))) or ""

@property
def host(self) -> str:
return self.get_setting("network.httpproxyserver") or ""

@property
def port(self) -> int:
return int(self.get_setting("network.httpproxyport")) or 0

@property
def username(self) -> str | None:
return self.get_setting("network.httpproxyusername") or None

@property
def password(self) -> str | None:
return self.get_setting("network.httpproxypassword") or None

@property
def is_correct(self):
return len(self.host) > 3 and self.port > 0

@property
def is_http(self) -> bool:
return self.type in ["http", "https"]

@property
def is_socks(self) -> bool:
return self.type in ["socks4", "socks4a", "socks5", "socks5r"]

@property
def is_socks4(self) -> bool:
return self.type in ["socks4", "socks4a"]

@property
def is_socks5(self) -> bool:
return self.type in ["socks5", "socks5r"]

@property
def with_auth(self) -> bool:
return bool(self.username and self.password)
14 changes: 7 additions & 7 deletions src/resources/settings.xml
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" ?>
<settings version="1">
<section id="video.kino.pub">
<category id="general" label="32069">
<group id="1">
<setting id="video_quality" type="string" label="32070">
<section id="video.kino.pub">
<category id="general" label="32069">
<group id="1">
<setting id="video_quality" type="string" label="32070">
<dependencies>
<dependency type="enable" setting="ask_quality">false</dependency>
<dependency type="enable" setting="stream_type">hls</dependency>
Expand Down Expand Up @@ -67,7 +67,7 @@
<default>false</default>
<control type="toggle"/>
</setting>
</group>
</group>
<group id="2" label="32078">
<setting id="use_inputstream_adaptive" type="boolean" label="32079" help="">
<dependencies>
Expand Down Expand Up @@ -167,7 +167,7 @@
</control>
</setting>
</group>
</category>
</category>
<category id="menu" label="32088">
<group id="6">
<setting id="show_search" type="boolean" label="32019">
Expand Down Expand Up @@ -242,5 +242,5 @@
</setting>
</group>
</category>
</section>
</section>
</settings>
Binary file modified tests/data/Database/Addons33.db
Binary file not shown.
15 changes: 15 additions & 0 deletions tests/data/addons/script.module.pysocks/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Change Log

Brief descriptions of changes will be logged here. Note that unless otherwise specified, all future versions should be backwards compatible with older versions.

## [1.6.8] - 2017-12-21
- Remove support for EOL Python 3.3

## [1.6.7] - 2017-03-22
- Make SocksiPy legacy functions kwarg-compatible. See issue [#71](https://github.com/Anorov/PySocks/pull/71).
- Use setuptools in setup.py to support wheel. See issue [#73](https://github.com/Anorov/PySocks/pull/73).
- Test and logging enhancements

## [1.6.6] - 2017-01-29
- Full test suite finally added
- Travis CI enabled for project
22 changes: 22 additions & 0 deletions tests/data/addons/script.module.pysocks/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Copyright 2006 Dan-Haim. All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of Dan Haim nor the names of his contributors may be used
to endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY DAN HAIM "AS IS" AND ANY EXPRESS OR IMPLIED
WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
EVENT SHALL DAN HAIM OR HIS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA
OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT
OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMANGE.
Loading

0 comments on commit d451b52

Please sign in to comment.