diff --git a/apps/common/utils/__init__.py b/apps/common/utils/__init__.py index d34592a3a..e7f2fef70 100644 --- a/apps/common/utils/__init__.py +++ b/apps/common/utils/__init__.py @@ -8,5 +8,6 @@ from .encode import * from .http import * from .ip import * from .jumpserver import * +from .proxy import * from .random import * from .translate import * diff --git a/apps/common/utils/proxy.py b/apps/common/utils/proxy.py new file mode 100644 index 000000000..c45817476 --- /dev/null +++ b/apps/common/utils/proxy.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- +# +import requests +import requests_unixsocket +from django.http import HttpResponse, QueryDict + + +def _get_headers(environ): + headers = {} + for key, value in environ.items(): + if key.startswith('HTTP_') and key != 'HTTP_HOST': + headers[key[5:].replace('_', '-')] = value + elif key in ('CONTENT_TYPE', 'CONTENT_LENGTH'): + headers[key.replace('_', '-')] = value + return headers + + +def unix_socket_proxy_view( + request, url, requests_args=None, rewrite_location=None, + error_prefix='Proxy request failed' + ): + requests_args = (requests_args or {}).copy() + headers = _get_headers(request.META) + params = request.GET.copy() + + if 'headers' not in requests_args: + requests_args['headers'] = {} + if 'data' not in requests_args: + requests_args['data'] = request.body + if 'params' not in requests_args: + requests_args['params'] = QueryDict('', mutable=True) + + headers.update(requests_args['headers']) + params.update(requests_args['params']) + + for key in list(headers.keys()): + if key.lower() == 'content-length': + del headers[key] + + requests_args['headers'] = headers + requests_args['params'] = params + + try: + with requests_unixsocket.Session() as session: + upstream_response = session.request(request.method, url, **requests_args) + except requests.exceptions.RequestException as exc: + return HttpResponse(f'{error_prefix}: {exc}', status=502) + + response = HttpResponse(upstream_response.content, status=upstream_response.status_code) + excluded_headers = { + 'connection', 'keep-alive', 'proxy-authenticate', + 'proxy-authorization', 'te', 'trailers', 'transfer-encoding', + 'upgrade', 'content-encoding', 'content-length', + } + for key, value in upstream_response.headers.items(): + if key.lower() in excluded_headers: + continue + if key.lower() == 'location' and callable(rewrite_location): + value = rewrite_location(value) + response[key] = value + + return response diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index e011fb3ab..51155b6b8 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -746,6 +746,10 @@ class Config(dict): 'OAUTH2_PROVIDER_ACCESS_TOKEN_EXPIRE_SECONDS': 60 * 60, 'OAUTH2_PROVIDER_REFRESH_TOKEN_EXPIRE_SECONDS': 60 * 60 * 24 * 7, 'VENDOR': 'jumpserver', + + # JDMC + 'JDMC_ENABLED': False, + 'JDMC_SOCK_PATH': '', } old_config_map = { diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 1c553ced0..77101254d 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -1,6 +1,11 @@ # -*- coding: utf-8 -*- # from pathlib import Path +<<<<<<< HEAD +======= +import os +from urllib.parse import quote +>>>>>>> 0319ee5d5 (feat: support jdmc proxy) from .base import TEMPLATES, STATIC_DIR from ..const import CONFIG @@ -275,3 +280,8 @@ VENDOR = CONFIG.VENDOR VENDOR_TEMPLATES_DIR = Path(STATIC_DIR) / VENDOR if Path(VENDOR_TEMPLATES_DIR).is_dir(): TEMPLATES[0]['DIRS'].insert(0, VENDOR_TEMPLATES_DIR) + +# JDMC +JDMC_ENABLED = CONFIG.JDMC_ENABLED +JDMC_SOCK_PATH = CONFIG.JDMC_SOCK_PATH +JDMC_BASE_URL = f"http+unix://{quote(JDMC_SOCK_PATH, safe='')}" diff --git a/apps/jumpserver/urls.py b/apps/jumpserver/urls.py index 328cdec1a..6e53b62d9 100644 --- a/apps/jumpserver/urls.py +++ b/apps/jumpserver/urls.py @@ -109,5 +109,10 @@ if os.environ.get('DEBUG_TOOLBAR', False): path('__debug__/', include('debug_toolbar.urls')), ] +if settings.JDMC_ENABLED: + urlpatterns += [ + re_path(r'jdmc/(?P.*)$', views.jdmc_proxy_view, name='jdmc-proxy'), + ] + handler404 = 'jumpserver.views.handler404' handler500 = 'jumpserver.views.handler500' diff --git a/apps/jumpserver/views/__init__.py b/apps/jumpserver/views/__init__.py index 990d22bab..c0c8bb503 100644 --- a/apps/jumpserver/views/__init__.py +++ b/apps/jumpserver/views/__init__.py @@ -5,3 +5,4 @@ from .error_views import * from .index import * from .other import * from .swagger import * +from .jdmc import * diff --git a/apps/jumpserver/views/jdmc.py b/apps/jumpserver/views/jdmc.py new file mode 100644 index 000000000..4442441e8 --- /dev/null +++ b/apps/jumpserver/views/jdmc.py @@ -0,0 +1,44 @@ +from django.conf import settings +from django.http import HttpResponse +from django.contrib.auth.views import redirect_to_login +from django.views.decorators.csrf import csrf_exempt +from urllib.parse import quote, urlsplit, urlunsplit +from common.utils.proxy import unix_socket_proxy_view + + +__all__ = ['jdmc_proxy_view'] + + +def _rewrite_location(location): + if not location: + return location + if location.startswith('http+unix://'): + parsed = urlsplit(location) + return urlunsplit(('', '', parsed.path, parsed.query, parsed.fragment)) + return location + + +@csrf_exempt +def jdmc_proxy_view(request, subpath=''): + if not request.user.is_authenticated or not request.user.is_superuser: + return redirect_to_login(request.get_full_path(), settings.LOGIN_URL) + + upstream_url = f"{settings.JDMC_BASE_URL}{request.path}" + requests_args = { + 'headers': { + 'X-Forwarded-Proto': request.scheme, + 'X-Forwarded-Host': request.get_host(), + }, + 'allow_redirects': False, + 'timeout': (5, 60), + } + if request.method in {'GET', 'HEAD'}: + requests_args['data'] = None + + return unix_socket_proxy_view( + request=request, + url=upstream_url, + requests_args=requests_args, + rewrite_location=_rewrite_location, + error_prefix='JDMC proxy failed', + )