5 changed files with 264 additions and 89 deletions
@ -1,66 +0,0 @@ |
|||
#!/usr/bin/python3 -OO |
|||
# Copyright 2007-2021 The SABnzbd-Team <team@sabnzbd.org> |
|||
# |
|||
# This program 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 2 |
|||
# of the License, or (at your option) any later version. |
|||
# |
|||
# This program 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 this program; if not, write to the Free Software |
|||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|||
|
|||
""" |
|||
tests.test_api - Tests for API functions |
|||
""" |
|||
|
|||
from tests.testhelper import * |
|||
|
|||
import sabnzbd.api as api |
|||
|
|||
|
|||
class TestApiInternals: |
|||
""" Test internal functions of the API """ |
|||
|
|||
def test_empty(self): |
|||
with pytest.raises(TypeError): |
|||
api.api_handler(None) |
|||
with pytest.raises(AttributeError): |
|||
api.api_handler("") |
|||
|
|||
@set_config({"disable_key": False}) |
|||
def test_mode_invalid(self): |
|||
expected_error = "error: not implemented" |
|||
assert api.api_handler({"mode": "invalid"}).strip() == expected_error |
|||
with pytest.raises(IndexError): |
|||
assert api.api_handler({"mode": []}).strip() == expected_error |
|||
assert api.api_handler({"mode": ""}).strip() == expected_error |
|||
assert api.api_handler({"mode": None}).strip() == expected_error |
|||
|
|||
def test_version(self): |
|||
assert api.api_handler({"mode": "version"}).strip() == sabnzbd.__version__ |
|||
|
|||
@set_config({"disable_key": False}) |
|||
def test_auth(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "apikey" |
|||
|
|||
@set_config({"disable_key": True, "username": "foo", "password": "bar"}) |
|||
def test_auth_apikey_disabled(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "login" |
|||
|
|||
@set_config({"disable_key": True, "username": "", "password": ""}) |
|||
def test_auth_unavailable(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "None" |
|||
|
|||
@set_config({"disable_key": True, "username": "foo", "password": ""}) |
|||
def test_auth_unavailable_username_set(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "None" |
|||
|
|||
@set_config({"disable_key": True, "username": "", "password": "bar"}) |
|||
def test_auth_unavailable_password_set(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "None" |
@ -0,0 +1,237 @@ |
|||
#!/usr/bin/python3 -OO |
|||
# Copyright 2007-2021 The SABnzbd-Team <team@sabnzbd.org> |
|||
# |
|||
# This program 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 2 |
|||
# of the License, or (at your option) any later version. |
|||
# |
|||
# This program 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 this program; if not, write to the Free Software |
|||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
|||
|
|||
""" |
|||
tests.test_api - Tests for API functions |
|||
""" |
|||
import cherrypy |
|||
import pytest |
|||
|
|||
from tests.testhelper import * |
|||
|
|||
import sabnzbd.api as api |
|||
import sabnzbd.interface as interface |
|||
|
|||
|
|||
class TestApiInternals: |
|||
""" Test internal functions of the API """ |
|||
|
|||
def test_empty(self): |
|||
with pytest.raises(TypeError): |
|||
api.api_handler(None) |
|||
with pytest.raises(AttributeError): |
|||
api.api_handler("") |
|||
|
|||
def test_mode_invalid(self): |
|||
expected_error = "error: not implemented" |
|||
assert api.api_handler({"mode": "invalid"}).strip() == expected_error |
|||
with pytest.raises(IndexError): |
|||
assert api.api_handler({"mode": []}).strip() == expected_error |
|||
assert api.api_handler({"mode": ""}).strip() == expected_error |
|||
assert api.api_handler({"mode": None}).strip() == expected_error |
|||
|
|||
def test_version(self): |
|||
assert api.api_handler({"mode": "version"}).strip() == sabnzbd.__version__ |
|||
|
|||
def test_auth(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "apikey" |
|||
|
|||
@set_config({"disable_key": True, "username": "foo", "password": "bar"}) |
|||
def test_auth_apikey_disabled(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "login" |
|||
|
|||
@set_config({"disable_key": True}) |
|||
def test_auth_unavailable(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "None" |
|||
|
|||
@set_config({"disable_key": True, "username": "foo", "password": ""}) |
|||
def test_auth_unavailable_username_set(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "None" |
|||
|
|||
@set_config({"disable_key": True, "username": "", "password": "bar"}) |
|||
def test_auth_unavailable_password_set(self): |
|||
assert api.api_handler({"mode": "auth"}).strip() == "None" |
|||
|
|||
|
|||
def set_remote_host_or_ip(hostname: str = "localhost", remote_ip: str = "127.0.0.1"): |
|||
""" Change CherryPy's "Host" and "remote.ip"-values """ |
|||
cherrypy.request.headers["Host"] = hostname |
|||
cherrypy.request.remote.ip = remote_ip |
|||
|
|||
|
|||
class TestSecuredExpose: |
|||
""" Test the security handling """ |
|||
|
|||
main_page = sabnzbd.interface.MainPage() |
|||
|
|||
def check_full_access(self, redirect_match: str = r".*wizard.*"): |
|||
"""Basic test if we have full access to API and interface""" |
|||
assert sabnzbd.__version__ in self.main_page.api(mode="version") |
|||
# Passed authentication |
|||
assert api._MSG_NOT_IMPLEMENTED in self.main_page.api(apikey=sabnzbd.cfg.api_key()) |
|||
# Raises a redirect to the wizard |
|||
with pytest.raises(cherrypy._cperror.HTTPRedirect, match=redirect_match): |
|||
self.main_page.index() |
|||
|
|||
def test_basic(self): |
|||
set_remote_host_or_ip() |
|||
self.check_full_access() |
|||
|
|||
def test_api_no_or_wrong_api_key(self): |
|||
set_remote_host_or_ip() |
|||
# Get blocked |
|||
assert interface._MSG_APIKEY_REQUIRED in self.main_page.api() |
|||
assert interface._MSG_APIKEY_REQUIRED in self.main_page.api(mode="queue") |
|||
# Allowed to access "auth" and "version" without key |
|||
assert "apikey" in self.main_page.api(mode="auth") |
|||
assert sabnzbd.__version__ in self.main_page.api(mode="version") |
|||
# Blocked when you do something wrong |
|||
assert interface._MSG_APIKEY_INCORRECT in self.main_page.api(mode="queue", apikey="wrong") |
|||
|
|||
@set_config({"disable_key": True}) |
|||
def test_api_disabled_key(self): |
|||
set_remote_host_or_ip() |
|||
assert api._MSG_NOT_IMPLEMENTED in self.main_page.api() |
|||
|
|||
@set_config({"disable_key": True, "username": "foo", "password": "bar"}) |
|||
def test_api_disabled_key_with_auth(self): |
|||
set_remote_host_or_ip() |
|||
assert interface._MSG_MISSING_AUTH in self.main_page.api() |
|||
assert interface._MSG_MISSING_AUTH in self.main_page.api(ma_username="foo") |
|||
assert interface._MSG_MISSING_AUTH in self.main_page.api(ma_password="bar") |
|||
assert interface._MSG_MISSING_AUTH in self.main_page.api(ma_username="wrong") |
|||
assert interface._MSG_MISSING_AUTH in self.main_page.api(ma_password="wrong") |
|||
assert api._MSG_NOT_IMPLEMENTED in self.main_page.api(ma_username="foo", ma_password="bar") |
|||
|
|||
def test_api_nzb_key(self): |
|||
set_remote_host_or_ip() |
|||
# It should only access the nzb-functions, nothing else |
|||
assert api._MSG_NO_VALUE in self.main_page.api(mode="addfile", apikey=sabnzbd.cfg.nzb_key()) |
|||
assert interface._MSG_APIKEY_INCORRECT in self.main_page.api(mode="set_config", apikey=sabnzbd.cfg.nzb_key()) |
|||
assert interface._MSG_APIKEY_INCORRECT in self.main_page.shutdown(apikey=sabnzbd.cfg.nzb_key()) |
|||
|
|||
def test_check_hostname_basic(self): |
|||
# Block bad host |
|||
set_remote_host_or_ip(hostname="not_me") |
|||
assert interface._MSG_ACCESS_DENIED_HOSTNAME in self.main_page.api() |
|||
assert interface._MSG_ACCESS_DENIED_HOSTNAME in self.main_page.index() |
|||
# Block empty value |
|||
set_remote_host_or_ip(hostname="") |
|||
assert interface._MSG_ACCESS_DENIED_HOSTNAME in self.main_page.api() |
|||
assert interface._MSG_ACCESS_DENIED_HOSTNAME in self.main_page.index() |
|||
|
|||
# Fine if ip-address |
|||
for test_hostname in ( |
|||
"100.100.100.100", |
|||
"100.100.100.100:8080", |
|||
"[2001:db8:3333:4444:5555:6666:7777:8888]", |
|||
"[2001:db8:3333:4444:5555:6666:7777:8888]:8080", |
|||
"test.local", |
|||
"test.local:8080", |
|||
"test.local.", |
|||
): |
|||
set_remote_host_or_ip(hostname=test_hostname) |
|||
self.check_full_access() |
|||
|
|||
@set_config({"username": "foo", "password": "bar"}) |
|||
def test_check_hostname_not_user_password(self): |
|||
set_remote_host_or_ip(hostname="not_me") |
|||
self.check_full_access(redirect_match=r".*login.*") |
|||
|
|||
@set_config({"host_whitelist": "test.com, not_evil"}) |
|||
def test_check_hostname_whitelist(self): |
|||
set_remote_host_or_ip(hostname="test.com") |
|||
self.check_full_access() |
|||
set_remote_host_or_ip(hostname="not_evil") |
|||
self.check_full_access() |
|||
|
|||
def check_inet_allows(self, inet_exposure: int): |
|||
"""Each should allow all previous ones and the current one""" |
|||
# Level 1: nzb |
|||
if inet_exposure >= 1: |
|||
assert api._MSG_NO_VALUE in self.main_page.api(mode="addfile", apikey=sabnzbd.cfg.nzb_key()) |
|||
assert api._MSG_NO_VALUE in self.main_page.api(mode="addfile", apikey=sabnzbd.cfg.api_key()) |
|||
|
|||
# Level 2: basic API |
|||
if inet_exposure >= 2: |
|||
assert api._MSG_NO_VALUE in self.main_page.api(mode="get_files", apikey=sabnzbd.cfg.api_key()) |
|||
assert api._MSG_NO_VALUE in self.main_page.api(mode="change_script", apikey=sabnzbd.cfg.api_key()) |
|||
# Sub-function |
|||
assert "status" in self.main_page.api(mode="queue", name="resume", apikey=sabnzbd.cfg.api_key()) |
|||
|
|||
# Level 3: full API |
|||
if inet_exposure >= 3: |
|||
assert "misc" in self.main_page.api(mode="get_config", apikey=sabnzbd.cfg.api_key()) |
|||
# Sub-function |
|||
assert api._MSG_NO_VALUE in self.main_page.api( |
|||
mode="config", name="set_colorscheme", apikey=sabnzbd.cfg.api_key() |
|||
) |
|||
|
|||
# Level 4: full interface |
|||
if inet_exposure >= 4: |
|||
self.check_full_access() |
|||
|
|||
def check_inet_blocks(self, inet_exposure: int): |
|||
"""We count from the most exposure down""" |
|||
# Level 4: full interface, no blocking |
|||
# Level 3: full API |
|||
if inet_exposure <= 3: |
|||
assert interface._MSG_ACCESS_DENIED in self.main_page.index() |
|||
|
|||
# Level 2: basic API |
|||
if inet_exposure <= 2: |
|||
assert interface._MSG_ACCESS_DENIED in self.main_page.api(mode="get_config", apikey=sabnzbd.cfg.api_key()) |
|||
assert interface._MSG_ACCESS_DENIED in self.main_page.api( |
|||
mode="config", name="set_colorscheme", apikey=sabnzbd.cfg.api_key() |
|||
) |
|||
# Level 1: nzb |
|||
if inet_exposure <= 1: |
|||
assert interface._MSG_ACCESS_DENIED in self.main_page.api(mode="rescan", apikey=sabnzbd.cfg.api_key()) |
|||
assert interface._MSG_ACCESS_DENIED in self.main_page.api( |
|||
mode="queue", name="resume", apikey=sabnzbd.cfg.api_key() |
|||
) |
|||
|
|||
# Level 0: nothing, already checked above, but just to be sure |
|||
if inet_exposure <= 0: |
|||
assert interface._MSG_ACCESS_DENIED in self.main_page.api(mode="addfile", apikey=sabnzbd.cfg.api_key()) |
|||
# Check with or without API-key |
|||
assert interface._MSG_ACCESS_DENIED in self.main_page.api(mode="auth", apikey=sabnzbd.cfg.api_key()) |
|||
assert interface._MSG_ACCESS_DENIED in self.main_page.api(mode="auth") |
|||
|
|||
def test_inet_exposure(self): |
|||
# Run all tests as external user |
|||
set_remote_host_or_ip(hostname="100.100.100.100", remote_ip="11.11.11.11") |
|||
|
|||
# We don't use the wrapper, it would require creating many extra functions |
|||
# Option 5 is special, so it also gets it's own special test |
|||
for inet_exposure in range(6): |
|||
sabnzbd.cfg.inet_exposure.set(inet_exposure) |
|||
self.check_inet_allows(inet_exposure=inet_exposure) |
|||
self.check_inet_blocks(inet_exposure=inet_exposure) |
|||
|
|||
# Reset it |
|||
sabnzbd.cfg.inet_exposure.set(sabnzbd.cfg.inet_exposure.default()) |
|||
|
|||
@set_config({"inet_exposure": 5, "username": "foo", "password": "bar"}) |
|||
def test_inet_exposure_login_for_external(self): |
|||
# Local user: full access |
|||
set_remote_host_or_ip() |
|||
self.check_full_access() |
|||
|
|||
# Remote user: redirect to login |
|||
set_remote_host_or_ip(hostname="100.100.100.100", remote_ip="11.11.11.11") |
|||
self.check_full_access(redirect_match=r".*login.*") |
Loading…
Reference in new issue