diff --git a/.dockerignore b/.dockerignore index 0353b6cd4..a504fb4ec 100644 --- a/.dockerignore +++ b/.dockerignore @@ -7,4 +7,5 @@ django.db celerybeat.pid ### Vagrant ### .vagrant/ -apps/xpack/.git \ No newline at end of file +apps/xpack/.git + diff --git a/.gitattributes b/.gitattributes index 24d9e1cd5..51d79b9d1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,4 @@ *.mmdb filter=lfs diff=lfs merge=lfs -text *.mo filter=lfs diff=lfs merge=lfs -text *.ipdb filter=lfs diff=lfs merge=lfs -text + diff --git a/.github/release-config.yml b/.github/release-config.yml index 1a75bdf6f..6dca58946 100644 --- a/.github/release-config.yml +++ b/.github/release-config.yml @@ -41,4 +41,5 @@ version-resolver: default: patch template: | ## 版本变化 What’s Changed - $CHANGES \ No newline at end of file + $CHANGES + diff --git a/.gitignore b/.gitignore index ec0378141..e1e5f32d7 100644 --- a/.gitignore +++ b/.gitignore @@ -41,3 +41,4 @@ release/* releashe /apps/script.py data/* + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 18c914718..04b8c547f 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -126,3 +126,4 @@ enforcement ladder](https://github.com/mozilla/diversity). For answers to common questions about this code of conduct, see the FAQ at https://www.contributor-covenant.org/faq. Translations are available at https://www.contributor-covenant.org/translations. + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f28982f9..62b3a9fca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,3 +23,4 @@ When reporting issues, always include: Because the issues are open to the public, when submitting files, be sure to remove any sensitive information, e.g. user name, password, IP address, and company name. You can replace those parts with "REDACTED" or other strings like "****". + diff --git a/Dockerfile b/Dockerfile index 7e6eac422..d78bd2e5f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,53 +1,65 @@ +FROM python:3.8-slim as stage-build +ARG TARGETARCH + +ARG VERSION +ENV VERSION=$VERSION + +WORKDIR /opt/jumpserver +ADD . . +RUN cd utils && bash -ixeu build.sh + FROM python:3.8-slim +ARG TARGETARCH MAINTAINER JumpServer Team ARG BUILD_DEPENDENCIES=" \ - g++ \ - make \ - pkg-config" + g++ \ + make \ + pkg-config" ARG DEPENDENCIES=" \ - default-libmysqlclient-dev \ - freetds-dev \ - libpq-dev \ - libffi-dev \ - libldap2-dev \ - libsasl2-dev \ - libxml2-dev \ - libxmlsec1-dev \ - libxmlsec1-openssl \ - libaio-dev \ - openssh-client \ - sshpass" + default-libmysqlclient-dev \ + freetds-dev \ + libpq-dev \ + libffi-dev \ + libldap2-dev \ + libsasl2-dev \ + libxml2-dev \ + libxmlsec1-dev \ + libxmlsec1-openssl \ + libaio-dev \ + openssh-client \ + sshpass" ARG TOOLS=" \ - curl \ - default-mysql-client \ - iproute2 \ - iputils-ping \ - locales \ - procps \ - redis-tools \ - telnet \ - vim \ - unzip \ - wget" + ca-certificates \ + curl \ + default-mysql-client \ + iputils-ping \ + locales \ + procps \ + redis-tools \ + telnet \ + vim \ + unzip \ + wget" -RUN sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ + sed -i 's@http://.*.debian.org@http://mirrors.ustc.edu.cn@g' /etc/apt/sources.list \ + && rm -f /etc/apt/apt.conf.d/docker-clean \ + && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && apt-get update \ && apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ && apt-get -y install --no-install-recommends ${TOOLS} \ - && localedef -c -f UTF-8 -i zh_CN zh_CN.UTF-8 \ - && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ && mkdir -p /root/.ssh/ \ && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \ && sed -i "s@# alias l@alias l@g" ~/.bashrc \ && echo "set mouse-=a" > ~/.vimrc \ && echo "no" | dpkg-reconfigure dash \ + && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ && rm -rf /var/lib/apt/lists/* -ARG TARGETARCH ARG ORACLE_LIB_MAJOR=19 ARG ORACLE_LIB_MINOR=10 ENV ORACLE_FILE="instantclient-basiclite-linux.${TARGETARCH:-amd64}-${ORACLE_LIB_MAJOR}.${ORACLE_LIB_MINOR}.0.0.0dbru.zip" @@ -68,21 +80,18 @@ ARG PIP_MIRROR=https://pypi.douban.com/simple ENV PIP_MIRROR=$PIP_MIRROR ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR -# 因为以 jms 或者 jumpserver 开头的 mirror 上可能没有 -RUN pip install --upgrade pip==20.2.4 setuptools==49.6.0 wheel==0.34.2 -i ${PIP_MIRROR} \ - && pip install --no-cache-dir $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ - && pip install --no-cache-dir -r requirements/requirements.txt -i ${PIP_MIRROR} \ - && rm -rf ~/.cache/pip -ARG VERSION -ENV VERSION=$VERSION +RUN --mount=type=cache,target=/root/.cache/pip \ + set -ex \ + && pip config set global.index-url ${PIP_MIRROR} \ + && pip install --upgrade pip \ + && pip install --upgrade setuptools wheel \ + && pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ + && pip install -r requirements/requirements.txt -ADD . . -RUN cd utils \ - && bash -ixeu build.sh \ - && mv ../release/jumpserver /opt/jumpserver \ - && rm -rf /tmp/build \ - && echo > /opt/jumpserver/config.yml +COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver +RUN echo > /opt/jumpserver/config.yml \ + && rm -rf /tmp/build WORKDIR /opt/jumpserver VOLUME /opt/jumpserver/data diff --git a/Dockerfile.loong64 b/Dockerfile.loong64 new file mode 100644 index 000000000..f27bd562d --- /dev/null +++ b/Dockerfile.loong64 @@ -0,0 +1,94 @@ +FROM python:3.8-slim as stage-build +ARG TARGETARCH + +ARG VERSION +ENV VERSION=$VERSION + +WORKDIR /opt/jumpserver +ADD . . +RUN cd utils && bash -ixeu build.sh + +FROM python:3.8-slim +ARG TARGETARCH +MAINTAINER JumpServer Team + +ARG BUILD_DEPENDENCIES=" \ + g++ \ + make \ + pkg-config" + +ARG DEPENDENCIES=" \ + default-libmysqlclient-dev \ + freetds-dev \ + libpq-dev \ + libffi-dev \ + libldap2-dev \ + libsasl2-dev \ + libxml2-dev \ + libxmlsec1-dev \ + libxmlsec1-openssl \ + libaio-dev \ + openssh-client \ + sshpass" + +ARG TOOLS=" \ + ca-certificates \ + curl \ + default-mysql-client \ + iputils-ping \ + locales \ + netcat \ + redis-server \ + telnet \ + vim \ + unzip \ + wget" + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=core \ + set -ex \ + && ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \ + && apt-get update \ + && apt-get -y install --no-install-recommends ${BUILD_DEPENDENCIES} \ + && apt-get -y install --no-install-recommends ${DEPENDENCIES} \ + && apt-get -y install --no-install-recommends ${TOOLS} \ + && mkdir -p /root/.ssh/ \ + && echo "Host *\n\tStrictHostKeyChecking no\n\tUserKnownHostsFile /dev/null" > /root/.ssh/config \ + && sed -i "s@# alias l@alias l@g" ~/.bashrc \ + && echo "set mouse-=a" > ~/.vimrc \ + && echo "no" | dpkg-reconfigure dash \ + && echo "zh_CN.UTF-8" | dpkg-reconfigure locales \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /tmp/build +COPY ./requirements ./requirements + +ARG PIP_MIRROR=https://pypi.douban.com/simple +ENV PIP_MIRROR=$PIP_MIRROR +ARG PIP_JMS_MIRROR=https://pypi.douban.com/simple +ENV PIP_JMS_MIRROR=$PIP_JMS_MIRROR + +RUN --mount=type=cache,target=/root/.cache/pip \ + set -ex \ + && pip config set global.index-url ${PIP_MIRROR} \ + && pip install --upgrade pip \ + && pip install --upgrade setuptools wheel \ + && pip install https://download.jumpserver.org/pypi/simple/cryptography/cryptography-36.0.1-cp38-cp38-linux_loongarch64.whl \ + && pip install https://download.jumpserver.org/pypi/simple/greenlet/greenlet-1.1.2-cp38-cp38-linux_loongarch64.whl \ + && pip install $(grep 'PyNaCl' requirements/requirements.txt) \ + && GRPC_PYTHON_BUILD_SYSTEM_OPENSSL=true pip install grpcio \ + && pip install $(grep -E 'jms|jumpserver' requirements/requirements.txt) -i ${PIP_JMS_MIRROR} \ + && pip install -r requirements/requirements.txt + +COPY --from=stage-build /opt/jumpserver/release/jumpserver /opt/jumpserver +RUN echo > /opt/jumpserver/config.yml \ + && rm -rf /tmp/build + +WORKDIR /opt/jumpserver +VOLUME /opt/jumpserver/data +VOLUME /opt/jumpserver/logs + +ENV LANG=zh_CN.UTF-8 + +EXPOSE 8070 +EXPOSE 8080 +ENTRYPOINT ["./entrypoint.sh"] diff --git a/LICENSE b/LICENSE index e72bfddab..53d1f3d01 100644 --- a/LICENSE +++ b/LICENSE @@ -671,4 +671,5 @@ into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read -. \ No newline at end of file +. + diff --git a/README.md b/README.md index 59c4e55cf..f4d9bcc16 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,7 @@ JumpServer 使用 Python 开发,配备了业界领先的 Web Terminal 方案 JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向扩展,无资产数量及并发限制。 -改变世界,从一点点开始 ... -> 如需进一步了解 JumpServer 开源项目,推荐阅读 [JumpServer 的初心和使命](https://mp.weixin.qq.com/s/S6q_2rP_9MwaVwyqLQnXzA) ### 特色优势 @@ -102,7 +100,7 @@ JumpServer 采纳分布式架构,支持多机房跨区域部署,支持横向 - [沐瞳游戏:通过JumpServer管控多项目分布式资产](https://blog.fit2cloud.com/?p=3213) - [携程:JumpServer 堡垒机部署与运营实战](https://blog.fit2cloud.com/?p=851) - [大智慧:JumpServer 堡垒机让“大智慧”的混合 IT 运维更智慧](https://blog.fit2cloud.com/?p=882) -- [小红书:的JumpServer堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516) +- [小红书:JumpServer 堡垒机大规模资产跨版本迁移之路](https://blog.fit2cloud.com/?p=516) - [中手游:JumpServer堡垒机助力中手游提升多云环境下安全运维能力](https://blog.fit2cloud.com/?p=732) - [中通快递:JumpServer主机安全运维实践](https://blog.fit2cloud.com/?p=708) - [东方明珠:JumpServer高效管控异构化、分布式云端资产](https://blog.fit2cloud.com/?p=687) diff --git a/README_EN.md b/README_EN.md index bb2b15097..7f302195e 100644 --- a/README_EN.md +++ b/README_EN.md @@ -92,4 +92,3 @@ Licensed under The GNU General Public License version 3 (GPLv3) (the "License") https://www.gnu.org/licenses/gpl-3.0.htmll Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. - diff --git a/SECURITY.md b/SECURITY.md index 2328ff7b6..2f2ce1d50 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,3 +18,4 @@ All security bugs should be reported to the contact as below: - ibuler@fit2cloud.com - support@fit2cloud.com - 400-052-0755 + diff --git a/Vagrantfile b/Vagrantfile deleted file mode 100644 index 98e82ca5a..000000000 --- a/Vagrantfile +++ /dev/null @@ -1,56 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -Vagrant.configure("2") do |config| - # The most common configuration options are documented and commented below. - # For a complete reference, please see the online documentation at - # https://docs.vagrantup.com. - - # Every Vagrant development environment requires a box. You can search for - # boxes at https://vagrantcloud.com/search. - config.vm.box_check_update = false - config.vm.box = "centos/7" - config.vm.hostname = "jumpserver" - config.vm.network "private_network", ip: "172.17.8.101" - config.vm.provider "virtualbox" do |vb| - vb.memory = "4096" - vb.cpus = 2 - vb.name = "jumpserver" - end - - config.vm.synced_folder ".", "/vagrant", type: "rsync", - rsync__verbose: true, - rsync__exclude: ['.git*', 'node_modules*','*.log','*.box','Vagrantfile'] - - config.vm.provision "shell", inline: <<-SHELL -## 设置yum的阿里云源 -sudo curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo -sudo sed -i -e '/mirrors.cloud.aliyuncs.com/d' -e '/mirrors.aliyuncs.com/d' /etc/yum.repos.d/CentOS-Base.repo -sudo curl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo -sudo yum makecache - -## 安装依赖包 -sudo yum install -y python36 python36-devel python36-pip \ - libtiff-devel libjpeg-devel libzip-devel freetype-devel \ - lcms2-devel libwebp-devel tcl-devel tk-devel sshpass \ - openldap-devel mariadb-devel mysql-devel libffi-devel \ - openssh-clients telnet openldap-clients gcc - -## 配置pip阿里云源 -mkdir /home/vagrant/.pip -cat << EOF | sudo tee -a /home/vagrant/.pip/pip.conf -[global] -timeout = 6000 -index-url = https://mirrors.aliyun.com/pypi/simple/ - -[install] -use-mirrors = true -mirrors = https://mirrors.aliyun.com/pypi/simple/ -trusted-host=mirrors.aliyun.com -EOF - -python3.6 -m venv /home/vagrant/venv -source /home/vagrant/venv/bin/activate -echo 'source /home/vagrant/venv/bin/activate' >> /home/vagrant/.bash_profile - SHELL -end diff --git a/apps/applications/api/application.py b/apps/applications/api/application.py index 71315cdcf..4d04e894f 100644 --- a/apps/applications/api/application.py +++ b/apps/applications/api/application.py @@ -4,6 +4,7 @@ from orgs.mixins.api import OrgBulkModelViewSet from rest_framework.decorators import action from rest_framework.response import Response + from common.tree import TreeNodeSerializer from common.mixins.api import SuggestionMixin from .. import serializers diff --git a/apps/applications/apps.py b/apps/applications/apps.py index 1662edcf0..a025a0517 100644 --- a/apps/applications/apps.py +++ b/apps/applications/apps.py @@ -7,3 +7,7 @@ from django.apps import AppConfig class ApplicationsConfig(AppConfig): name = 'applications' verbose_name = _('Applications') + + def ready(self): + from . import signal_handlers + super().ready() diff --git a/apps/applications/const.py b/apps/applications/const.py index 4e0d2fe50..313477c25 100644 --- a/apps/applications/const.py +++ b/apps/applications/const.py @@ -83,9 +83,3 @@ class AppType(models.TextChoices): if AppCategory.is_xpack(category): return True return tp in ['oracle', 'postgresql', 'sqlserver'] - - -class OracleVersion(models.TextChoices): - version_11g = '11g', '11g' - version_12c = '12c', '12c' - version_other = 'other', _('Other') diff --git a/apps/applications/models/application.py b/apps/applications/models/application.py index cc98abaf8..3661188fe 100644 --- a/apps/applications/models/application.py +++ b/apps/applications/models/application.py @@ -10,9 +10,7 @@ from common.mixins import CommonModelMixin from common.tree import TreeNode from common.utils import is_uuid from assets.models import Asset, SystemUser -from ..const import OracleVersion -from ..utils import KubernetesTree from .. import const @@ -175,6 +173,7 @@ class ApplicationTreeNodeMixin: return pid def as_tree_node(self, pid, k8s_as_tree=False): + from ..utils import KubernetesTree if self.type == const.AppType.k8s and k8s_as_tree: node = KubernetesTree(pid).as_tree_node(self) else: @@ -304,15 +303,6 @@ class Application(CommonModelMixin, OrgModelMixin, ApplicationTreeNodeMixin): target_ip = self.attrs.get('host') return target_ip - def get_target_protocol_for_oracle(self): - """ Oracle 类型需要单独处理,因为要携带版本号 """ - if not self.is_type(self.APP_TYPE.oracle): - return - version = self.attrs.get('version', OracleVersion.version_12c) - if version == OracleVersion.version_other: - return - return 'oracle_%s' % version - class ApplicationUser(SystemUser): class Meta: diff --git a/apps/applications/serializers/attrs/application_type/oracle.py b/apps/applications/serializers/attrs/application_type/oracle.py index fdc8016d2..c87c4904d 100644 --- a/apps/applications/serializers/attrs/application_type/oracle.py +++ b/apps/applications/serializers/attrs/application_type/oracle.py @@ -2,15 +2,9 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from ..application_category import DBSerializer -from applications.const import OracleVersion __all__ = ['OracleSerializer'] class OracleSerializer(DBSerializer): - version = serializers.ChoiceField( - choices=OracleVersion.choices, default=OracleVersion.version_12c, - allow_null=True, label=_('Version'), - help_text=_('Magnus currently supports only 11g and 12c connections') - ) port = serializers.IntegerField(default=1521, label=_('Port'), allow_null=True) diff --git a/apps/applications/signal_handlers.py b/apps/applications/signal_handlers.py new file mode 100644 index 000000000..4aa11c79b --- /dev/null +++ b/apps/applications/signal_handlers.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +# \ No newline at end of file diff --git a/apps/assets/api/system_user.py b/apps/assets/api/system_user.py index f95303a5e..64457db75 100644 --- a/apps/assets/api/system_user.py +++ b/apps/assets/api/system_user.py @@ -208,7 +208,8 @@ class SystemUserTaskApi(generics.CreateAPIView): class SystemUserCommandFilterRuleListApi(generics.ListAPIView): rbac_perms = { - 'list': 'assets.view_commandfilterule' + 'list': 'assets.view_commandfilterule', + 'GET': 'assets.view_commandfilterule', } def get_serializer_class(self): @@ -223,12 +224,14 @@ class SystemUserCommandFilterRuleListApi(generics.ListAPIView): if not system_user: system_user_id = self.request.query_params.get('system_user_id') asset_id = self.request.query_params.get('asset_id') + node_id = self.request.query_params.get('node_id') application_id = self.request.query_params.get('application_id') rules = CommandFilterRule.get_queryset( user_id=user_id, user_group_id=user_group_id, system_user_id=system_user_id, asset_id=asset_id, + node_id=node_id, application_id=application_id ) return rules diff --git a/apps/assets/migrations/0092_commandfilter_nodes.py b/apps/assets/migrations/0092_commandfilter_nodes.py new file mode 100644 index 000000000..b3c1916e3 --- /dev/null +++ b/apps/assets/migrations/0092_commandfilter_nodes.py @@ -0,0 +1,18 @@ +# Generated by Django 3.2.15 on 2022-10-09 09:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('assets', '0091_auto_20220629_1826'), + ] + + operations = [ + migrations.AddField( + model_name='commandfilter', + name='nodes', + field=models.ManyToManyField(blank=True, related_name='cmd_filters', to='assets.Node', verbose_name='Nodes'), + ), + ] diff --git a/apps/assets/models/asset.py b/apps/assets/models/asset.py index 7dde0862f..95ba28d21 100644 --- a/apps/assets/models/asset.py +++ b/apps/assets/models/asset.py @@ -116,7 +116,7 @@ class NodesRelationMixin: nodes = [] for node in self.get_nodes(): _nodes = node.get_ancestors(with_self=True) - nodes.append(_nodes) + nodes.extend(list(_nodes)) if flat: nodes = list(reduce(lambda x, y: set(x) | set(y), nodes)) return nodes diff --git a/apps/assets/models/cmd_filter.py b/apps/assets/models/cmd_filter.py index c7fa33aae..7f44094e0 100644 --- a/apps/assets/models/cmd_filter.py +++ b/apps/assets/models/cmd_filter.py @@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _ from users.models import User, UserGroup from applications.models import Application -from ..models import SystemUser, Asset +from ..models import SystemUser, Asset, Node from common.utils import lazyproperty, get_logger, get_object_or_none from orgs.mixins.models import OrgModelMixin @@ -33,6 +33,10 @@ class CommandFilter(OrgModelMixin): 'users.UserGroup', related_name='cmd_filters', blank=True, verbose_name=_("User group"), ) + nodes = models.ManyToManyField( + 'assets.Node', related_name='cmd_filters', blank=True, + verbose_name=_("Nodes") + ) assets = models.ManyToManyField( 'assets.Asset', related_name='cmd_filters', blank=True, verbose_name=_("Asset") @@ -189,7 +193,8 @@ class CommandFilterRule(OrgModelMixin): @classmethod def get_queryset(cls, user_id=None, user_group_id=None, system_user_id=None, - asset_id=None, application_id=None, org_id=None): + asset_id=None, node_id=None, application_id=None, org_id=None): + # user & user_group user_groups = [] user = get_object_or_none(User, pk=user_id) if user: @@ -198,8 +203,18 @@ class CommandFilterRule(OrgModelMixin): if user_group: org_id = user_group.org_id user_groups.append(user_group) - system_user = get_object_or_none(SystemUser, pk=system_user_id) + + # asset & node + nodes = [] asset = get_object_or_none(Asset, pk=asset_id) + if asset: + nodes.extend(asset.get_all_nodes()) + node = get_object_or_none(Node, pk=node_id) + if node: + org_id = node.org_id + nodes.extend(list(node.get_ancestors(with_self=True))) + + system_user = get_object_or_none(SystemUser, pk=system_user_id) application = get_object_or_none(Application, pk=application_id) q = Q() if user: @@ -212,6 +227,8 @@ class CommandFilterRule(OrgModelMixin): if asset: org_id = asset.org_id q |= Q(assets=asset) + if nodes: + q |= Q(nodes__in=set(nodes)) if application: org_id = application.org_id q |= Q(applications=application) diff --git a/apps/assets/serializers/cmd_filter.py b/apps/assets/serializers/cmd_filter.py index 9a33dd6fa..9bd5d38cc 100644 --- a/apps/assets/serializers/cmd_filter.py +++ b/apps/assets/serializers/cmd_filter.py @@ -21,7 +21,7 @@ class CommandFilterSerializer(BulkOrgResourceModelSerializer): 'comment', 'created_by', ] fields_fk = ['rules'] - fields_m2m = ['users', 'user_groups', 'system_users', 'assets', 'applications'] + fields_m2m = ['users', 'user_groups', 'system_users', 'nodes', 'assets', 'applications'] fields = fields_small + fields_fk + fields_m2m extra_kwargs = { 'rules': {'read_only': True}, diff --git a/apps/authentication/api/connection_token.py b/apps/authentication/api/connection_token.py index 2b4ce7e2b..e52f76577 100644 --- a/apps/authentication/api/connection_token.py +++ b/apps/authentication/api/connection_token.py @@ -62,12 +62,15 @@ class ConnectionTokenMixin: def get_smart_endpoint(self, protocol, asset=None, application=None): if asset: + target_instance = asset target_ip = asset.get_target_ip() elif application: + target_instance = application target_ip = application.get_target_ip() else: + target_instance = None target_ip = '' - endpoint = EndpointRule.match_endpoint(target_ip, protocol, self.request) + endpoint = EndpointRule.match_endpoint(target_instance, target_ip, protocol, self.request) return endpoint @staticmethod diff --git a/apps/authentication/backends/oauth2/views.py b/apps/authentication/backends/oauth2/views.py index d3f4865a2..c0fe06759 100644 --- a/apps/authentication/backends/oauth2/views.py +++ b/apps/authentication/backends/oauth2/views.py @@ -55,7 +55,9 @@ class OAuth2AuthCallbackView(View): ) logger.debug(log_prompt.format('Redirect')) - return HttpResponseRedirect(settings.AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI) + # OAuth2 服务端认证成功, 但是用户被禁用了, 这时候需要调用服务端的logout + redirect_url = settings.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT + return HttpResponseRedirect(redirect_url) class OAuth2EndSessionView(View): diff --git a/apps/authentication/backends/saml2/views.py b/apps/authentication/backends/saml2/views.py index 9bc3ddc97..e0fa97590 100644 --- a/apps/authentication/backends/saml2/views.py +++ b/apps/authentication/backends/saml2/views.py @@ -271,7 +271,10 @@ class Saml2AuthCallbackView(View, PrepareRequestMixin): auth.login(self.request, user) logger.debug(log_prompt.format('Redirect')) - next_url = saml_instance.redirect_to(post_data.get('RelayState', '/')) + redir = post_data.get('RelayState') + if not redir or len(redir) == 0: + redir = "/" + next_url = saml_instance.redirect_to(redir) return HttpResponseRedirect(next_url) @csrf_exempt diff --git a/apps/common/management/commands/services/services/celery_base.py b/apps/common/management/commands/services/services/celery_base.py index 435e4ebe2..92fa99cba 100644 --- a/apps/common/management/commands/services/services/celery_base.py +++ b/apps/common/management/commands/services/services/celery_base.py @@ -30,7 +30,9 @@ class CeleryBaseService(BaseService): '-l', 'INFO', '-c', str(self.num), '-Q', self.queue, - '-n', f'{self.queue}@{server_hostname}' + '--heartbeat-interval', '10', + '-n', f'{self.queue}@{server_hostname}', + '--without-mingle', ] return cmd diff --git a/apps/common/management/commands/services/services/flower.py b/apps/common/management/commands/services/services/flower.py index 402e9f37d..38dc72be8 100644 --- a/apps/common/management/commands/services/services/flower.py +++ b/apps/common/management/commands/services/services/flower.py @@ -19,11 +19,13 @@ class FlowerService(BaseService): 'celery', '-A', 'ops', 'flower', - '-l', 'INFO', + '-logging=info', '--url_prefix=/core/flower', '--auto_refresh=False', '--max_tasks=1000', - '--tasks_columns=uuid,name,args,state,received,started,runtime,worker' + '--persistent=True', + '-db=/opt/jumpserver/data/flower.db', + '--state_save_interval=600000' ] return cmd diff --git a/apps/common/sdk/gm/piico/cipher.py b/apps/common/sdk/gm/piico/cipher.py index 7f3a0b7b0..250e19d9b 100644 --- a/apps/common/sdk/gm/piico/cipher.py +++ b/apps/common/sdk/gm/piico/cipher.py @@ -33,7 +33,7 @@ class EBCCipher: def __padding(val): # padding val = bytes(val) - while len(val) % 16 != 0: + while len(val) == 0 or len(val) % 16 != 0: val += b'\0' return val diff --git a/apps/common/utils/connection.py b/apps/common/utils/connection.py index 381f29443..9d1e7fd9d 100644 --- a/apps/common/utils/connection.py +++ b/apps/common/utils/connection.py @@ -53,7 +53,9 @@ class Subscription: error(msg, item) logger.error('Subscribe handler handle msg error: {}'.format(e)) except Exception as e: - logger.error('Consume msg error: {}'.format(e)) + # 正常的 websocket 断开时, redis 会断开连接,避免日志太多 + # logger.error('Consume msg error: {}'.format(e)) + pass try: complete() diff --git a/apps/common/utils/crypto.py b/apps/common/utils/crypto.py index da921b947..4ae6814c5 100644 --- a/apps/common/utils/crypto.py +++ b/apps/common/utils/crypto.py @@ -82,7 +82,8 @@ class PiicoSM4EcbCrypto(BaseCrypto): return self.cipher.encrypt(self.to_16(data)) def _decrypt(self, data: bytes) -> bytes: - return self.cipher.decrypt(data) + bs = self.cipher.decrypt(data) + return bs.rstrip(b'\0') class AESCrypto: @@ -232,6 +233,8 @@ class Crypto: return self.cryptos[0] def encrypt(self, text): + if text is None: + return text return self.encryptor.encrypt(text) def decrypt(self, text): @@ -241,7 +244,7 @@ class Crypto: if origin_text: # 有时不同算法解密不报错,但是返回空字符串 return origin_text - except (TypeError, ValueError, UnicodeDecodeError, IndexError): + except Exception: continue diff --git a/apps/common/utils/random.py b/apps/common/utils/random.py index db3b39c05..fae1d80bd 100644 --- a/apps/common/utils/random.py +++ b/apps/common/utils/random.py @@ -35,7 +35,7 @@ def random_string(length, lower=True, upper=True, digit=True, special_char=False if special_char: spc = random.choice(string_punctuation) - i = random.choice(range(len(password))) + i = random.choice(range(1, len(password))) password[i] = spc password = ''.join(password) diff --git a/apps/jumpserver/conf.py b/apps/jumpserver/conf.py index f31f4250a..7edc28610 100644 --- a/apps/jumpserver/conf.py +++ b/apps/jumpserver/conf.py @@ -484,6 +484,9 @@ class Config(dict): 'SERVER_REPLAY_STORAGE': {}, 'SECURITY_DATA_CRYPTO_ALGO': None, 'GMSSL_ENABLED': False, + # Magnus 组件需要监听的端口范围 + 'MAGNUS_DB_PORTS_START': 30000, + 'MAGNUS_DB_PORTS_LIMIT_COUNT': 1000, # 记录清理清理 'LOGIN_LOG_KEEP_DAYS': 200, diff --git a/apps/jumpserver/settings/auth.py b/apps/jumpserver/settings/auth.py index 462aefc57..2cf47e9fe 100644 --- a/apps/jumpserver/settings/auth.py +++ b/apps/jumpserver/settings/auth.py @@ -161,6 +161,8 @@ AUTH_OAUTH2_CLIENT_SECRET = CONFIG.AUTH_OAUTH2_CLIENT_SECRET AUTH_OAUTH2_CLIENT_ID = CONFIG.AUTH_OAUTH2_CLIENT_ID AUTH_OAUTH2_SCOPE = CONFIG.AUTH_OAUTH2_SCOPE AUTH_OAUTH2_USER_ATTR_MAP = CONFIG.AUTH_OAUTH2_USER_ATTR_MAP +AUTH_OAUTH2_LOGOUT_COMPLETELY = CONFIG.AUTH_OAUTH2_LOGOUT_COMPLETELY +AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT = CONFIG.AUTH_OAUTH2_PROVIDER_END_SESSION_ENDPOINT AUTH_OAUTH2_AUTH_LOGIN_CALLBACK_URL_NAME = 'authentication:oauth2:login-callback' AUTH_OAUTH2_AUTHENTICATION_REDIRECT_URI = '/' AUTH_OAUTH2_AUTHENTICATION_FAILURE_REDIRECT_URI = '/' diff --git a/apps/jumpserver/settings/custom.py b/apps/jumpserver/settings/custom.py index 1fdb530cb..caa712ab3 100644 --- a/apps/jumpserver/settings/custom.py +++ b/apps/jumpserver/settings/custom.py @@ -177,3 +177,7 @@ HELP_SUPPORT_URL = CONFIG.HELP_SUPPORT_URL SESSION_RSA_PRIVATE_KEY_NAME = 'jms_private_key' SESSION_RSA_PUBLIC_KEY_NAME = 'jms_public_key' + +# Magnus DB Port +MAGNUS_DB_PORTS_START = CONFIG.MAGNUS_DB_PORTS_START +MAGNUS_DB_PORTS_LIMIT_COUNT = CONFIG.MAGNUS_DB_PORTS_LIMIT_COUNT diff --git a/apps/jumpserver/settings/libs.py b/apps/jumpserver/settings/libs.py index b3cc0dfde..10f69cffd 100644 --- a/apps/jumpserver/settings/libs.py +++ b/apps/jumpserver/settings/libs.py @@ -9,7 +9,6 @@ from .base import ( ) from ..const import CONFIG, PROJECT_DIR - REST_FRAMEWORK = { # Use Django's standard `django.contrib.auth` permissions, # or allow read-only access for unauthenticated users. @@ -64,7 +63,6 @@ SWAGGER_SETTINGS = { 'DEFAULT_INFO': 'jumpserver.views.swagger.api_info', } - # Captcha settings, more see https://django-simple-captcha.readthedocs.io/en/latest/advanced.html CAPTCHA_IMAGE_SIZE = (180, 38) CAPTCHA_FOREGROUND_COLOR = '#001100' @@ -81,7 +79,6 @@ BOOTSTRAP3 = { 'required_css_class': 'required', } - # Django channels support websocket if not REDIS_USE_SSL: redis_ssl = None @@ -101,14 +98,13 @@ CHANNEL_LAYERS = { 'address': (CONFIG.REDIS_HOST, CONFIG.REDIS_PORT), 'db': CONFIG.REDIS_DB_WS, 'password': CONFIG.REDIS_PASSWORD or None, - 'ssl': redis_ssl + 'ssl': redis_ssl }], }, }, } ASGI_APPLICATION = 'jumpserver.routing.application' - # Dump all celery log to here CELERY_LOG_DIR = os.path.join(PROJECT_DIR, 'data', 'celery') @@ -131,6 +127,7 @@ CELERY_TASK_EAGER_PROPAGATES = True CELERY_WORKER_REDIRECT_STDOUTS = True CELERY_WORKER_REDIRECT_STDOUTS_LEVEL = "INFO" CELERY_TASK_SOFT_TIME_LIMIT = 3600 +CELERY_WORKER_CANCEL_LONG_RUNNING_TASKS_ON_CONNECTION_LOSS = True if REDIS_USE_SSL: CELERY_BROKER_USE_SSL = CELERY_REDIS_BACKEND_USE_SSL = { @@ -148,3 +145,7 @@ REDIS_PORT = CONFIG.REDIS_PORT REDIS_PASSWORD = CONFIG.REDIS_PASSWORD DJANGO_REDIS_SCAN_ITERSIZE = 1000 + +# GM DEVICE +PIICO_DEVICE_ENABLE = CONFIG.PIICO_DEVICE_ENABLE +PIICO_DRIVER_PATH = CONFIG.PIICO_DRIVER_PATH diff --git a/apps/locale/ja/LC_MESSAGES/django.mo b/apps/locale/ja/LC_MESSAGES/django.mo index e43defdca..2be8bf9ac 100644 --- a/apps/locale/ja/LC_MESSAGES/django.mo +++ b/apps/locale/ja/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4d5dcc300fa64f04b513b670af0d9717fcdb108b54ddf264971b355cc72178de -size 132193 +oid sha256:529cf82721ab8594a7ee8e4e1bff1f80fb702d3bfcbb5fb6e7bfb8b897d4920b +size 132560 diff --git a/apps/locale/ja/LC_MESSAGES/django.po b/apps/locale/ja/LC_MESSAGES/django.po index ea517add0..9a11dd47b 100644 --- a/apps/locale/ja/LC_MESSAGES/django.po +++ b/apps/locale/ja/LC_MESSAGES/django.po @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-09-15 15:52+0800\n" +"POT-Creation-Date: 2022-09-22 19:01+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -23,13 +23,13 @@ msgid "Acls" msgstr "Acls" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 -#: applications/models/application.py:220 assets/models/asset.py:138 +#: applications/models/application.py:219 assets/models/asset.py:138 #: assets/models/base.py:175 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:27 assets/models/domain.py:23 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 #: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29 #: settings/models.py:33 settings/serializers/sms.py:6 -#: terminal/models/endpoint.py:10 terminal/models/endpoint.py:86 +#: terminal/models/endpoint.py:14 terminal/models/endpoint.py:87 #: terminal/models/storage.py:26 terminal/models/task.py:16 #: terminal/models/terminal.py:100 users/forms/profile.py:33 #: users/models/group.py:15 users/models/user.py:669 @@ -38,12 +38,12 @@ msgid "Name" msgstr "名前" #: acls/models/base.py:27 assets/models/cmd_filter.py:84 -#: assets/models/user.py:251 terminal/models/endpoint.py:89 +#: assets/models/user.py:251 terminal/models/endpoint.py:90 msgid "Priority" msgstr "優先順位" #: acls/models/base.py:28 assets/models/cmd_filter.py:84 -#: assets/models/user.py:251 terminal/models/endpoint.py:90 +#: assets/models/user.py:251 terminal/models/endpoint.py:91 msgid "1-100, the lower the value will be match first" msgstr "1-100、低い値は最初に一致します" @@ -53,7 +53,7 @@ msgstr "1-100、低い値は最初に一致します" msgid "Active" msgstr "アクティブ" -#: acls/models/base.py:32 applications/models/application.py:233 +#: acls/models/base.py:32 applications/models/application.py:232 #: assets/models/asset.py:143 assets/models/asset.py:231 #: assets/models/backup.py:54 assets/models/base.py:180 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:48 @@ -61,7 +61,7 @@ msgstr "アクティブ" #: assets/models/domain.py:65 assets/models/group.py:23 #: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73 #: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/endpoint.py:23 terminal/models/endpoint.py:96 +#: terminal/models/endpoint.py:22 terminal/models/endpoint.py:97 #: terminal/models/storage.py:29 terminal/models/terminal.py:114 #: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 #: users/models/group.py:16 users/models/user.py:706 @@ -241,7 +241,7 @@ msgstr "" msgid "Time Period" msgstr "期間" -#: applications/apps.py:9 applications/models/application.py:64 +#: applications/apps.py:9 applications/models/application.py:62 msgid "Applications" msgstr "アプリケーション" @@ -260,11 +260,7 @@ msgstr "リモートアプリ" msgid "Custom" msgstr "カスタム" -#: applications/const.py:91 rbac/tree.py:29 -msgid "Other" -msgstr "その他" - -#: applications/models/account.py:12 applications/models/application.py:237 +#: applications/models/account.py:12 applications/models/application.py:236 #: assets/models/backup.py:32 assets/models/cmd_filter.py:45 #: authentication/models.py:67 authentication/models.py:95 #: perms/models/application_permission.py:28 @@ -282,9 +278,8 @@ msgstr "アプリケーション" msgid "System user" msgstr "システムユーザー" -#: applications/models/account.py:17 -#: applications/serializers/attrs/application_type/oracle.py:13 -#: assets/models/authbook.py:21 settings/serializers/auth/cas.py:18 +#: applications/models/account.py:17 assets/models/authbook.py:21 +#: settings/serializers/auth/cas.py:18 msgid "Version" msgstr "バージョン" @@ -300,7 +295,7 @@ msgstr "アプリケーションアカウントの秘密を表示できます" msgid "Can change application account secret" msgstr "アプリケーションアカウントの秘密を変更できます" -#: applications/models/application.py:222 +#: applications/models/application.py:221 #: applications/serializers/application.py:101 assets/models/label.py:21 #: perms/models/application_permission.py:21 #: perms/serializers/application/user_permission.py:33 @@ -309,7 +304,7 @@ msgstr "アプリケーションアカウントの秘密を変更できます" msgid "Category" msgstr "カテゴリ" -#: applications/models/application.py:225 +#: applications/models/application.py:224 #: applications/serializers/application.py:103 assets/models/backup.py:49 #: assets/models/cmd_filter.py:82 assets/models/user.py:250 #: authentication/models.py:70 perms/models/application_permission.py:24 @@ -323,21 +318,21 @@ msgstr "カテゴリ" msgid "Type" msgstr "タイプ" -#: applications/models/application.py:229 assets/models/asset.py:217 +#: applications/models/application.py:228 assets/models/asset.py:217 #: assets/models/domain.py:29 assets/models/domain.py:64 msgid "Domain" msgstr "ドメイン" -#: applications/models/application.py:231 xpack/plugins/cloud/models.py:33 +#: applications/models/application.py:230 xpack/plugins/cloud/models.py:33 #: xpack/plugins/cloud/serializers/account.py:63 msgid "Attrs" msgstr "ツールバーの" -#: applications/models/application.py:241 +#: applications/models/application.py:240 msgid "Can match application" msgstr "アプリケーションを一致させることができます" -#: applications/models/application.py:320 +#: applications/models/application.py:310 msgid "Application user" msgstr "アプリケーションユーザー" @@ -393,7 +388,7 @@ msgstr "クラスター" #: applications/serializers/attrs/application_category/db.py:11 #: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14 -#: settings/serializers/auth/sms.py:65 terminal/models/endpoint.py:11 +#: settings/serializers/auth/sms.py:65 terminal/models/endpoint.py:15 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "ホスト" @@ -402,7 +397,7 @@ msgstr "ホスト" #: applications/serializers/attrs/application_type/mongodb.py:10 #: applications/serializers/attrs/application_type/mysql.py:10 #: applications/serializers/attrs/application_type/mysql_workbench.py:22 -#: applications/serializers/attrs/application_type/oracle.py:16 +#: applications/serializers/attrs/application_type/oracle.py:10 #: applications/serializers/attrs/application_type/pgsql.py:10 #: applications/serializers/attrs/application_type/redis.py:10 #: applications/serializers/attrs/application_type/sqlserver.py:10 @@ -497,10 +492,6 @@ msgstr "Mysql workbench のユーザー名" msgid "Mysql workbench password" msgstr "Mysql workbench パスワード" -#: applications/serializers/attrs/application_type/oracle.py:14 -msgid "Magnus currently supports only 11g and 12c connections" -msgstr "現在、Magnusは11gおよび12cバージョンへの接続のみをサポートしています" - #: applications/serializers/attrs/application_type/vmware_client.py:26 msgid "Vmware username" msgstr "Vmware ユーザー名" @@ -3457,6 +3448,10 @@ msgstr "監査ビュー" msgid "System setting" msgstr "システム設定" +#: rbac/tree.py:29 +msgid "Other" +msgstr "その他" + #: rbac/tree.py:37 msgid "Accounts" msgstr "アカウント" @@ -4956,58 +4951,34 @@ msgstr "ストレージが無効です" msgid "Command record" msgstr "コマンドレコード" -#: terminal/models/endpoint.py:13 +#: terminal/models/endpoint.py:17 msgid "HTTPS Port" msgstr "HTTPS ポート" -#: terminal/models/endpoint.py:14 terminal/models/terminal.py:107 +#: terminal/models/endpoint.py:18 terminal/models/terminal.py:107 msgid "HTTP Port" msgstr "HTTP ポート" -#: terminal/models/endpoint.py:15 terminal/models/terminal.py:106 +#: terminal/models/endpoint.py:19 terminal/models/terminal.py:106 msgid "SSH Port" msgstr "SSH ポート" -#: terminal/models/endpoint.py:16 +#: terminal/models/endpoint.py:20 msgid "RDP Port" msgstr "RDP ポート" -#: terminal/models/endpoint.py:17 -msgid "MySQL Port" -msgstr "MySQL ポート" - -#: terminal/models/endpoint.py:18 -msgid "MariaDB Port" -msgstr "MariaDB ポート" - -#: terminal/models/endpoint.py:19 -msgid "PostgreSQL Port" -msgstr "PostgreSQL ポート" - -#: terminal/models/endpoint.py:20 -msgid "Redis Port" -msgstr "Redis ポート" - -#: terminal/models/endpoint.py:21 -msgid "Oracle 11g Port" -msgstr "Oracle 11g ポート" - -#: terminal/models/endpoint.py:22 -msgid "Oracle 12c Port" -msgstr "Oracle 12c ポート" - -#: terminal/models/endpoint.py:28 terminal/models/endpoint.py:94 +#: terminal/models/endpoint.py:27 terminal/models/endpoint.py:95 #: terminal/serializers/endpoint.py:57 terminal/serializers/storage.py:38 #: terminal/serializers/storage.py:50 terminal/serializers/storage.py:80 #: terminal/serializers/storage.py:90 terminal/serializers/storage.py:98 msgid "Endpoint" msgstr "エンドポイント" -#: terminal/models/endpoint.py:87 +#: terminal/models/endpoint.py:88 msgid "IP group" msgstr "IP グループ" -#: terminal/models/endpoint.py:99 +#: terminal/models/endpoint.py:100 msgid "Endpoint rule" msgstr "エンドポイントルール" @@ -5188,9 +5159,19 @@ msgstr "レベル" msgid "Batch danger command alert" msgstr "一括危険コマンド警告" -#: terminal/serializers/endpoint.py:12 -msgid "Oracle port" -msgstr "" +#: terminal/serializers/endpoint.py:14 +msgid "Magnus listen db port" +msgstr "Magnus がリッスンするデータベース ポート" + +#: terminal/serializers/endpoint.py:17 +msgid "Magnus Listen port range" +msgstr "Magnus がリッスンするポート範囲" + +#: terminal/serializers/endpoint.py:19 +msgid "" +"The range of ports that Magnus listens on is modified in the configuration " +"file" +msgstr "Magnus がリッスンするポート範囲を構成ファイルで変更してください" #: terminal/serializers/endpoint.py:51 msgid "" @@ -5321,6 +5302,16 @@ msgstr "見つかりません" msgid "view" msgstr "表示" +#: terminal/utils/db_port_mapper.py:77 +msgid "" +"No ports can be used, check and modify the limit on the number of ports that " +"Magnus listens on in the configuration file." +msgstr "使用できるポートがありません。設定ファイルで Magnus がリッスンするポート数の制限を確認して変更してください. " + +#: terminal/utils/db_port_mapper.py:79 +msgid "All available port count: {}, Already use port count: {}" +msgstr "使用可能なすべてのポート数: {}、すでに使用しているポート数: {}" + #: tickets/apps.py:7 msgid "Tickets" msgstr "チケット" diff --git a/apps/locale/zh/LC_MESSAGES/django.mo b/apps/locale/zh/LC_MESSAGES/django.mo index dd47ce08a..a804b70c8 100644 --- a/apps/locale/zh/LC_MESSAGES/django.mo +++ b/apps/locale/zh/LC_MESSAGES/django.mo @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:618dce0f2521591410fb16b3d0afa536333d2e4f317d75d1955fe413dc0e64cb -size 108949 +oid sha256:a346a8166af782cbc41eac33475b4cfac2e3713b26f84ddf6fa532742133b89d +size 109216 diff --git a/apps/locale/zh/LC_MESSAGES/django.po b/apps/locale/zh/LC_MESSAGES/django.po index 946eaa1f0..f307cbb71 100644 --- a/apps/locale/zh/LC_MESSAGES/django.po +++ b/apps/locale/zh/LC_MESSAGES/django.po @@ -7,7 +7,7 @@ msgid "" msgstr "" "Project-Id-Version: JumpServer 0.3.3\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-09-15 15:52+0800\n" +"POT-Creation-Date: 2022-09-22 19:01+0800\n" "PO-Revision-Date: 2021-05-20 10:54+0800\n" "Last-Translator: ibuler \n" "Language-Team: JumpServer team\n" @@ -22,13 +22,13 @@ msgid "Acls" msgstr "访问控制" #: acls/models/base.py:25 acls/serializers/login_asset_acl.py:47 -#: applications/models/application.py:220 assets/models/asset.py:138 +#: applications/models/application.py:219 assets/models/asset.py:138 #: assets/models/base.py:175 assets/models/cluster.py:18 #: assets/models/cmd_filter.py:27 assets/models/domain.py:23 #: assets/models/group.py:20 assets/models/label.py:18 ops/mixin.py:24 #: orgs/models.py:70 perms/models/base.py:83 rbac/models/role.py:29 #: settings/models.py:33 settings/serializers/sms.py:6 -#: terminal/models/endpoint.py:10 terminal/models/endpoint.py:86 +#: terminal/models/endpoint.py:14 terminal/models/endpoint.py:87 #: terminal/models/storage.py:26 terminal/models/task.py:16 #: terminal/models/terminal.py:100 users/forms/profile.py:33 #: users/models/group.py:15 users/models/user.py:669 @@ -37,12 +37,12 @@ msgid "Name" msgstr "名称" #: acls/models/base.py:27 assets/models/cmd_filter.py:84 -#: assets/models/user.py:251 terminal/models/endpoint.py:89 +#: assets/models/user.py:251 terminal/models/endpoint.py:90 msgid "Priority" msgstr "优先级" #: acls/models/base.py:28 assets/models/cmd_filter.py:84 -#: assets/models/user.py:251 terminal/models/endpoint.py:90 +#: assets/models/user.py:251 terminal/models/endpoint.py:91 msgid "1-100, the lower the value will be match first" msgstr "优先级可选范围为 1-100 (数值越小越优先)" @@ -52,7 +52,7 @@ msgstr "优先级可选范围为 1-100 (数值越小越优先)" msgid "Active" msgstr "激活中" -#: acls/models/base.py:32 applications/models/application.py:233 +#: acls/models/base.py:32 applications/models/application.py:232 #: assets/models/asset.py:143 assets/models/asset.py:231 #: assets/models/backup.py:54 assets/models/base.py:180 #: assets/models/cluster.py:29 assets/models/cmd_filter.py:48 @@ -60,7 +60,7 @@ msgstr "激活中" #: assets/models/domain.py:65 assets/models/group.py:23 #: assets/models/label.py:23 ops/models/adhoc.py:38 orgs/models.py:73 #: perms/models/base.py:93 rbac/models/role.py:37 settings/models.py:38 -#: terminal/models/endpoint.py:23 terminal/models/endpoint.py:96 +#: terminal/models/endpoint.py:22 terminal/models/endpoint.py:97 #: terminal/models/storage.py:29 terminal/models/terminal.py:114 #: tickets/models/comment.py:32 tickets/models/ticket/general.py:288 #: users/models/group.py:16 users/models/user.py:706 @@ -236,7 +236,7 @@ msgstr "" msgid "Time Period" msgstr "时段" -#: applications/apps.py:9 applications/models/application.py:64 +#: applications/apps.py:9 applications/models/application.py:62 msgid "Applications" msgstr "应用管理" @@ -255,11 +255,7 @@ msgstr "远程应用" msgid "Custom" msgstr "自定义" -#: applications/const.py:91 rbac/tree.py:29 -msgid "Other" -msgstr "其它" - -#: applications/models/account.py:12 applications/models/application.py:237 +#: applications/models/account.py:12 applications/models/application.py:236 #: assets/models/backup.py:32 assets/models/cmd_filter.py:45 #: authentication/models.py:67 authentication/models.py:95 #: perms/models/application_permission.py:28 @@ -277,9 +273,8 @@ msgstr "应用程序" msgid "System user" msgstr "系统用户" -#: applications/models/account.py:17 -#: applications/serializers/attrs/application_type/oracle.py:13 -#: assets/models/authbook.py:21 settings/serializers/auth/cas.py:18 +#: applications/models/account.py:17 assets/models/authbook.py:21 +#: settings/serializers/auth/cas.py:18 msgid "Version" msgstr "版本" @@ -295,7 +290,7 @@ msgstr "可以查看应用账号密码" msgid "Can change application account secret" msgstr "可以查看应用账号密码" -#: applications/models/application.py:222 +#: applications/models/application.py:221 #: applications/serializers/application.py:101 assets/models/label.py:21 #: perms/models/application_permission.py:21 #: perms/serializers/application/user_permission.py:33 @@ -304,7 +299,7 @@ msgstr "可以查看应用账号密码" msgid "Category" msgstr "类别" -#: applications/models/application.py:225 +#: applications/models/application.py:224 #: applications/serializers/application.py:103 assets/models/backup.py:49 #: assets/models/cmd_filter.py:82 assets/models/user.py:250 #: authentication/models.py:70 perms/models/application_permission.py:24 @@ -318,21 +313,21 @@ msgstr "类别" msgid "Type" msgstr "类型" -#: applications/models/application.py:229 assets/models/asset.py:217 +#: applications/models/application.py:228 assets/models/asset.py:217 #: assets/models/domain.py:29 assets/models/domain.py:64 msgid "Domain" msgstr "网域" -#: applications/models/application.py:231 xpack/plugins/cloud/models.py:33 +#: applications/models/application.py:230 xpack/plugins/cloud/models.py:33 #: xpack/plugins/cloud/serializers/account.py:63 msgid "Attrs" msgstr "属性" -#: applications/models/application.py:241 +#: applications/models/application.py:240 msgid "Can match application" msgstr "匹配应用" -#: applications/models/application.py:320 +#: applications/models/application.py:310 msgid "Application user" msgstr "应用用户" @@ -388,7 +383,7 @@ msgstr "集群" #: applications/serializers/attrs/application_category/db.py:11 #: ops/models/adhoc.py:157 settings/serializers/auth/radius.py:14 -#: settings/serializers/auth/sms.py:65 terminal/models/endpoint.py:11 +#: settings/serializers/auth/sms.py:65 terminal/models/endpoint.py:15 #: xpack/plugins/cloud/serializers/account_attrs.py:72 msgid "Host" msgstr "主机" @@ -397,7 +392,7 @@ msgstr "主机" #: applications/serializers/attrs/application_type/mongodb.py:10 #: applications/serializers/attrs/application_type/mysql.py:10 #: applications/serializers/attrs/application_type/mysql_workbench.py:22 -#: applications/serializers/attrs/application_type/oracle.py:16 +#: applications/serializers/attrs/application_type/oracle.py:10 #: applications/serializers/attrs/application_type/pgsql.py:10 #: applications/serializers/attrs/application_type/redis.py:10 #: applications/serializers/attrs/application_type/sqlserver.py:10 @@ -492,10 +487,6 @@ msgstr "Mysql 工作台 用户名" msgid "Mysql workbench password" msgstr "Mysql 工作台 密码" -#: applications/serializers/attrs/application_type/oracle.py:14 -msgid "Magnus currently supports only 11g and 12c connections" -msgstr "目前 Magnus 只支持连接 11g、12c 版本" - #: applications/serializers/attrs/application_type/vmware_client.py:26 msgid "Vmware username" msgstr "Vmware 用户名" @@ -3414,6 +3405,10 @@ msgstr "审计台" msgid "System setting" msgstr "系统设置" +#: rbac/tree.py:29 +msgid "Other" +msgstr "其它" + #: rbac/tree.py:37 msgid "Accounts" msgstr "账号管理" @@ -4879,58 +4874,34 @@ msgstr "存储无效" msgid "Command record" msgstr "命令记录" -#: terminal/models/endpoint.py:13 +#: terminal/models/endpoint.py:17 msgid "HTTPS Port" msgstr "HTTPS 端口" -#: terminal/models/endpoint.py:14 terminal/models/terminal.py:107 +#: terminal/models/endpoint.py:18 terminal/models/terminal.py:107 msgid "HTTP Port" msgstr "HTTP 端口" -#: terminal/models/endpoint.py:15 terminal/models/terminal.py:106 +#: terminal/models/endpoint.py:19 terminal/models/terminal.py:106 msgid "SSH Port" msgstr "SSH 端口" -#: terminal/models/endpoint.py:16 +#: terminal/models/endpoint.py:20 msgid "RDP Port" msgstr "RDP 端口" -#: terminal/models/endpoint.py:17 -msgid "MySQL Port" -msgstr "MySQL 端口" - -#: terminal/models/endpoint.py:18 -msgid "MariaDB Port" -msgstr "MariaDB 端口" - -#: terminal/models/endpoint.py:19 -msgid "PostgreSQL Port" -msgstr "PostgreSQL 端口" - -#: terminal/models/endpoint.py:20 -msgid "Redis Port" -msgstr "Redis 端口" - -#: terminal/models/endpoint.py:21 -msgid "Oracle 11g Port" -msgstr "Oracle 11g 端口" - -#: terminal/models/endpoint.py:22 -msgid "Oracle 12c Port" -msgstr "Oracle 12c 端口" - -#: terminal/models/endpoint.py:28 terminal/models/endpoint.py:94 +#: terminal/models/endpoint.py:27 terminal/models/endpoint.py:95 #: terminal/serializers/endpoint.py:57 terminal/serializers/storage.py:38 #: terminal/serializers/storage.py:50 terminal/serializers/storage.py:80 #: terminal/serializers/storage.py:90 terminal/serializers/storage.py:98 msgid "Endpoint" msgstr "端点" -#: terminal/models/endpoint.py:87 +#: terminal/models/endpoint.py:88 msgid "IP group" msgstr "IP 组" -#: terminal/models/endpoint.py:99 +#: terminal/models/endpoint.py:100 msgid "Endpoint rule" msgstr "端点规则" @@ -5111,9 +5082,19 @@ msgstr "级别" msgid "Batch danger command alert" msgstr "批量危险命令告警" -#: terminal/serializers/endpoint.py:12 -msgid "Oracle port" -msgstr "" +#: terminal/serializers/endpoint.py:14 +msgid "Magnus listen db port" +msgstr "Magnus 监听的数据库端口" + +#: terminal/serializers/endpoint.py:17 +msgid "Magnus Listen port range" +msgstr "Magnus 监听的端口范围" + +#: terminal/serializers/endpoint.py:19 +msgid "" +"The range of ports that Magnus listens on is modified in the configuration " +"file" +msgstr "请在配置文件中修改 Magnus 监听的端口范围" #: terminal/serializers/endpoint.py:51 msgid "" @@ -5242,6 +5223,16 @@ msgstr "没有发现" msgid "view" msgstr "查看" +#: terminal/utils/db_port_mapper.py:77 +msgid "" +"No ports can be used, check and modify the limit on the number of ports that " +"Magnus listens on in the configuration file." +msgstr "没有端口可以使用,检查并修改配置文件中 Magnus 监听的端口数量限制。" + +#: terminal/utils/db_port_mapper.py:79 +msgid "All available port count: {}, Already use port count: {}" +msgstr "所有可用端口数量:{},已使用端口数量:{}" + #: tickets/apps.py:7 msgid "Tickets" msgstr "工单管理" diff --git a/apps/ops/templates/ops/celery_task_log.html b/apps/ops/templates/ops/celery_task_log.html index 599893db8..2305218a9 100644 --- a/apps/ops/templates/ops/celery_task_log.html +++ b/apps/ops/templates/ops/celery_task_log.html @@ -2,7 +2,7 @@ {% load i18n %} {% trans 'Task log' %} - + diff --git a/apps/static/js/jquery-3.1.1.min.js b/apps/static/js/jquery-3.1.1.min.js deleted file mode 100755 index 4c5be4c0f..000000000 --- a/apps/static/js/jquery-3.1.1.min.js +++ /dev/null @@ -1,4 +0,0 @@ -/*! jQuery v3.1.1 | (c) jQuery Foundation | jquery.org/license */ -!function(a,b){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=a.document?b(a,!0):function(a){if(!a.document)throw new Error("jQuery requires a window with a document");return b(a)}:b(a)}("undefined"!=typeof window?window:this,function(a,b){"use strict";var c=[],d=a.document,e=Object.getPrototypeOf,f=c.slice,g=c.concat,h=c.push,i=c.indexOf,j={},k=j.toString,l=j.hasOwnProperty,m=l.toString,n=m.call(Object),o={};function p(a,b){b=b||d;var c=b.createElement("script");c.text=a,b.head.appendChild(c).parentNode.removeChild(c)}var q="3.1.1",r=function(a,b){return new r.fn.init(a,b)},s=/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,t=/^-ms-/,u=/-([a-z])/g,v=function(a,b){return b.toUpperCase()};r.fn=r.prototype={jquery:q,constructor:r,length:0,toArray:function(){return f.call(this)},get:function(a){return null==a?f.call(this):a<0?this[a+this.length]:this[a]},pushStack:function(a){var b=r.merge(this.constructor(),a);return b.prevObject=this,b},each:function(a){return r.each(this,a)},map:function(a){return this.pushStack(r.map(this,function(b,c){return a.call(b,c,b)}))},slice:function(){return this.pushStack(f.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},eq:function(a){var b=this.length,c=+a+(a<0?b:0);return this.pushStack(c>=0&&c0&&b-1 in a)}var x=function(a){var b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u="sizzle"+1*new Date,v=a.document,w=0,x=0,y=ha(),z=ha(),A=ha(),B=function(a,b){return a===b&&(l=!0),0},C={}.hasOwnProperty,D=[],E=D.pop,F=D.push,G=D.push,H=D.slice,I=function(a,b){for(var c=0,d=a.length;c+~]|"+K+")"+K+"*"),S=new RegExp("="+K+"*([^\\]'\"]*?)"+K+"*\\]","g"),T=new RegExp(N),U=new RegExp("^"+L+"$"),V={ID:new RegExp("^#("+L+")"),CLASS:new RegExp("^\\.("+L+")"),TAG:new RegExp("^("+L+"|[*])"),ATTR:new RegExp("^"+M),PSEUDO:new RegExp("^"+N),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+K+"*(even|odd|(([+-]|)(\\d*)n|)"+K+"*(?:([+-]|)"+K+"*(\\d+)|))"+K+"*\\)|)","i"),bool:new RegExp("^(?:"+J+")$","i"),needsContext:new RegExp("^"+K+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+K+"*((?:-\\d)?\\d*)"+K+"*\\)|)(?=[^-]|$)","i")},W=/^(?:input|select|textarea|button)$/i,X=/^h\d$/i,Y=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,_=new RegExp("\\\\([\\da-f]{1,6}"+K+"?|("+K+")|.)","ig"),aa=function(a,b,c){var d="0x"+b-65536;return d!==d||c?b:d<0?String.fromCharCode(d+65536):String.fromCharCode(d>>10|55296,1023&d|56320)},ba=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ca=function(a,b){return b?"\0"===a?"\ufffd":a.slice(0,-1)+"\\"+a.charCodeAt(a.length-1).toString(16)+" ":"\\"+a},da=function(){m()},ea=ta(function(a){return a.disabled===!0&&("form"in a||"label"in a)},{dir:"parentNode",next:"legend"});try{G.apply(D=H.call(v.childNodes),v.childNodes),D[v.childNodes.length].nodeType}catch(fa){G={apply:D.length?function(a,b){F.apply(a,H.call(b))}:function(a,b){var c=a.length,d=0;while(a[c++]=b[d++]);a.length=c-1}}}function ga(a,b,d,e){var f,h,j,k,l,o,r,s=b&&b.ownerDocument,w=b?b.nodeType:9;if(d=d||[],"string"!=typeof a||!a||1!==w&&9!==w&&11!==w)return d;if(!e&&((b?b.ownerDocument||b:v)!==n&&m(b),b=b||n,p)){if(11!==w&&(l=Z.exec(a)))if(f=l[1]){if(9===w){if(!(j=b.getElementById(f)))return d;if(j.id===f)return d.push(j),d}else if(s&&(j=s.getElementById(f))&&t(b,j)&&j.id===f)return d.push(j),d}else{if(l[2])return G.apply(d,b.getElementsByTagName(a)),d;if((f=l[3])&&c.getElementsByClassName&&b.getElementsByClassName)return G.apply(d,b.getElementsByClassName(f)),d}if(c.qsa&&!A[a+" "]&&(!q||!q.test(a))){if(1!==w)s=b,r=a;else if("object"!==b.nodeName.toLowerCase()){(k=b.getAttribute("id"))?k=k.replace(ba,ca):b.setAttribute("id",k=u),o=g(a),h=o.length;while(h--)o[h]="#"+k+" "+sa(o[h]);r=o.join(","),s=$.test(a)&&qa(b.parentNode)||b}if(r)try{return G.apply(d,s.querySelectorAll(r)),d}catch(x){}finally{k===u&&b.removeAttribute("id")}}}return i(a.replace(P,"$1"),b,d,e)}function ha(){var a=[];function b(c,e){return a.push(c+" ")>d.cacheLength&&delete b[a.shift()],b[c+" "]=e}return b}function ia(a){return a[u]=!0,a}function ja(a){var b=n.createElement("fieldset");try{return!!a(b)}catch(c){return!1}finally{b.parentNode&&b.parentNode.removeChild(b),b=null}}function ka(a,b){var c=a.split("|"),e=c.length;while(e--)d.attrHandle[c[e]]=b}function la(a,b){var c=b&&a,d=c&&1===a.nodeType&&1===b.nodeType&&a.sourceIndex-b.sourceIndex;if(d)return d;if(c)while(c=c.nextSibling)if(c===b)return-1;return a?1:-1}function ma(a){return function(b){var c=b.nodeName.toLowerCase();return"input"===c&&b.type===a}}function na(a){return function(b){var c=b.nodeName.toLowerCase();return("input"===c||"button"===c)&&b.type===a}}function oa(a){return function(b){return"form"in b?b.parentNode&&b.disabled===!1?"label"in b?"label"in b.parentNode?b.parentNode.disabled===a:b.disabled===a:b.isDisabled===a||b.isDisabled!==!a&&ea(b)===a:b.disabled===a:"label"in b&&b.disabled===a}}function pa(a){return ia(function(b){return b=+b,ia(function(c,d){var e,f=a([],c.length,b),g=f.length;while(g--)c[e=f[g]]&&(c[e]=!(d[e]=c[e]))})})}function qa(a){return a&&"undefined"!=typeof a.getElementsByTagName&&a}c=ga.support={},f=ga.isXML=function(a){var b=a&&(a.ownerDocument||a).documentElement;return!!b&&"HTML"!==b.nodeName},m=ga.setDocument=function(a){var b,e,g=a?a.ownerDocument||a:v;return g!==n&&9===g.nodeType&&g.documentElement?(n=g,o=n.documentElement,p=!f(n),v!==n&&(e=n.defaultView)&&e.top!==e&&(e.addEventListener?e.addEventListener("unload",da,!1):e.attachEvent&&e.attachEvent("onunload",da)),c.attributes=ja(function(a){return a.className="i",!a.getAttribute("className")}),c.getElementsByTagName=ja(function(a){return a.appendChild(n.createComment("")),!a.getElementsByTagName("*").length}),c.getElementsByClassName=Y.test(n.getElementsByClassName),c.getById=ja(function(a){return o.appendChild(a).id=u,!n.getElementsByName||!n.getElementsByName(u).length}),c.getById?(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){return a.getAttribute("id")===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c=b.getElementById(a);return c?[c]:[]}}):(d.filter.ID=function(a){var b=a.replace(_,aa);return function(a){var c="undefined"!=typeof a.getAttributeNode&&a.getAttributeNode("id");return c&&c.value===b}},d.find.ID=function(a,b){if("undefined"!=typeof b.getElementById&&p){var c,d,e,f=b.getElementById(a);if(f){if(c=f.getAttributeNode("id"),c&&c.value===a)return[f];e=b.getElementsByName(a),d=0;while(f=e[d++])if(c=f.getAttributeNode("id"),c&&c.value===a)return[f]}return[]}}),d.find.TAG=c.getElementsByTagName?function(a,b){return"undefined"!=typeof b.getElementsByTagName?b.getElementsByTagName(a):c.qsa?b.querySelectorAll(a):void 0}:function(a,b){var c,d=[],e=0,f=b.getElementsByTagName(a);if("*"===a){while(c=f[e++])1===c.nodeType&&d.push(c);return d}return f},d.find.CLASS=c.getElementsByClassName&&function(a,b){if("undefined"!=typeof b.getElementsByClassName&&p)return b.getElementsByClassName(a)},r=[],q=[],(c.qsa=Y.test(n.querySelectorAll))&&(ja(function(a){o.appendChild(a).innerHTML="",a.querySelectorAll("[msallowcapture^='']").length&&q.push("[*^$]="+K+"*(?:''|\"\")"),a.querySelectorAll("[selected]").length||q.push("\\["+K+"*(?:value|"+J+")"),a.querySelectorAll("[id~="+u+"-]").length||q.push("~="),a.querySelectorAll(":checked").length||q.push(":checked"),a.querySelectorAll("a#"+u+"+*").length||q.push(".#.+[+~]")}),ja(function(a){a.innerHTML="";var b=n.createElement("input");b.setAttribute("type","hidden"),a.appendChild(b).setAttribute("name","D"),a.querySelectorAll("[name=d]").length&&q.push("name"+K+"*[*^$|!~]?="),2!==a.querySelectorAll(":enabled").length&&q.push(":enabled",":disabled"),o.appendChild(a).disabled=!0,2!==a.querySelectorAll(":disabled").length&&q.push(":enabled",":disabled"),a.querySelectorAll("*,:x"),q.push(",.*:")})),(c.matchesSelector=Y.test(s=o.matches||o.webkitMatchesSelector||o.mozMatchesSelector||o.oMatchesSelector||o.msMatchesSelector))&&ja(function(a){c.disconnectedMatch=s.call(a,"*"),s.call(a,"[s!='']:x"),r.push("!=",N)}),q=q.length&&new RegExp(q.join("|")),r=r.length&&new RegExp(r.join("|")),b=Y.test(o.compareDocumentPosition),t=b||Y.test(o.contains)?function(a,b){var c=9===a.nodeType?a.documentElement:a,d=b&&b.parentNode;return a===d||!(!d||1!==d.nodeType||!(c.contains?c.contains(d):a.compareDocumentPosition&&16&a.compareDocumentPosition(d)))}:function(a,b){if(b)while(b=b.parentNode)if(b===a)return!0;return!1},B=b?function(a,b){if(a===b)return l=!0,0;var d=!a.compareDocumentPosition-!b.compareDocumentPosition;return d?d:(d=(a.ownerDocument||a)===(b.ownerDocument||b)?a.compareDocumentPosition(b):1,1&d||!c.sortDetached&&b.compareDocumentPosition(a)===d?a===n||a.ownerDocument===v&&t(v,a)?-1:b===n||b.ownerDocument===v&&t(v,b)?1:k?I(k,a)-I(k,b):0:4&d?-1:1)}:function(a,b){if(a===b)return l=!0,0;var c,d=0,e=a.parentNode,f=b.parentNode,g=[a],h=[b];if(!e||!f)return a===n?-1:b===n?1:e?-1:f?1:k?I(k,a)-I(k,b):0;if(e===f)return la(a,b);c=a;while(c=c.parentNode)g.unshift(c);c=b;while(c=c.parentNode)h.unshift(c);while(g[d]===h[d])d++;return d?la(g[d],h[d]):g[d]===v?-1:h[d]===v?1:0},n):n},ga.matches=function(a,b){return ga(a,null,null,b)},ga.matchesSelector=function(a,b){if((a.ownerDocument||a)!==n&&m(a),b=b.replace(S,"='$1']"),c.matchesSelector&&p&&!A[b+" "]&&(!r||!r.test(b))&&(!q||!q.test(b)))try{var d=s.call(a,b);if(d||c.disconnectedMatch||a.document&&11!==a.document.nodeType)return d}catch(e){}return ga(b,n,null,[a]).length>0},ga.contains=function(a,b){return(a.ownerDocument||a)!==n&&m(a),t(a,b)},ga.attr=function(a,b){(a.ownerDocument||a)!==n&&m(a);var e=d.attrHandle[b.toLowerCase()],f=e&&C.call(d.attrHandle,b.toLowerCase())?e(a,b,!p):void 0;return void 0!==f?f:c.attributes||!p?a.getAttribute(b):(f=a.getAttributeNode(b))&&f.specified?f.value:null},ga.escape=function(a){return(a+"").replace(ba,ca)},ga.error=function(a){throw new Error("Syntax error, unrecognized expression: "+a)},ga.uniqueSort=function(a){var b,d=[],e=0,f=0;if(l=!c.detectDuplicates,k=!c.sortStable&&a.slice(0),a.sort(B),l){while(b=a[f++])b===a[f]&&(e=d.push(f));while(e--)a.splice(d[e],1)}return k=null,a},e=ga.getText=function(a){var b,c="",d=0,f=a.nodeType;if(f){if(1===f||9===f||11===f){if("string"==typeof a.textContent)return a.textContent;for(a=a.firstChild;a;a=a.nextSibling)c+=e(a)}else if(3===f||4===f)return a.nodeValue}else while(b=a[d++])c+=e(b);return c},d=ga.selectors={cacheLength:50,createPseudo:ia,match:V,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(a){return a[1]=a[1].replace(_,aa),a[3]=(a[3]||a[4]||a[5]||"").replace(_,aa),"~="===a[2]&&(a[3]=" "+a[3]+" "),a.slice(0,4)},CHILD:function(a){return a[1]=a[1].toLowerCase(),"nth"===a[1].slice(0,3)?(a[3]||ga.error(a[0]),a[4]=+(a[4]?a[5]+(a[6]||1):2*("even"===a[3]||"odd"===a[3])),a[5]=+(a[7]+a[8]||"odd"===a[3])):a[3]&&ga.error(a[0]),a},PSEUDO:function(a){var b,c=!a[6]&&a[2];return V.CHILD.test(a[0])?null:(a[3]?a[2]=a[4]||a[5]||"":c&&T.test(c)&&(b=g(c,!0))&&(b=c.indexOf(")",c.length-b)-c.length)&&(a[0]=a[0].slice(0,b),a[2]=c.slice(0,b)),a.slice(0,3))}},filter:{TAG:function(a){var b=a.replace(_,aa).toLowerCase();return"*"===a?function(){return!0}:function(a){return a.nodeName&&a.nodeName.toLowerCase()===b}},CLASS:function(a){var b=y[a+" "];return b||(b=new RegExp("(^|"+K+")"+a+"("+K+"|$)"))&&y(a,function(a){return b.test("string"==typeof a.className&&a.className||"undefined"!=typeof a.getAttribute&&a.getAttribute("class")||"")})},ATTR:function(a,b,c){return function(d){var e=ga.attr(d,a);return null==e?"!="===b:!b||(e+="","="===b?e===c:"!="===b?e!==c:"^="===b?c&&0===e.indexOf(c):"*="===b?c&&e.indexOf(c)>-1:"$="===b?c&&e.slice(-c.length)===c:"~="===b?(" "+e.replace(O," ")+" ").indexOf(c)>-1:"|="===b&&(e===c||e.slice(0,c.length+1)===c+"-"))}},CHILD:function(a,b,c,d,e){var f="nth"!==a.slice(0,3),g="last"!==a.slice(-4),h="of-type"===b;return 1===d&&0===e?function(a){return!!a.parentNode}:function(b,c,i){var j,k,l,m,n,o,p=f!==g?"nextSibling":"previousSibling",q=b.parentNode,r=h&&b.nodeName.toLowerCase(),s=!i&&!h,t=!1;if(q){if(f){while(p){m=b;while(m=m[p])if(h?m.nodeName.toLowerCase()===r:1===m.nodeType)return!1;o=p="only"===a&&!o&&"nextSibling"}return!0}if(o=[g?q.firstChild:q.lastChild],g&&s){m=q,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n&&j[2],m=n&&q.childNodes[n];while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if(1===m.nodeType&&++t&&m===b){k[a]=[w,n,t];break}}else if(s&&(m=b,l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),j=k[a]||[],n=j[0]===w&&j[1],t=n),t===!1)while(m=++n&&m&&m[p]||(t=n=0)||o.pop())if((h?m.nodeName.toLowerCase()===r:1===m.nodeType)&&++t&&(s&&(l=m[u]||(m[u]={}),k=l[m.uniqueID]||(l[m.uniqueID]={}),k[a]=[w,t]),m===b))break;return t-=e,t===d||t%d===0&&t/d>=0}}},PSEUDO:function(a,b){var c,e=d.pseudos[a]||d.setFilters[a.toLowerCase()]||ga.error("unsupported pseudo: "+a);return e[u]?e(b):e.length>1?(c=[a,a,"",b],d.setFilters.hasOwnProperty(a.toLowerCase())?ia(function(a,c){var d,f=e(a,b),g=f.length;while(g--)d=I(a,f[g]),a[d]=!(c[d]=f[g])}):function(a){return e(a,0,c)}):e}},pseudos:{not:ia(function(a){var b=[],c=[],d=h(a.replace(P,"$1"));return d[u]?ia(function(a,b,c,e){var f,g=d(a,null,e,[]),h=a.length;while(h--)(f=g[h])&&(a[h]=!(b[h]=f))}):function(a,e,f){return b[0]=a,d(b,null,f,c),b[0]=null,!c.pop()}}),has:ia(function(a){return function(b){return ga(a,b).length>0}}),contains:ia(function(a){return a=a.replace(_,aa),function(b){return(b.textContent||b.innerText||e(b)).indexOf(a)>-1}}),lang:ia(function(a){return U.test(a||"")||ga.error("unsupported lang: "+a),a=a.replace(_,aa).toLowerCase(),function(b){var c;do if(c=p?b.lang:b.getAttribute("xml:lang")||b.getAttribute("lang"))return c=c.toLowerCase(),c===a||0===c.indexOf(a+"-");while((b=b.parentNode)&&1===b.nodeType);return!1}}),target:function(b){var c=a.location&&a.location.hash;return c&&c.slice(1)===b.id},root:function(a){return a===o},focus:function(a){return a===n.activeElement&&(!n.hasFocus||n.hasFocus())&&!!(a.type||a.href||~a.tabIndex)},enabled:oa(!1),disabled:oa(!0),checked:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&!!a.checked||"option"===b&&!!a.selected},selected:function(a){return a.parentNode&&a.parentNode.selectedIndex,a.selected===!0},empty:function(a){for(a=a.firstChild;a;a=a.nextSibling)if(a.nodeType<6)return!1;return!0},parent:function(a){return!d.pseudos.empty(a)},header:function(a){return X.test(a.nodeName)},input:function(a){return W.test(a.nodeName)},button:function(a){var b=a.nodeName.toLowerCase();return"input"===b&&"button"===a.type||"button"===b},text:function(a){var b;return"input"===a.nodeName.toLowerCase()&&"text"===a.type&&(null==(b=a.getAttribute("type"))||"text"===b.toLowerCase())},first:pa(function(){return[0]}),last:pa(function(a,b){return[b-1]}),eq:pa(function(a,b,c){return[c<0?c+b:c]}),even:pa(function(a,b){for(var c=0;c=0;)a.push(d);return a}),gt:pa(function(a,b,c){for(var d=c<0?c+b:c;++d1?function(b,c,d){var e=a.length;while(e--)if(!a[e](b,c,d))return!1;return!0}:a[0]}function va(a,b,c){for(var d=0,e=b.length;d-1&&(f[j]=!(g[j]=l))}}else r=wa(r===g?r.splice(o,r.length):r),e?e(null,g,r,i):G.apply(g,r)})}function ya(a){for(var b,c,e,f=a.length,g=d.relative[a[0].type],h=g||d.relative[" "],i=g?1:0,k=ta(function(a){return a===b},h,!0),l=ta(function(a){return I(b,a)>-1},h,!0),m=[function(a,c,d){var e=!g&&(d||c!==j)||((b=c).nodeType?k(a,c,d):l(a,c,d));return b=null,e}];i1&&ua(m),i>1&&sa(a.slice(0,i-1).concat({value:" "===a[i-2].type?"*":""})).replace(P,"$1"),c,i0,e=a.length>0,f=function(f,g,h,i,k){var l,o,q,r=0,s="0",t=f&&[],u=[],v=j,x=f||e&&d.find.TAG("*",k),y=w+=null==v?1:Math.random()||.1,z=x.length;for(k&&(j=g===n||g||k);s!==z&&null!=(l=x[s]);s++){if(e&&l){o=0,g||l.ownerDocument===n||(m(l),h=!p);while(q=a[o++])if(q(l,g||n,h)){i.push(l);break}k&&(w=y)}c&&((l=!q&&l)&&r--,f&&t.push(l))}if(r+=s,c&&s!==r){o=0;while(q=b[o++])q(t,u,g,h);if(f){if(r>0)while(s--)t[s]||u[s]||(u[s]=E.call(i));u=wa(u)}G.apply(i,u),k&&!f&&u.length>0&&r+b.length>1&&ga.uniqueSort(i)}return k&&(w=y,j=v),t};return c?ia(f):f}return h=ga.compile=function(a,b){var c,d=[],e=[],f=A[a+" "];if(!f){b||(b=g(a)),c=b.length;while(c--)f=ya(b[c]),f[u]?d.push(f):e.push(f);f=A(a,za(e,d)),f.selector=a}return f},i=ga.select=function(a,b,c,e){var f,i,j,k,l,m="function"==typeof a&&a,n=!e&&g(a=m.selector||a);if(c=c||[],1===n.length){if(i=n[0]=n[0].slice(0),i.length>2&&"ID"===(j=i[0]).type&&9===b.nodeType&&p&&d.relative[i[1].type]){if(b=(d.find.ID(j.matches[0].replace(_,aa),b)||[])[0],!b)return c;m&&(b=b.parentNode),a=a.slice(i.shift().value.length)}f=V.needsContext.test(a)?0:i.length;while(f--){if(j=i[f],d.relative[k=j.type])break;if((l=d.find[k])&&(e=l(j.matches[0].replace(_,aa),$.test(i[0].type)&&qa(b.parentNode)||b))){if(i.splice(f,1),a=e.length&&sa(i),!a)return G.apply(c,e),c;break}}}return(m||h(a,n))(e,b,!p,c,!b||$.test(a)&&qa(b.parentNode)||b),c},c.sortStable=u.split("").sort(B).join("")===u,c.detectDuplicates=!!l,m(),c.sortDetached=ja(function(a){return 1&a.compareDocumentPosition(n.createElement("fieldset"))}),ja(function(a){return a.innerHTML="","#"===a.firstChild.getAttribute("href")})||ka("type|href|height|width",function(a,b,c){if(!c)return a.getAttribute(b,"type"===b.toLowerCase()?1:2)}),c.attributes&&ja(function(a){return a.innerHTML="",a.firstChild.setAttribute("value",""),""===a.firstChild.getAttribute("value")})||ka("value",function(a,b,c){if(!c&&"input"===a.nodeName.toLowerCase())return a.defaultValue}),ja(function(a){return null==a.getAttribute("disabled")})||ka(J,function(a,b,c){var d;if(!c)return a[b]===!0?b.toLowerCase():(d=a.getAttributeNode(b))&&d.specified?d.value:null}),ga}(a);r.find=x,r.expr=x.selectors,r.expr[":"]=r.expr.pseudos,r.uniqueSort=r.unique=x.uniqueSort,r.text=x.getText,r.isXMLDoc=x.isXML,r.contains=x.contains,r.escapeSelector=x.escape;var y=function(a,b,c){var d=[],e=void 0!==c;while((a=a[b])&&9!==a.nodeType)if(1===a.nodeType){if(e&&r(a).is(c))break;d.push(a)}return d},z=function(a,b){for(var c=[];a;a=a.nextSibling)1===a.nodeType&&a!==b&&c.push(a);return c},A=r.expr.match.needsContext,B=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i,C=/^.[^:#\[\.,]*$/;function D(a,b,c){return r.isFunction(b)?r.grep(a,function(a,d){return!!b.call(a,d,a)!==c}):b.nodeType?r.grep(a,function(a){return a===b!==c}):"string"!=typeof b?r.grep(a,function(a){return i.call(b,a)>-1!==c}):C.test(b)?r.filter(b,a,c):(b=r.filter(b,a),r.grep(a,function(a){return i.call(b,a)>-1!==c&&1===a.nodeType}))}r.filter=function(a,b,c){var d=b[0];return c&&(a=":not("+a+")"),1===b.length&&1===d.nodeType?r.find.matchesSelector(d,a)?[d]:[]:r.find.matches(a,r.grep(b,function(a){return 1===a.nodeType}))},r.fn.extend({find:function(a){var b,c,d=this.length,e=this;if("string"!=typeof a)return this.pushStack(r(a).filter(function(){for(b=0;b1?r.uniqueSort(c):c},filter:function(a){return this.pushStack(D(this,a||[],!1))},not:function(a){return this.pushStack(D(this,a||[],!0))},is:function(a){return!!D(this,"string"==typeof a&&A.test(a)?r(a):a||[],!1).length}});var E,F=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/,G=r.fn.init=function(a,b,c){var e,f;if(!a)return this;if(c=c||E,"string"==typeof a){if(e="<"===a[0]&&">"===a[a.length-1]&&a.length>=3?[null,a,null]:F.exec(a),!e||!e[1]&&b)return!b||b.jquery?(b||c).find(a):this.constructor(b).find(a);if(e[1]){if(b=b instanceof r?b[0]:b,r.merge(this,r.parseHTML(e[1],b&&b.nodeType?b.ownerDocument||b:d,!0)),B.test(e[1])&&r.isPlainObject(b))for(e in b)r.isFunction(this[e])?this[e](b[e]):this.attr(e,b[e]);return this}return f=d.getElementById(e[2]),f&&(this[0]=f,this.length=1),this}return a.nodeType?(this[0]=a,this.length=1,this):r.isFunction(a)?void 0!==c.ready?c.ready(a):a(r):r.makeArray(a,this)};G.prototype=r.fn,E=r(d);var H=/^(?:parents|prev(?:Until|All))/,I={children:!0,contents:!0,next:!0,prev:!0};r.fn.extend({has:function(a){var b=r(a,this),c=b.length;return this.filter(function(){for(var a=0;a-1:1===c.nodeType&&r.find.matchesSelector(c,a))){f.push(c);break}return this.pushStack(f.length>1?r.uniqueSort(f):f)},index:function(a){return a?"string"==typeof a?i.call(r(a),this[0]):i.call(this,a.jquery?a[0]:a):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(a,b){return this.pushStack(r.uniqueSort(r.merge(this.get(),r(a,b))))},addBack:function(a){return this.add(null==a?this.prevObject:this.prevObject.filter(a))}});function J(a,b){while((a=a[b])&&1!==a.nodeType);return a}r.each({parent:function(a){var b=a.parentNode;return b&&11!==b.nodeType?b:null},parents:function(a){return y(a,"parentNode")},parentsUntil:function(a,b,c){return y(a,"parentNode",c)},next:function(a){return J(a,"nextSibling")},prev:function(a){return J(a,"previousSibling")},nextAll:function(a){return y(a,"nextSibling")},prevAll:function(a){return y(a,"previousSibling")},nextUntil:function(a,b,c){return y(a,"nextSibling",c)},prevUntil:function(a,b,c){return y(a,"previousSibling",c)},siblings:function(a){return z((a.parentNode||{}).firstChild,a)},children:function(a){return z(a.firstChild)},contents:function(a){return a.contentDocument||r.merge([],a.childNodes)}},function(a,b){r.fn[a]=function(c,d){var e=r.map(this,b,c);return"Until"!==a.slice(-5)&&(d=c),d&&"string"==typeof d&&(e=r.filter(d,e)),this.length>1&&(I[a]||r.uniqueSort(e),H.test(a)&&e.reverse()),this.pushStack(e)}});var K=/[^\x20\t\r\n\f]+/g;function L(a){var b={};return r.each(a.match(K)||[],function(a,c){b[c]=!0}),b}r.Callbacks=function(a){a="string"==typeof a?L(a):r.extend({},a);var b,c,d,e,f=[],g=[],h=-1,i=function(){for(e=a.once,d=b=!0;g.length;h=-1){c=g.shift();while(++h-1)f.splice(c,1),c<=h&&h--}),this},has:function(a){return a?r.inArray(a,f)>-1:f.length>0},empty:function(){return f&&(f=[]),this},disable:function(){return e=g=[],f=c="",this},disabled:function(){return!f},lock:function(){return e=g=[],c||b||(f=c=""),this},locked:function(){return!!e},fireWith:function(a,c){return e||(c=c||[],c=[a,c.slice?c.slice():c],g.push(c),b||i()),this},fire:function(){return j.fireWith(this,arguments),this},fired:function(){return!!d}};return j};function M(a){return a}function N(a){throw a}function O(a,b,c){var d;try{a&&r.isFunction(d=a.promise)?d.call(a).done(b).fail(c):a&&r.isFunction(d=a.then)?d.call(a,b,c):b.call(void 0,a)}catch(a){c.call(void 0,a)}}r.extend({Deferred:function(b){var c=[["notify","progress",r.Callbacks("memory"),r.Callbacks("memory"),2],["resolve","done",r.Callbacks("once memory"),r.Callbacks("once memory"),0,"resolved"],["reject","fail",r.Callbacks("once memory"),r.Callbacks("once memory"),1,"rejected"]],d="pending",e={state:function(){return d},always:function(){return f.done(arguments).fail(arguments),this},"catch":function(a){return e.then(null,a)},pipe:function(){var a=arguments;return r.Deferred(function(b){r.each(c,function(c,d){var e=r.isFunction(a[d[4]])&&a[d[4]];f[d[1]](function(){var a=e&&e.apply(this,arguments);a&&r.isFunction(a.promise)?a.promise().progress(b.notify).done(b.resolve).fail(b.reject):b[d[0]+"With"](this,e?[a]:arguments)})}),a=null}).promise()},then:function(b,d,e){var f=0;function g(b,c,d,e){return function(){var h=this,i=arguments,j=function(){var a,j;if(!(b=f&&(d!==N&&(h=void 0,i=[a]),c.rejectWith(h,i))}};b?k():(r.Deferred.getStackHook&&(k.stackTrace=r.Deferred.getStackHook()),a.setTimeout(k))}}return r.Deferred(function(a){c[0][3].add(g(0,a,r.isFunction(e)?e:M,a.notifyWith)),c[1][3].add(g(0,a,r.isFunction(b)?b:M)),c[2][3].add(g(0,a,r.isFunction(d)?d:N))}).promise()},promise:function(a){return null!=a?r.extend(a,e):e}},f={};return r.each(c,function(a,b){var g=b[2],h=b[5];e[b[1]]=g.add,h&&g.add(function(){d=h},c[3-a][2].disable,c[0][2].lock),g.add(b[3].fire),f[b[0]]=function(){return f[b[0]+"With"](this===f?void 0:this,arguments),this},f[b[0]+"With"]=g.fireWith}),e.promise(f),b&&b.call(f,f),f},when:function(a){var b=arguments.length,c=b,d=Array(c),e=f.call(arguments),g=r.Deferred(),h=function(a){return function(c){d[a]=this,e[a]=arguments.length>1?f.call(arguments):c,--b||g.resolveWith(d,e)}};if(b<=1&&(O(a,g.done(h(c)).resolve,g.reject),"pending"===g.state()||r.isFunction(e[c]&&e[c].then)))return g.then();while(c--)O(e[c],h(c),g.reject);return g.promise()}});var P=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;r.Deferred.exceptionHook=function(b,c){a.console&&a.console.warn&&b&&P.test(b.name)&&a.console.warn("jQuery.Deferred exception: "+b.message,b.stack,c)},r.readyException=function(b){a.setTimeout(function(){throw b})};var Q=r.Deferred();r.fn.ready=function(a){return Q.then(a)["catch"](function(a){r.readyException(a)}),this},r.extend({isReady:!1,readyWait:1,holdReady:function(a){a?r.readyWait++:r.ready(!0)},ready:function(a){(a===!0?--r.readyWait:r.isReady)||(r.isReady=!0,a!==!0&&--r.readyWait>0||Q.resolveWith(d,[r]))}}),r.ready.then=Q.then;function R(){d.removeEventListener("DOMContentLoaded",R), -a.removeEventListener("load",R),r.ready()}"complete"===d.readyState||"loading"!==d.readyState&&!d.documentElement.doScroll?a.setTimeout(r.ready):(d.addEventListener("DOMContentLoaded",R),a.addEventListener("load",R));var S=function(a,b,c,d,e,f,g){var h=0,i=a.length,j=null==c;if("object"===r.type(c)){e=!0;for(h in c)S(a,b,h,c[h],!0,f,g)}else if(void 0!==d&&(e=!0,r.isFunction(d)||(g=!0),j&&(g?(b.call(a,d),b=null):(j=b,b=function(a,b,c){return j.call(r(a),c)})),b))for(;h1,null,!0)},removeData:function(a){return this.each(function(){W.remove(this,a)})}}),r.extend({queue:function(a,b,c){var d;if(a)return b=(b||"fx")+"queue",d=V.get(a,b),c&&(!d||r.isArray(c)?d=V.access(a,b,r.makeArray(c)):d.push(c)),d||[]},dequeue:function(a,b){b=b||"fx";var c=r.queue(a,b),d=c.length,e=c.shift(),f=r._queueHooks(a,b),g=function(){r.dequeue(a,b)};"inprogress"===e&&(e=c.shift(),d--),e&&("fx"===b&&c.unshift("inprogress"),delete f.stop,e.call(a,g,f)),!d&&f&&f.empty.fire()},_queueHooks:function(a,b){var c=b+"queueHooks";return V.get(a,c)||V.access(a,c,{empty:r.Callbacks("once memory").add(function(){V.remove(a,[b+"queue",c])})})}}),r.fn.extend({queue:function(a,b){var c=2;return"string"!=typeof a&&(b=a,a="fx",c--),arguments.length\x20\t\r\n\f]+)/i,ka=/^$|\/(?:java|ecma)script/i,la={option:[1,""],thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};la.optgroup=la.option,la.tbody=la.tfoot=la.colgroup=la.caption=la.thead,la.th=la.td;function ma(a,b){var c;return c="undefined"!=typeof a.getElementsByTagName?a.getElementsByTagName(b||"*"):"undefined"!=typeof a.querySelectorAll?a.querySelectorAll(b||"*"):[],void 0===b||b&&r.nodeName(a,b)?r.merge([a],c):c}function na(a,b){for(var c=0,d=a.length;c-1)e&&e.push(f);else if(j=r.contains(f.ownerDocument,f),g=ma(l.appendChild(f),"script"),j&&na(g),c){k=0;while(f=g[k++])ka.test(f.type||"")&&c.push(f)}return l}!function(){var a=d.createDocumentFragment(),b=a.appendChild(d.createElement("div")),c=d.createElement("input");c.setAttribute("type","radio"),c.setAttribute("checked","checked"),c.setAttribute("name","t"),b.appendChild(c),o.checkClone=b.cloneNode(!0).cloneNode(!0).lastChild.checked,b.innerHTML="",o.noCloneChecked=!!b.cloneNode(!0).lastChild.defaultValue}();var qa=d.documentElement,ra=/^key/,sa=/^(?:mouse|pointer|contextmenu|drag|drop)|click/,ta=/^([^.]*)(?:\.(.+)|)/;function ua(){return!0}function va(){return!1}function wa(){try{return d.activeElement}catch(a){}}function xa(a,b,c,d,e,f){var g,h;if("object"==typeof b){"string"!=typeof c&&(d=d||c,c=void 0);for(h in b)xa(a,h,c,d,b[h],f);return a}if(null==d&&null==e?(e=c,d=c=void 0):null==e&&("string"==typeof c?(e=d,d=void 0):(e=d,d=c,c=void 0)),e===!1)e=va;else if(!e)return a;return 1===f&&(g=e,e=function(a){return r().off(a),g.apply(this,arguments)},e.guid=g.guid||(g.guid=r.guid++)),a.each(function(){r.event.add(this,b,e,d,c)})}r.event={global:{},add:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.get(a);if(q){c.handler&&(f=c,c=f.handler,e=f.selector),e&&r.find.matchesSelector(qa,e),c.guid||(c.guid=r.guid++),(i=q.events)||(i=q.events={}),(g=q.handle)||(g=q.handle=function(b){return"undefined"!=typeof r&&r.event.triggered!==b.type?r.event.dispatch.apply(a,arguments):void 0}),b=(b||"").match(K)||[""],j=b.length;while(j--)h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n&&(l=r.event.special[n]||{},n=(e?l.delegateType:l.bindType)||n,l=r.event.special[n]||{},k=r.extend({type:n,origType:p,data:d,handler:c,guid:c.guid,selector:e,needsContext:e&&r.expr.match.needsContext.test(e),namespace:o.join(".")},f),(m=i[n])||(m=i[n]=[],m.delegateCount=0,l.setup&&l.setup.call(a,d,o,g)!==!1||a.addEventListener&&a.addEventListener(n,g)),l.add&&(l.add.call(a,k),k.handler.guid||(k.handler.guid=c.guid)),e?m.splice(m.delegateCount++,0,k):m.push(k),r.event.global[n]=!0)}},remove:function(a,b,c,d,e){var f,g,h,i,j,k,l,m,n,o,p,q=V.hasData(a)&&V.get(a);if(q&&(i=q.events)){b=(b||"").match(K)||[""],j=b.length;while(j--)if(h=ta.exec(b[j])||[],n=p=h[1],o=(h[2]||"").split(".").sort(),n){l=r.event.special[n]||{},n=(d?l.delegateType:l.bindType)||n,m=i[n]||[],h=h[2]&&new RegExp("(^|\\.)"+o.join("\\.(?:.*\\.|)")+"(\\.|$)"),g=f=m.length;while(f--)k=m[f],!e&&p!==k.origType||c&&c.guid!==k.guid||h&&!h.test(k.namespace)||d&&d!==k.selector&&("**"!==d||!k.selector)||(m.splice(f,1),k.selector&&m.delegateCount--,l.remove&&l.remove.call(a,k));g&&!m.length&&(l.teardown&&l.teardown.call(a,o,q.handle)!==!1||r.removeEvent(a,n,q.handle),delete i[n])}else for(n in i)r.event.remove(a,n+b[j],c,d,!0);r.isEmptyObject(i)&&V.remove(a,"handle events")}},dispatch:function(a){var b=r.event.fix(a),c,d,e,f,g,h,i=new Array(arguments.length),j=(V.get(this,"events")||{})[b.type]||[],k=r.event.special[b.type]||{};for(i[0]=b,c=1;c=1))for(;j!==this;j=j.parentNode||this)if(1===j.nodeType&&("click"!==a.type||j.disabled!==!0)){for(f=[],g={},c=0;c-1:r.find(e,this,null,[j]).length),g[e]&&f.push(d);f.length&&h.push({elem:j,handlers:f})}return j=this,i\x20\t\r\n\f]*)[^>]*)\/>/gi,za=/\s*$/g;function Da(a,b){return r.nodeName(a,"table")&&r.nodeName(11!==b.nodeType?b:b.firstChild,"tr")?a.getElementsByTagName("tbody")[0]||a:a}function Ea(a){return a.type=(null!==a.getAttribute("type"))+"/"+a.type,a}function Fa(a){var b=Ba.exec(a.type);return b?a.type=b[1]:a.removeAttribute("type"),a}function Ga(a,b){var c,d,e,f,g,h,i,j;if(1===b.nodeType){if(V.hasData(a)&&(f=V.access(a),g=V.set(b,f),j=f.events)){delete g.handle,g.events={};for(e in j)for(c=0,d=j[e].length;c1&&"string"==typeof q&&!o.checkClone&&Aa.test(q))return a.each(function(e){var f=a.eq(e);s&&(b[0]=q.call(this,e,f.html())),Ia(f,b,c,d)});if(m&&(e=pa(b,a[0].ownerDocument,!1,a,d),f=e.firstChild,1===e.childNodes.length&&(e=f),f||d)){for(h=r.map(ma(e,"script"),Ea),i=h.length;l")},clone:function(a,b,c){var d,e,f,g,h=a.cloneNode(!0),i=r.contains(a.ownerDocument,a);if(!(o.noCloneChecked||1!==a.nodeType&&11!==a.nodeType||r.isXMLDoc(a)))for(g=ma(h),f=ma(a),d=0,e=f.length;d0&&na(g,!i&&ma(a,"script")),h},cleanData:function(a){for(var b,c,d,e=r.event.special,f=0;void 0!==(c=a[f]);f++)if(T(c)){if(b=c[V.expando]){if(b.events)for(d in b.events)e[d]?r.event.remove(c,d):r.removeEvent(c,d,b.handle);c[V.expando]=void 0}c[W.expando]&&(c[W.expando]=void 0)}}}),r.fn.extend({detach:function(a){return Ja(this,a,!0)},remove:function(a){return Ja(this,a)},text:function(a){return S(this,function(a){return void 0===a?r.text(this):this.empty().each(function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=a)})},null,a,arguments.length)},append:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.appendChild(a)}})},prepend:function(){return Ia(this,arguments,function(a){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var b=Da(this,a);b.insertBefore(a,b.firstChild)}})},before:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this)})},after:function(){return Ia(this,arguments,function(a){this.parentNode&&this.parentNode.insertBefore(a,this.nextSibling)})},empty:function(){for(var a,b=0;null!=(a=this[b]);b++)1===a.nodeType&&(r.cleanData(ma(a,!1)),a.textContent="");return this},clone:function(a,b){return a=null!=a&&a,b=null==b?a:b,this.map(function(){return r.clone(this,a,b)})},html:function(a){return S(this,function(a){var b=this[0]||{},c=0,d=this.length;if(void 0===a&&1===b.nodeType)return b.innerHTML;if("string"==typeof a&&!za.test(a)&&!la[(ja.exec(a)||["",""])[1].toLowerCase()]){a=r.htmlPrefilter(a);try{for(;c1)}});function Ya(a,b,c,d,e){return new Ya.prototype.init(a,b,c,d,e)}r.Tween=Ya,Ya.prototype={constructor:Ya,init:function(a,b,c,d,e,f){this.elem=a,this.prop=c,this.easing=e||r.easing._default,this.options=b,this.start=this.now=this.cur(),this.end=d,this.unit=f||(r.cssNumber[c]?"":"px")},cur:function(){var a=Ya.propHooks[this.prop];return a&&a.get?a.get(this):Ya.propHooks._default.get(this)},run:function(a){var b,c=Ya.propHooks[this.prop];return this.options.duration?this.pos=b=r.easing[this.easing](a,this.options.duration*a,0,1,this.options.duration):this.pos=b=a,this.now=(this.end-this.start)*b+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),c&&c.set?c.set(this):Ya.propHooks._default.set(this),this}},Ya.prototype.init.prototype=Ya.prototype,Ya.propHooks={_default:{get:function(a){var b;return 1!==a.elem.nodeType||null!=a.elem[a.prop]&&null==a.elem.style[a.prop]?a.elem[a.prop]:(b=r.css(a.elem,a.prop,""),b&&"auto"!==b?b:0)},set:function(a){r.fx.step[a.prop]?r.fx.step[a.prop](a):1!==a.elem.nodeType||null==a.elem.style[r.cssProps[a.prop]]&&!r.cssHooks[a.prop]?a.elem[a.prop]=a.now:r.style(a.elem,a.prop,a.now+a.unit)}}},Ya.propHooks.scrollTop=Ya.propHooks.scrollLeft={set:function(a){a.elem.nodeType&&a.elem.parentNode&&(a.elem[a.prop]=a.now)}},r.easing={linear:function(a){return a},swing:function(a){return.5-Math.cos(a*Math.PI)/2},_default:"swing"},r.fx=Ya.prototype.init,r.fx.step={};var Za,$a,_a=/^(?:toggle|show|hide)$/,ab=/queueHooks$/;function bb(){$a&&(a.requestAnimationFrame(bb),r.fx.tick())}function cb(){return a.setTimeout(function(){Za=void 0}),Za=r.now()}function db(a,b){var c,d=0,e={height:a};for(b=b?1:0;d<4;d+=2-b)c=ba[d],e["margin"+c]=e["padding"+c]=a;return b&&(e.opacity=e.width=a),e}function eb(a,b,c){for(var d,e=(hb.tweeners[b]||[]).concat(hb.tweeners["*"]),f=0,g=e.length;f1)},removeAttr:function(a){return this.each(function(){r.removeAttr(this,a)})}}),r.extend({attr:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return"undefined"==typeof a.getAttribute?r.prop(a,b,c):(1===f&&r.isXMLDoc(a)||(e=r.attrHooks[b.toLowerCase()]||(r.expr.match.bool.test(b)?ib:void 0)), -void 0!==c?null===c?void r.removeAttr(a,b):e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:(a.setAttribute(b,c+""),c):e&&"get"in e&&null!==(d=e.get(a,b))?d:(d=r.find.attr(a,b),null==d?void 0:d))},attrHooks:{type:{set:function(a,b){if(!o.radioValue&&"radio"===b&&r.nodeName(a,"input")){var c=a.value;return a.setAttribute("type",b),c&&(a.value=c),b}}}},removeAttr:function(a,b){var c,d=0,e=b&&b.match(K);if(e&&1===a.nodeType)while(c=e[d++])a.removeAttribute(c)}}),ib={set:function(a,b,c){return b===!1?r.removeAttr(a,c):a.setAttribute(c,c),c}},r.each(r.expr.match.bool.source.match(/\w+/g),function(a,b){var c=jb[b]||r.find.attr;jb[b]=function(a,b,d){var e,f,g=b.toLowerCase();return d||(f=jb[g],jb[g]=e,e=null!=c(a,b,d)?g:null,jb[g]=f),e}});var kb=/^(?:input|select|textarea|button)$/i,lb=/^(?:a|area)$/i;r.fn.extend({prop:function(a,b){return S(this,r.prop,a,b,arguments.length>1)},removeProp:function(a){return this.each(function(){delete this[r.propFix[a]||a]})}}),r.extend({prop:function(a,b,c){var d,e,f=a.nodeType;if(3!==f&&8!==f&&2!==f)return 1===f&&r.isXMLDoc(a)||(b=r.propFix[b]||b,e=r.propHooks[b]),void 0!==c?e&&"set"in e&&void 0!==(d=e.set(a,c,b))?d:a[b]=c:e&&"get"in e&&null!==(d=e.get(a,b))?d:a[b]},propHooks:{tabIndex:{get:function(a){var b=r.find.attr(a,"tabindex");return b?parseInt(b,10):kb.test(a.nodeName)||lb.test(a.nodeName)&&a.href?0:-1}}},propFix:{"for":"htmlFor","class":"className"}}),o.optSelected||(r.propHooks.selected={get:function(a){var b=a.parentNode;return b&&b.parentNode&&b.parentNode.selectedIndex,null},set:function(a){var b=a.parentNode;b&&(b.selectedIndex,b.parentNode&&b.parentNode.selectedIndex)}}),r.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],function(){r.propFix[this.toLowerCase()]=this});function mb(a){var b=a.match(K)||[];return b.join(" ")}function nb(a){return a.getAttribute&&a.getAttribute("class")||""}r.fn.extend({addClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).addClass(a.call(this,b,nb(this)))});if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])d.indexOf(" "+f+" ")<0&&(d+=f+" ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},removeClass:function(a){var b,c,d,e,f,g,h,i=0;if(r.isFunction(a))return this.each(function(b){r(this).removeClass(a.call(this,b,nb(this)))});if(!arguments.length)return this.attr("class","");if("string"==typeof a&&a){b=a.match(K)||[];while(c=this[i++])if(e=nb(c),d=1===c.nodeType&&" "+mb(e)+" "){g=0;while(f=b[g++])while(d.indexOf(" "+f+" ")>-1)d=d.replace(" "+f+" "," ");h=mb(d),e!==h&&c.setAttribute("class",h)}}return this},toggleClass:function(a,b){var c=typeof a;return"boolean"==typeof b&&"string"===c?b?this.addClass(a):this.removeClass(a):r.isFunction(a)?this.each(function(c){r(this).toggleClass(a.call(this,c,nb(this),b),b)}):this.each(function(){var b,d,e,f;if("string"===c){d=0,e=r(this),f=a.match(K)||[];while(b=f[d++])e.hasClass(b)?e.removeClass(b):e.addClass(b)}else void 0!==a&&"boolean"!==c||(b=nb(this),b&&V.set(this,"__className__",b),this.setAttribute&&this.setAttribute("class",b||a===!1?"":V.get(this,"__className__")||""))})},hasClass:function(a){var b,c,d=0;b=" "+a+" ";while(c=this[d++])if(1===c.nodeType&&(" "+mb(nb(c))+" ").indexOf(b)>-1)return!0;return!1}});var ob=/\r/g;r.fn.extend({val:function(a){var b,c,d,e=this[0];{if(arguments.length)return d=r.isFunction(a),this.each(function(c){var e;1===this.nodeType&&(e=d?a.call(this,c,r(this).val()):a,null==e?e="":"number"==typeof e?e+="":r.isArray(e)&&(e=r.map(e,function(a){return null==a?"":a+""})),b=r.valHooks[this.type]||r.valHooks[this.nodeName.toLowerCase()],b&&"set"in b&&void 0!==b.set(this,e,"value")||(this.value=e))});if(e)return b=r.valHooks[e.type]||r.valHooks[e.nodeName.toLowerCase()],b&&"get"in b&&void 0!==(c=b.get(e,"value"))?c:(c=e.value,"string"==typeof c?c.replace(ob,""):null==c?"":c)}}}),r.extend({valHooks:{option:{get:function(a){var b=r.find.attr(a,"value");return null!=b?b:mb(r.text(a))}},select:{get:function(a){var b,c,d,e=a.options,f=a.selectedIndex,g="select-one"===a.type,h=g?null:[],i=g?f+1:e.length;for(d=f<0?i:g?f:0;d-1)&&(c=!0);return c||(a.selectedIndex=-1),f}}}}),r.each(["radio","checkbox"],function(){r.valHooks[this]={set:function(a,b){if(r.isArray(b))return a.checked=r.inArray(r(a).val(),b)>-1}},o.checkOn||(r.valHooks[this].get=function(a){return null===a.getAttribute("value")?"on":a.value})});var pb=/^(?:focusinfocus|focusoutblur)$/;r.extend(r.event,{trigger:function(b,c,e,f){var g,h,i,j,k,m,n,o=[e||d],p=l.call(b,"type")?b.type:b,q=l.call(b,"namespace")?b.namespace.split("."):[];if(h=i=e=e||d,3!==e.nodeType&&8!==e.nodeType&&!pb.test(p+r.event.triggered)&&(p.indexOf(".")>-1&&(q=p.split("."),p=q.shift(),q.sort()),k=p.indexOf(":")<0&&"on"+p,b=b[r.expando]?b:new r.Event(p,"object"==typeof b&&b),b.isTrigger=f?2:3,b.namespace=q.join("."),b.rnamespace=b.namespace?new RegExp("(^|\\.)"+q.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,b.result=void 0,b.target||(b.target=e),c=null==c?[b]:r.makeArray(c,[b]),n=r.event.special[p]||{},f||!n.trigger||n.trigger.apply(e,c)!==!1)){if(!f&&!n.noBubble&&!r.isWindow(e)){for(j=n.delegateType||p,pb.test(j+p)||(h=h.parentNode);h;h=h.parentNode)o.push(h),i=h;i===(e.ownerDocument||d)&&o.push(i.defaultView||i.parentWindow||a)}g=0;while((h=o[g++])&&!b.isPropagationStopped())b.type=g>1?j:n.bindType||p,m=(V.get(h,"events")||{})[b.type]&&V.get(h,"handle"),m&&m.apply(h,c),m=k&&h[k],m&&m.apply&&T(h)&&(b.result=m.apply(h,c),b.result===!1&&b.preventDefault());return b.type=p,f||b.isDefaultPrevented()||n._default&&n._default.apply(o.pop(),c)!==!1||!T(e)||k&&r.isFunction(e[p])&&!r.isWindow(e)&&(i=e[k],i&&(e[k]=null),r.event.triggered=p,e[p](),r.event.triggered=void 0,i&&(e[k]=i)),b.result}},simulate:function(a,b,c){var d=r.extend(new r.Event,c,{type:a,isSimulated:!0});r.event.trigger(d,null,b)}}),r.fn.extend({trigger:function(a,b){return this.each(function(){r.event.trigger(a,b,this)})},triggerHandler:function(a,b){var c=this[0];if(c)return r.event.trigger(a,b,c,!0)}}),r.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(a,b){r.fn[b]=function(a,c){return arguments.length>0?this.on(b,null,a,c):this.trigger(b)}}),r.fn.extend({hover:function(a,b){return this.mouseenter(a).mouseleave(b||a)}}),o.focusin="onfocusin"in a,o.focusin||r.each({focus:"focusin",blur:"focusout"},function(a,b){var c=function(a){r.event.simulate(b,a.target,r.event.fix(a))};r.event.special[b]={setup:function(){var d=this.ownerDocument||this,e=V.access(d,b);e||d.addEventListener(a,c,!0),V.access(d,b,(e||0)+1)},teardown:function(){var d=this.ownerDocument||this,e=V.access(d,b)-1;e?V.access(d,b,e):(d.removeEventListener(a,c,!0),V.remove(d,b))}}});var qb=a.location,rb=r.now(),sb=/\?/;r.parseXML=function(b){var c;if(!b||"string"!=typeof b)return null;try{c=(new a.DOMParser).parseFromString(b,"text/xml")}catch(d){c=void 0}return c&&!c.getElementsByTagName("parsererror").length||r.error("Invalid XML: "+b),c};var tb=/\[\]$/,ub=/\r?\n/g,vb=/^(?:submit|button|image|reset|file)$/i,wb=/^(?:input|select|textarea|keygen)/i;function xb(a,b,c,d){var e;if(r.isArray(b))r.each(b,function(b,e){c||tb.test(a)?d(a,e):xb(a+"["+("object"==typeof e&&null!=e?b:"")+"]",e,c,d)});else if(c||"object"!==r.type(b))d(a,b);else for(e in b)xb(a+"["+e+"]",b[e],c,d)}r.param=function(a,b){var c,d=[],e=function(a,b){var c=r.isFunction(b)?b():b;d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(null==c?"":c)};if(r.isArray(a)||a.jquery&&!r.isPlainObject(a))r.each(a,function(){e(this.name,this.value)});else for(c in a)xb(c,a[c],b,e);return d.join("&")},r.fn.extend({serialize:function(){return r.param(this.serializeArray())},serializeArray:function(){return this.map(function(){var a=r.prop(this,"elements");return a?r.makeArray(a):this}).filter(function(){var a=this.type;return this.name&&!r(this).is(":disabled")&&wb.test(this.nodeName)&&!vb.test(a)&&(this.checked||!ia.test(a))}).map(function(a,b){var c=r(this).val();return null==c?null:r.isArray(c)?r.map(c,function(a){return{name:b.name,value:a.replace(ub,"\r\n")}}):{name:b.name,value:c.replace(ub,"\r\n")}}).get()}});var yb=/%20/g,zb=/#.*$/,Ab=/([?&])_=[^&]*/,Bb=/^(.*?):[ \t]*([^\r\n]*)$/gm,Cb=/^(?:about|app|app-storage|.+-extension|file|res|widget):$/,Db=/^(?:GET|HEAD)$/,Eb=/^\/\//,Fb={},Gb={},Hb="*/".concat("*"),Ib=d.createElement("a");Ib.href=qb.href;function Jb(a){return function(b,c){"string"!=typeof b&&(c=b,b="*");var d,e=0,f=b.toLowerCase().match(K)||[];if(r.isFunction(c))while(d=f[e++])"+"===d[0]?(d=d.slice(1)||"*",(a[d]=a[d]||[]).unshift(c)):(a[d]=a[d]||[]).push(c)}}function Kb(a,b,c,d){var e={},f=a===Gb;function g(h){var i;return e[h]=!0,r.each(a[h]||[],function(a,h){var j=h(b,c,d);return"string"!=typeof j||f||e[j]?f?!(i=j):void 0:(b.dataTypes.unshift(j),g(j),!1)}),i}return g(b.dataTypes[0])||!e["*"]&&g("*")}function Lb(a,b){var c,d,e=r.ajaxSettings.flatOptions||{};for(c in b)void 0!==b[c]&&((e[c]?a:d||(d={}))[c]=b[c]);return d&&r.extend(!0,a,d),a}function Mb(a,b,c){var d,e,f,g,h=a.contents,i=a.dataTypes;while("*"===i[0])i.shift(),void 0===d&&(d=a.mimeType||b.getResponseHeader("Content-Type"));if(d)for(e in h)if(h[e]&&h[e].test(d)){i.unshift(e);break}if(i[0]in c)f=i[0];else{for(e in c){if(!i[0]||a.converters[e+" "+i[0]]){f=e;break}g||(g=e)}f=f||g}if(f)return f!==i[0]&&i.unshift(f),c[f]}function Nb(a,b,c,d){var e,f,g,h,i,j={},k=a.dataTypes.slice();if(k[1])for(g in a.converters)j[g.toLowerCase()]=a.converters[g];f=k.shift();while(f)if(a.responseFields[f]&&(c[a.responseFields[f]]=b),!i&&d&&a.dataFilter&&(b=a.dataFilter(b,a.dataType)),i=f,f=k.shift())if("*"===f)f=i;else if("*"!==i&&i!==f){if(g=j[i+" "+f]||j["* "+f],!g)for(e in j)if(h=e.split(" "),h[1]===f&&(g=j[i+" "+h[0]]||j["* "+h[0]])){g===!0?g=j[e]:j[e]!==!0&&(f=h[0],k.unshift(h[1]));break}if(g!==!0)if(g&&a["throws"])b=g(b);else try{b=g(b)}catch(l){return{state:"parsererror",error:g?l:"No conversion from "+i+" to "+f}}}return{state:"success",data:b}}r.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:qb.href,type:"GET",isLocal:Cb.test(qb.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Hb,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":r.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(a,b){return b?Lb(Lb(a,r.ajaxSettings),b):Lb(r.ajaxSettings,a)},ajaxPrefilter:Jb(Fb),ajaxTransport:Jb(Gb),ajax:function(b,c){"object"==typeof b&&(c=b,b=void 0),c=c||{};var e,f,g,h,i,j,k,l,m,n,o=r.ajaxSetup({},c),p=o.context||o,q=o.context&&(p.nodeType||p.jquery)?r(p):r.event,s=r.Deferred(),t=r.Callbacks("once memory"),u=o.statusCode||{},v={},w={},x="canceled",y={readyState:0,getResponseHeader:function(a){var b;if(k){if(!h){h={};while(b=Bb.exec(g))h[b[1].toLowerCase()]=b[2]}b=h[a.toLowerCase()]}return null==b?null:b},getAllResponseHeaders:function(){return k?g:null},setRequestHeader:function(a,b){return null==k&&(a=w[a.toLowerCase()]=w[a.toLowerCase()]||a,v[a]=b),this},overrideMimeType:function(a){return null==k&&(o.mimeType=a),this},statusCode:function(a){var b;if(a)if(k)y.always(a[y.status]);else for(b in a)u[b]=[u[b],a[b]];return this},abort:function(a){var b=a||x;return e&&e.abort(b),A(0,b),this}};if(s.promise(y),o.url=((b||o.url||qb.href)+"").replace(Eb,qb.protocol+"//"),o.type=c.method||c.type||o.method||o.type,o.dataTypes=(o.dataType||"*").toLowerCase().match(K)||[""],null==o.crossDomain){j=d.createElement("a");try{j.href=o.url,j.href=j.href,o.crossDomain=Ib.protocol+"//"+Ib.host!=j.protocol+"//"+j.host}catch(z){o.crossDomain=!0}}if(o.data&&o.processData&&"string"!=typeof o.data&&(o.data=r.param(o.data,o.traditional)),Kb(Fb,o,c,y),k)return y;l=r.event&&o.global,l&&0===r.active++&&r.event.trigger("ajaxStart"),o.type=o.type.toUpperCase(),o.hasContent=!Db.test(o.type),f=o.url.replace(zb,""),o.hasContent?o.data&&o.processData&&0===(o.contentType||"").indexOf("application/x-www-form-urlencoded")&&(o.data=o.data.replace(yb,"+")):(n=o.url.slice(f.length),o.data&&(f+=(sb.test(f)?"&":"?")+o.data,delete o.data),o.cache===!1&&(f=f.replace(Ab,"$1"),n=(sb.test(f)?"&":"?")+"_="+rb++ +n),o.url=f+n),o.ifModified&&(r.lastModified[f]&&y.setRequestHeader("If-Modified-Since",r.lastModified[f]),r.etag[f]&&y.setRequestHeader("If-None-Match",r.etag[f])),(o.data&&o.hasContent&&o.contentType!==!1||c.contentType)&&y.setRequestHeader("Content-Type",o.contentType),y.setRequestHeader("Accept",o.dataTypes[0]&&o.accepts[o.dataTypes[0]]?o.accepts[o.dataTypes[0]]+("*"!==o.dataTypes[0]?", "+Hb+"; q=0.01":""):o.accepts["*"]);for(m in o.headers)y.setRequestHeader(m,o.headers[m]);if(o.beforeSend&&(o.beforeSend.call(p,y,o)===!1||k))return y.abort();if(x="abort",t.add(o.complete),y.done(o.success),y.fail(o.error),e=Kb(Gb,o,c,y)){if(y.readyState=1,l&&q.trigger("ajaxSend",[y,o]),k)return y;o.async&&o.timeout>0&&(i=a.setTimeout(function(){y.abort("timeout")},o.timeout));try{k=!1,e.send(v,A)}catch(z){if(k)throw z;A(-1,z)}}else A(-1,"No Transport");function A(b,c,d,h){var j,m,n,v,w,x=c;k||(k=!0,i&&a.clearTimeout(i),e=void 0,g=h||"",y.readyState=b>0?4:0,j=b>=200&&b<300||304===b,d&&(v=Mb(o,y,d)),v=Nb(o,v,y,j),j?(o.ifModified&&(w=y.getResponseHeader("Last-Modified"),w&&(r.lastModified[f]=w),w=y.getResponseHeader("etag"),w&&(r.etag[f]=w)),204===b||"HEAD"===o.type?x="nocontent":304===b?x="notmodified":(x=v.state,m=v.data,n=v.error,j=!n)):(n=x,!b&&x||(x="error",b<0&&(b=0))),y.status=b,y.statusText=(c||x)+"",j?s.resolveWith(p,[m,x,y]):s.rejectWith(p,[y,x,n]),y.statusCode(u),u=void 0,l&&q.trigger(j?"ajaxSuccess":"ajaxError",[y,o,j?m:n]),t.fireWith(p,[y,x]),l&&(q.trigger("ajaxComplete",[y,o]),--r.active||r.event.trigger("ajaxStop")))}return y},getJSON:function(a,b,c){return r.get(a,b,c,"json")},getScript:function(a,b){return r.get(a,void 0,b,"script")}}),r.each(["get","post"],function(a,b){r[b]=function(a,c,d,e){return r.isFunction(c)&&(e=e||d,d=c,c=void 0),r.ajax(r.extend({url:a,type:b,dataType:e,data:c,success:d},r.isPlainObject(a)&&a))}}),r._evalUrl=function(a){return r.ajax({url:a,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,"throws":!0})},r.fn.extend({wrapAll:function(a){var b;return this[0]&&(r.isFunction(a)&&(a=a.call(this[0])),b=r(a,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstElementChild)a=a.firstElementChild;return a}).append(this)),this},wrapInner:function(a){return r.isFunction(a)?this.each(function(b){r(this).wrapInner(a.call(this,b))}):this.each(function(){var b=r(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=r.isFunction(a);return this.each(function(c){r(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(a){return this.parent(a).not("body").each(function(){r(this).replaceWith(this.childNodes)}),this}}),r.expr.pseudos.hidden=function(a){return!r.expr.pseudos.visible(a)},r.expr.pseudos.visible=function(a){return!!(a.offsetWidth||a.offsetHeight||a.getClientRects().length)},r.ajaxSettings.xhr=function(){try{return new a.XMLHttpRequest}catch(b){}};var Ob={0:200,1223:204},Pb=r.ajaxSettings.xhr();o.cors=!!Pb&&"withCredentials"in Pb,o.ajax=Pb=!!Pb,r.ajaxTransport(function(b){var c,d;if(o.cors||Pb&&!b.crossDomain)return{send:function(e,f){var g,h=b.xhr();if(h.open(b.type,b.url,b.async,b.username,b.password),b.xhrFields)for(g in b.xhrFields)h[g]=b.xhrFields[g];b.mimeType&&h.overrideMimeType&&h.overrideMimeType(b.mimeType),b.crossDomain||e["X-Requested-With"]||(e["X-Requested-With"]="XMLHttpRequest");for(g in e)h.setRequestHeader(g,e[g]);c=function(a){return function(){c&&(c=d=h.onload=h.onerror=h.onabort=h.onreadystatechange=null,"abort"===a?h.abort():"error"===a?"number"!=typeof h.status?f(0,"error"):f(h.status,h.statusText):f(Ob[h.status]||h.status,h.statusText,"text"!==(h.responseType||"text")||"string"!=typeof h.responseText?{binary:h.response}:{text:h.responseText},h.getAllResponseHeaders()))}},h.onload=c(),d=h.onerror=c("error"),void 0!==h.onabort?h.onabort=d:h.onreadystatechange=function(){4===h.readyState&&a.setTimeout(function(){c&&d()})},c=c("abort");try{h.send(b.hasContent&&b.data||null)}catch(i){if(c)throw i}},abort:function(){c&&c()}}}),r.ajaxPrefilter(function(a){a.crossDomain&&(a.contents.script=!1)}),r.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(a){return r.globalEval(a),a}}}),r.ajaxPrefilter("script",function(a){void 0===a.cache&&(a.cache=!1),a.crossDomain&&(a.type="GET")}),r.ajaxTransport("script",function(a){if(a.crossDomain){var b,c;return{send:function(e,f){b=r(" + diff --git a/apps/terminal/api/__init__.py b/apps/terminal/api/__init__.py index 16021a5ed..4cdabc9dd 100644 --- a/apps/terminal/api/__init__.py +++ b/apps/terminal/api/__init__.py @@ -8,3 +8,4 @@ from .storage import * from .status import * from .sharing import * from .endpoint import * +from .db_listen_port import * diff --git a/apps/terminal/api/db_listen_port.py b/apps/terminal/api/db_listen_port.py new file mode 100644 index 000000000..4170a33d4 --- /dev/null +++ b/apps/terminal/api/db_listen_port.py @@ -0,0 +1,36 @@ +# coding: utf-8 +# +from rest_framework import status +from rest_framework.decorators import action +from rest_framework.response import Response +from rest_framework.viewsets import GenericViewSet + +from ..utils import db_port_manager, DBPortManager +from applications import serializers + + +db_port_manager: DBPortManager + + +__all__ = ['DBListenPortViewSet'] + + +class DBListenPortViewSet(GenericViewSet): + rbac_perms = { + 'GET': 'applications.view_application', + 'list': 'applications.view_application', + 'db_info': 'applications.view_application', + } + + http_method_names = ['get', 'post'] + + def list(self, request, *args, **kwargs): + ports = db_port_manager.get_already_use_ports() + return Response(data=ports, status=status.HTTP_200_OK) + + @action(methods=['get'], detail=False, url_path='db-info') + def db_info(self, request, *args, **kwargs): + port = request.query_params.get("port") + db = db_port_manager.get_db_by_port(port) + serializer = serializers.AppSerializer(instance=db) + return Response(data=serializer.data, status=status.HTTP_200_OK) diff --git a/apps/terminal/api/endpoint.py b/apps/terminal/api/endpoint.py index ca745d412..37de98576 100644 --- a/apps/terminal/api/endpoint.py +++ b/apps/terminal/api/endpoint.py @@ -47,11 +47,12 @@ class SmartEndpointViewMixin: return Endpoint.match_by_instance_label(self.target_instance, self.target_protocol) def match_endpoint_by_target_ip(self): - # 用来方便测试 - target_ip = self.request.GET.get('target_ip', '') + target_ip = self.request.GET.get('target_ip', '') # 支持target_ip参数,用来方便测试 if not target_ip and callable(getattr(self.target_instance, 'get_target_ip', None)): target_ip = self.target_instance.get_target_ip() - endpoint = EndpointRule.match_endpoint(target_ip, self.target_protocol, self.request) + endpoint = EndpointRule.match_endpoint( + self.target_instance, target_ip, self.target_protocol, self.request + ) return endpoint def get_target_instance(self): @@ -83,12 +84,7 @@ class SmartEndpointViewMixin: return instance def get_target_protocol(self): - protocol = None - if isinstance(self.target_instance, Application) and self.target_instance.is_type(Application.APP_TYPE.oracle): - protocol = self.target_instance.get_target_protocol_for_oracle() - if not protocol: - protocol = self.request.GET.get('protocol') - return protocol + return self.request.GET.get('protocol') class EndpointViewSet(SmartEndpointViewMixin, JMSBulkModelViewSet): diff --git a/apps/terminal/backends/command/es.py b/apps/terminal/backends/command/es.py index 20602769c..4d830b1b2 100644 --- a/apps/terminal/backends/command/es.py +++ b/apps/terminal/backends/command/es.py @@ -169,6 +169,14 @@ class CommandStore(object): return self.es.index(index=self.index, doc_type=self.doc_type, body=data) def filter(self, query: dict, from_=None, size=None, sort=None): + try: + data = self._filter(query, from_, size, sort) + except Exception as e: + logger.error('ES filter error: {}'.format(e)) + data = [] + return data + + def _filter(self, query: dict, from_=None, size=None, sort=None): body = self.get_query_body(**query) data = self.es.search( @@ -184,9 +192,14 @@ class CommandStore(object): return Command.from_multi_dict(source_data) def count(self, **query): - body = self.get_query_body(**query) - data = self.es.count(index=self.query_index, doc_type=self.doc_type, body=body) - return data["count"] + try: + body = self.get_query_body(**query) + data = self.es.count(index=self.query_index, doc_type=self.doc_type, body=body) + count = data["count"] + except Exception as e: + logger.error('ES count error: {}'.format(e)) + count = 0 + return count def __getattr__(self, item): return getattr(self.es, item) diff --git a/apps/terminal/migrations/0053_auto_20221009_1755.py b/apps/terminal/migrations/0053_auto_20221009_1755.py new file mode 100644 index 000000000..0a6e30993 --- /dev/null +++ b/apps/terminal/migrations/0053_auto_20221009_1755.py @@ -0,0 +1,37 @@ +# Generated by Django 3.2.15 on 2022-10-09 09:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('terminal', '0052_auto_20220713_1417'), + ] + + operations = [ + migrations.RemoveField( + model_name='endpoint', + name='mariadb_port', + ), + migrations.RemoveField( + model_name='endpoint', + name='mysql_port', + ), + migrations.RemoveField( + model_name='endpoint', + name='oracle_11g_port', + ), + migrations.RemoveField( + model_name='endpoint', + name='oracle_12c_port', + ), + migrations.RemoveField( + model_name='endpoint', + name='postgresql_port', + ), + migrations.RemoveField( + model_name='endpoint', + name='redis_port', + ), + ] diff --git a/apps/terminal/models/endpoint.py b/apps/terminal/models/endpoint.py index beefdeb69..fec3a1282 100644 --- a/apps/terminal/models/endpoint.py +++ b/apps/terminal/models/endpoint.py @@ -1,25 +1,24 @@ from django.db import models from django.utils.translation import ugettext_lazy as _ from django.core.validators import MinValueValidator, MaxValueValidator +from applications.models import Application +from ..utils import db_port_manager, DBPortManager from common.db.models import JMSModel from common.db.fields import PortField from common.utils.ip import contains_ip +db_port_manager: DBPortManager + class Endpoint(JMSModel): name = models.CharField(max_length=128, verbose_name=_('Name'), unique=True) host = models.CharField(max_length=256, blank=True, verbose_name=_('Host')) - # disabled value=0 + # value=0 表示 disabled https_port = PortField(default=443, verbose_name=_('HTTPS Port')) http_port = PortField(default=80, verbose_name=_('HTTP Port')) ssh_port = PortField(default=2222, verbose_name=_('SSH Port')) rdp_port = PortField(default=3389, verbose_name=_('RDP Port')) - mysql_port = PortField(default=33060, verbose_name=_('MySQL Port')) - mariadb_port = PortField(default=33061, verbose_name=_('MariaDB Port')) - postgresql_port = PortField(default=54320, verbose_name=_('PostgreSQL Port')) - redis_port = PortField(default=63790, verbose_name=_('Redis Port')) - oracle_11g_port = PortField(default=15211, verbose_name=_('Oracle 11g Port')) - oracle_12c_port = PortField(default=15212, verbose_name=_('Oracle 12c Port')) + comment = models.TextField(default='', blank=True, verbose_name=_('Comment')) default_id = '00000000-0000-0000-0000-000000000001' @@ -31,12 +30,14 @@ class Endpoint(JMSModel): def __str__(self): return self.name - def get_port(self, protocol): - return getattr(self, f'{protocol}_port', 0) - - def get_oracle_port(self, version): - protocol = f'oracle_{version}' - return self.get_port(protocol) + def get_port(self, target_instance, protocol): + if protocol in ['https', 'http', 'ssh', 'rdp']: + port = getattr(self, f'{protocol}_port', 0) + elif isinstance(target_instance, Application) and target_instance.category_db: + port = db_port_manager.get_port_by_db(target_instance) + else: + port = 0 + return port def is_default(self): return str(self.id) == self.default_id @@ -46,10 +47,10 @@ class Endpoint(JMSModel): return return super().delete(using, keep_parents) - def is_valid_for(self, protocol): + def is_valid_for(self, target_instance, protocol): if self.is_default(): return True - if self.host and self.get_port(protocol) != 0: + if self.host and self.get_port(target_instance, protocol) != 0: return True return False @@ -103,19 +104,19 @@ class EndpointRule(JMSModel): return f'{self.name}({self.priority})' @classmethod - def match(cls, target_ip, protocol): + def match(cls, target_instance, target_ip, protocol): for endpoint_rule in cls.objects.all().prefetch_related('endpoint'): if not contains_ip(target_ip, endpoint_rule.ip_group): continue if not endpoint_rule.endpoint: continue - if not endpoint_rule.endpoint.is_valid_for(protocol): + if not endpoint_rule.endpoint.is_valid_for(target_instance, protocol): continue return endpoint_rule @classmethod - def match_endpoint(cls, target_ip, protocol, request=None): - endpoint_rule = cls.match(target_ip, protocol) + def match_endpoint(cls, target_instance, target_ip, protocol, request=None): + endpoint_rule = cls.match(target_instance, target_ip, protocol) if endpoint_rule: endpoint = endpoint_rule.endpoint else: diff --git a/apps/terminal/serializers/endpoint.py b/apps/terminal/serializers/endpoint.py index 3d8e858ac..ce45ffa3d 100644 --- a/apps/terminal/serializers/endpoint.py +++ b/apps/terminal/serializers/endpoint.py @@ -2,25 +2,31 @@ from rest_framework import serializers from django.utils.translation import ugettext_lazy as _ from common.drf.serializers import BulkModelSerializer from acls.serializers.rules import ip_group_child_validator, ip_group_help_text +from ..utils import db_port_manager from ..models import Endpoint, EndpointRule + __all__ = ['EndpointSerializer', 'EndpointRuleSerializer'] class EndpointSerializer(BulkModelSerializer): - # 解决 luna 处理繁琐的问题,oracle_port 返回匹配到的端口 - oracle_port = serializers.SerializerMethodField(label=_('Oracle port')) + # 解决 luna 处理繁琐的问题, 返回 magnus 监听的当前 db 的 port + magnus_listen_db_port = serializers.SerializerMethodField(label=_('Magnus listen db port')) + magnus_listen_port_range = serializers.CharField( + max_length=128, default=db_port_manager.magnus_listen_port_range, read_only=True, + label=_('Magnus Listen port range'), + help_text=_( + 'The range of ports that Magnus listens on is modified in the configuration file' + ) + ) class Meta: model = Endpoint fields_mini = ['id', 'name'] fields_small = [ 'host', - 'https_port', 'http_port', 'ssh_port', - 'rdp_port', 'mysql_port', 'mariadb_port', - 'postgresql_port', 'redis_port', - 'oracle_11g_port', 'oracle_12c_port', - 'oracle_port', + 'https_port', 'http_port', 'ssh_port', 'rdp_port', + 'magnus_listen_db_port', 'magnus_listen_port_range', ] fields = fields_mini + fields_small + [ 'comment', 'date_created', 'date_updated', 'created_by' @@ -30,19 +36,13 @@ class EndpointSerializer(BulkModelSerializer): 'http_port': {'default': 80}, 'ssh_port': {'default': 2222}, 'rdp_port': {'default': 3389}, - 'mysql_port': {'default': 33060}, - 'mariadb_port': {'default': 33061}, - 'postgresql_port': {'default': 54320}, - 'redis_port': {'default': 63790}, - 'oracle_11g_port': {'default': 15211}, - 'oracle_12c_port': {'default': 15212}, } - def get_oracle_port(self, obj: Endpoint): + def get_magnus_listen_db_port(self, obj: Endpoint): view = self.context.get('view') if not view or view.action not in ['smart']: return 0 - return obj.get_port(view.target_protocol) + return obj.get_port(view.target_instance, view.target_protocol) class EndpointRuleSerializer(BulkModelSerializer): diff --git a/apps/terminal/signal_handlers.py b/apps/terminal/signal_handlers.py index ec51c5a2b..0d92a9f96 100644 --- a/apps/terminal/signal_handlers.py +++ b/apps/terminal/signal_handlers.py @@ -1,2 +1,36 @@ # -*- coding: utf-8 -*- # +from django.db.models.signals import post_save, post_delete + +from common.signals import django_ready +from django.dispatch import receiver +from common.utils import get_logger +from .models import Application +from .utils import db_port_manager, DBPortManager + +db_port_manager: DBPortManager + + +logger = get_logger(__file__) + + +@receiver(django_ready) +def init_db_port_mapper(sender, **kwargs): + logger.info('Init db port mapper') + db_port_manager.init() + + +@receiver(post_save, sender=Application) +def on_db_app_created(sender, instance: Application, created, **kwargs): + if not instance.category_db: + return + if not created: + return + db_port_manager.add(instance) + + +@receiver(post_delete, sender=Application) +def on_db_app_delete(sender, instance, **kwargs): + if not instance.category_db: + return + db_port_manager.pop(instance) diff --git a/apps/terminal/urls/api_urls.py b/apps/terminal/urls/api_urls.py index 3f0445350..8adcb8f52 100644 --- a/apps/terminal/urls/api_urls.py +++ b/apps/terminal/urls/api_urls.py @@ -24,6 +24,7 @@ router.register(r'session-sharings', api.SessionSharingViewSet, 'session-sharing router.register(r'session-join-records', api.SessionJoinRecordsViewSet, 'session-sharing-record') router.register(r'endpoints', api.EndpointViewSet, 'endpoint') router.register(r'endpoint-rules', api.EndpointRuleViewSet, 'endpoint-rule') +router.register(r'db-listen-ports', api.DBListenPortViewSet, 'db-listen-ports') urlpatterns = [ path('my-sessions/', api.MySessionAPIView.as_view(), name='my-session'), diff --git a/apps/terminal/utils/__init__.py b/apps/terminal/utils/__init__.py new file mode 100644 index 000000000..b55827a8d --- /dev/null +++ b/apps/terminal/utils/__init__.py @@ -0,0 +1,4 @@ +from .components import * +from .common import * +from .session_replay import * +from .db_port_mapper import * diff --git a/apps/terminal/utils/common.py b/apps/terminal/utils/common.py new file mode 100644 index 000000000..26fd303b2 --- /dev/null +++ b/apps/terminal/utils/common.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- +# + +from common.utils import get_logger +from .. import const +from tickets.models import TicketSession + + +logger = get_logger(__name__) + + +class ComputeStatUtil: + # system status + @staticmethod + def _common_compute_system_status(value, thresholds): + if thresholds[0] <= value <= thresholds[1]: + return const.ComponentStatusChoices.normal.value + elif thresholds[1] < value <= thresholds[2]: + return const.ComponentStatusChoices.high.value + else: + return const.ComponentStatusChoices.critical.value + + @classmethod + def _compute_system_stat_status(cls, stat): + system_stat_thresholds_mapper = { + 'cpu_load': [0, 5, 20], + 'memory_used': [0, 85, 95], + 'disk_used': [0, 80, 99] + } + system_status = {} + for stat_key, thresholds in system_stat_thresholds_mapper.items(): + stat_value = getattr(stat, stat_key) + if stat_value is None: + msg = 'stat: {}, stat_key: {}, stat_value: {}' + logger.debug(msg.format(stat, stat_key, stat_value)) + stat_value = 0 + status = cls._common_compute_system_status(stat_value, thresholds) + system_status[stat_key] = status + return system_status + + @classmethod + def compute_component_status(cls, stat): + if not stat: + return const.ComponentStatusChoices.offline + system_status_values = cls._compute_system_stat_status(stat).values() + if const.ComponentStatusChoices.critical in system_status_values: + return const.ComponentStatusChoices.critical + elif const.ComponentStatusChoices.high in system_status_values: + return const.ComponentStatusChoices.high + else: + return const.ComponentStatusChoices.normal + + +def is_session_approver(session_id, user_id): + ticket = TicketSession.get_ticket_by_session_id(session_id) + if not ticket: + return False + ok = ticket.has_all_assignee(user_id) + return ok diff --git a/apps/terminal/utils.py b/apps/terminal/utils/components.py similarity index 55% rename from apps/terminal/utils.py rename to apps/terminal/utils/components.py index abdfbd738..0610b0b79 100644 --- a/apps/terminal/utils.py +++ b/apps/terminal/utils/components.py @@ -1,124 +1,13 @@ # -*- coding: utf-8 -*- # -import os -from itertools import groupby, chain - -from django.conf import settings -from django.core.files.storage import default_storage - -import jms_storage +from itertools import groupby from common.utils import get_logger -from . import const -from .models import ReplayStorage -from tickets.models import TicketSession, TicketStep, TicketAssignee -from tickets.const import StepState logger = get_logger(__name__) -def find_session_replay_local(session): - # 存在外部存储上,所有可能的路径名 - session_paths = session.get_all_possible_relative_path() - - # 存在本地存储上,所有可能的路径名 - local_paths = session.get_all_possible_local_path() - - for _local_path in chain(session_paths, local_paths): - if default_storage.exists(_local_path): - url = default_storage.url(_local_path) - return _local_path, url - return None, None - - -def download_session_replay(session): - replay_storages = ReplayStorage.objects.all() - configs = { - storage.name: storage.config - for storage in replay_storages - if not storage.type_null_or_server - } - if settings.SERVER_REPLAY_STORAGE: - configs['SERVER_REPLAY_STORAGE'] = settings.SERVER_REPLAY_STORAGE - if not configs: - msg = "Not found replay file, and not remote storage set" - return None, msg - storage = jms_storage.get_multi_object_storage(configs) - - # 获取外部存储路径名 - session_path = session.find_ok_relative_path_in_storage(storage) - if not session_path: - msg = "Not found session replay file" - return None, msg - - # 通过外部存储路径名后缀,构造真实的本地存储路径 - local_path = session.get_local_path_by_relative_path(session_path) - - # 保存到storage的路径 - target_path = os.path.join(default_storage.base_location, local_path) - target_dir = os.path.dirname(target_path) - if not os.path.isdir(target_dir): - os.makedirs(target_dir, exist_ok=True) - - ok, err = storage.download(session_path, target_path) - if not ok: - msg = "Failed download replay file: {}".format(err) - logger.error(msg) - return None, msg - url = default_storage.url(local_path) - return local_path, url - - -def get_session_replay_url(session): - local_path, url = find_session_replay_local(session) - if local_path is None: - local_path, url = download_session_replay(session) - return local_path, url - - -class ComputeStatUtil: - # system status - @staticmethod - def _common_compute_system_status(value, thresholds): - if thresholds[0] <= value <= thresholds[1]: - return const.ComponentStatusChoices.normal.value - elif thresholds[1] < value <= thresholds[2]: - return const.ComponentStatusChoices.high.value - else: - return const.ComponentStatusChoices.critical.value - - @classmethod - def _compute_system_stat_status(cls, stat): - system_stat_thresholds_mapper = { - 'cpu_load': [0, 5, 20], - 'memory_used': [0, 85, 95], - 'disk_used': [0, 80, 99] - } - system_status = {} - for stat_key, thresholds in system_stat_thresholds_mapper.items(): - stat_value = getattr(stat, stat_key) - if stat_value is None: - msg = 'stat: {}, stat_key: {}, stat_value: {}' - logger.debug(msg.format(stat, stat_key, stat_value)) - stat_value = 0 - status = cls._common_compute_system_status(stat_value, thresholds) - system_status[stat_key] = status - return system_status - - @classmethod - def compute_component_status(cls, stat): - if not stat: - return const.ComponentStatusChoices.offline - system_status_values = cls._compute_system_stat_status(stat).values() - if const.ComponentStatusChoices.critical in system_status_values: - return const.ComponentStatusChoices.critical - elif const.ComponentStatusChoices.high in system_status_values: - return const.ComponentStatusChoices.high - else: - return const.ComponentStatusChoices.normal - - class TypedComponentsStatusMetricsUtil(object): def __init__(self): self.components = [] @@ -126,7 +15,7 @@ class TypedComponentsStatusMetricsUtil(object): self.get_components() def get_components(self): - from .models import Terminal + from ..models import Terminal components = Terminal.objects.filter(is_deleted=False).order_by('type') grouped_components = groupby(components, lambda c: c.type) grouped_components = [(i[0], list(i[1])) for i in grouped_components] @@ -251,10 +140,3 @@ class ComponentsPrometheusMetricsUtil(TypedComponentsStatusMetricsUtil): prometheus_metrics_text = '\n'.join(prometheus_metrics) return prometheus_metrics_text - -def is_session_approver(session_id, user_id): - ticket = TicketSession.get_ticket_by_session_id(session_id) - if not ticket: - return False - ok = ticket.has_all_assignee(user_id) - return ok diff --git a/apps/terminal/utils/db_port_mapper.py b/apps/terminal/utils/db_port_mapper.py new file mode 100644 index 000000000..ab52aa2a3 --- /dev/null +++ b/apps/terminal/utils/db_port_mapper.py @@ -0,0 +1,112 @@ +from django.utils.translation import ugettext_lazy as _ +from django.core.cache import cache +from django.conf import settings + +from common.decorator import Singleton +from common.utils import get_logger, get_object_or_none +from common.exceptions import JMSException +from applications.const import AppCategory +from applications.models import Application +from orgs.utils import tmp_to_root_org + + +logger = get_logger(__file__) + + +@Singleton +class DBPortManager(object): + """ 管理端口-数据库ID的映射, Magnus 要使用 """ + + CACHE_KEY = 'PORT_DB_MAPPER' + + def __init__(self): + self.port_start = settings.MAGNUS_DB_PORTS_START + self.port_limit = settings.MAGNUS_DB_PORTS_LIMIT_COUNT + self.port_end = self.port_start + self.port_limit + # 可以使用的端口列表 + self.all_available_ports = list(range(self.port_start, self.port_end)) + + @property + def magnus_listen_port_range(self): + return f'{self.port_start}-{self.port_end - 1}' + + def init(self): + with tmp_to_root_org(): + db_ids = Application.objects.filter(category=AppCategory.db).values_list('id', flat=True) + db_ids = [str(i) for i in db_ids] + mapper = dict(zip(self.all_available_ports, list(db_ids))) + self.set_mapper(mapper) + + def add(self, db: Application): + mapper = self.get_mapper() + available_port = self.get_next_available_port() + mapper.update({available_port: str(db.id)}) + self.set_mapper(mapper) + return True + + def pop(self, db: Application): + mapper = self.get_mapper() + to_delete_port = self.get_port_by_db(db) + mapper.pop(to_delete_port, None) + self.set_mapper(mapper) + + def get_port_by_db(self, db): + mapper = self.get_mapper() + for port, db_id in mapper.items(): + if db_id == str(db.id): + return port + raise JMSException( + 'Not matched db port, db id: {}, mapper length: {}'.format(db.id, len(mapper)) + ) + + def get_db_by_port(self, port): + try: + port = int(port) + except Exception as e: + raise JMSException('Port type error: {}'.format(e)) + mapper = self.get_mapper() + db_id = mapper.get(port, None) + if not db_id: + raise JMSException('Database not in port-db mapper, port: {}'.format(port)) + with tmp_to_root_org(): + db = get_object_or_none(Application, id=db_id) + if not db: + raise JMSException('Database not exists, db id: {}'.format(db_id)) + return db + + def get_next_available_port(self): + already_use_ports = self.get_already_use_ports() + available_ports = sorted(list(set(self.all_available_ports) - set(already_use_ports))) + if len(available_ports) <= 0: + msg = _('No ports can be used, check and modify the limit on the number ' + 'of ports that Magnus listens on in the configuration file.') + tips = _('All available port count: {}, Already use port count: {}').format( + len(self.all_available_ports), len(already_use_ports) + ) + error = msg + tips + raise JMSException(error) + port = available_ports[0] + logger.debug('Get next available port: {}'.format(port)) + return port + + def get_already_use_ports(self): + mapper = self.get_mapper() + return sorted(list(mapper.keys())) + + def get_mapper(self): + mapper = cache.get(self.CACHE_KEY, {}) + if not mapper: + # redis 可能被清空,重新初始化一下 + self.init() + return cache.get(self.CACHE_KEY, {}) + + def set_mapper(self, value): + """ + value: { + port: db_id + } + """ + cache.set(self.CACHE_KEY, value, timeout=None) + + +db_port_manager = DBPortManager() diff --git a/apps/terminal/utils/session_replay.py b/apps/terminal/utils/session_replay.py new file mode 100644 index 000000000..f1b061cb0 --- /dev/null +++ b/apps/terminal/utils/session_replay.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# +import os +from itertools import groupby, chain + +from django.conf import settings +from django.core.files.storage import default_storage + +import jms_storage + +from common.utils import get_logger +from ..models import ReplayStorage + + +logger = get_logger(__name__) + + +def find_session_replay_local(session): + # 存在外部存储上,所有可能的路径名 + session_paths = session.get_all_possible_relative_path() + + # 存在本地存储上,所有可能的路径名 + local_paths = session.get_all_possible_local_path() + + for _local_path in chain(session_paths, local_paths): + if default_storage.exists(_local_path): + url = default_storage.url(_local_path) + return _local_path, url + return None, None + + +def download_session_replay(session): + replay_storages = ReplayStorage.objects.all() + configs = { + storage.name: storage.config + for storage in replay_storages + if not storage.type_null_or_server + } + if settings.SERVER_REPLAY_STORAGE: + configs['SERVER_REPLAY_STORAGE'] = settings.SERVER_REPLAY_STORAGE + if not configs: + msg = "Not found replay file, and not remote storage set" + return None, msg + storage = jms_storage.get_multi_object_storage(configs) + + # 获取外部存储路径名 + session_path = session.find_ok_relative_path_in_storage(storage) + if not session_path: + msg = "Not found session replay file" + return None, msg + + # 通过外部存储路径名后缀,构造真实的本地存储路径 + local_path = session.get_local_path_by_relative_path(session_path) + + # 保存到storage的路径 + target_path = os.path.join(default_storage.base_location, local_path) + target_dir = os.path.dirname(target_path) + if not os.path.isdir(target_dir): + os.makedirs(target_dir, exist_ok=True) + + ok, err = storage.download(session_path, target_path) + if not ok: + msg = "Failed download replay file: {}".format(err) + logger.error(msg) + return None, msg + url = default_storage.url(local_path) + return local_path, url + + +def get_session_replay_url(session): + local_path, url = find_session_replay_local(session) + if local_path is None: + local_path, url = download_session_replay(session) + return local_path, url + diff --git a/apps/tickets/serializers/ticket/apply_application.py b/apps/tickets/serializers/ticket/apply_application.py index 12b3f230d..740eda645 100644 --- a/apps/tickets/serializers/ticket/apply_application.py +++ b/apps/tickets/serializers/ticket/apply_application.py @@ -19,7 +19,7 @@ class ApplyApplicationSerializer(BaseApplyAssetApplicationSerializer, TicketAppl class Meta: model = ApplyApplicationTicket writeable_fields = [ - 'id', 'title', 'type', 'apply_category', + 'id', 'title', 'type', 'apply_category', 'comment', 'apply_type', 'apply_applications', 'apply_system_users', 'apply_actions', 'apply_date_start', 'apply_date_expired', 'org_id' ] diff --git a/apps/tickets/serializers/ticket/apply_asset.py b/apps/tickets/serializers/ticket/apply_asset.py index 93a4026c1..b1de8cf53 100644 --- a/apps/tickets/serializers/ticket/apply_asset.py +++ b/apps/tickets/serializers/ticket/apply_asset.py @@ -23,7 +23,7 @@ class ApplyAssetSerializer(BaseApplyAssetApplicationSerializer, TicketApplySeria model = ApplyAssetTicket writeable_fields = [ 'id', 'title', 'type', 'apply_nodes', 'apply_assets', - 'apply_system_users', 'apply_actions', + 'apply_system_users', 'apply_actions', 'comment', 'apply_date_start', 'apply_date_expired', 'org_id' ] fields = TicketApplySerializer.Meta.fields + \ diff --git a/apps/users/notifications.py b/apps/users/notifications.py index 645e206cd..cbc6e1976 100644 --- a/apps/users/notifications.py +++ b/apps/users/notifications.py @@ -191,7 +191,7 @@ class UserExpirationReminderMsg(UserMessage): class ResetSSHKeyMsg(UserMessage): def get_html_msg(self) -> dict: subject = _('Reset SSH Key') - update_url = urljoin(settings.SITE_URL, '/ui/#/users/profile/?activeTab=SSHUpdate') + update_url = urljoin(settings.SITE_URL, '/ui/#/profile/setting/?activeTab=SSHUpdate') context = { 'name': self.user.name, 'url': update_url, diff --git a/config_example.yml b/config_example.yml index f8f458a80..a21edb01b 100644 --- a/config_example.yml +++ b/config_example.yml @@ -80,8 +80,6 @@ REDIS_PORT: 6379 # 启用定时任务 # PERIOD_TASK_ENABLED: True # -# Windows 登录跳过手动输入密码 -# WINDOWS_SKIP_ALL_MANUAL_PASSWORD: False # 是否开启 Luna 水印 # SECURITY_WATERMARK_ENABLED: False @@ -97,3 +95,4 @@ REDIS_PORT: 6379 # 仅允许已存在的用户登录,不允许第三方认证后,自动创建用户 # ONLY_ALLOW_EXIST_USER_AUTH: False + diff --git a/data/caution.txt b/data/caution.txt index 4e85670a1..3feabb2e4 100644 --- a/data/caution.txt +++ b/data/caution.txt @@ -1,2 +1 @@ - 你想偷看啥 !!! - What are you trying to peek at !!! \ No newline at end of file + What are you trying to peek at !!! diff --git a/docs/README.md b/docs/README.md index e4d922911..e29c64391 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,3 +3,4 @@ ## 访问在线文档 [访问](https://docs.jumpserver.org) + diff --git a/entrypoint.sh b/entrypoint.sh index 58ed0b104..a752ccc8c 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -22,3 +22,4 @@ elif [[ "$action" == "sleep" ]];then else python jms "${action}" "${service}" fi + diff --git a/jms b/jms index 5d5b5c433..bc230b268 100755 --- a/jms +++ b/jms @@ -189,3 +189,4 @@ if __name__ == '__main__': collect_static() else: start_services() + diff --git a/logs/.gitkeep b/logs/.gitkeep index e69de29bb..1a4baf536 100644 --- a/logs/.gitkeep +++ b/logs/.gitkeep @@ -0,0 +1 @@ + diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 863759fad..b8fb4d09a 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -62,7 +62,7 @@ jsonfield2==4.0.0.post0 geoip2==4.5.0 ipip-ipdb==1.6.1 # Django environment -Django==3.2.14 +Django==3.2.15 django-bootstrap3==14.2.0 django-filter==2.4.0 django-formtools==2.2 @@ -91,7 +91,7 @@ eventlet==0.33.1 greenlet==1.1.2 gunicorn==20.1.0 celery==5.2.7 -flower==1.0.0 +flower==1.2.0 django-celery-beat==2.3.0 kombu==5.2.4 # Auth @@ -133,6 +133,7 @@ pymssql==2.1.5 django-mysql==3.9.0 django-redis==5.2.0 python-redis-lock==3.7.0 +pyOpenSSL==22.0.0 redis==4.3.3 pymongo==4.2.0 # Debug diff --git a/run_server.py b/run_server.py index c7ec7bccb..b7cce251b 100644 --- a/run_server.py +++ b/run_server.py @@ -6,6 +6,5 @@ import subprocess if __name__ == '__main__': - subprocess.call('python3 jms start all', shell=True, - stdin=sys.stdin, stdout=sys.stdout) + subprocess.call('python3 jms start all', shell=True, stdin=sys.stdin, stdout=sys.stdout) diff --git a/tmp/.gitkeep b/tmp/.gitkeep index e69de29bb..8d1c8b69c 100644 --- a/tmp/.gitkeep +++ b/tmp/.gitkeep @@ -0,0 +1 @@ + diff --git a/utils/redis.conf b/utils/redis.conf index 0621db3bf..c28fbc6fa 100644 --- a/utils/redis.conf +++ b/utils/redis.conf @@ -1,4 +1,5 @@ daemonize yes port 6379 logfile "/opt/jumpserver/logs/redis.log" -dir /opt/jumpserver/ \ No newline at end of file +dir /opt/jumpserver/ +