mirror of
https://github.com/dgtlmoon/changedetection.io.git
synced 2026-01-11 01:30:24 +00:00
Compare commits
5 Commits
icon-fixes
...
python-311
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
25a25d41ff | ||
|
|
572f9b8a31 | ||
|
|
fcfd1b5e10 | ||
|
|
0790dd555e | ||
|
|
0b20dc7712 |
21
Dockerfile
21
Dockerfile
@@ -1,5 +1,5 @@
|
|||||||
# pip dependencies install stage
|
# pip dependencies install stage
|
||||||
FROM python:3.8-slim as builder
|
FROM python:3.11-slim as builder
|
||||||
|
|
||||||
# See `cryptography` pin comment in requirements.txt
|
# See `cryptography` pin comment in requirements.txt
|
||||||
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
||||||
@@ -29,23 +29,16 @@ RUN pip install --target=/dependencies playwright~=1.27.1 \
|
|||||||
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
|
|| echo "WARN: Failed to install Playwright. The application can still run, but the Playwright option will be disabled."
|
||||||
|
|
||||||
# Final image stage
|
# Final image stage
|
||||||
FROM python:3.8-slim
|
FROM python:3.11-slim
|
||||||
|
|
||||||
# See `cryptography` pin comment in requirements.txt
|
|
||||||
ARG CRYPTOGRAPHY_DONT_BUILD_RUST=1
|
|
||||||
|
|
||||||
# Re #93, #73, excluding rustc (adds another 430Mb~)
|
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||||
g++ \
|
libssl1.1 \
|
||||||
gcc \
|
libxslt1.1 \
|
||||||
# For pdftohtml
|
# For pdftohtml
|
||||||
poppler-utils \
|
poppler-utils \
|
||||||
libc-dev \
|
zlib1g \
|
||||||
libffi-dev \
|
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||||
libjpeg-dev \
|
|
||||||
libssl-dev \
|
|
||||||
libxslt-dev \
|
|
||||||
zlib1g-dev
|
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/58701233/docker-logs-erroneously-appears-empty-until-container-stops
|
# https://stackoverflow.com/questions/58701233/docker-logs-erroneously-appears-empty-until-container-stops
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|||||||
@@ -243,6 +243,14 @@ class base_html_playwright(Fetcher):
|
|||||||
if proxy_override:
|
if proxy_override:
|
||||||
self.proxy = {'server': proxy_override}
|
self.proxy = {'server': proxy_override}
|
||||||
|
|
||||||
|
if self.proxy:
|
||||||
|
# Playwright needs separate username and password values
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
parsed = urlparse(self.proxy.get('server'))
|
||||||
|
if parsed.username:
|
||||||
|
self.proxy['username'] = parsed.username
|
||||||
|
self.proxy['password'] = parsed.password
|
||||||
|
|
||||||
def screenshot_step(self, step_n=''):
|
def screenshot_step(self, step_n=''):
|
||||||
|
|
||||||
# There's a bug where we need to do it twice or it doesnt take the whole page, dont know why.
|
# There's a bug where we need to do it twice or it doesnt take the whole page, dont know why.
|
||||||
@@ -370,7 +378,7 @@ class base_html_playwright(Fetcher):
|
|||||||
context.close()
|
context.close()
|
||||||
browser.close()
|
browser.close()
|
||||||
print ("Content Fetcher > Content was empty")
|
print ("Content Fetcher > Content was empty")
|
||||||
raise EmptyReply(url=url, status_code=None)
|
raise EmptyReply(url=url, status_code=response.status)
|
||||||
|
|
||||||
# Bug 2(?) Set the viewport size AFTER loading the page
|
# Bug 2(?) Set the viewport size AFTER loading the page
|
||||||
self.page.set_viewport_size({"width": 1280, "height": 1024})
|
self.page.set_viewport_size({"width": 1280, "height": 1024})
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
FROM python:3.8-slim
|
|
||||||
|
|
||||||
# https://stackoverflow.com/questions/58701233/docker-logs-erroneously-appears-empty-until-container-stops
|
|
||||||
ENV PYTHONUNBUFFERED=1
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
RUN [ ! -d "/datastore" ] && mkdir /datastore
|
|
||||||
|
|
||||||
COPY sleep.py /
|
|
||||||
CMD [ "python", "/sleep.py" ]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
import time
|
|
||||||
|
|
||||||
print ("Sleep loop, you should run your script from the console")
|
|
||||||
|
|
||||||
while True:
|
|
||||||
# Wait for 5 seconds
|
|
||||||
time.sleep(2)
|
|
||||||
@@ -426,6 +426,13 @@ class watchForm(commonSettingsForm):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class SingleExtraProxy(Form):
|
||||||
|
|
||||||
|
# maybe better to set some <script>var..
|
||||||
|
proxy_name = StringField('Name', [validators.Optional()], render_kw={"placeholder": "Name"})
|
||||||
|
proxy_url = StringField('Proxy URL', [validators.Optional()], render_kw={"placeholder": "http://user:pass@...:3128", "size":50})
|
||||||
|
# @todo do the validation here instead
|
||||||
|
|
||||||
# datastore.data['settings']['requests']..
|
# datastore.data['settings']['requests']..
|
||||||
class globalSettingsRequestForm(Form):
|
class globalSettingsRequestForm(Form):
|
||||||
time_between_check = FormField(TimeBetweenCheckForm)
|
time_between_check = FormField(TimeBetweenCheckForm)
|
||||||
@@ -433,6 +440,15 @@ class globalSettingsRequestForm(Form):
|
|||||||
jitter_seconds = IntegerField('Random jitter seconds ± check',
|
jitter_seconds = IntegerField('Random jitter seconds ± check',
|
||||||
render_kw={"style": "width: 5em;"},
|
render_kw={"style": "width: 5em;"},
|
||||||
validators=[validators.NumberRange(min=0, message="Should contain zero or more seconds")])
|
validators=[validators.NumberRange(min=0, message="Should contain zero or more seconds")])
|
||||||
|
extra_proxies = FieldList(FormField(SingleExtraProxy), min_entries=5)
|
||||||
|
|
||||||
|
def validate_extra_proxies(self, extra_validators=None):
|
||||||
|
for e in self.data['extra_proxies']:
|
||||||
|
if e.get('proxy_name') or e.get('proxy_url'):
|
||||||
|
if not e.get('proxy_name','').strip() or not e.get('proxy_url','').strip():
|
||||||
|
self.extra_proxies.errors.append('Both a name, and a Proxy URL is required.')
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
# datastore.data['settings']['application']..
|
# datastore.data['settings']['application']..
|
||||||
class globalSettingsApplicationForm(commonSettingsForm):
|
class globalSettingsApplicationForm(commonSettingsForm):
|
||||||
|
|||||||
@@ -15,11 +15,12 @@ class model(dict):
|
|||||||
'headers': {
|
'headers': {
|
||||||
},
|
},
|
||||||
'requests': {
|
'requests': {
|
||||||
'timeout': int(getenv("DEFAULT_SETTINGS_REQUESTS_TIMEOUT", "45")), # Default 45 seconds
|
'extra_proxies': [], # Configurable extra proxies via the UI
|
||||||
'time_between_check': {'weeks': None, 'days': None, 'hours': 3, 'minutes': None, 'seconds': None},
|
|
||||||
'jitter_seconds': 0,
|
'jitter_seconds': 0,
|
||||||
|
'proxy': None, # Preferred proxy connection
|
||||||
|
'time_between_check': {'weeks': None, 'days': None, 'hours': 3, 'minutes': None, 'seconds': None},
|
||||||
|
'timeout': int(getenv("DEFAULT_SETTINGS_REQUESTS_TIMEOUT", "45")), # Default 45 seconds
|
||||||
'workers': int(getenv("DEFAULT_SETTINGS_REQUESTS_WORKERS", "10")), # Number of threads, lower is better for slow connections
|
'workers': int(getenv("DEFAULT_SETTINGS_REQUESTS_WORKERS", "10")), # Number of threads, lower is better for slow connections
|
||||||
'proxy': None # Preferred proxy connection
|
|
||||||
},
|
},
|
||||||
'application': {
|
'application': {
|
||||||
'api_access_token_enabled': True,
|
'api_access_token_enabled': True,
|
||||||
|
|||||||
@@ -8,6 +8,15 @@ set -e
|
|||||||
docker run --network changedet-network -d --name squid-one --hostname squid-one --rm -v `pwd`/tests/proxy_list/squid.conf:/etc/squid/conf.d/debian.conf ubuntu/squid:4.13-21.10_edge
|
docker run --network changedet-network -d --name squid-one --hostname squid-one --rm -v `pwd`/tests/proxy_list/squid.conf:/etc/squid/conf.d/debian.conf ubuntu/squid:4.13-21.10_edge
|
||||||
docker run --network changedet-network -d --name squid-two --hostname squid-two --rm -v `pwd`/tests/proxy_list/squid.conf:/etc/squid/conf.d/debian.conf ubuntu/squid:4.13-21.10_edge
|
docker run --network changedet-network -d --name squid-two --hostname squid-two --rm -v `pwd`/tests/proxy_list/squid.conf:/etc/squid/conf.d/debian.conf ubuntu/squid:4.13-21.10_edge
|
||||||
|
|
||||||
|
# Used for configuring a custom proxy URL via the UI
|
||||||
|
docker run --network changedet-network -d \
|
||||||
|
--name squid-custom \
|
||||||
|
--hostname squid-squid-custom \
|
||||||
|
--rm \
|
||||||
|
-v `pwd`/tests/proxy_list/squid-auth.conf:/etc/squid/conf.d/debian.conf \
|
||||||
|
-v `pwd`/tests/proxy_list/squid-passwords.txt:/etc/squid3/passwords \
|
||||||
|
ubuntu/squid:4.13-21.10_edge
|
||||||
|
|
||||||
|
|
||||||
## 2nd test actually choose the preferred proxy from proxies.json
|
## 2nd test actually choose the preferred proxy from proxies.json
|
||||||
|
|
||||||
@@ -32,3 +41,19 @@ then
|
|||||||
echo "Did not see a request to chosen.changedetection.io in the squid logs (while checking preferred proxy - squid two)"
|
echo "Did not see a request to chosen.changedetection.io in the squid logs (while checking preferred proxy - squid two)"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# Test the UI configurable proxies
|
||||||
|
|
||||||
|
docker run --network changedet-network \
|
||||||
|
test-changedetectionio \
|
||||||
|
bash -c 'cd changedetectionio && pytest tests/proxy_list/test_select_custom_proxy.py'
|
||||||
|
|
||||||
|
|
||||||
|
# Should see a request for one.changedetection.io in there
|
||||||
|
docker logs squid-custom 2>/dev/null|grep "TCP_TUNNEL.200.*changedetection.io"
|
||||||
|
if [ $? -ne 0 ]
|
||||||
|
then
|
||||||
|
echo "Did not see a valid request to changedetection.io in the squid logs (while checking preferred proxy - squid two)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
ul#requests-extra_proxies {
|
||||||
|
list-style: none;
|
||||||
|
/* tidy up the table to look more "inline" */
|
||||||
|
li {
|
||||||
|
> label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
/* each proxy entry is a `table` */
|
||||||
|
table {
|
||||||
|
tr {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,10 +2,11 @@
|
|||||||
* -- BASE STYLES --
|
* -- BASE STYLES --
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import "parts/_variables";
|
|
||||||
@import "parts/_spinners";
|
|
||||||
@import "parts/_browser-steps";
|
|
||||||
@import "parts/_arrows";
|
@import "parts/_arrows";
|
||||||
|
@import "parts/_browser-steps";
|
||||||
|
@import "parts/_extra_proxies";
|
||||||
|
@import "parts/_spinners";
|
||||||
|
@import "parts/_variables";
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
@@ -22,6 +23,13 @@ body {
|
|||||||
width: 1px;
|
width: 1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Row icons like chrome, pdf, share, etc
|
||||||
|
.status-icon {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
.pure-table-even {
|
.pure-table-even {
|
||||||
background: var(--color-background);
|
background: var(--color-background);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,139 @@
|
|||||||
/*
|
/*
|
||||||
* -- BASE STYLES --
|
* -- BASE STYLES --
|
||||||
*/
|
*/
|
||||||
|
.arrow {
|
||||||
|
border: solid #1b98f8;
|
||||||
|
border-width: 0 2px 2px 0;
|
||||||
|
display: inline-block;
|
||||||
|
padding: 3px; }
|
||||||
|
.arrow.right {
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
-webkit-transform: rotate(-45deg); }
|
||||||
|
.arrow.left {
|
||||||
|
transform: rotate(135deg);
|
||||||
|
-webkit-transform: rotate(135deg); }
|
||||||
|
.arrow.up, .arrow.asc {
|
||||||
|
transform: rotate(-135deg);
|
||||||
|
-webkit-transform: rotate(-135deg); }
|
||||||
|
.arrow.down, .arrow.desc {
|
||||||
|
transform: rotate(45deg);
|
||||||
|
-webkit-transform: rotate(45deg); }
|
||||||
|
|
||||||
|
#browser_steps {
|
||||||
|
/* convert rows to horizontal cells */ }
|
||||||
|
#browser_steps th {
|
||||||
|
display: none; }
|
||||||
|
#browser_steps li {
|
||||||
|
list-style: decimal;
|
||||||
|
padding: 5px; }
|
||||||
|
#browser_steps li:not(:first-child):hover {
|
||||||
|
opacity: 1.0; }
|
||||||
|
#browser_steps li .control {
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 5px; }
|
||||||
|
#browser_steps li .control a {
|
||||||
|
font-size: 70%; }
|
||||||
|
#browser_steps li.empty {
|
||||||
|
padding: 0px;
|
||||||
|
opacity: 0.35; }
|
||||||
|
#browser_steps li.empty .control {
|
||||||
|
display: none; }
|
||||||
|
#browser_steps li:hover {
|
||||||
|
background: #eee; }
|
||||||
|
#browser_steps li > label {
|
||||||
|
display: none; }
|
||||||
|
|
||||||
|
#browser-steps-fieldlist {
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: scroll; }
|
||||||
|
|
||||||
|
#browser-steps .flex-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row;
|
||||||
|
height: 600px;
|
||||||
|
/*@todo make this dynamic */ }
|
||||||
|
|
||||||
|
/* this is duplicate :( */
|
||||||
|
#browsersteps-selector-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
position: relative;
|
||||||
|
/* nice tall skinny one */ }
|
||||||
|
#browsersteps-selector-wrapper > img {
|
||||||
|
position: absolute;
|
||||||
|
max-width: 100%; }
|
||||||
|
#browsersteps-selector-wrapper > canvas {
|
||||||
|
position: relative;
|
||||||
|
max-width: 100%; }
|
||||||
|
#browsersteps-selector-wrapper > canvas:hover {
|
||||||
|
cursor: pointer; }
|
||||||
|
#browsersteps-selector-wrapper .loader {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
margin-left: -40px;
|
||||||
|
z-index: 100;
|
||||||
|
max-width: 350px;
|
||||||
|
text-align: center; }
|
||||||
|
#browsersteps-selector-wrapper .spinner, #browsersteps-selector-wrapper .spinner:after {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
font-size: 3px; }
|
||||||
|
#browsersteps-selector-wrapper #browsersteps-click-start {
|
||||||
|
color: var(--color-grey-400); }
|
||||||
|
#browsersteps-selector-wrapper #browsersteps-click-start:hover {
|
||||||
|
cursor: pointer; }
|
||||||
|
|
||||||
|
ul#requests-extra_proxies {
|
||||||
|
list-style: none;
|
||||||
|
/* tidy up the table to look more "inline" */
|
||||||
|
/* each proxy entry is a `table` */ }
|
||||||
|
ul#requests-extra_proxies li > label {
|
||||||
|
display: none; }
|
||||||
|
ul#requests-extra_proxies table tr {
|
||||||
|
display: inline; }
|
||||||
|
|
||||||
|
/* spinner */
|
||||||
|
.spinner,
|
||||||
|
.spinner:after {
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px; }
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
margin: 0px auto;
|
||||||
|
font-size: 3px;
|
||||||
|
vertical-align: middle;
|
||||||
|
display: inline-block;
|
||||||
|
text-indent: -9999em;
|
||||||
|
border-top: 1.1em solid rgba(38, 104, 237, 0.2);
|
||||||
|
border-right: 1.1em solid rgba(38, 104, 237, 0.2);
|
||||||
|
border-bottom: 1.1em solid rgba(38, 104, 237, 0.2);
|
||||||
|
border-left: 1.1em solid #2668ed;
|
||||||
|
-webkit-transform: translateZ(0);
|
||||||
|
-ms-transform: translateZ(0);
|
||||||
|
transform: translateZ(0);
|
||||||
|
-webkit-animation: load8 1.1s infinite linear;
|
||||||
|
animation: load8 1.1s infinite linear; }
|
||||||
|
|
||||||
|
@-webkit-keyframes load8 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg); }
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg); } }
|
||||||
|
|
||||||
|
@keyframes load8 {
|
||||||
|
0% {
|
||||||
|
-webkit-transform: rotate(0deg);
|
||||||
|
transform: rotate(0deg); }
|
||||||
|
100% {
|
||||||
|
-webkit-transform: rotate(360deg);
|
||||||
|
transform: rotate(360deg); } }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* CSS custom properties (aka variables).
|
* CSS custom properties (aka variables).
|
||||||
*/
|
*/
|
||||||
@@ -138,130 +271,6 @@ html[data-darkmode="true"] {
|
|||||||
html[data-darkmode="true"] .watch-table .unviewed.error {
|
html[data-darkmode="true"] .watch-table .unviewed.error {
|
||||||
color: var(--color-watch-table-error); }
|
color: var(--color-watch-table-error); }
|
||||||
|
|
||||||
/* spinner */
|
|
||||||
.spinner,
|
|
||||||
.spinner:after {
|
|
||||||
border-radius: 50%;
|
|
||||||
width: 10px;
|
|
||||||
height: 10px; }
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
margin: 0px auto;
|
|
||||||
font-size: 3px;
|
|
||||||
vertical-align: middle;
|
|
||||||
display: inline-block;
|
|
||||||
text-indent: -9999em;
|
|
||||||
border-top: 1.1em solid rgba(38, 104, 237, 0.2);
|
|
||||||
border-right: 1.1em solid rgba(38, 104, 237, 0.2);
|
|
||||||
border-bottom: 1.1em solid rgba(38, 104, 237, 0.2);
|
|
||||||
border-left: 1.1em solid #2668ed;
|
|
||||||
-webkit-transform: translateZ(0);
|
|
||||||
-ms-transform: translateZ(0);
|
|
||||||
transform: translateZ(0);
|
|
||||||
-webkit-animation: load8 1.1s infinite linear;
|
|
||||||
animation: load8 1.1s infinite linear; }
|
|
||||||
|
|
||||||
@-webkit-keyframes load8 {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: rotate(0deg);
|
|
||||||
transform: rotate(0deg); }
|
|
||||||
100% {
|
|
||||||
-webkit-transform: rotate(360deg);
|
|
||||||
transform: rotate(360deg); } }
|
|
||||||
|
|
||||||
@keyframes load8 {
|
|
||||||
0% {
|
|
||||||
-webkit-transform: rotate(0deg);
|
|
||||||
transform: rotate(0deg); }
|
|
||||||
100% {
|
|
||||||
-webkit-transform: rotate(360deg);
|
|
||||||
transform: rotate(360deg); } }
|
|
||||||
|
|
||||||
#browser_steps {
|
|
||||||
/* convert rows to horizontal cells */ }
|
|
||||||
#browser_steps th {
|
|
||||||
display: none; }
|
|
||||||
#browser_steps li {
|
|
||||||
list-style: decimal;
|
|
||||||
padding: 5px; }
|
|
||||||
#browser_steps li:not(:first-child):hover {
|
|
||||||
opacity: 1.0; }
|
|
||||||
#browser_steps li .control {
|
|
||||||
padding-left: 5px;
|
|
||||||
padding-right: 5px; }
|
|
||||||
#browser_steps li .control a {
|
|
||||||
font-size: 70%; }
|
|
||||||
#browser_steps li.empty {
|
|
||||||
padding: 0px;
|
|
||||||
opacity: 0.35; }
|
|
||||||
#browser_steps li.empty .control {
|
|
||||||
display: none; }
|
|
||||||
#browser_steps li:hover {
|
|
||||||
background: #eee; }
|
|
||||||
#browser_steps li > label {
|
|
||||||
display: none; }
|
|
||||||
|
|
||||||
#browser-steps-fieldlist {
|
|
||||||
height: 100%;
|
|
||||||
overflow-y: scroll; }
|
|
||||||
|
|
||||||
#browser-steps .flex-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
height: 600px;
|
|
||||||
/*@todo make this dynamic */ }
|
|
||||||
|
|
||||||
/* this is duplicate :( */
|
|
||||||
#browsersteps-selector-wrapper {
|
|
||||||
height: 100%;
|
|
||||||
width: 100%;
|
|
||||||
overflow-y: scroll;
|
|
||||||
position: relative;
|
|
||||||
/* nice tall skinny one */ }
|
|
||||||
#browsersteps-selector-wrapper > img {
|
|
||||||
position: absolute;
|
|
||||||
max-width: 100%; }
|
|
||||||
#browsersteps-selector-wrapper > canvas {
|
|
||||||
position: relative;
|
|
||||||
max-width: 100%; }
|
|
||||||
#browsersteps-selector-wrapper > canvas:hover {
|
|
||||||
cursor: pointer; }
|
|
||||||
#browsersteps-selector-wrapper .loader {
|
|
||||||
position: absolute;
|
|
||||||
left: 50%;
|
|
||||||
top: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
margin-left: -40px;
|
|
||||||
z-index: 100;
|
|
||||||
max-width: 350px;
|
|
||||||
text-align: center; }
|
|
||||||
#browsersteps-selector-wrapper .spinner, #browsersteps-selector-wrapper .spinner:after {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
font-size: 3px; }
|
|
||||||
#browsersteps-selector-wrapper #browsersteps-click-start {
|
|
||||||
color: var(--color-grey-400); }
|
|
||||||
#browsersteps-selector-wrapper #browsersteps-click-start:hover {
|
|
||||||
cursor: pointer; }
|
|
||||||
|
|
||||||
.arrow {
|
|
||||||
border: solid #1b98f8;
|
|
||||||
border-width: 0 2px 2px 0;
|
|
||||||
display: inline-block;
|
|
||||||
padding: 3px; }
|
|
||||||
.arrow.right {
|
|
||||||
transform: rotate(-45deg);
|
|
||||||
-webkit-transform: rotate(-45deg); }
|
|
||||||
.arrow.left {
|
|
||||||
transform: rotate(135deg);
|
|
||||||
-webkit-transform: rotate(135deg); }
|
|
||||||
.arrow.up, .arrow.asc {
|
|
||||||
transform: rotate(-135deg);
|
|
||||||
-webkit-transform: rotate(-135deg); }
|
|
||||||
.arrow.down, .arrow.desc {
|
|
||||||
transform: rotate(45deg);
|
|
||||||
-webkit-transform: rotate(45deg); }
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
background: var(--color-background-page); }
|
background: var(--color-background-page); }
|
||||||
@@ -275,6 +284,11 @@ body {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
width: 1px; }
|
width: 1px; }
|
||||||
|
|
||||||
|
.status-icon {
|
||||||
|
display: inline-block;
|
||||||
|
height: 1rem;
|
||||||
|
vertical-align: middle; }
|
||||||
|
|
||||||
.pure-table-even {
|
.pure-table-even {
|
||||||
background: var(--color-background); }
|
background: var(--color-background); }
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ class ChangeDetectionStore:
|
|||||||
self.datastore_path = datastore_path
|
self.datastore_path = datastore_path
|
||||||
self.json_store_path = "{}/url-watches.json".format(self.datastore_path)
|
self.json_store_path = "{}/url-watches.json".format(self.datastore_path)
|
||||||
self.needs_write = False
|
self.needs_write = False
|
||||||
self.proxy_list = None
|
|
||||||
self.start_time = time.time()
|
self.start_time = time.time()
|
||||||
self.stop_thread = False
|
self.stop_thread = False
|
||||||
# Base definition for all watchers
|
# Base definition for all watchers
|
||||||
@@ -116,11 +115,6 @@ class ChangeDetectionStore:
|
|||||||
secret = secrets.token_hex(16)
|
secret = secrets.token_hex(16)
|
||||||
self.__data['settings']['application']['api_access_token'] = secret
|
self.__data['settings']['application']['api_access_token'] = secret
|
||||||
|
|
||||||
# Proxy list support - available as a selection in settings when text file is imported
|
|
||||||
proxy_list_file = "{}/proxies.json".format(self.datastore_path)
|
|
||||||
if path.isfile(proxy_list_file):
|
|
||||||
self.import_proxy_list(proxy_list_file)
|
|
||||||
|
|
||||||
# Bump the update version by running updates
|
# Bump the update version by running updates
|
||||||
self.run_updates()
|
self.run_updates()
|
||||||
|
|
||||||
@@ -463,10 +457,30 @@ class ChangeDetectionStore:
|
|||||||
print ("Removing",item)
|
print ("Removing",item)
|
||||||
unlink(item)
|
unlink(item)
|
||||||
|
|
||||||
def import_proxy_list(self, filename):
|
@property
|
||||||
with open(filename) as f:
|
def proxy_list(self):
|
||||||
self.proxy_list = json.load(f)
|
proxy_list = {}
|
||||||
print ("Registered proxy list", list(self.proxy_list.keys()))
|
proxy_list_file = os.path.join(self.datastore_path, 'proxies.json')
|
||||||
|
|
||||||
|
# Load from external config file
|
||||||
|
if path.isfile(proxy_list_file):
|
||||||
|
with open("{}/proxies.json".format(self.datastore_path)) as f:
|
||||||
|
proxy_list = json.load(f)
|
||||||
|
|
||||||
|
# Mapping from UI config if available
|
||||||
|
extras = self.data['settings']['requests'].get('extra_proxies')
|
||||||
|
if extras:
|
||||||
|
i=0
|
||||||
|
for proxy in extras:
|
||||||
|
i += 0
|
||||||
|
if proxy.get('proxy_name') and proxy.get('proxy_url'):
|
||||||
|
k = "ui-" + str(i) + proxy.get('proxy_name')
|
||||||
|
proxy_list[k] = {'label': proxy.get('proxy_name'), 'url': proxy.get('proxy_url')}
|
||||||
|
|
||||||
|
|
||||||
|
return proxy_list if len(proxy_list) else None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_preferred_proxy_for_watch(self, uuid):
|
def get_preferred_proxy_for_watch(self, uuid):
|
||||||
@@ -476,11 +490,10 @@ class ChangeDetectionStore:
|
|||||||
:return: proxy "key" id
|
:return: proxy "key" id
|
||||||
"""
|
"""
|
||||||
|
|
||||||
proxy_id = None
|
|
||||||
if self.proxy_list is None:
|
if self.proxy_list is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# If its a valid one
|
# If it's a valid one
|
||||||
watch = self.data['watching'].get(uuid)
|
watch = self.data['watching'].get(uuid)
|
||||||
|
|
||||||
if watch.get('proxy') and watch.get('proxy') in list(self.proxy_list.keys()):
|
if watch.get('proxy') and watch.get('proxy') in list(self.proxy_list.keys()):
|
||||||
@@ -493,8 +506,9 @@ class ChangeDetectionStore:
|
|||||||
if self.proxy_list.get(system_proxy_id):
|
if self.proxy_list.get(system_proxy_id):
|
||||||
return system_proxy_id
|
return system_proxy_id
|
||||||
|
|
||||||
# Fallback - Did not resolve anything, use the first available
|
|
||||||
if system_proxy_id is None:
|
# Fallback - Did not resolve anything, or doesnt exist, use the first available
|
||||||
|
if system_proxy_id is None or not self.proxy_list.get(system_proxy_id):
|
||||||
first_default = list(self.proxy_list)[0]
|
first_default = list(self.proxy_list)[0]
|
||||||
return first_default
|
return first_default
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
<li class="tab"><a href="#fetching">Fetching</a></li>
|
<li class="tab"><a href="#fetching">Fetching</a></li>
|
||||||
<li class="tab"><a href="#filters">Global Filters</a></li>
|
<li class="tab"><a href="#filters">Global Filters</a></li>
|
||||||
<li class="tab"><a href="#api">API</a></li>
|
<li class="tab"><a href="#api">API</a></li>
|
||||||
|
<li class="tab"><a href="#proxies">CAPTCHA & Proxies</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="box-wrap inner">
|
<div class="box-wrap inner">
|
||||||
@@ -170,7 +171,23 @@ nav
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="tab-pane-inner" id="proxies">
|
||||||
|
|
||||||
|
<p><strong>Tip</strong>: You can connect to websites using <a href="https://brightdata.grsm.io/n0r16zf7eivq">BrightData</a> proxies, their service <strong>WebUnlocker</strong> will solve most CAPTCHAs, whilst their <strong>Residential Proxies</strong> may help to avoid CAPTCHA altogether. </p>
|
||||||
|
<p>It may be easier to try <strong>WebUnlocker</strong> first, WebUnlocker also supports country selection.</p>
|
||||||
|
<p>
|
||||||
|
When you have <a href="https://brightdata.grsm.io/n0r16zf7eivq">registered</a>, enabled the required services, visit the <A href="https://brightdata.com/cp/api_example?">API example page</A>, then select <strong>Python</strong>, set the country you wish to use, then copy+paste the example URL below<br/>
|
||||||
|
The Proxy URL with BrightData should start with <code>http://brd-customer...</code>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p>When you sign up using <a href="https://brightdata.grsm.io/n0r16zf7eivq">https://brightdata.grsm.io/n0r16zf7eivq</a> BrightData will match any first deposit up to $150</p>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="pure-control-group">
|
||||||
|
{{ render_field(form.requests.form.extra_proxies) }}
|
||||||
|
<div class="pure-form-message-inline">"Name" will be used for selecting the proxy in the Watch Edit settings</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="actions">
|
<div id="actions">
|
||||||
<div class="pure-control-group">
|
<div class="pure-control-group">
|
||||||
{{ render_button(form.save_button) }}
|
{{ render_button(form.save_button) }}
|
||||||
|
|||||||
@@ -89,10 +89,10 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}
|
<td class="title-col inline">{{watch.title if watch.title is not none and watch.title|length > 0 else watch.url}}
|
||||||
<a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a>
|
<a class="external" target="_blank" rel="noopener" href="{{ watch.link.replace('source:','') }}"></a>
|
||||||
<a class="link-spread" href="{{url_for('form_share_put_watch', uuid=watch.uuid)}}"><img style="height: 1em;display:inline-block; vertical-align: middle;" src="{{url_for('static_content', group='images', filename='spread.svg')}}" class="icon icon-spread" title="Create a link to share watch config with others" /></a>
|
<a class="link-spread" href="{{url_for('form_share_put_watch', uuid=watch.uuid)}}"><img class="status-icon" src="{{url_for('static_content', group='images', filename='spread.svg')}}" class="status-icon icon icon-spread" title="Create a link to share watch config with others" /></a>
|
||||||
|
|
||||||
{%if watch.get_fetch_backend == "html_webdriver" %}<img style="height: 1em; display:inline-block;" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" title="Using a chrome browser" />{% endif %}
|
{%if watch.get_fetch_backend == "html_webdriver" %}<img class="status-icon" src="{{url_for('static_content', group='images', filename='Google-Chrome-icon.png')}}" title="Using a chrome browser" />{% endif %}
|
||||||
{%if watch.is_pdf %}<img style="height: 1.2em; vertical-align: middle; display:inline-block;" src="{{url_for('static_content', group='images', filename='pdf-icon.svg')}}" title="Converting PDF to text" />{% endif %}
|
{%if watch.is_pdf %}<img class="status-icon" src="{{url_for('static_content', group='images', filename='pdf-icon.svg')}}" title="Converting PDF to text" />{% endif %}
|
||||||
{% if watch.last_error is defined and watch.last_error != False %}
|
{% if watch.last_error is defined and watch.last_error != False %}
|
||||||
<div class="fetch-error">{{ watch.last_error }}</div>
|
<div class="fetch-error">{{ watch.last_error }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<div class="ldjson-price-track-offer">Embedded price data detected, follow only price data? <a href="{{url_for('price_data_follower.accept', uuid=watch.uuid)}}" class="pure-button button-xsmall">Yes</a> <a href="{{url_for('price_data_follower.reject', uuid=watch.uuid)}}" class="">No</a></div>
|
<div class="ldjson-price-track-offer">Embedded price data detected, follow only price data? <a href="{{url_for('price_data_follower.accept', uuid=watch.uuid)}}" class="pure-button button-xsmall">Yes</a> <a href="{{url_for('price_data_follower.reject', uuid=watch.uuid)}}" class="">No</a></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if watch['track_ldjson_price_data'] == 'accepted' %}
|
{% if watch['track_ldjson_price_data'] == 'accepted' %}
|
||||||
<span class="tracking-ldjson-price-data" title="Automatically following embedded price information"><img src="{{url_for('static_content', group='images', filename='price-tag-icon.svg')}}" class="price-follow-tag-icon"/> Price</span>
|
<span class="tracking-ldjson-price-data" title="Automatically following embedded price information"><img src="{{url_for('static_content', group='images', filename='price-tag-icon.svg')}}" class="status-icon price-follow-tag-icon"/> Price</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not active_tag %}
|
{% if not active_tag %}
|
||||||
<span class="watch-tag-list">{{ watch.tag}}</span>
|
<span class="watch-tag-list">{{ watch.tag}}</span>
|
||||||
|
|||||||
48
changedetectionio/tests/proxy_list/squid-auth.conf
Normal file
48
changedetectionio/tests/proxy_list/squid-auth.conf
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN)
|
||||||
|
acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN)
|
||||||
|
acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN)
|
||||||
|
acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines
|
||||||
|
acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN)
|
||||||
|
acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN)
|
||||||
|
acl localnet src fc00::/7 # RFC 4193 local private network range
|
||||||
|
acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines
|
||||||
|
acl localnet src 159.65.224.174
|
||||||
|
acl SSL_ports port 443
|
||||||
|
acl Safe_ports port 80 # http
|
||||||
|
acl Safe_ports port 21 # ftp
|
||||||
|
acl Safe_ports port 443 # https
|
||||||
|
acl Safe_ports port 70 # gopher
|
||||||
|
acl Safe_ports port 210 # wais
|
||||||
|
acl Safe_ports port 1025-65535 # unregistered ports
|
||||||
|
acl Safe_ports port 280 # http-mgmt
|
||||||
|
acl Safe_ports port 488 # gss-http
|
||||||
|
acl Safe_ports port 591 # filemaker
|
||||||
|
acl Safe_ports port 777 # multiling http
|
||||||
|
acl CONNECT method CONNECT
|
||||||
|
|
||||||
|
http_access deny !Safe_ports
|
||||||
|
http_access deny CONNECT !SSL_ports
|
||||||
|
#http_access allow localhost manager
|
||||||
|
http_access deny manager
|
||||||
|
#http_access allow localhost
|
||||||
|
#http_access allow localnet
|
||||||
|
|
||||||
|
auth_param basic program /usr/lib/squid3/basic_ncsa_auth /etc/squid3/passwords
|
||||||
|
auth_param basic realm proxy
|
||||||
|
acl authenticated proxy_auth REQUIRED
|
||||||
|
http_access allow authenticated
|
||||||
|
http_access deny all
|
||||||
|
|
||||||
|
|
||||||
|
http_port 3128
|
||||||
|
coredump_dir /var/spool/squid
|
||||||
|
refresh_pattern ^ftp: 1440 20% 10080
|
||||||
|
refresh_pattern ^gopher: 1440 0% 1440
|
||||||
|
refresh_pattern -i (/cgi-bin/|\?) 0 0% 0
|
||||||
|
refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
|
||||||
|
refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims
|
||||||
|
refresh_pattern \/InRelease$ 0 0% 0 refresh-ims
|
||||||
|
refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims
|
||||||
|
refresh_pattern . 0 20% 4320
|
||||||
|
logfile_rotate 0
|
||||||
|
|
||||||
1
changedetectionio/tests/proxy_list/squid-passwords.txt
Normal file
1
changedetectionio/tests/proxy_list/squid-passwords.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
test:$apr1$xvhFolTA$E/kz5/Rw1ewcyaSUdwqZs.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
|
||||||
|
import time
|
||||||
|
from flask import url_for
|
||||||
|
from ..util import live_server_setup, wait_for_all_checks
|
||||||
|
|
||||||
|
# just make a request, we will grep in the docker logs to see it actually got called
|
||||||
|
def test_select_custom(client, live_server):
|
||||||
|
live_server_setup(live_server)
|
||||||
|
|
||||||
|
# Goto settings, add our custom one
|
||||||
|
res = client.post(
|
||||||
|
url_for("settings_page"),
|
||||||
|
data={
|
||||||
|
"requests-time_between_check-minutes": 180,
|
||||||
|
"application-ignore_whitespace": "y",
|
||||||
|
"application-fetch_backend": "html_requests",
|
||||||
|
"requests-extra_proxies-0-proxy_name": "custom-test-proxy",
|
||||||
|
# test:awesome is set in tests/proxy_list/squid-passwords.txt
|
||||||
|
"requests-extra_proxies-0-proxy_url": "http://test:awesome@squid-custom:3128",
|
||||||
|
},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"Settings updated." in res.data
|
||||||
|
|
||||||
|
res = client.post(
|
||||||
|
url_for("import_page"),
|
||||||
|
# Because a URL wont show in squid/proxy logs due it being SSLed
|
||||||
|
# Use plain HTTP or a specific domain-name here
|
||||||
|
data={"urls": "https://changedetection.io/CHANGELOG.txt"},
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
|
||||||
|
assert b"1 Imported" in res.data
|
||||||
|
wait_for_all_checks(client)
|
||||||
|
|
||||||
|
res = client.get(url_for("index"))
|
||||||
|
assert b'Proxy Authentication Required' not in res.data
|
||||||
|
|
||||||
|
|
||||||
|
res = client.get(
|
||||||
|
url_for("preview_page", uuid="first"),
|
||||||
|
follow_redirects=True
|
||||||
|
)
|
||||||
|
# We should see something via proxy
|
||||||
|
assert b'HEAD' in res.data
|
||||||
|
|
||||||
|
#
|
||||||
|
# Now we should see the request in the container logs for "squid-squid-custom" because it will be the only default
|
||||||
|
|
||||||
Reference in New Issue
Block a user