mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2025-11-10 03:27:14 +00:00
Compare commits
1 Commits
update-bas
...
windows-mi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61bb58924b |
4
.github/test/Dockerfile-alpine
vendored
4
.github/test/Dockerfile-alpine
vendored
@@ -2,7 +2,7 @@
|
|||||||
# Test that we can still build on Alpine (musl modified libc https://musl.libc.org/)
|
# Test that we can still build on Alpine (musl modified libc https://musl.libc.org/)
|
||||||
# Some packages wont install via pypi because they dont have a wheel available under this architecture.
|
# Some packages wont install via pypi because they dont have a wheel available under this architecture.
|
||||||
|
|
||||||
FROM ghcr.io/linuxserver/baseimage-alpine:3.21
|
FROM ghcr.io/linuxserver/baseimage-alpine:3.18
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
COPY requirements.txt /requirements.txt
|
COPY requirements.txt /requirements.txt
|
||||||
@@ -28,6 +28,6 @@ RUN \
|
|||||||
py3-pip && \
|
py3-pip && \
|
||||||
echo "**** pip3 install test of changedetection.io ****" && \
|
echo "**** pip3 install test of changedetection.io ****" && \
|
||||||
pip3 install -U pip wheel setuptools && \
|
pip3 install -U pip wheel setuptools && \
|
||||||
pip3 install -U --no-cache-dir --find-links https://wheel-index.linuxserver.io/alpine-3.21/ -r /requirements.txt && \
|
pip3 install -U --no-cache-dir --find-links https://wheel-index.linuxserver.io/alpine-3.18/ -r /requirements.txt && \
|
||||||
apk del --purge \
|
apk del --purge \
|
||||||
build-dependencies
|
build-dependencies
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
# Read more https://github.com/dgtlmoon/changedetection.io/wiki
|
||||||
|
|
||||||
__version__ = '0.48.05'
|
__version__ = '0.48.03'
|
||||||
|
|
||||||
from changedetectionio.strtobool import strtobool
|
from changedetectionio.strtobool import strtobool
|
||||||
from json.decoder import JSONDecodeError
|
from json.decoder import JSONDecodeError
|
||||||
|
|||||||
@@ -247,32 +247,37 @@ class model(watch_base):
|
|||||||
bump = self.history
|
bump = self.history
|
||||||
return self.__newest_history_key
|
return self.__newest_history_key
|
||||||
|
|
||||||
# Given an arbitrary timestamp, find the best history key for the [diff] button so it can preset a smarter from_version
|
# Given an arbitrary timestamp, find the closest next key
|
||||||
|
# For example, last_viewed = 1000 so it should return the next 1001 timestamp
|
||||||
|
#
|
||||||
|
# used for the [diff] button so it can preset a smarter from_version
|
||||||
@property
|
@property
|
||||||
def get_from_version_based_on_last_viewed(self):
|
def get_next_snapshot_key_to_last_viewed(self):
|
||||||
|
|
||||||
"""Unfortunately for now timestamp is stored as string key"""
|
"""Unfortunately for now timestamp is stored as string key"""
|
||||||
keys = list(self.history.keys())
|
keys = list(self.history.keys())
|
||||||
if not keys:
|
if not keys:
|
||||||
return None
|
return None
|
||||||
if len(keys) == 1:
|
|
||||||
return keys[0]
|
|
||||||
|
|
||||||
last_viewed = int(self.get('last_viewed'))
|
last_viewed = int(self.get('last_viewed'))
|
||||||
|
prev_k = keys[0]
|
||||||
sorted_keys = sorted(keys, key=lambda x: int(x))
|
sorted_keys = sorted(keys, key=lambda x: int(x))
|
||||||
sorted_keys.reverse()
|
sorted_keys.reverse()
|
||||||
|
|
||||||
# When the 'last viewed' timestamp is greater than or equal the newest snapshot, return second newest
|
# When the 'last viewed' timestamp is greater than the newest snapshot, return second last
|
||||||
if last_viewed >= int(sorted_keys[0]):
|
if last_viewed > int(sorted_keys[0]):
|
||||||
return sorted_keys[1]
|
return sorted_keys[1]
|
||||||
|
|
||||||
# When the 'last viewed' timestamp is between snapshots, return the older snapshot
|
for k in sorted_keys:
|
||||||
for newer, older in list(zip(sorted_keys[0:], sorted_keys[1:])):
|
if int(k) < last_viewed:
|
||||||
if last_viewed < int(newer) and last_viewed >= int(older):
|
if prev_k == sorted_keys[0]:
|
||||||
return older
|
# Return the second last one so we dont recommend the same version compares itself
|
||||||
|
return sorted_keys[1]
|
||||||
|
|
||||||
# When the 'last viewed' timestamp is less than the oldest snapshot, return oldest
|
return prev_k
|
||||||
return sorted_keys[-1]
|
prev_k = k
|
||||||
|
|
||||||
|
return keys[0]
|
||||||
|
|
||||||
def get_history_snapshot(self, timestamp):
|
def get_history_snapshot(self, timestamp):
|
||||||
import brotli
|
import brotli
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ class difference_detection_processor():
|
|||||||
|
|
||||||
url = self.watch.link
|
url = self.watch.link
|
||||||
|
|
||||||
# Protect against file:, file:/, file:// access, check the real "link" without any meta "source:" etc prepended.
|
# Protect against file://, file:/ access, check the real "link" without any meta "source:" etc prepended.
|
||||||
if re.search(r'^file:', url.strip(), re.IGNORECASE):
|
if re.search(r'^file:/', url.strip(), re.IGNORECASE):
|
||||||
if not strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
if not strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"file:// type access is denied for security reasons."
|
"file:// type access is denied for security reasons."
|
||||||
|
|||||||
@@ -191,7 +191,7 @@
|
|||||||
{% if watch.history_n >= 2 %}
|
{% if watch.history_n >= 2 %}
|
||||||
|
|
||||||
{% if is_unviewed %}
|
{% if is_unviewed %}
|
||||||
<a href="{{ url_for('diff_history_page', uuid=watch.uuid, from_version=watch.get_from_version_based_on_last_viewed) }}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">History</a>
|
<a href="{{ url_for('diff_history_page', uuid=watch.uuid, from_version=watch.get_next_snapshot_key_to_last_viewed) }}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">History</a>
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{{ url_for('diff_history_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">History</a>
|
<a href="{{ url_for('diff_history_page', uuid=watch.uuid)}}" target="{{watch.uuid}}" class="pure-button pure-button-primary diff-link">History</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from flask import url_for
|
from flask import url_for
|
||||||
from .util import live_server_setup, wait_for_all_checks
|
from .util import set_original_response, set_modified_response, live_server_setup, wait_for_all_checks
|
||||||
|
import time
|
||||||
|
|
||||||
from .. import strtobool
|
from .. import strtobool
|
||||||
|
|
||||||
|
|
||||||
@@ -59,44 +61,54 @@ def test_bad_access(client, live_server, measure_memory_usage):
|
|||||||
assert b'Watch protocol is not permitted by SAFE_PROTOCOL_REGEX' in res.data
|
assert b'Watch protocol is not permitted by SAFE_PROTOCOL_REGEX' in res.data
|
||||||
|
|
||||||
|
|
||||||
def _runner_test_various_file_slash(client, file_uri):
|
def test_file_slashslash_access(client, live_server, measure_memory_usage):
|
||||||
|
#live_server_setup(live_server)
|
||||||
|
|
||||||
|
test_file_path = os.path.abspath(__file__)
|
||||||
|
|
||||||
|
# file:// is permitted by default, but it will be caught by ALLOW_FILE_URI
|
||||||
client.post(
|
client.post(
|
||||||
url_for("form_quick_watch_add"),
|
url_for("form_quick_watch_add"),
|
||||||
data={"url": file_uri, "tags": ''},
|
data={"url": f"file://{test_file_path}", "tags": ''},
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
wait_for_all_checks(client)
|
wait_for_all_checks(client)
|
||||||
res = client.get(url_for("index"))
|
res = client.get(url_for("index"))
|
||||||
|
|
||||||
substrings = [b"URLs with hostname components are not permitted", b"No connection adapters were found for"]
|
|
||||||
|
|
||||||
|
|
||||||
# If it is enabled at test time
|
# If it is enabled at test time
|
||||||
if strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
if strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
||||||
if file_uri.startswith('file:///'):
|
|
||||||
# This one should be the full qualified path to the file and should get the contents of this file
|
|
||||||
res = client.get(
|
res = client.get(
|
||||||
url_for("preview_page", uuid="first"),
|
url_for("preview_page", uuid="first"),
|
||||||
follow_redirects=True
|
follow_redirects=True
|
||||||
)
|
)
|
||||||
assert b'_runner_test_various_file_slash' in res.data
|
|
||||||
else:
|
|
||||||
# This will give some error from requests or if it went to chrome, will give some other error :-)
|
|
||||||
assert any(s in res.data for s in substrings)
|
|
||||||
|
|
||||||
res = client.get(url_for("form_delete", uuid="all"), follow_redirects=True)
|
assert b"test_file_slashslash_access" in res.data
|
||||||
assert b'Deleted' in res.data
|
else:
|
||||||
|
# Default should be here
|
||||||
|
assert b'file:// type access is denied for security reasons.' in res.data
|
||||||
|
|
||||||
def test_file_slash_access(client, live_server, measure_memory_usage):
|
def test_file_slash_access(client, live_server, measure_memory_usage):
|
||||||
#live_server_setup(live_server)
|
#live_server_setup(live_server)
|
||||||
|
|
||||||
# file: is NOT permitted by default, so it will be caught by ALLOW_FILE_URI check
|
|
||||||
|
|
||||||
test_file_path = os.path.abspath(__file__)
|
test_file_path = os.path.abspath(__file__)
|
||||||
_runner_test_various_file_slash(client, file_uri=f"file://{test_file_path}")
|
|
||||||
_runner_test_various_file_slash(client, file_uri=f"file:/{test_file_path}")
|
# file:// is permitted by default, but it will be caught by ALLOW_FILE_URI
|
||||||
_runner_test_various_file_slash(client, file_uri=f"file:{test_file_path}") # CVE-2024-56509
|
client.post(
|
||||||
|
url_for("form_quick_watch_add"),
|
||||||
|
data={"url": f"file:/{test_file_path}", "tags": ''},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
res = client.get(url_for("index"))
|
||||||
|
|
||||||
|
# If it is enabled at test time
|
||||||
|
if strtobool(os.getenv('ALLOW_FILE_URI', 'false')):
|
||||||
|
# So it should permit it, but it should fall back to the 'requests' library giving an error
|
||||||
|
# (but means it gets passed to playwright etc)
|
||||||
|
assert b"URLs with hostname components are not permitted" in res.data
|
||||||
|
else:
|
||||||
|
# Default should be here
|
||||||
|
assert b'file:// type access is denied for security reasons.' in res.data
|
||||||
|
|
||||||
def test_xss(client, live_server, measure_memory_usage):
|
def test_xss(client, live_server, measure_memory_usage):
|
||||||
#live_server_setup(live_server)
|
#live_server_setup(live_server)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class TestDiffBuilder(unittest.TestCase):
|
|||||||
watch = Watch.model(datastore_path='/tmp', default={})
|
watch = Watch.model(datastore_path='/tmp', default={})
|
||||||
watch.ensure_data_dir_exists()
|
watch.ensure_data_dir_exists()
|
||||||
|
|
||||||
|
watch['last_viewed'] = 110
|
||||||
|
|
||||||
# Contents from the browser are always returned from the browser/requests/etc as str, str is basically UTF-16 in python
|
# Contents from the browser are always returned from the browser/requests/etc as str, str is basically UTF-16 in python
|
||||||
watch.save_history_text(contents="hello world", timestamp=100, snapshot_id=str(uuid_builder.uuid4()))
|
watch.save_history_text(contents="hello world", timestamp=100, snapshot_id=str(uuid_builder.uuid4()))
|
||||||
@@ -25,41 +26,30 @@ class TestDiffBuilder(unittest.TestCase):
|
|||||||
watch.save_history_text(contents="hello world", timestamp=115, snapshot_id=str(uuid_builder.uuid4()))
|
watch.save_history_text(contents="hello world", timestamp=115, snapshot_id=str(uuid_builder.uuid4()))
|
||||||
watch.save_history_text(contents="hello world", timestamp=117, snapshot_id=str(uuid_builder.uuid4()))
|
watch.save_history_text(contents="hello world", timestamp=117, snapshot_id=str(uuid_builder.uuid4()))
|
||||||
|
|
||||||
p = watch.get_from_version_based_on_last_viewed
|
p = watch.get_next_snapshot_key_to_last_viewed
|
||||||
assert p == "100", "Correct 'last viewed' timestamp was detected"
|
assert p == "112", "Correct last-viewed timestamp was detected"
|
||||||
|
|
||||||
watch['last_viewed'] = 110
|
|
||||||
p = watch.get_from_version_based_on_last_viewed
|
|
||||||
assert p == "109", "Correct 'last viewed' timestamp was detected"
|
|
||||||
|
|
||||||
|
# When there is only one step of difference from the end of the list, it should return second-last change
|
||||||
watch['last_viewed'] = 116
|
watch['last_viewed'] = 116
|
||||||
p = watch.get_from_version_based_on_last_viewed
|
p = watch.get_next_snapshot_key_to_last_viewed
|
||||||
assert p == "115", "Correct 'last viewed' timestamp was detected"
|
assert p == "115", "Correct 'second last' last-viewed timestamp was detected when using the last timestamp"
|
||||||
|
|
||||||
watch['last_viewed'] = 99
|
watch['last_viewed'] = 99
|
||||||
p = watch.get_from_version_based_on_last_viewed
|
p = watch.get_next_snapshot_key_to_last_viewed
|
||||||
assert p == "100", "When the 'last viewed' timestamp is less than the oldest snapshot, return oldest"
|
assert p == "100"
|
||||||
|
|
||||||
watch['last_viewed'] = 200
|
watch['last_viewed'] = 200
|
||||||
p = watch.get_from_version_based_on_last_viewed
|
p = watch.get_next_snapshot_key_to_last_viewed
|
||||||
assert p == "115", "When the 'last viewed' timestamp is greater than the newest snapshot, return second newest"
|
assert p == "115", "When the 'last viewed' timestamp is greater than the newest snapshot, return second last "
|
||||||
|
|
||||||
watch['last_viewed'] = 109
|
watch['last_viewed'] = 109
|
||||||
p = watch.get_from_version_based_on_last_viewed
|
p = watch.get_next_snapshot_key_to_last_viewed
|
||||||
assert p == "109", "Correct when its the same time"
|
assert p == "109", "Correct when its the same time"
|
||||||
|
|
||||||
# new empty one
|
# new empty one
|
||||||
watch = Watch.model(datastore_path='/tmp', default={})
|
watch = Watch.model(datastore_path='/tmp', default={})
|
||||||
p = watch.get_from_version_based_on_last_viewed
|
p = watch.get_next_snapshot_key_to_last_viewed
|
||||||
assert p == None, "None when no history available"
|
assert p == None, "None when no history available"
|
||||||
|
|
||||||
watch.save_history_text(contents="hello world", timestamp=100, snapshot_id=str(uuid_builder.uuid4()))
|
|
||||||
p = watch.get_from_version_based_on_last_viewed
|
|
||||||
assert p == "100", "Correct with only one history snapshot"
|
|
||||||
|
|
||||||
watch['last_viewed'] = 200
|
|
||||||
p = watch.get_from_version_based_on_last_viewed
|
|
||||||
assert p == "100", "Correct with only one history snapshot"
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ services:
|
|||||||
# environment:
|
# environment:
|
||||||
# Default listening port, can also be changed with the -p option
|
# Default listening port, can also be changed with the -p option
|
||||||
# - PORT=5000
|
# - PORT=5000
|
||||||
|
|
||||||
|
# - PUID=1000
|
||||||
|
# - PGID=1000
|
||||||
#
|
#
|
||||||
# Log levels are in descending order. (TRACE is the most detailed one)
|
# Log levels are in descending order. (TRACE is the most detailed one)
|
||||||
# Log output levels: TRACE, DEBUG(default), INFO, SUCCESS, WARNING, ERROR, CRITICAL
|
# Log output levels: TRACE, DEBUG(default), INFO, SUCCESS, WARNING, ERROR, CRITICAL
|
||||||
|
|||||||
Reference in New Issue
Block a user