mirror of
https://github.com/netbox-community/netbox.git
synced 2025-07-27 19:08:38 -06:00
Introduce proxy routing
This commit is contained in:
parent
697610db94
commit
a95d97aaaf
@ -7,13 +7,13 @@ from pathlib import Path
|
|||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
|
||||||
from netbox.data_backends import DataBackend
|
from netbox.data_backends import DataBackend
|
||||||
from netbox.utils import register_data_backend
|
from netbox.utils import register_data_backend
|
||||||
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
|
from utilities.constants import HTTP_PROXY_SUPPORTED_SCHEMAS, HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS
|
||||||
|
from utilities.proxy import resolve_proxies
|
||||||
from utilities.socks import ProxyPoolManager
|
from utilities.socks import ProxyPoolManager
|
||||||
from .exceptions import SyncError
|
from .exceptions import SyncError
|
||||||
|
|
||||||
@ -70,18 +70,18 @@ class GitBackend(DataBackend):
|
|||||||
|
|
||||||
# Initialize backend config
|
# Initialize backend config
|
||||||
config = ConfigDict()
|
config = ConfigDict()
|
||||||
self.use_socks = False
|
self.socks_proxy = None
|
||||||
|
|
||||||
# Apply HTTP proxy (if configured)
|
# Apply HTTP proxy (if configured)
|
||||||
if settings.HTTP_PROXIES:
|
proxies = resolve_proxies(url=self.url, context={'client': self}) or {}
|
||||||
if proxy := settings.HTTP_PROXIES.get(self.url_scheme, None):
|
if proxy := proxies.get(self.url_scheme):
|
||||||
if urlparse(proxy).scheme not in HTTP_PROXY_SUPPORTED_SCHEMAS:
|
if urlparse(proxy).scheme not in HTTP_PROXY_SUPPORTED_SCHEMAS:
|
||||||
raise ImproperlyConfigured(f"Unsupported Git DataSource proxy scheme: {urlparse(proxy).scheme}")
|
raise ImproperlyConfigured(f"Unsupported Git DataSource proxy scheme: {urlparse(proxy).scheme}")
|
||||||
|
|
||||||
if self.url_scheme in ('http', 'https'):
|
if self.url_scheme in ('http', 'https'):
|
||||||
config.set("http", "proxy", proxy)
|
config.set("http", "proxy", proxy)
|
||||||
if urlparse(proxy).scheme in HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS:
|
if urlparse(proxy).scheme in HTTP_PROXY_SUPPORTED_SOCK_SCHEMAS:
|
||||||
self.use_socks = True
|
self.socks_proxy = proxy
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
@ -98,8 +98,8 @@ class GitBackend(DataBackend):
|
|||||||
}
|
}
|
||||||
|
|
||||||
# check if using socks for proxy - if so need to use custom pool_manager
|
# check if using socks for proxy - if so need to use custom pool_manager
|
||||||
if self.use_socks:
|
if self.socks_proxy:
|
||||||
clone_args['pool_manager'] = ProxyPoolManager(settings.HTTP_PROXIES.get(self.url_scheme))
|
clone_args['pool_manager'] = ProxyPoolManager(self.socks_proxy)
|
||||||
|
|
||||||
if self.url_scheme in ('http', 'https'):
|
if self.url_scheme in ('http', 'https'):
|
||||||
if self.params.get('username'):
|
if self.params.get('username'):
|
||||||
@ -147,7 +147,7 @@ class S3Backend(DataBackend):
|
|||||||
|
|
||||||
# Initialize backend config
|
# Initialize backend config
|
||||||
return Boto3Config(
|
return Boto3Config(
|
||||||
proxies=settings.HTTP_PROXIES,
|
proxies=resolve_proxies(url=self.url, context={'client': self}),
|
||||||
)
|
)
|
||||||
|
|
||||||
@contextmanager
|
@contextmanager
|
||||||
|
@ -5,6 +5,7 @@ import sys
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from netbox.jobs import JobRunner, system_job
|
from netbox.jobs import JobRunner, system_job
|
||||||
from netbox.search.backends import search_backend
|
from netbox.search.backends import search_backend
|
||||||
|
from utilities.proxy import resolve_proxies
|
||||||
from .choices import DataSourceStatusChoices, JobIntervalChoices
|
from .choices import DataSourceStatusChoices, JobIntervalChoices
|
||||||
from .exceptions import SyncError
|
from .exceptions import SyncError
|
||||||
from .models import DataSource
|
from .models import DataSource
|
||||||
@ -71,7 +72,7 @@ class SystemHousekeepingJob(JobRunner):
|
|||||||
url=settings.CENSUS_URL,
|
url=settings.CENSUS_URL,
|
||||||
params=census_data,
|
params=census_data,
|
||||||
timeout=3,
|
timeout=3,
|
||||||
proxies=settings.HTTP_PROXIES
|
proxies=resolve_proxies(url=settings.CENSUS_URL)
|
||||||
)
|
)
|
||||||
except requests.exceptions.RequestException:
|
except requests.exceptions.RequestException:
|
||||||
pass
|
pass
|
||||||
|
@ -11,6 +11,7 @@ from django.core.cache import cache
|
|||||||
from netbox.plugins import PluginConfig
|
from netbox.plugins import PluginConfig
|
||||||
from netbox.registry import registry
|
from netbox.registry import registry
|
||||||
from utilities.datetime import datetime_from_timestamp
|
from utilities.datetime import datetime_from_timestamp
|
||||||
|
from utilities.proxy import resolve_proxies
|
||||||
|
|
||||||
USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
|
USER_AGENT_STRING = f'NetBox/{settings.RELEASE.version} {settings.RELEASE.edition}'
|
||||||
CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed'
|
CACHE_KEY_CATALOG_FEED = 'plugins-catalog-feed'
|
||||||
@ -120,10 +121,11 @@ def get_catalog_plugins():
|
|||||||
def get_pages():
|
def get_pages():
|
||||||
# TODO: pagination is currently broken in API
|
# TODO: pagination is currently broken in API
|
||||||
payload = {'page': '1', 'per_page': '50'}
|
payload = {'page': '1', 'per_page': '50'}
|
||||||
|
proxies = resolve_proxies(url=settings.PLUGIN_CATALOG_URL)
|
||||||
first_page = session.get(
|
first_page = session.get(
|
||||||
settings.PLUGIN_CATALOG_URL,
|
settings.PLUGIN_CATALOG_URL,
|
||||||
headers={'User-Agent': USER_AGENT_STRING},
|
headers={'User-Agent': USER_AGENT_STRING},
|
||||||
proxies=settings.HTTP_PROXIES,
|
proxies=proxies,
|
||||||
timeout=3,
|
timeout=3,
|
||||||
params=payload
|
params=payload
|
||||||
).json()
|
).json()
|
||||||
@ -135,7 +137,7 @@ def get_catalog_plugins():
|
|||||||
next_page = session.get(
|
next_page = session.get(
|
||||||
settings.PLUGIN_CATALOG_URL,
|
settings.PLUGIN_CATALOG_URL,
|
||||||
headers={'User-Agent': USER_AGENT_STRING},
|
headers={'User-Agent': USER_AGENT_STRING},
|
||||||
proxies=settings.HTTP_PROXIES,
|
proxies=proxies,
|
||||||
timeout=3,
|
timeout=3,
|
||||||
params=payload
|
params=payload
|
||||||
).json()
|
).json()
|
||||||
|
@ -17,6 +17,7 @@ from core.models import ObjectType
|
|||||||
from extras.choices import BookmarkOrderingChoices
|
from extras.choices import BookmarkOrderingChoices
|
||||||
from utilities.object_types import object_type_identifier, object_type_name
|
from utilities.object_types import object_type_identifier, object_type_name
|
||||||
from utilities.permissions import get_permission_for_model
|
from utilities.permissions import get_permission_for_model
|
||||||
|
from utilities.proxy import resolve_proxies
|
||||||
from utilities.querydict import dict_to_querydict
|
from utilities.querydict import dict_to_querydict
|
||||||
from utilities.templatetags.builtins.filters import render_markdown
|
from utilities.templatetags.builtins.filters import render_markdown
|
||||||
from utilities.views import get_viewname
|
from utilities.views import get_viewname
|
||||||
@ -330,7 +331,7 @@ class RSSFeedWidget(DashboardWidget):
|
|||||||
response = requests.get(
|
response = requests.get(
|
||||||
url=self.config['feed_url'],
|
url=self.config['feed_url'],
|
||||||
headers={'User-Agent': f'NetBox/{settings.RELEASE.version}'},
|
headers={'User-Agent': f'NetBox/{settings.RELEASE.version}'},
|
||||||
proxies=settings.HTTP_PROXIES,
|
proxies=resolve_proxies(url=self.config['feed_url']),
|
||||||
timeout=3
|
timeout=3
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
@ -11,6 +11,7 @@ from packaging import version
|
|||||||
|
|
||||||
from core.models import Job, ObjectChange
|
from core.models import Job, ObjectChange
|
||||||
from netbox.config import Config
|
from netbox.config import Config
|
||||||
|
from utilities.proxy import resolve_proxies
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -107,7 +108,7 @@ class Command(BaseCommand):
|
|||||||
response = requests.get(
|
response = requests.get(
|
||||||
url=settings.RELEASE_CHECK_URL,
|
url=settings.RELEASE_CHECK_URL,
|
||||||
headers=headers,
|
headers=headers,
|
||||||
proxies=settings.HTTP_PROXIES
|
proxies=resolve_proxies(url=settings.RELEASE_CHECK_URL)
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
@ -3,10 +3,10 @@ import hmac
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings
|
|
||||||
from django_rq import job
|
from django_rq import job
|
||||||
from jinja2.exceptions import TemplateError
|
from jinja2.exceptions import TemplateError
|
||||||
|
|
||||||
|
from utilities.proxy import resolve_proxies
|
||||||
from .constants import WEBHOOK_EVENT_TYPES
|
from .constants import WEBHOOK_EVENT_TYPES
|
||||||
|
|
||||||
logger = logging.getLogger('netbox.webhooks')
|
logger = logging.getLogger('netbox.webhooks')
|
||||||
@ -63,9 +63,10 @@ def send_webhook(event_rule, model_name, event_type, data, timestamp, username,
|
|||||||
raise e
|
raise e
|
||||||
|
|
||||||
# Prepare the HTTP request
|
# Prepare the HTTP request
|
||||||
|
url = webhook.render_payload_url(context)
|
||||||
params = {
|
params = {
|
||||||
'method': webhook.http_method,
|
'method': webhook.http_method,
|
||||||
'url': webhook.render_payload_url(context),
|
'url': url,
|
||||||
'headers': headers,
|
'headers': headers,
|
||||||
'data': body.encode('utf8'),
|
'data': body.encode('utf8'),
|
||||||
}
|
}
|
||||||
@ -88,7 +89,8 @@ def send_webhook(event_rule, model_name, event_type, data, timestamp, username,
|
|||||||
session.verify = webhook.ssl_verification
|
session.verify = webhook.ssl_verification
|
||||||
if webhook.ca_file_path:
|
if webhook.ca_file_path:
|
||||||
session.verify = webhook.ca_file_path
|
session.verify = webhook.ca_file_path
|
||||||
response = session.send(prepared_request, proxies=settings.HTTP_PROXIES)
|
proxies = resolve_proxies(url=url, context={'webhook': webhook})
|
||||||
|
response = session.send(prepared_request, proxies=proxies)
|
||||||
|
|
||||||
if 200 <= response.status_code <= 299:
|
if 200 <= response.status_code <= 299:
|
||||||
logger.info(f"Request succeeded; response status {response.status_code}")
|
logger.info(f"Request succeeded; response status {response.status_code}")
|
||||||
|
@ -9,6 +9,7 @@ import warnings
|
|||||||
from django.contrib.messages import constants as messages
|
from django.contrib.messages import constants as messages
|
||||||
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||||
from django.core.validators import URLValidator
|
from django.core.validators import URLValidator
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.config import PARAMS as CONFIG_PARAMS
|
from netbox.config import PARAMS as CONFIG_PARAMS
|
||||||
@ -131,6 +132,7 @@ MEDIA_ROOT = getattr(configuration, 'MEDIA_ROOT', os.path.join(BASE_DIR, 'media'
|
|||||||
METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
|
METRICS_ENABLED = getattr(configuration, 'METRICS_ENABLED', False)
|
||||||
PLUGINS = getattr(configuration, 'PLUGINS', [])
|
PLUGINS = getattr(configuration, 'PLUGINS', [])
|
||||||
PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
|
PLUGINS_CONFIG = getattr(configuration, 'PLUGINS_CONFIG', {})
|
||||||
|
PROXY_ROUTERS = getattr(configuration, 'PROXY_ROUTERS', ['utilities.proxy.DefaultProxyRouter'])
|
||||||
QUEUE_MAPPINGS = getattr(configuration, 'QUEUE_MAPPINGS', {})
|
QUEUE_MAPPINGS = getattr(configuration, 'QUEUE_MAPPINGS', {})
|
||||||
REDIS = getattr(configuration, 'REDIS') # Required
|
REDIS = getattr(configuration, 'REDIS') # Required
|
||||||
RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
|
RELEASE_CHECK_URL = getattr(configuration, 'RELEASE_CHECK_URL', None)
|
||||||
@ -201,6 +203,14 @@ if RELEASE_CHECK_URL:
|
|||||||
"RELEASE_CHECK_URL must be a valid URL. Example: https://api.github.com/repos/netbox-community/netbox"
|
"RELEASE_CHECK_URL must be a valid URL. Example: https://api.github.com/repos/netbox-community/netbox"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Validate configured proxy routers
|
||||||
|
for path in PROXY_ROUTERS:
|
||||||
|
if type(path) is str:
|
||||||
|
try:
|
||||||
|
import_string(path)
|
||||||
|
except ImportError:
|
||||||
|
raise ImproperlyConfigured(f"Invalid proxy router path: {path}")
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# Database
|
# Database
|
||||||
@ -577,6 +587,7 @@ if SENTRY_ENABLED:
|
|||||||
sample_rate=SENTRY_SAMPLE_RATE,
|
sample_rate=SENTRY_SAMPLE_RATE,
|
||||||
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
traces_sample_rate=SENTRY_TRACES_SAMPLE_RATE,
|
||||||
send_default_pii=SENTRY_SEND_DEFAULT_PII,
|
send_default_pii=SENTRY_SEND_DEFAULT_PII,
|
||||||
|
# TODO: Support proxy routing
|
||||||
http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
|
http_proxy=HTTP_PROXIES.get('http') if HTTP_PROXIES else None,
|
||||||
https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
|
https_proxy=HTTP_PROXIES.get('https') if HTTP_PROXIES else None
|
||||||
)
|
)
|
||||||
|
46
netbox/utilities/proxy.py
Normal file
46
netbox/utilities/proxy.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
from django.conf import settings
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DefaultProxyRouter',
|
||||||
|
'resolve_proxies',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultProxyRouter:
|
||||||
|
"""
|
||||||
|
Base class for a proxy router.
|
||||||
|
"""
|
||||||
|
@staticmethod
|
||||||
|
def _get_protocol_from_url(url):
|
||||||
|
"""
|
||||||
|
Determine the applicable protocol (e.g. HTTP or HTTPS) from the given URL.
|
||||||
|
"""
|
||||||
|
return urlparse(url).scheme
|
||||||
|
|
||||||
|
def route(self, url=None, protocol=None, context=None):
|
||||||
|
if url and protocol is None:
|
||||||
|
protocol = self._get_protocol_from_url(url)
|
||||||
|
if protocol and protocol in settings.HTTP_PROXIES:
|
||||||
|
return {
|
||||||
|
protocol: settings.HTTP_PROXIES[protocol]
|
||||||
|
}
|
||||||
|
return settings.HTTP_PROXIES
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_proxies(url=None, protocol=None, context=None):
|
||||||
|
"""
|
||||||
|
Return a dictionary of candidate proxies (compatible with the requests module), or None.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The specific request URL for which the proxy will be used (optional)
|
||||||
|
protocol: The protocol in use (e.g. http or https) (optional)
|
||||||
|
context: Arbitrary additional context to aid in proxy selection (optional)
|
||||||
|
"""
|
||||||
|
context = context or {}
|
||||||
|
|
||||||
|
for item in settings.PROXY_ROUTERS:
|
||||||
|
router = import_string(item) if type(item) is str else item
|
||||||
|
if proxies := router.route(url=url, protocol=protocol, context=context):
|
||||||
|
return proxies
|
Loading…
Reference in New Issue
Block a user