From 52ce719bc1c64b4c5579d57e3911e532d180404b Mon Sep 17 00:00:00 2001 From: Badanin Maksim Date: Fri, 8 Mar 2024 22:03:59 +0300 Subject: [PATCH] init --- .env | 135 ++ README.md | 30 + config/createdb.sql | 4 + config/docspace-logs | 33 + config/docspace-ssl-setup | 151 ++ config/install_scripts/docspace-install.sh | 187 +++ config/install_scripts/install-Docker.sh | 1390 +++++++++++++++++ config/mysql/conf.d/mysql.cnf | 5 + .../10-listen-on-ipv6-by-default.sh | 67 + .../15-local-resolvers.envsh | 11 + .../20-envsubst-on-templates.sh | 78 + .../30-tune-worker-processes.sh | 188 +++ config/nginx/docker-entrypoint.sh | 47 + config/nginx/letsencrypt.conf | 4 + config/nginx/onlyoffice-proxy-ssl.conf | 76 + config/nginx/onlyoffice-proxy.conf | 19 + config/nginx/templates/nginx.conf.template | 33 + .../templates/proxy.upstream.conf.template | 32 + config/nginx/templates/upstream.conf.template | 79 + docker-compose.yml | 383 +++++ plugins/drawio.zip | Bin 0 -> 41833 bytes plugins/pdf-converter.zip | Bin 0 -> 28709 bytes 22 files changed, 2952 insertions(+) create mode 100644 .env create mode 100644 README.md create mode 100644 config/createdb.sql create mode 100644 config/docspace-logs create mode 100644 config/docspace-ssl-setup create mode 100644 config/install_scripts/docspace-install.sh create mode 100644 config/install_scripts/install-Docker.sh create mode 100644 config/mysql/conf.d/mysql.cnf create mode 100755 config/nginx/docker-entrypoint.d/10-listen-on-ipv6-by-default.sh create mode 100644 config/nginx/docker-entrypoint.d/15-local-resolvers.envsh create mode 100755 config/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh create mode 100755 config/nginx/docker-entrypoint.d/30-tune-worker-processes.sh create mode 100755 config/nginx/docker-entrypoint.sh create mode 100644 config/nginx/letsencrypt.conf create mode 100644 config/nginx/onlyoffice-proxy-ssl.conf create mode 100644 config/nginx/onlyoffice-proxy.conf create mode 100644 config/nginx/templates/nginx.conf.template create mode 100644 config/nginx/templates/proxy.upstream.conf.template create mode 100644 config/nginx/templates/upstream.conf.template create mode 100644 docker-compose.yml create mode 100644 plugins/drawio.zip create mode 100644 plugins/pdf-converter.zip diff --git a/.env b/.env new file mode 100644 index 0000000..f457d98 --- /dev/null +++ b/.env @@ -0,0 +1,135 @@ +# docker-compose tags # + PRODUCT=onlyoffice + REPO=${PRODUCT} + INSTALLATION_TYPE=COMMUNITY + STATUS="" + DOCKER_IMAGE_PREFIX=${STATUS}docspace + DOCKER_TAG=2.0.3 + CONTAINER_PREFIX=${PRODUCT}- + MYSQL_VERSION=8.0.32 + MYSQL_IMAGE=mysql:${MYSQL_VERSION} + ELK_VERSION=7.16.3 + SERVICE_PORT=5050 + DOCUMENT_SERVER_IMAGE_NAME= # onlyoffice/documentserver-unlim:7.5.1.1 + DOCKERFILE=Dockerfile.app + APP_DOTNET_ENV="" + EXTERNAL_PORT=80 + +# zookeeper # + # ZOO_PORT=2181 + # ZOO_HOST=${CONTAINER_PREFIX}zookeeper + # ZOO_SERVER=server.1=${ZOO_HOST}:2888:3888 + +# kafka # + # KAFKA_HOST=${CONTAINER_PREFIX}kafka + # KAFKA_ADVERTISED_LISTENERS=LISTENER_DOCKER_INTERNAL://${KAFKA_HOST}:9092 + # KAFKA_LISTENER_SECURITY_PROTOCOL_MAP=LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT + # KAFKA_INTER_BROKER_LISTENER_NAME=LISTENER_DOCKER_INTERNAL + # KAFKA_ZOOKEEPER_CONNECT=${ZOO_HOST}:2181 + # KAFKA_BROKER_ID=1 + # KAFKA_LOG4J_LOGGERS=kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO + # KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR=1 + +# elasticsearch # + ELK_CONTAINER_NAME=${CONTAINER_PREFIX}elasticsearch + ELK_SHEME=http + ELK_HOST="" + ELK_PORT=9200 + +# app service environment # + ENV_EXTENSION=none + APP_CORE_BASE_DOMAIN=localhost + # APP_URL_PORTAL=http://onlyoffice-router:8092 + APP_URL_PORTAL= # Example: https://office.example.com + OAUTH_REDIRECT_URL="https://service.onlyoffice.com/oauth2.aspx" + WRONG_PORTAL_NAME_URL="" + LOG_LEVEL="Warning" + DEBUG_INFO="false" + + APP_KNOWN_PROXIES="" + APP_KNOWN_NETWORKS="" + APP_CORE_MACHINEKEY= # CHANGE Example: cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 12 + + CERTIFICATE_PATH="" + CERTIFICATE_KEY_PATH="" + DHPARAM_PATH="" + +# docs # + DOCUMENT_CONTAINER_NAME=${CONTAINER_PREFIX}document-server + DOCUMENT_SERVER_URL_EXTERNAL= # CHANGE Example: "https://docs.example.com" + DOCUMENT_SERVER_JWT_SECRET= # CHANGE Example: cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 + DOCUMENT_SERVER_JWT_HEADER=AuthorizationJwt + DOCUMENT_SERVER_URL_PUBLIC=/ds-vpath/ + +# redis # + REDIS_CONTAINER_NAME=${CONTAINER_PREFIX}redis + REDIS_HOST="" + REDIS_PORT=6379 + REDIS_USER_NAME="" + REDIS_PASSWORD="" + +# rabbitmq # + RABBIT_CONTAINER_NAME=${CONTAINER_PREFIX}rabbitmq + RABBIT_HOST="" + RABBIT_PORT=5672 + RABBIT_VIRTUAL_HOST=/ + RABBIT_USER_NAME=guest + RABBIT_PASSWORD=guest + +# mysql # + MYSQL_CONTAINER_NAME=${CONTAINER_PREFIX}mysql-server + MYSQL_HOST="" + MYSQL_PORT=3306 + MYSQL_ROOT_PASSWORD= # CHANGE Example: cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 20 + MYSQL_DATABASE=docspace + MYSQL_USER=${PRODUCT}_user + MYSQL_PASSWORD= # CHANGE Example: cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 20 + DATABASE_MIGRATION=true + MIGRATION_TYPE="SAAS" + +# service host # + API_SYSTEM_HOST=${CONTAINER_PREFIX}api-system + BACKUP_HOST=${CONTAINER_PREFIX}backup + BACKUP_BACKGRUOND_TASKS_HOST=${CONTAINER_PREFIX}backup-background-tasks + CLEAR_EVENTS_HOST=${CONTAINER_PREFIX}clear-events + FILES_HOST=${CONTAINER_PREFIX}files + FILES_SERVICES_HOST=${CONTAINER_PREFIX}files-services + STORAGE_MIGRATION_HOST=${CONTAINER_PREFIX}storage-migration + NOTIFY_HOST=${CONTAINER_PREFIX}notify + PEOPLE_SERVER_HOST=${CONTAINER_PREFIX}people-server + SOCKET_HOST=${CONTAINER_PREFIX}socket + STUDIO_NOTIFY_HOST=${CONTAINER_PREFIX}studio-notify + API_HOST=${CONTAINER_PREFIX}api + STUDIO_HOST=${CONTAINER_PREFIX}studio + SSOAUTH_HOST=${CONTAINER_PREFIX}ssoauth + TELEGRAMREPORTS_HOST=${CONTAINER_PREFIX}telegramreports + MIGRATION_RUNNER_HOST=${CONTAINER_PREFIX}migration-runner + PROXY_HOST=${CONTAINER_PREFIX}proxy + ROUTER_HOST=${CONTAINER_PREFIX}router + DOCEDITOR_HOST=${CONTAINER_PREFIX}doceditor + LOGIN_HOST=${CONTAINER_PREFIX}login + HELTHCHECKS_HOST=${CONTAINER_PREFIX}healthchecks + +# router upstream environment # + SERVICE_API_SYSTEM=${API_SYSTEM_HOST}:${SERVICE_PORT} + SERVICE_BACKUP=${BACKUP_HOST}:${SERVICE_PORT} + SERVICE_BACKUP_BACKGRUOND_TASKS=${BACKUP_BACKGRUOND_TASKS_HOST}:${SERVICE_PORT} + SERVICE_CLEAR_EVENTS=${CLEAR_EVENTS_HOST}:${SERVICE_PORT} + SERVICE_FILES=${FILES_HOST}:${SERVICE_PORT} + SERVICE_FILES_SERVICES=${FILES_SERVICES_HOST}:${SERVICE_PORT} + SERVICE_STORAGE_MIGRATION=${STORAGE_MIGRATION_HOST}:${SERVICE_PORT} + SERVICE_NOTIFY=${NOTIFY_HOST}:${SERVICE_PORT} + SERVICE_PEOPLE_SERVER=${PEOPLE_SERVER_HOST}:${SERVICE_PORT} + SERVICE_SOCKET=${SOCKET_HOST}:${SERVICE_PORT} + SERVICE_STUDIO_NOTIFY=${STUDIO_NOTIFY_HOST}:${SERVICE_PORT} + SERVICE_API=${API_HOST}:${SERVICE_PORT} + SERVICE_STUDIO=${STUDIO_HOST}:${SERVICE_PORT} + SERVICE_SSOAUTH=${SSOAUTH_HOST}:${SERVICE_PORT} + SERVICE_TELEGRAMREPORTS=${TELEGRAMREPORTS_HOST}:${SERVICE_PORT} + SERVICE_DOCEDITOR=${DOCEDITOR_HOST}:5013 + SERVICE_LOGIN=${LOGIN_HOST}:5011 + SERVICE_HELTHCHECKS=${HELTHCHECKS_HOST}:${SERVICE_PORT} + + NETWORK_NAME=onlyoffice + + COMPOSE_IGNORE_ORPHANS=True diff --git a/README.md b/README.md new file mode 100644 index 0000000..34cbf52 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +## ONLYOFFICE DocSpace + +Установочный скрипт: + +[ONLYOFFICE DocSpace Community](https://www.onlyoffice.com/download-docspace.aspx?from=downloadintegrationmenu#docspace-community) + +Плагины для DocSpace: + +[ONLYOFFICE DocSpace plugins](https://github.com/ONLYOFFICE/docspace-plugins) + +[Building plugin](https://api.onlyoffice.com/docspace/pluginssdk/buildingplugin) + + +#### Заменить в файле `.env`: + +DOCUMENT_SERVER_IMAGE_NAME= # onlyoffice/documentserver-unlim:7.5.1.1 +APP_URL_PORTAL= # Example: https://office.example.com +APP_CORE_MACHINEKEY= # Example: cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 12 +DOCUMENT_SERVER_URL_EXTERNAL= # Example: "https://docs.example.com" +DOCUMENT_SERVER_JWT_SECRET= # Example: cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 32 +MYSQL_ROOT_PASSWORD= # Example: cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 20 +MYSQL_PASSWORD= # Example: cat /dev/urandom | tr -dc A-Za-z0-9 | head -c 20 + +#### Запуск: + +``` +git clone https://git.badms.ru/bms/docspace +cd docspace +docker compose up -d +``` diff --git a/config/createdb.sql b/config/createdb.sql new file mode 100644 index 0000000..074548c --- /dev/null +++ b/config/createdb.sql @@ -0,0 +1,4 @@ +CREATE DATABASE IF NOT EXISTS `DB_NAME` CHARACTER SET utf8 COLLATE 'utf8_general_ci'; +use `DB_NAME`; +set @@global.max_allowed_packet = 104857600; +set @@global.group_concat_max_len = 20971520; diff --git a/config/docspace-logs b/config/docspace-logs new file mode 100644 index 0000000..ad3d163 --- /dev/null +++ b/config/docspace-logs @@ -0,0 +1,33 @@ +#!/bin/bash + +set -e + +PRODUCT="docspace" +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +DOCKERCOMPOSE=$(dirname "$DIR") + +if [ -f "${DOCKERCOMPOSE}/docspace.yml" ]; then + : +elif [ -f "/app/onlyoffice/${PRODUCT}.yml" ]; then + DOCKERCOMPOSE="/app/onlyoffice" +else + echo "Error: yml files not found." && exit 1 +fi + +FILES=("${PRODUCT}" "notify" "healthchecks" "proxy" "ds" "rabbitmq" "redis" "elasticsearch" "db") + +LOG_DIR="${DOCKERCOMPOSE}/logs" +mkdir -p ${LOG_DIR} + +echo "Creating ${PRODUCT} logs to a directory ${LOG_DIR}..." +for FILE in "${FILES[@]}"; do + SERVICE_NAMES=($(docker-compose -f ${DOCKERCOMPOSE}/${FILE}.yml config --services)) + for SERVICE_NAME in "${SERVICE_NAMES[@]}"; do + if [[ $(docker-compose -f ${DOCKERCOMPOSE}/${FILE}.yml ps -q ${SERVICE_NAME} | wc -l) -eq 1 ]]; then + docker-compose -f ${DOCKERCOMPOSE}/${FILE}.yml logs ${SERVICE_NAME} > ${LOG_DIR}/${SERVICE_NAME}.log + else + echo "The ${SERVICE_NAME} service is not running" + fi + done +done +echo "OK" diff --git a/config/docspace-ssl-setup b/config/docspace-ssl-setup new file mode 100644 index 0000000..32f7ff3 --- /dev/null +++ b/config/docspace-ssl-setup @@ -0,0 +1,151 @@ +#!/bin/bash + +set -e + +PRODUCT="docspace" +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +DOCKERCOMPOSE=$(dirname "$DIR") +LETSENCRYPT="/etc/letsencrypt/live"; +DHPARAM_FILE="/etc/ssl/certs/dhparam.pem" +WEBROOT_PATH="/letsencrypt" + +# Check if configuration files are present +if [ -f "/app/onlyoffice/.env" -a -f "/app/onlyoffice/proxy.yml" -a -f "/app/onlyoffice/proxy-ssl.yml" ]; then + DOCKERCOMPOSE="/app/onlyoffice" + DIR="/app/onlyoffice/config" +elif [ -f "${DOCKERCOMPOSE}/.env" -a -f "${DOCKERCOMPOSE}/proxy.yml" -a -f "${DOCKERCOMPOSE}/proxy-ssl.yml" ]; then + : +else + echo "Error: configuration files not found." && exit 1 +fi + +help(){ + echo "" + echo "This script provided to automatically setup SSL Certificates for DocSpace" + echo "Automatically get Let's Encrypt SSL Certificates:" + echo " docspace-ssl-setup EMAIL DOMAIN" + echo " EMAIL Email used for registration and recovery contact." + echo " Use comma to register multiple emails, ex:" + echo " u1@example.com,u2@example.com." + echo " DOMAIN Domain name to apply" + echo "" + echo "Using your own certificates via the -f or --file parameter:" + echo " docspace-ssl-setup --file DOMAIN CERTIFICATE PRIVATEKEY" + echo " DOMAIN Domain name to apply." + echo " CERTIFICATE Path to the certificate file for the domain." + echo " PRIVATEKEY Path to the private key file for the certificate." + echo "" + echo "Return to the default proxy configuration using the -d or --default parameter:" + echo " docspace-ssl-setup --default" + echo "" + exit 0 +} + +case $1 in + -f | --file ) + if [ -n "$2" ] && [ -n "$3" ] && [ -n "$4" ]; then + echo "Using specified files to configure SSL..." + DOMAIN=$2 + CERTIFICATE_FILE=$3 + PRIVATEKEY_FILE=$4 + else + help + fi + ;; + + -d | --default ) + echo "Return to the default proxy configuration..." + if [ -z "$(awk -F '=' '/^\s*DOCUMENT_SERVER_URL_EXTERNAL/{gsub(/^[[:space:]]*"|"[[:space:]]*$/, "", $2); print $2}' ${DOCKERCOMPOSE}/.env)" ]; then + sed "s#\(APP_URL_PORTAL=\).*#\1\"http://onlyoffice-router:8092\"#g" -i ${DOCKERCOMPOSE}/.env + else + sed "s#\(APP_URL_PORTAL=\).*#\1\"http://$(curl -s ifconfig.me)\"#g" -i ${DOCKERCOMPOSE}/.env + fi + + [[ -f "${DIR}/${PRODUCT}-renew-letsencrypt" ]] && rm -rf "${DIR}/${PRODUCT}-renew-letsencrypt" + + if docker ps -f "name=onlyoffice-proxy" --format '{{.Names}}' | grep -q "onlyoffice-proxy"; then + if docker ps -f "name=onlyoffice-proxy" --format "{{.Ports}}" | grep -q "443"; then + docker-compose -f ${DOCKERCOMPOSE}/proxy-ssl.yml down + fi + fi + + docker-compose -f ${DOCKERCOMPOSE}/proxy.yml up -d + docker-compose -f ${DOCKERCOMPOSE}/docspace.yml restart onlyoffice-files + + echo "OK" + exit 0 + ;; + + * ) + if [ "$#" -ge "2" ]; then + MAIL=$1 + DOMAIN=$2 + LETSENCRYPT_ENABLE="true" + + if ! docker volume inspect "onlyoffice_webroot_path" &> /dev/null; then + echo "Error: missing webroot_path volume" && exit 1 + fi + + if ! docker ps -f "name=onlyoffice-proxy" --format '{{.Names}}' | grep -q "onlyoffice-proxy"; then + echo "Error: the proxy container is not running" && exit 1 + fi + + echo "Generating Let's Encrypt SSL Certificates..." + + # Request and generate Let's Encrypt SSL certificate + docker run -it --rm \ + -v /etc/letsencrypt:/etc/letsencrypt \ + -v /var/lib/letsencrypt:/var/lib/letsencrypt \ + -v /var/log:/var/log \ + -v onlyoffice_webroot_path:${WEBROOT_PATH} \ + certbot/certbot certonly \ + --expand --webroot -w ${WEBROOT_PATH} \ + --cert-name ${PRODUCT} --non-interactive --agree-tos --email ${MAIL} -d ${DOMAIN} + else + help + fi + ;; +esac + +[[ ! -f "${DHPARAM_FILE}" ]] && openssl dhparam -out ${DHPARAM_FILE} 2048 +CERTIFICATE_FILE="${CERTIFICATE_FILE:-"${LETSENCRYPT}/${PRODUCT}/fullchain.pem"}" +PRIVATEKEY_FILE="${PRIVATEKEY_FILE:-"${LETSENCRYPT}/${PRODUCT}/privkey.pem"}" + +if [ -f "${CERTIFICATE_FILE}" ]; then + if [ -f "${PRIVATEKEY_FILE}" ]; then + docker-compose -f ${DOCKERCOMPOSE}/proxy.yml down + docker-compose -f ${DOCKERCOMPOSE}/docspace.yml stop onlyoffice-files + + sed -i "s~\(APP_URL_PORTAL=\).*~\1\"https://${DOMAIN}\"~g" ${DOCKERCOMPOSE}/.env + sed -i "s~\(CERTIFICATE_PATH=\).*~\1\"${CERTIFICATE_FILE}\"~g" ${DOCKERCOMPOSE}/.env + sed -i "s~\(CERTIFICATE_KEY_PATH=\).*~\1\"${PRIVATEKEY_FILE}\"~g" ${DOCKERCOMPOSE}/.env + sed -i "s~\(DHPARAM_PATH=\).*~\1\"${DHPARAM_FILE}\"~g" ${DOCKERCOMPOSE}/.env + + if [[ "${LETSENCRYPT_ENABLE}" = "true" ]]; then + # Create and set permissions for docspace-renew-letsencrypt + echo '#!/bin/bash' > ${DIR}/${PRODUCT}-renew-letsencrypt + echo "docker-compose -f ${DOCKERCOMPOSE}/proxy-ssl.yml down" >> ${DIR}/${PRODUCT}-renew-letsencrypt + echo 'docker run -it --rm \' >> ${DIR}/${PRODUCT}-renew-letsencrypt + echo ' -v /etc/letsencrypt:/etc/letsencrypt \' >> ${DIR}/${PRODUCT}-renew-letsencrypt + echo ' -v /var/lib/letsencrypt:/var/lib/letsencrypt \' >> ${DIR}/${PRODUCT}-renew-letsencrypt + echo ' certbot/certbot renew' >> ${DIR}/${PRODUCT}-renew-letsencrypt + echo "docker-compose -f ${DOCKERCOMPOSE}/proxy-ssl.yml up -d" >> ${DIR}/${PRODUCT}-renew-letsencrypt + + chmod a+x ${DIR}/${PRODUCT}-renew-letsencrypt + + # Add cron job if /etc/cron.d directory exists + if [ -d /etc/cron.d ]; then + echo -e "@weekly root ${DIR}/${PRODUCT}-renew-letsencrypt" | tee /etc/cron.d/${PRODUCT}-letsencrypt + fi + fi + + docker-compose -f ${DOCKERCOMPOSE}/proxy-ssl.yml up -d + docker-compose -f ${DOCKERCOMPOSE}/docspace.yml up -d onlyoffice-files + + echo "OK" + else + echo "Error: private key file at path ${PRIVATEKEY_FILE} not found." && exit 1 + fi +else + echo "Error: certificate file at path ${CERTIFICATE_FILE} not found." && exit 1 +fi diff --git a/config/install_scripts/docspace-install.sh b/config/install_scripts/docspace-install.sh new file mode 100644 index 0000000..b95ca36 --- /dev/null +++ b/config/install_scripts/docspace-install.sh @@ -0,0 +1,187 @@ +#!/bin/bash + + # + # (c) Copyright Ascensio System SIA 2021 + # + # This program is a free software product. You can redistribute it and/or + # modify it under the terms of the GNU Affero General Public License (AGPL) + # version 3 as published by the Free Software Foundation. In accordance with + # Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + # that Ascensio System SIA expressly excludes the warranty of non-infringement + # of any third-party rights. + # + # This program is distributed WITHOUT ANY WARRANTY; without even the implied + # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + # details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + # + # You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha + # street, Riga, Latvia, EU, LV-1050. + # + # The interactive user interfaces in modified source and object code versions + # of the Program must display Appropriate Legal Notices, as required under + # Section 5 of the GNU AGPL version 3. + # + # Pursuant to Section 7(b) of the License you must retain the original Product + # logo when distributing the program. Pursuant to Section 7(e) we decline to + # grant you any rights under trademark law for use of our trademarks. + # + # All the Product's GUI elements, including illustrations and icon sets, as + # well as technical writing content are licensed under the terms of the + # Creative Commons Attribution-ShareAlike 4.0 International. See the License + # terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + # + +PARAMETERS="$PARAMETERS -it COMMUNITY"; +DOCKER=""; +LOCAL_SCRIPTS="false" +product="docspace" +FILE_NAME="$(basename "$0")" + +while [ "$1" != "" ]; do + case $1 in + -ls | --localscripts ) + if [ "$2" == "true" ] || [ "$2" == "false" ]; then + PARAMETERS="$PARAMETERS ${1}"; + LOCAL_SCRIPTS=$2 + shift + fi + ;; + + -gb | --gitbranch ) + if [ "$2" != "" ]; then + PARAMETERS="$PARAMETERS ${1}"; + GIT_BRANCH=$2 + shift + fi + ;; + + docker ) + DOCKER="true"; + shift && continue + ;; + + package ) + DOCKER="false"; + shift && continue + ;; + + "-?" | -h | --help ) + if [ -z "$DOCKER" ]; then + echo "Run 'bash $FILE_NAME docker' to install docker version of application or 'bash $FILE_NAME package' to install deb/rpm version." + echo "Run 'bash $FILE_NAME docker -h' or 'bash $FILE_NAME package -h' to get more details." + exit 0; + fi + PARAMETERS="$PARAMETERS -ht $FILE_NAME"; + ;; + esac + PARAMETERS="$PARAMETERS ${1}"; + shift +done + +root_checking () { + if [ ! $( id -u ) -eq 0 ]; then + echo "To perform this action you must be logged in with root rights" + exit 1; + fi +} + +command_exists () { + type "$1" &> /dev/null; +} + +install_curl () { + if command_exists apt-get; then + apt-get -y update + apt-get -y -q install curl + elif command_exists yum; then + yum -y install curl + fi + + if ! command_exists curl; then + echo "command curl not found" + exit 1; + fi +} + +read_installation_method () { + echo "Select 'Y' to install ONLYOFFICE $product using Docker (recommended). Select 'N' to install it using RPM/DEB packages."; + read -p "Install with Docker [Y/N/C]? " choice + case "$choice" in + y|Y ) + DOCKER="true"; + ;; + + n|N ) + DOCKER="false"; + ;; + + c|C ) + exit 0; + ;; + + * ) + echo "Please, enter Y, N or C to cancel"; + ;; + esac + + if [ "$DOCKER" == "" ]; then + read_installation_method; + fi +} + +root_checking + +if ! command_exists curl ; then + install_curl; +fi + +if [ -z "$DOCKER" ]; then + read_installation_method; +fi + +if [ -z $GIT_BRANCH ]; then + DOWNLOAD_URL_PREFIX="https://download.onlyoffice.com/${product}" +else + DOWNLOAD_URL_PREFIX="https://raw.githubusercontent.com/ONLYOFFICE/${product}-buildtools/${GIT_BRANCH}/install/OneClickInstall" +fi + +if [ "$DOCKER" == "true" ]; then + if [ "$LOCAL_SCRIPTS" == "true" ]; then + bash install-Docker.sh ${PARAMETERS} + else + curl -s -O ${DOWNLOAD_URL_PREFIX}/install-Docker.sh + bash install-Docker.sh ${PARAMETERS} + rm install-Docker.sh + fi +else + if [ -f /etc/redhat-release ] ; then + DIST=$(cat /etc/redhat-release |sed s/\ release.*//); + REV=$(cat /etc/redhat-release | sed s/.*release\ // | sed s/\ .*//); + + REV_PARTS=(${REV//\./ }); + REV=${REV_PARTS[0]}; + + if [[ "${DIST}" == CentOS* ]] && [ ${REV} -lt 7 ]; then + echo "CentOS 7 or later is required"; + exit 1; + fi + if [ "$LOCAL_SCRIPTS" == "true" ]; then + bash install-RedHat.sh ${PARAMETERS} + else + curl -s -O ${DOWNLOAD_URL_PREFIX}/install-RedHat.sh + bash install-RedHat.sh ${PARAMETERS} + rm install-RedHat.sh + fi + elif [ -f /etc/debian_version ] ; then + if [ "$LOCAL_SCRIPTS" == "true" ]; then + bash install-Debian.sh ${PARAMETERS} + else + curl -s -O ${DOWNLOAD_URL_PREFIX}/install-Debian.sh + bash install-Debian.sh ${PARAMETERS} + rm install-Debian.sh + fi + else + echo "Not supported OS"; + exit 1; + fi +fi diff --git a/config/install_scripts/install-Docker.sh b/config/install_scripts/install-Docker.sh new file mode 100644 index 0000000..8995154 --- /dev/null +++ b/config/install_scripts/install-Docker.sh @@ -0,0 +1,1390 @@ +#!/bin/bash + + # + # (c) Copyright Ascensio System SIA 2021 + # + # This program is a free software product. You can redistribute it and/or + # modify it under the terms of the GNU Affero General Public License (AGPL) + # version 3 as published by the Free Software Foundation. In accordance with + # Section 7(a) of the GNU AGPL its Section 15 shall be amended to the effect + # that Ascensio System SIA expressly excludes the warranty of non-infringement + # of any third-party rights. + # + # This program is distributed WITHOUT ANY WARRANTY; without even the implied + # warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. For + # details, see the GNU AGPL at: http://www.gnu.org/licenses/agpl-3.0.html + # + # You can contact Ascensio System SIA at 20A-12 Ernesta Birznieka-Upisha + # street, Riga, Latvia, EU, LV-1050. + # + # The interactive user interfaces in modified source and object code versions + # of the Program must display Appropriate Legal Notices, as required under + # Section 5 of the GNU AGPL version 3. + # + # Pursuant to Section 7(b) of the License you must retain the original Product + # logo when distributing the program. Pursuant to Section 7(e) we decline to + # grant you any rights under trademark law for use of our trademarks. + # + # All the Product's GUI elements, including illustrations and icon sets, as + # well as technical writing content are licensed under the terms of the + # Creative Commons Attribution-ShareAlike 4.0 International. See the License + # terms at http://creativecommons.org/licenses/by-sa/4.0/legalcode + # + +PACKAGE_SYSNAME="onlyoffice" +PRODUCT_NAME="DocSpace" +PRODUCT=$(tr '[:upper:]' '[:lower:]' <<< ${PRODUCT_NAME}) +BASE_DIR="/app/$PACKAGE_SYSNAME"; +PROXY_YML="${BASE_DIR}/proxy.yml" +STATUS="" +DOCKER_TAG="" +INSTALLATION_TYPE="ENTERPRISE" +IMAGE_NAME="${PACKAGE_SYSNAME}/${PRODUCT}-api" +CONTAINER_NAME="${PACKAGE_SYSNAME}-api" + +NETWORK_NAME=${PACKAGE_SYSNAME} + +SWAPFILE="/${PRODUCT}_swapfile"; +MAKESWAP="true"; + +DISK_REQUIREMENTS=40960; +MEMORY_REQUIREMENTS=8192; +CORE_REQUIREMENTS=4; + +DIST=""; +REV=""; +KERNEL=""; + +INSTALL_REDIS="true"; +INSTALL_RABBITMQ="true"; +INSTALL_MYSQL_SERVER="true"; +INSTALL_DOCUMENT_SERVER="true"; +INSTALL_ELASTICSEARCH="true"; +INSTALL_PRODUCT="true"; +UPDATE="false"; + +HUB=""; +USERNAME=""; +PASSWORD=""; + +MYSQL_VERSION="" +MYSQL_DATABASE="" +MYSQL_USER="" +MYSQL_PASSWORD="" +MYSQL_ROOT_PASSWORD="" +MYSQL_HOST="" +MYSQL_PORT="" +DATABASE_MIGRATION="true" + +ELK_VERSION="" +ELK_SHEME="" +ELK_HOST="" +ELK_PORT="" + +REDIS_HOST="" +REDIS_PORT="" +REDIS_USER_NAME="" +REDIS_PASSWORD="" + +RABBIT_HOST="" +RABBIT_PORT="" +RABBIT_USER_NAME="" +RABBIT_PASSWORD="" + +DOCUMENT_SERVER_IMAGE_NAME="" +DOCUMENT_SERVER_VERSION="" +DOCUMENT_SERVER_JWT_SECRET="" +DOCUMENT_SERVER_JWT_HEADER="" +DOCUMENT_SERVER_URL_EXTERNAL="" + +APP_CORE_BASE_DOMAIN="" +APP_CORE_MACHINEKEY="" +ENV_EXTENSION="" +LETS_ENCRYPT_DOMAIN="" +LETS_ENCRYPT_MAIL="" + +HELP_TARGET="install-Docker.sh"; + +SKIP_HARDWARE_CHECK="false"; + +EXTERNAL_PORT="80" + +while [ "$1" != "" ]; do + case $1 in + + -u | --update ) + if [ "$2" != "" ]; then + UPDATE=$2 + shift + fi + ;; + + -hub | --hub ) + if [ "$2" != "" ]; then + HUB=$2 + shift + fi + ;; + + -un | --username ) + if [ "$2" != "" ]; then + USERNAME=$2 + shift + fi + ;; + + -p | --password ) + if [ "$2" != "" ]; then + PASSWORD=$2 + shift + fi + ;; + + -ids | --installdocspace ) + if [ "$2" != "" ]; then + INSTALL_PRODUCT=$2 + shift + fi + ;; + + -idocs | --installdocs ) + if [ "$2" != "" ]; then + INSTALL_DOCUMENT_SERVER=$2 + shift + fi + ;; + + -imysql | --installmysql ) + if [ "$2" != "" ]; then + INSTALL_MYSQL_SERVER=$2 + shift + fi + ;; + + -irbt | --installrabbitmq ) + if [ "$2" != "" ]; then + INSTALL_RABBITMQ=$2 + shift + fi + ;; + + -irds | --installredis ) + if [ "$2" != "" ]; then + INSTALL_REDIS=$2 + shift + fi + ;; + + -ht | --helptarget ) + if [ "$2" != "" ]; then + HELP_TARGET=$2 + shift + fi + ;; + + -mysqld | --mysqldatabase ) + if [ "$2" != "" ]; then + MYSQL_DATABASE=$2 + shift + fi + ;; + + -mysqlrp | --mysqlrootpassword ) + if [ "$2" != "" ]; then + MYSQL_ROOT_PASSWORD=$2 + shift + fi + ;; + + -mysqlu | --mysqluser ) + if [ "$2" != "" ]; then + MYSQL_USER=$2 + shift + fi + ;; + + -mysqlh | --mysqlhost ) + if [ "$2" != "" ]; then + MYSQL_HOST=$2 + shift + fi + ;; + + -mysqlport | --mysqlport ) + if [ "$2" != "" ]; then + MYSQL_PORT=$2 + shift + fi + ;; + + -mysqlp | --mysqlpassword ) + if [ "$2" != "" ]; then + MYSQL_PASSWORD=$2 + shift + fi + ;; + + -espr | --elasticprotocol ) + if [ "$2" != "" ]; then + ELK_SHEME=$2 + shift + fi + ;; + + -esh | --elastichost ) + if [ "$2" != "" ]; then + ELK_HOST=$2 + shift + fi + ;; + + -esp | --elasticport ) + if [ "$2" != "" ]; then + ELK_PORT=$2 + shift + fi + ;; + + -skiphc | --skiphardwarecheck ) + if [ "$2" != "" ]; then + SKIP_HARDWARE_CHECK=$2 + shift + fi + ;; + + -ep | --externalport ) + if [ "$2" != "" ]; then + EXTERNAL_PORT=$2 + shift + fi + ;; + + -dsh | --docspacehost ) + if [ "$2" != "" ]; then + APP_URL_PORTAL=$2 + shift + fi + ;; + + -mk | --machinekey ) + if [ "$2" != "" ]; then + APP_CORE_MACHINEKEY=$2 + shift + fi + ;; + + -env | --environment ) + if [ "$2" != "" ]; then + ENV_EXTENSION=$2 + shift + fi + ;; + + -s | --status ) + if [ "$2" != "" ]; then + STATUS=$2 + IMAGE_NAME="${PACKAGE_SYSNAME}/${STATUS}${PRODUCT}-api" + shift + fi + ;; + + -ls | --localscripts ) + if [ "$2" != "" ]; then + shift + fi + ;; + + -dsv | --docspaceversion ) + if [ "$2" != "" ]; then + DOCKER_TAG=$2 + shift + fi + ;; + + -gb | --gitbranch ) + if [ "$2" != "" ]; then + PARAMETERS="$PARAMETERS ${1}"; + GIT_BRANCH=$2 + shift + fi + ;; + + -docsi | --docsimage ) + if [ "$2" != "" ]; then + DOCUMENT_SERVER_IMAGE_NAME=$2 + shift + fi + ;; + + -docsv | --docsversion ) + if [ "$2" != "" ]; then + DOCUMENT_SERVER_VERSION=$2 + shift + fi + ;; + + -docsurl | --docsurl ) + if [ "$2" != "" ]; then + DOCUMENT_SERVER_URL_EXTERNAL=$2 + shift + fi + ;; + + -dbm | --databasemigration ) + if [ "$2" != "" ]; then + DATABASE_MIGRATION=$2 + shift + fi + ;; + + -jh | --jwtheader ) + if [ "$2" != "" ]; then + DOCUMENT_SERVER_JWT_HEADER=$2 + shift + fi + ;; + + -js | --jwtsecret ) + if [ "$2" != "" ]; then + DOCUMENT_SERVER_JWT_SECRET=$2 + shift + fi + ;; + + -it | --installation_type ) + if [ "$2" != "" ]; then + INSTALLATION_TYPE=$(echo "$2" | awk '{print toupper($0)}'); + shift + fi + ;; + + -ms | --makeswap ) + if [ "$2" != "" ]; then + MAKESWAP=$2 + shift + fi + ;; + + -ies | --installelastic ) + if [ "$2" != "" ]; then + INSTALL_ELASTICSEARCH=$2 + shift + fi + ;; + + -rdsh | --redishost ) + if [ "$2" != "" ]; then + REDIS_HOST=$2 + shift + fi + ;; + + -rdsp | --redisport ) + if [ "$2" != "" ]; then + REDIS_PORT=$2 + shift + fi + ;; + + -rdsu | --redisusername ) + if [ "$2" != "" ]; then + REDIS_USER_NAME=$2 + shift + fi + ;; + + -rdspass | --redispassword ) + if [ "$2" != "" ]; then + REDIS_PASSWORD=$2 + shift + fi + ;; + + -rbth | --rabbitmqhost ) + if [ "$2" != "" ]; then + RABBIT_HOST=$2 + shift + fi + ;; + + -rbtp | --rabbitmqport ) + if [ "$2" != "" ]; then + RABBIT_PORT=$2 + shift + fi + ;; + + -rbtu | --rabbitmqusername ) + if [ "$2" != "" ]; then + RABBIT_USER_NAME=$2 + shift + fi + ;; + + -rbtpass | --rabbitmqpassword ) + if [ "$2" != "" ]; then + RABBIT_PASSWORD=$2 + shift + fi + ;; + + -rbtvh | --rabbitmqvirtualhost ) + if [ "$2" != "" ]; then + RABBIT_VIRTUAL_HOST=$2 + shift + fi + ;; + + -led | --letsencryptdomain ) + if [ "$2" != "" ]; then + LETS_ENCRYPT_DOMAIN=$2 + shift + fi + ;; + + -lem | --letsencryptmail ) + if [ "$2" != "" ]; then + LETS_ENCRYPT_MAIL=$2 + shift + fi + ;; + + -cf | --certfile ) + if [ "$2" != "" ]; then + CERTIFICATE_PATH=$2 + shift + fi + ;; + + -ckf | --certkeyfile ) + if [ "$2" != "" ]; then + CERTIFICATE_KEY_PATH=$2 + shift + fi + ;; + + -? | -h | --help ) + echo " Usage: bash $HELP_TARGET [PARAMETER] [[PARAMETER], ...]" + echo + echo " Parameters:" + echo " -hub, --hub dockerhub name" + echo " -un, --username dockerhub username" + echo " -p, --password dockerhub password" + echo " -it, --installation_type installation type (community|enterprise)" + echo " -skiphc, --skiphardwarecheck skip hardware check (true|false)" + echo " -u, --update use to update existing components (true|false)" + echo " -ids, --installdocspace install or update $PRODUCT (true|false)" + echo " -dsv, --docspaceversion select the $PRODUCT version" + echo " -dsh, --docspacehost $PRODUCT host" + echo " -env, --environment $PRODUCT environment" + echo " -mk, --machinekey setting for core.machinekey" + echo " -ep, --externalport external $PRODUCT port (default value 80)" + echo " -idocs, --installdocs install or update document server (true|false)" + echo " -docsi, --docsimage document server image name" + echo " -docsv, --docsversion document server version" + echo " -docsurl, --docsurl $PACKAGE_SYSNAME docs server address (example http://$PACKAGE_SYSNAME-docs-address:8083)" + echo " -jh, --jwtheader defines the http header that will be used to send the JWT" + echo " -js, --jwtsecret defines the secret key to validate the JWT in the request" + echo " -irbt, --installrabbitmq install or update rabbitmq (true|false)" + echo " -irds, --installredis install or update redis (true|false)" + echo " -imysql, --installmysql install or update mysql (true|false)" + echo " -ies, --installelastic install or update elasticsearch (true|false)" + echo " -espr, --elasticprotocol the protocol for the connection to elasticsearch (default value http)" + echo " -esh, --elastichost the IP address or hostname of the elasticsearch" + echo " -esp, --elasticport elasticsearch port number (default value 9200)" + echo " -rdsh, --redishost the IP address or hostname of the redis server" + echo " -rdsp, --redisport redis server port number (default value 6379)" + echo " -rdsu, --redisusername redis user name" + echo " -rdspass, --redispassword password set for redis account" + echo " -rbth, --rabbitmqhost the IP address or hostname of the rabbitmq server" + echo " -rbtp, --rabbitmqport rabbitmq server port number (default value 5672)" + echo " -rbtu, --rabbitmqusername username for rabbitmq server account" + echo " -rbtpass, --rabbitmqpassword password set for rabbitmq server account" + echo " -rbtvh, --rabbitmqvirtualhost rabbitmq virtual host (default value \"/\")" + echo " -mysqlrp, --mysqlrootpassword mysql server root password" + echo " -mysqld, --mysqldatabase $PRODUCT database name" + echo " -mysqlu, --mysqluser $PRODUCT database user" + echo " -mysqlp, --mysqlpassword $PRODUCT database password" + echo " -mysqlh, --mysqlhost mysql server host" + echo " -mysqlport, --mysqlport mysql server port number (default value 3306)" + echo " -led, --letsencryptdomain defines the domain for Let's Encrypt certificate" + echo " -lem, --letsencryptmail defines the domain administator mail address for Let's Encrypt certificate" + echo " -cf, --certfile path to the certificate file for the domain" + echo " -ckf, --certkeyfile path to the private key file for the certificate" + echo " -dbm, --databasemigration database migration (true|false)" + echo " -ms, --makeswap make swap file (true|false)" + echo " -?, -h, --help this help" + echo + echo " Install all the components without document server:" + echo " bash $HELP_TARGET -idocs false" + echo + echo " Install Document Server only. Skip the installation of mysql, $PRODUCT, rabbitmq, redis:" + echo " bash $HELP_TARGET -ids false -idocs true -imysql false -irbt false -irds false" + echo + echo " Update all installed components. Stop the containers that need to be updated, remove them and run the latest versions of the corresponding components." + echo " The portal data should be picked up automatically:" + echo " bash $HELP_TARGET -u true" + echo + echo " Update Document Server only to version 7.2.1.34 and skip the update for all other components:" + echo " bash $HELP_TARGET -u true -docsi ${PACKAGE_SYSNAME}/documentserver-ee -docsv 7.2.1.34 -idocs true -ids false -irbt false -irds false" + echo + echo " Update $PRODUCT only to version 1.2.0 and skip the update for all other components:" + echo " bash $HELP_TARGET -u true -dsv v1.2.0 -idocs false -irbt false -irds false" + echo + exit 0 + ;; + + * ) + echo "Unknown parameter $1" 1>&2 + exit 1 + ;; + esac + shift +done + +root_checking () { + PID=$$ + if [ ! $( id -u ) -eq 0 ]; then + echo "To perform this action you must be logged in with root rights" + exit 1; + fi +} + +command_exists () { + type "$1" &> /dev/null; +} + +file_exists () { + if [ -z "$1" ]; then + echo "file path is empty" + exit 1; + fi + + if [ -f "$1" ]; then + return 0; #true + else + return 1; #false + fi +} + +to_lowercase () { + echo "$1" | awk '{print tolower($0)}' +} + +trim () { + echo -e "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' +} + +get_random_str () { + LENGTH=$1; + + if [[ -z ${LENGTH} ]]; then + LENGTH=12; + fi + + VALUE=$(cat /dev/urandom | tr -dc A-Za-z0-9 | head -c ${LENGTH}); + echo "$VALUE" +} + +get_os_info () { + OS=`to_lowercase \`uname\`` + + if [ "${OS}" == "windowsnt" ]; then + echo "Not supported OS"; + exit 1; + elif [ "${OS}" == "darwin" ]; then + echo "Not supported OS"; + exit 1; + else + OS=`uname` + + if [ "${OS}" == "SunOS" ] ; then + echo "Not supported OS"; + exit 1; + elif [ "${OS}" == "AIX" ] ; then + echo "Not supported OS"; + exit 1; + elif [ "${OS}" == "Linux" ] ; then + MACH=`uname -m` + + if [ "${MACH}" != "x86_64" ]; then + echo "Currently only supports 64bit OS's"; + exit 1; + fi + + KERNEL=`uname -r` + + if [ -f /etc/redhat-release ] ; then + CONTAINS=$(cat /etc/redhat-release | { grep -sw release || true; }); + if [[ -n ${CONTAINS} ]]; then + DIST=`cat /etc/redhat-release |sed s/\ release.*//` + REV=`cat /etc/redhat-release | grep -oP '(?<=release )\d+'` + else + DIST=`cat /etc/os-release | grep -sw 'ID' | awk -F= '{ print $2 }' | sed -e 's/^"//' -e 's/"$//'` + REV=`cat /etc/os-release | grep -sw 'VERSION_ID' | awk -F= '{ print $2 }' | sed -e 's/^"//' -e 's/"$//'` + fi + elif [ -f /etc/SuSE-release ] ; then + REV=`cat /etc/os-release | grep '^VERSION_ID' | awk -F= '{ print $2 }' | sed -e 's/^"//' -e 's/"$//'` + DIST='SuSe' + elif [ -f /etc/debian_version ] ; then + REV=`cat /etc/debian_version` + DIST='Debian' + if [ -f /etc/lsb-release ] ; then + DIST=`cat /etc/lsb-release | grep '^DISTRIB_ID' | awk -F= '{ print $2 }'` + REV=`cat /etc/lsb-release | grep '^DISTRIB_RELEASE' | awk -F= '{ print $2 }'` + elif [ -f /etc/lsb_release ] || [ -f /usr/bin/lsb_release ] ; then + DIST=`lsb_release -a 2>&1 | grep 'Distributor ID:' | awk -F ":" '{print $2 }'` + REV=`lsb_release -a 2>&1 | grep 'Release:' | awk -F ":" '{print $2 }'` + fi + elif [ -f /etc/os-release ] ; then + DIST=`cat /etc/os-release | grep -sw 'ID' | awk -F= '{ print $2 }' | sed -e 's/^"//' -e 's/"$//'` + REV=`cat /etc/os-release | grep -sw 'VERSION_ID' | awk -F= '{ print $2 }' | sed -e 's/^"//' -e 's/"$//'` + fi + fi + + DIST=$(trim $DIST); + REV=$(trim $REV); + fi +} + +check_os_info () { + if [[ -z ${KERNEL} || -z ${DIST} || -z ${REV} ]]; then + echo "$KERNEL, $DIST, $REV"; + echo "Not supported OS"; + exit 1; + fi + + if [ -f /etc/needrestart/needrestart.conf ]; then + sed -e "s_#\$nrconf{restart}_\$nrconf{restart}_" -e "s_\(\$nrconf{restart} =\).*_\1 'a';_" -i /etc/needrestart/needrestart.conf + fi +} + +check_kernel () { + MIN_NUM_ARR=(3 10 0); + CUR_NUM_ARR=(); + + CUR_STR_ARR=$(echo $KERNEL | grep -Po "[0-9]+\.[0-9]+\.[0-9]+" | tr "." " "); + for CUR_STR_ITEM in $CUR_STR_ARR + do + CUR_NUM_ARR=(${CUR_NUM_ARR[@]} $CUR_STR_ITEM) + done + + INDEX=0; + + while [[ $INDEX -lt 3 ]]; do + if [ ${CUR_NUM_ARR[INDEX]} -lt ${MIN_NUM_ARR[INDEX]} ]; then + echo "Not supported OS Kernel" + exit 1; + elif [ ${CUR_NUM_ARR[INDEX]} -gt ${MIN_NUM_ARR[INDEX]} ]; then + INDEX=3 + fi + (( INDEX++ )) + done +} + +check_hardware () { + AVAILABLE_DISK_SPACE=$(df -m / | tail -1 | awk '{ print $4 }'); + + if [ ${AVAILABLE_DISK_SPACE} -lt ${DISK_REQUIREMENTS} ]; then + echo "Minimal requirements are not met: need at least $DISK_REQUIREMENTS MB of free HDD space" + exit 1; + fi + + TOTAL_MEMORY=$(free -m | grep -oP '\d+' | head -n 1); + + if [ ${TOTAL_MEMORY} -lt ${MEMORY_REQUIREMENTS} ]; then + echo "Minimal requirements are not met: need at least $MEMORY_REQUIREMENTS MB of RAM" + exit 1; + fi + + CPU_CORES_NUMBER=$(cat /proc/cpuinfo | grep processor | wc -l); + + if [ ${CPU_CORES_NUMBER} -lt ${CORE_REQUIREMENTS} ]; then + echo "The system does not meet the minimal hardware requirements. CPU with at least $CORE_REQUIREMENTS cores is required" + exit 1; + fi +} + +install_service () { + local COMMAND_NAME=$1 + local PACKAGE_NAME=$2 + + PACKAGE_NAME=${PACKAGE_NAME:-"$COMMAND_NAME"} + + if command_exists apt-get; then + apt-get -y update -qq + apt-get -y -q install $PACKAGE_NAME + elif command_exists yum; then + yum -y install $PACKAGE_NAME + fi + + if ! command_exists $COMMAND_NAME; then + echo "Command $COMMAND_NAME not found" + exit 1; + fi +} + +install_docker_compose () { + curl -L "https://github.com/docker/compose/releases/latest/download/docker-compose-$(uname -s)-$(uname -m)" -o /usr/bin/docker-compose + chmod +x /usr/bin/docker-compose +} + +check_ports () { + RESERVED_PORTS=(); + ARRAY_PORTS=(); + USED_PORTS=""; + + if ! command_exists netstat; then + install_service netstat net-tools + fi + + if [ "${EXTERNAL_PORT//[0-9]}" = "" ]; then + for RESERVED_PORT in "${RESERVED_PORTS[@]}" + do + if [ "$RESERVED_PORT" -eq "$EXTERNAL_PORT" ] ; then + echo "External port $EXTERNAL_PORT is reserved. Select another port" + exit 1; + fi + done + else + echo "Invalid external port $EXTERNAL_PORT" + exit 1; + fi + + if [ "$INSTALL_PRODUCT" == "true" ]; then + ARRAY_PORTS=(${ARRAY_PORTS[@]} "$EXTERNAL_PORT"); + fi + + for PORT in "${ARRAY_PORTS[@]}" + do + REGEXP=":$PORT$" + CHECK_RESULT=$(netstat -lnt | awk '{print $4}' | { grep $REGEXP || true; }) + + if [[ $CHECK_RESULT != "" ]]; then + if [[ $USED_PORTS != "" ]]; then + USED_PORTS="$USED_PORTS, $PORT" + else + USED_PORTS="$PORT" + fi + fi + done + + if [[ $USED_PORTS != "" ]]; then + echo "The following TCP Ports must be available: $USED_PORTS" + exit 1; + fi +} + +check_docker_version () { + CUR_FULL_VERSION=$(docker -v | cut -d ' ' -f3 | cut -d ',' -f1); + CUR_VERSION=$(echo $CUR_FULL_VERSION | cut -d '-' -f1); + CUR_EDITION=$(echo $CUR_FULL_VERSION | cut -d '-' -f2); + + if [ "${CUR_EDITION}" == "ce" ] || [ "${CUR_EDITION}" == "ee" ]; then + return 0; + fi + + if [ "${CUR_VERSION}" != "${CUR_EDITION}" ]; then + echo "Unspecific docker version" + exit 1; + fi + + MIN_NUM_ARR=(1 10 0); + CUR_NUM_ARR=(); + + CUR_STR_ARR=$(echo $CUR_VERSION | grep -Po "[0-9]+\.[0-9]+\.[0-9]+" | tr "." " "); + + for CUR_STR_ITEM in $CUR_STR_ARR + do + CUR_NUM_ARR=(${CUR_NUM_ARR[@]} $CUR_STR_ITEM) + done + + INDEX=0; + + while [[ $INDEX -lt 3 ]]; do + if [ ${CUR_NUM_ARR[INDEX]} -lt ${MIN_NUM_ARR[INDEX]} ]; then + echo "The outdated Docker version has been found. Please update to the latest version." + exit 1; + elif [ ${CUR_NUM_ARR[INDEX]} -gt ${MIN_NUM_ARR[INDEX]} ]; then + return 0; + fi + (( INDEX++ )) + done +} + +install_docker_using_script () { + if ! command_exists curl ; then + install_service curl + fi + + curl -fsSL https://get.docker.com -o get-docker.sh + sh get-docker.sh + rm get-docker.sh +} + +install_docker () { + + if [ "${DIST}" == "Ubuntu" ] || [ "${DIST}" == "Debian" ] || [[ "${DIST}" == CentOS* ]] || [ "${DIST}" == "Fedora" ]; then + + install_docker_using_script + systemctl start docker + systemctl enable docker + + elif [ "${DIST}" == "Red Hat Enterprise Linux Server" ]; then + + echo "" + echo "Your operating system does not allow Docker CE installation." + echo "You can install Docker EE using the manual here - https://docs.docker.com/engine/installation/linux/rhel/" + echo "" + exit 1; + + elif [ "${DIST}" == "SuSe" ]; then + + echo "" + echo "Your operating system does not allow Docker CE installation." + echo "You can install Docker EE using the manual here - https://docs.docker.com/engine/installation/linux/suse/" + echo "" + exit 1; + + elif [ "${DIST}" == "altlinux" ]; then + + apt-get -y install docker-io + chkconfig docker on + service docker start + systemctl enable docker + + else + + echo "" + echo "Docker could not be installed automatically." + echo "Please use this official instruction https://docs.docker.com/engine/installation/linux/other/ for its manual installation." + echo "" + exit 1; + + fi + + if ! command_exists docker ; then + echo "error while installing docker" + exit 1; + fi +} + +docker_login () { + if [[ -n ${USERNAME} && -n ${PASSWORD} ]]; then + docker login ${HUB} --username ${USERNAME} --password ${PASSWORD} + fi +} + +create_network () { + NETWORT_EXIST=$(docker network ls | awk '{print $2;}' | { grep -x ${NETWORK_NAME} || true; }); + + if [[ -z ${NETWORT_EXIST} ]]; then + docker network create --driver bridge ${NETWORK_NAME} + fi +} + +read_continue_installation () { + read -p "Continue installation [Y/N]? " CHOICE_INSTALLATION + case "$CHOICE_INSTALLATION" in + y|Y ) + return 0 + ;; + + n|N ) + exit 0; + ;; + + * ) + echo "Please, enter Y or N"; + read_continue_installation + ;; + esac +} + +domain_check () { + if ! command_exists dig; then + if command_exists apt-get; then + install_service dig dnsutils + elif command_exists yum; then + install_service dig bind-utils + fi + fi + + if ! command_exists ping; then + if command_exists apt-get; then + install_service ping iputils-ping + elif command_exists yum; then + install_service ping iputils + fi + fi + + if ! command_exists ip; then + if command_exists apt-get; then + install_service ip iproute2 + elif command_exists yum; then + install_service ip iproute + fi + fi + + APP_DOMAIN_PORTAL=${LETS_ENCRYPT_DOMAIN:-${APP_URL_PORTAL:-$(get_env_parameter "APP_URL_PORTAL" "${PACKAGE_SYSNAME}-files" | awk -F[/:] '{if ($1 == "https") print $4; else print ""}')}} + + while IFS= read -r DOMAIN; do + IP_ADDRESS=$(ping -c 1 -W 1 $DOMAIN | grep -oP '(\d+\.\d+\.\d+\.\d+)' | head -n 1) + if [[ -n "$IP_ADDRESS" && "$IP_ADDRESS" =~ ^(10\.|127\.|172\.(1[6-9]|2[0-9]|3[0-1])\.|192\.168\.) ]]; then + LOCAL_RESOLVED_DOMAINS+="$DOMAIN" + fi + done <<< "${APP_DOMAIN_PORTAL:-$(dig +short -x $(curl -s ifconfig.me) | sed 's/\.$//')}" + + if [[ -n "${LOCAL_RESOLVED_DOMAINS}" ]] || [[ $(ip route get 8.8.8.8 | awk '{print $7}') != $(curl -s ifconfig.me) ]]; then + DOCKER_DAEMON_FILE="/etc/docker/daemon.json" + if ! grep -q '"dns"' "$DOCKER_DAEMON_FILE" 2>/dev/null; then + echo "A problem was detected for ${APP_DOMAIN_PORTAL:-${LOCAL_RESOLVED_DOMAINS}} domains when using a loopback IP address or when using NAT." + echo "Select 'Y' to continue installing with configuring the use of external IP in Docker via Google Public DNS." + echo "Select 'N' to cancel ${PACKAGE_SYSNAME^^} ${PRODUCT_NAME} installation." + if read_continue_installation; then + if [[ -f "$DOCKER_DAEMON_FILE" ]]; then + sed -i 's!{!& "dns": ["8.8.8.8", "8.8.4.4"],!' "$DOCKER_DAEMON_FILE" + else + echo "{\"dns\": [\"8.8.8.8\", \"8.8.4.4\"]}" | tee "$DOCKER_DAEMON_FILE" >/dev/null + fi + systemctl restart docker + fi + fi + fi + + [[ -n "${APP_DOMAIN_PORTAL}" ]] && APP_URL_PORTAL="http://${APP_DOMAIN_PORTAL}:${EXTERNAL_PORT}" +} + +establish_conn() { + echo -n "Trying to establish $3 connection... " + + exec {FD}<> /dev/tcp/$1/$2 && exec {FD}>&- + + if [ "$?" != 0 ]; then + echo "FAILURE"; + exit 1; + fi + + echo "OK" +} + +get_env_parameter () { + local PARAMETER_NAME=$1; + local CONTAINER_NAME=$2; + + if [[ -z ${PARAMETER_NAME} ]]; then + echo "Empty parameter name" + exit 1; + fi + + if command_exists docker ; then + [ -n "$CONTAINER_NAME" ] && CONTAINER_EXIST=$(docker ps -aqf "name=$CONTAINER_NAME"); + + if [[ -n ${CONTAINER_EXIST} ]]; then + VALUE=$(docker inspect --format='{{range .Config.Env}}{{println .}}{{end}}' ${CONTAINER_NAME} | grep "${PARAMETER_NAME}=" | sed 's/^.*=//'); + fi + fi + + if [ -z ${VALUE} ] && [ -f ${BASE_DIR}/.env ]; then + VALUE=$(awk -F= "/${PARAMETER_NAME}/ {print \$2}" ${BASE_DIR}/.env | tr -d '\r') + fi + + echo ${VALUE//\"} +} + +get_available_version () { + if [[ -z "$1" ]]; then + echo "image name is empty"; + exit 1; + fi + + if ! command_exists curl ; then + install_curl; + fi + + CREDENTIALS=""; + AUTH_HEADER=""; + TAGS_RESP=""; + + if [[ -n ${HUB} ]]; then + DOCKER_CONFIG="$HOME/.docker/config.json"; + + if [[ -f "$DOCKER_CONFIG" ]]; then + CREDENTIALS=$(jq -r '.auths."'$HUB'".auth' < "$DOCKER_CONFIG"); + if [ "$CREDENTIALS" == "null" ]; then + CREDENTIALS=""; + fi + fi + + if [[ -z ${CREDENTIALS} && -n ${USERNAME} && -n ${PASSWORD} ]]; then + CREDENTIALS=$(echo -n "$USERNAME:$PASSWORD" | base64); + fi + + if [[ -n ${CREDENTIALS} ]]; then + AUTH_HEADER="Authorization: Basic $CREDENTIALS"; + fi + + REPO=$(echo $1 | sed "s/$HUB\///g"); + TAGS_RESP=$(curl -s -H "$AUTH_HEADER" -X GET https://$HUB/v2/$REPO/tags/list); + TAGS_RESP=$(echo $TAGS_RESP | jq -r '.tags') + else + if [[ -n ${USERNAME} && -n ${PASSWORD} ]]; then + CREDENTIALS="{\"username\":\"$USERNAME\",\"password\":\"$PASSWORD\"}"; + fi + + if [[ -n ${CREDENTIALS} ]]; then + LOGIN_RESP=$(curl -s -H "Content-Type: application/json" -X POST -d "$CREDENTIALS" https://hub.docker.com/v2/users/login/); + TOKEN=$(echo $LOGIN_RESP | jq -r '.token'); + AUTH_HEADER="Authorization: JWT $TOKEN"; + sleep 1; + fi + + TAGS_RESP=$(curl -s -H "$AUTH_HEADER" -X GET https://hub.docker.com/v2/repositories/$1/tags/); + TAGS_RESP=$(echo $TAGS_RESP | jq -r '.results[].name') + fi + + VERSION_REGEX="[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$" + + TAG_LIST="" + + for item in $TAGS_RESP + do + if [[ $item =~ $VERSION_REGEX ]]; then + TAG_LIST="$item,$TAG_LIST" + fi + done + + LATEST_TAG=$(echo $TAG_LIST | tr ',' '\n' | sort -t. -k 1,1n -k 2,2n -k 3,3n -k 4,4n | awk '/./{line=$0} END{print line}'); + + if [ ! -z "${LATEST_TAG}" ]; then + echo "${LATEST_TAG}" | sed "s/\"//g" + else + echo "Unable to retrieve tag from ${1} repository" >&2 + kill -s TERM $PID + fi +} + +set_docs_url_external () { + DOCUMENT_SERVER_URL_EXTERNAL=${DOCUMENT_SERVER_URL_EXTERNAL:-$(get_env_parameter "DOCUMENT_SERVER_URL_EXTERNAL" "${CONTAINER_NAME}")}; + + if [[ ! -z ${DOCUMENT_SERVER_URL_EXTERNAL} ]] && [[ $DOCUMENT_SERVER_URL_EXTERNAL =~ ^(https?://)?([^:/]+)(:([0-9]+))?(/.*)?$ ]]; then + [[ -z ${BASH_REMATCH[1]} ]] && DOCUMENT_SERVER_URL_EXTERNAL="http://$DOCUMENT_SERVER_URL_EXTERNAL" + DOCUMENT_SERVER_HOST="${BASH_REMATCH[2]}" + DOCUMENT_SERVER_PORT="${BASH_REMATCH[4]:-"80"}" + fi +} + +set_jwt_secret () { + DOCUMENT_SERVER_JWT_SECRET="${DOCUMENT_SERVER_JWT_SECRET:-$(get_env_parameter "JWT_SECRET" "${PACKAGE_SYSNAME}-document-server")}" + DOCUMENT_SERVER_JWT_SECRET="${DOCUMENT_SERVER_JWT_SECRET:-$(get_env_parameter "DOCUMENT_SERVER_JWT_SECRET" "${CONTAINER_NAME}")}" + DOCUMENT_SERVER_JWT_SECRET="${DOCUMENT_SERVER_JWT_SECRET:-$(get_random_str 32)}" +} + +set_jwt_header () { + DOCUMENT_SERVER_JWT_HEADER="${DOCUMENT_SERVER_JWT_HEADER:-$(get_env_parameter "JWT_HEADER" "${PACKAGE_SYSNAME}-document-server")}" + DOCUMENT_SERVER_JWT_HEADER="${DOCUMENT_SERVER_JWT_HEADER:-$(get_env_parameter "DOCUMENT_SERVER_JWT_HEADER" "${CONTAINER_NAME}")}" + DOCUMENT_SERVER_JWT_HEADER="${DOCUMENT_SERVER_JWT_HEADER:-"AuthorizationJwt"}" +} + +set_core_machinekey () { + APP_CORE_MACHINEKEY="${APP_CORE_MACHINEKEY:-$(get_env_parameter "APP_CORE_MACHINEKEY" "${CONTAINER_NAME}")}" + [[ "$UPDATE" != "true" ]] && APP_CORE_MACHINEKEY="${APP_CORE_MACHINEKEY:-$(get_random_str 12))}" +} + +set_mysql_params () { + MYSQL_PASSWORD="${MYSQL_PASSWORD:-$(get_env_parameter "MYSQL_PASSWORD" "${CONTAINER_NAME}")}" + MYSQL_PASSWORD="${MYSQL_PASSWORD:-$(get_random_str 20)}" + + MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-$(get_env_parameter "MYSQL_ROOT_PASSWORD" "${CONTAINER_NAME}")}" + MYSQL_ROOT_PASSWORD="${MYSQL_ROOT_PASSWORD:-$(get_random_str 20)}" + + MYSQL_DATABASE="${MYSQL_DATABASE:-$(get_env_parameter "MYSQL_DATABASE" "${CONTAINER_NAME}")}" + MYSQL_USER="${MYSQL_USER:-$(get_env_parameter "MYSQL_USER" "${CONTAINER_NAME}")}" + MYSQL_HOST="${MYSQL_HOST:-$(get_env_parameter "MYSQL_HOST" "${CONTAINER_NAME}")}" + MYSQL_PORT="${MYSQL_PORT:-$(get_env_parameter "MYSQL_PORT" "${CONTAINER_NAME}")}" +} + +set_docspace_params() { + ENV_EXTENSION=${ENV_EXTENSION:-$(get_env_parameter "ENV_EXTENSION" "${CONTAINER_NAME}")}; + APP_CORE_BASE_DOMAIN=${APP_CORE_BASE_DOMAIN:-$(get_env_parameter "APP_CORE_BASE_DOMAIN" "${CONTAINER_NAME}")}; + EXTERNAL_PORT=${EXTERNAL_PORT:-$(get_env_parameter "EXTERNAL_PORT" "${CONTAINER_NAME}")}; + + ELK_SHEME=${ELK_SHEME:-$(get_env_parameter "ELK_SHEME" "${CONTAINER_NAME}")}; + ELK_HOST=${ELK_HOST:-$(get_env_parameter "ELK_HOST" "${CONTAINER_NAME}")}; + ELK_PORT=${ELK_PORT:-$(get_env_parameter "ELK_PORT" "${CONTAINER_NAME}")}; + + REDIS_HOST=${REDIS_HOST:-$(get_env_parameter "REDIS_HOST" "${CONTAINER_NAME}")}; + REDIS_PORT=${REDIS_PORT:-$(get_env_parameter "REDIS_PORT" "${CONTAINER_NAME}")}; + REDIS_USER_NAME=${REDIS_USER_NAME:-$(get_env_parameter "REDIS_USER_NAME" "${CONTAINER_NAME}")}; + REDIS_PASSWORD=${REDIS_PASSWORD:-$(get_env_parameter "REDIS_PASSWORD" "${CONTAINER_NAME}")}; + + RABBIT_HOST=${RABBIT_HOST:-$(get_env_parameter "RABBIT_HOST" "${CONTAINER_NAME}")}; + RABBIT_PORT=${RABBIT_PORT:-$(get_env_parameter "RABBIT_PORT" "${CONTAINER_NAME}")}; + RABBIT_USER_NAME=${RABBIT_USER_NAME:-$(get_env_parameter "RABBIT_USER_NAME" "${CONTAINER_NAME}")}; + RABBIT_PASSWORD=${RABBIT_PASSWORD:-$(get_env_parameter "RABBIT_PASSWORD" "${CONTAINER_NAME}")}; + RABBIT_VIRTUAL_HOST=${RABBIT_VIRTUAL_HOST:-$(get_env_parameter "RABBIT_VIRTUAL_HOST" "${CONTAINER_NAME}")}; + + CERTIFICATE_PATH=${CERTIFICATE_PATH:-$(get_env_parameter "CERTIFICATE_PATH")}; + CERTIFICATE_KEY_PATH=${CERTIFICATE_KEY_PATH:-$(get_env_parameter "CERTIFICATE_KEY_PATH")}; + DHPARAM_PATH=${DHPARAM_PATH:-$(get_env_parameter "DHPARAM_PATH")}; +} + +set_installation_type_data () { + if [ "$INSTALLATION_TYPE" == "COMMUNITY" ]; then + DOCUMENT_SERVER_IMAGE_NAME=${DOCUMENT_SERVER_IMAGE_NAME:-"${PACKAGE_SYSNAME}/${STATUS}documentserver"} + elif [ "$INSTALLATION_TYPE" == "ENTERPRISE" ]; then + DOCUMENT_SERVER_IMAGE_NAME=${DOCUMENT_SERVER_IMAGE_NAME:-"${PACKAGE_SYSNAME}/${STATUS}documentserver-ee"} + fi +} + +download_files () { + if ! command_exists jq ; then + if command_exists yum; then + rpm -ivh https://dl.fedoraproject.org/pub/epel/epel-release-latest-$REV.noarch.rpm + fi + install_service jq + fi + + if ! command_exists docker-compose; then + install_docker_compose + fi + + # Fixes issues with variables when upgrading to v1.1.3 + HOSTS=("ELK_HOST" "REDIS_HOST" "RABBIT_HOST" "MYSQL_HOST"); + for HOST in "${HOSTS[@]}"; do [[ "${!HOST}" == *CONTAINER_PREFIX* || "${!HOST}" == *$PACKAGE_SYSNAME* ]] && export "$HOST="; done + [[ "${APP_URL_PORTAL}" == *${PACKAGE_SYSNAME}-proxy* ]] && APP_URL_PORTAL="" + + echo -n "Downloading configuration files to the ${BASE_DIR} directory..." + + if ! command_exists tar; then + install_service tar + fi + + [ -d "${BASE_DIR}" ] && rm -rf "${BASE_DIR}" + mkdir -p ${BASE_DIR} + + if [ -z "${GIT_BRANCH}" ]; then + curl -sL -o docker.tar.gz "https://download.${PACKAGE_SYSNAME}.com/${PRODUCT}/docker.tar.gz" + tar -xf docker.tar.gz -C ${BASE_DIR} + else + curl -sL -o docker.tar.gz "https://github.com/${PACKAGE_SYSNAME}/${PRODUCT}-buildtools/archive/${GIT_BRANCH}.tar.gz" + tar -xf docker.tar.gz --strip-components=3 -C ${BASE_DIR} --wildcards '*/install/docker/*' + fi + + rm -rf docker.tar.gz + + echo "OK" + + reconfigure STATUS ${STATUS} + reconfigure INSTALLATION_TYPE ${INSTALLATION_TYPE} + reconfigure NETWORK_NAME ${NETWORK_NAME} +} + +reconfigure () { + local VARIABLE_NAME="$1" + local VARIABLE_VALUE="$2" + + if [[ -n ${VARIABLE_VALUE} ]]; then + sed -i "s~${VARIABLE_NAME}=.*~${VARIABLE_NAME}=${VARIABLE_VALUE}~g" $BASE_DIR/.env + fi +} + +install_mysql_server () { + reconfigure DATABASE_MIGRATION ${DATABASE_MIGRATION} + reconfigure MYSQL_DATABASE ${MYSQL_DATABASE} + reconfigure MYSQL_USER ${MYSQL_USER} + reconfigure MYSQL_PASSWORD ${MYSQL_PASSWORD} + reconfigure MYSQL_ROOT_PASSWORD ${MYSQL_ROOT_PASSWORD} + + if [[ -z ${MYSQL_HOST} ]] && [ "$INSTALL_MYSQL_SERVER" == "true" ]; then + reconfigure MYSQL_VERSION ${MYSQL_VERSION} + docker-compose -f $BASE_DIR/db.yml up -d + elif [ ! -z "$MYSQL_HOST" ]; then + establish_conn ${MYSQL_HOST} "${MYSQL_PORT:-"3306"}" "MySQL" + reconfigure MYSQL_HOST ${MYSQL_HOST} + reconfigure MYSQL_PORT "${MYSQL_PORT:-"3306"}" + fi +} + +install_document_server () { + reconfigure DOCUMENT_SERVER_JWT_HEADER ${DOCUMENT_SERVER_JWT_HEADER} + reconfigure DOCUMENT_SERVER_JWT_SECRET ${DOCUMENT_SERVER_JWT_SECRET} + if [[ -z ${DOCUMENT_SERVER_HOST} ]] && [ "$INSTALL_DOCUMENT_SERVER" == "true" ]; then + reconfigure DOCUMENT_SERVER_IMAGE_NAME "${DOCUMENT_SERVER_IMAGE_NAME}:${DOCUMENT_SERVER_VERSION:-$(get_available_version "$DOCUMENT_SERVER_IMAGE_NAME")}" + docker-compose -f $BASE_DIR/ds.yml up -d + elif [ ! -z "$DOCUMENT_SERVER_HOST" ]; then + APP_URL_PORTAL=${APP_URL_PORTAL:-"http://$(curl -s ifconfig.me):${EXTERNAL_PORT}"} + establish_conn ${DOCUMENT_SERVER_HOST} ${DOCUMENT_SERVER_PORT} "${PACKAGE_SYSNAME^^} Docs" + reconfigure DOCUMENT_SERVER_URL_EXTERNAL ${DOCUMENT_SERVER_URL_EXTERNAL} + reconfigure DOCUMENT_SERVER_URL_PUBLIC ${DOCUMENT_SERVER_URL_EXTERNAL} + fi +} + +install_rabbitmq () { + if [[ -z ${RABBIT_HOST} ]] && [ "$INSTALL_RABBITMQ" == "true" ]; then + docker-compose -f $BASE_DIR/rabbitmq.yml up -d + elif [ ! -z "$RABBIT_HOST" ]; then + establish_conn ${RABBIT_HOST} "${RABBIT_PORT:-"5672"}" "RabbitMQ" + reconfigure RABBIT_HOST ${RABBIT_HOST} + reconfigure RABBIT_PORT "${RABBIT_PORT:-"5672"}" + reconfigure RABBIT_USER_NAME ${RABBIT_USER_NAME} + reconfigure RABBIT_PASSWORD ${RABBIT_PASSWORD} + reconfigure RABBIT_VIRTUAL_HOST "${RABBIT_VIRTUAL_HOST:-"/"}" + fi +} + +install_redis () { + if [[ -z ${REDIS_HOST} ]] && [ "$INSTALL_REDIS" == "true" ]; then + docker-compose -f $BASE_DIR/redis.yml up -d + elif [ ! -z "$REDIS_HOST" ]; then + establish_conn ${REDIS_HOST} "${REDIS_PORT:-"6379"}" "Redis" + reconfigure REDIS_HOST ${REDIS_HOST} + reconfigure REDIS_PORT "${REDIS_PORT:-"6379"}" + reconfigure REDIS_USER_NAME ${REDIS_USER_NAME} + reconfigure REDIS_PASSWORD ${REDIS_PASSWORD} + fi +} + +install_elasticsearch () { + if [[ -z ${ELK_HOST} ]] && [ "$INSTALL_ELASTICSEARCH" == "true" ]; then + if [ $(free -m | grep -oP '\d+' | head -n 1) -gt "12228" ]; then #RAM ~12Gb + sed -i 's/Xms[0-9]g/Xms4g/g; s/Xmx[0-9]g/Xmx4g/g' $BASE_DIR/elasticsearch.yml + else + sed -i 's/Xms[0-9]g/Xms1g/g; s/Xmx[0-9]g/Xmx1g/g' $BASE_DIR/elasticsearch.yml + fi + reconfigure ELK_VERSION ${ELK_VERSION} + docker-compose -f $BASE_DIR/elasticsearch.yml up -d + elif [ ! -z "$ELK_HOST" ]; then + establish_conn ${ELK_HOST} "${ELK_PORT:-"9200"}" "Elasticsearch" + reconfigure ELK_SHEME "${ELK_SHEME:-"http"}" + reconfigure ELK_HOST ${ELK_HOST} + reconfigure ELK_PORT "${ELK_PORT:-"9200"}" + fi +} + +install_product () { + DOCKER_TAG="${DOCKER_TAG:-$(get_available_version ${IMAGE_NAME})}" + reconfigure DOCKER_TAG ${DOCKER_TAG} + + [ "${UPDATE}" = "true" ] && LOCAL_CONTAINER_TAG="$(docker inspect --format='{{index .Config.Image}}' ${CONTAINER_NAME} | awk -F':' '{print $2}')" + + if [ "${UPDATE}" = "true" ] && [ "${LOCAL_CONTAINER_TAG}" != "${DOCKER_TAG}" ]; then + docker-compose -f $BASE_DIR/build.yml pull + docker-compose -f $BASE_DIR/migration-runner.yml -f $BASE_DIR/notify.yml -f $BASE_DIR/healthchecks.yml -f ${PROXY_YML} down + docker-compose -f $BASE_DIR/${PRODUCT}.yml down --volumes + fi + + reconfigure ENV_EXTENSION ${ENV_EXTENSION} + reconfigure APP_CORE_MACHINEKEY ${APP_CORE_MACHINEKEY} + reconfigure APP_CORE_BASE_DOMAIN ${APP_CORE_BASE_DOMAIN} + reconfigure APP_URL_PORTAL "${APP_URL_PORTAL:-"http://${PACKAGE_SYSNAME}-router:8092"}" + reconfigure EXTERNAL_PORT ${EXTERNAL_PORT} + + docker-compose -f $BASE_DIR/migration-runner.yml up -d + docker-compose -f $BASE_DIR/${PRODUCT}.yml up -d + docker-compose -f ${PROXY_YML} up -d + docker-compose -f $BASE_DIR/notify.yml up -d + docker-compose -f $BASE_DIR/healthchecks.yml up -d + + if [ ! -z "${CERTIFICATE_PATH}" ] && [ ! -z "${CERTIFICATE_KEY_PATH}" ] && [[ ! -z "${APP_DOMAIN_PORTAL}" ]]; then + bash $BASE_DIR/config/${PRODUCT}-ssl-setup -f "${APP_DOMAIN_PORTAL}" "${CERTIFICATE_PATH}" "${CERTIFICATE_KEY_PATH}" + elif [ ! -z "${LETS_ENCRYPT_DOMAIN}" ] && [ ! -z "${LETS_ENCRYPT_MAIL}" ]; then + bash $BASE_DIR/config/${PRODUCT}-ssl-setup "${LETS_ENCRYPT_MAIL}" "${LETS_ENCRYPT_DOMAIN}" + fi +} + +make_swap () { + DISK_REQUIREMENTS=6144; #6Gb free space + MEMORY_REQUIREMENTS=11000; #RAM ~12Gb + + AVAILABLE_DISK_SPACE=$(df -m / | tail -1 | awk '{ print $4 }'); + TOTAL_MEMORY=$(free -m | grep -oP '\d+' | head -n 1); + EXIST=$(swapon -s | awk '{ print $1 }' | { grep -x ${SWAPFILE} || true; }); + + if [[ -z $EXIST ]] && [ ${TOTAL_MEMORY} -lt ${MEMORY_REQUIREMENTS} ] && [ ${AVAILABLE_DISK_SPACE} -gt ${DISK_REQUIREMENTS} ]; then + + if [ "${DIST}" == "Ubuntu" ] || [ "${DIST}" == "Debian" ]; then + fallocate -l 6G ${SWAPFILE} + else + dd if=/dev/zero of=${SWAPFILE} count=6144 bs=1MiB + fi + + chmod 600 ${SWAPFILE} + mkswap ${SWAPFILE} + swapon ${SWAPFILE} + echo "$SWAPFILE none swap sw 0 0" >> /etc/fstab + fi +} + + +start_installation () { + root_checking + + set_installation_type_data + + get_os_info + check_os_info + check_kernel + + if [ "$UPDATE" != "true" ]; then + check_ports + fi + + if [ "$SKIP_HARDWARE_CHECK" != "true" ]; then + check_hardware + fi + + if [ "$MAKESWAP" == "true" ]; then + make_swap + fi + + if command_exists docker ; then + check_docker_version + service docker start + else + install_docker + fi + + docker_login + + create_network + + domain_check + + if [ "$UPDATE" = "true" ]; then + set_docspace_params + fi + + set_docs_url_external + set_jwt_secret + set_jwt_header + + set_core_machinekey + + set_mysql_params + + download_files + + install_mysql_server + + install_document_server + + install_rabbitmq + + install_redis + + install_elasticsearch + + if [ "$INSTALL_PRODUCT" == "true" ]; then + install_product + fi + + echo "" + echo "Thank you for installing ${PACKAGE_SYSNAME^^} ${PRODUCT_NAME}." + echo "In case you have any questions contact us via http://support.${PACKAGE_SYSNAME}.com or visit our forum at http://forum.${PACKAGE_SYSNAME}.com" + echo "" + + exit 0; +} + +start_installation diff --git a/config/mysql/conf.d/mysql.cnf b/config/mysql/conf.d/mysql.cnf new file mode 100644 index 0000000..e37dc6a --- /dev/null +++ b/config/mysql/conf.d/mysql.cnf @@ -0,0 +1,5 @@ +[mysqld] +sql_mode = 'NO_ENGINE_SUBSTITUTION' +max_connections = 1000 +max_allowed_packet = 1048576000 +group_concat_max_len = 2048 diff --git a/config/nginx/docker-entrypoint.d/10-listen-on-ipv6-by-default.sh b/config/nginx/docker-entrypoint.d/10-listen-on-ipv6-by-default.sh new file mode 100755 index 0000000..b265586 --- /dev/null +++ b/config/nginx/docker-entrypoint.d/10-listen-on-ipv6-by-default.sh @@ -0,0 +1,67 @@ +#!/bin/sh +# vim:sw=4:ts=4:et + +set -e + +entrypoint_log() { + if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "$@" + fi +} + +ME=$(basename $0) +DEFAULT_CONF_FILE="etc/nginx/conf.d/default.conf" + +# check if we have ipv6 available +if [ ! -f "/proc/net/if_inet6" ]; then + entrypoint_log "$ME: info: ipv6 not available" + exit 0 +fi + +if [ ! -f "/$DEFAULT_CONF_FILE" ]; then + entrypoint_log "$ME: info: /$DEFAULT_CONF_FILE is not a file or does not exist" + exit 0 +fi + +# check if the file can be modified, e.g. not on a r/o filesystem +touch /$DEFAULT_CONF_FILE 2>/dev/null || { entrypoint_log "$ME: info: can not modify /$DEFAULT_CONF_FILE (read-only file system?)"; exit 0; } + +# check if the file is already modified, e.g. on a container restart +grep -q "listen \[::]\:80;" /$DEFAULT_CONF_FILE && { entrypoint_log "$ME: info: IPv6 listen already enabled"; exit 0; } + +if [ -f "/etc/os-release" ]; then + . /etc/os-release +else + entrypoint_log "$ME: info: can not guess the operating system" + exit 0 +fi + +entrypoint_log "$ME: info: Getting the checksum of /$DEFAULT_CONF_FILE" + +case "$ID" in + "debian") + CHECKSUM=$(dpkg-query --show --showformat='${Conffiles}\n' nginx | grep $DEFAULT_CONF_FILE | cut -d' ' -f 3) + echo "$CHECKSUM /$DEFAULT_CONF_FILE" | md5sum -c - >/dev/null 2>&1 || { + entrypoint_log "$ME: info: /$DEFAULT_CONF_FILE differs from the packaged version" + exit 0 + } + ;; + "alpine") + CHECKSUM=$(apk manifest nginx 2>/dev/null| grep $DEFAULT_CONF_FILE | cut -d' ' -f 1 | cut -d ':' -f 2) + echo "$CHECKSUM /$DEFAULT_CONF_FILE" | sha1sum -c - >/dev/null 2>&1 || { + entrypoint_log "$ME: info: /$DEFAULT_CONF_FILE differs from the packaged version" + exit 0 + } + ;; + *) + entrypoint_log "$ME: info: Unsupported distribution" + exit 0 + ;; +esac + +# enable ipv6 on default.conf listen sockets +sed -i -E 's,listen 80;,listen 80;\n listen [::]:80;,' /$DEFAULT_CONF_FILE + +entrypoint_log "$ME: info: Enabled listen on IPv6 in /$DEFAULT_CONF_FILE" + +exit 0 diff --git a/config/nginx/docker-entrypoint.d/15-local-resolvers.envsh b/config/nginx/docker-entrypoint.d/15-local-resolvers.envsh new file mode 100644 index 0000000..9306215 --- /dev/null +++ b/config/nginx/docker-entrypoint.d/15-local-resolvers.envsh @@ -0,0 +1,11 @@ +#!/bin/sh +# vim:sw=2:ts=2:sts=2:et + +set -eu + +LC_ALL=C +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +[ "${NGINX_ENTRYPOINT_LOCAL_RESOLVERS:-}" ] || return 0 + +export NGINX_LOCAL_RESOLVERS=$(awk 'BEGIN{ORS=" "} $1=="nameserver" {print $2}' /etc/resolv.conf) diff --git a/config/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh b/config/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh new file mode 100755 index 0000000..f3fb9fc --- /dev/null +++ b/config/nginx/docker-entrypoint.d/20-envsubst-on-templates.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +set -e + +ME=$(basename $0) + +entrypoint_log() { + if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "$@" + fi +} + +add_stream_block() { + local conffile="/etc/nginx/nginx.conf" + + if grep -q -E "\s*stream\s*\{" "$conffile"; then + entrypoint_log "$ME: $conffile contains a stream block; include $stream_output_dir/*.conf to enable stream templates" + else + # check if the file can be modified, e.g. not on a r/o filesystem + touch "$conffile" 2>/dev/null || { entrypoint_log "$ME: info: can not modify $conffile (read-only file system?)"; exit 0; } + entrypoint_log "$ME: Appending stream block to $conffile to include $stream_output_dir/*.conf" + cat << END >> "$conffile" +# added by "$ME" on "$(date)" +stream { + include $stream_output_dir/*.conf; +} +END + fi +} + +auto_envsubst() { + local template_dir="${NGINX_ENVSUBST_TEMPLATE_DIR:-/etc/nginx/templates}" + local suffix="${NGINX_ENVSUBST_TEMPLATE_SUFFIX:-.template}" + local output_dir="${NGINX_ENVSUBST_OUTPUT_DIR:-/etc/nginx/conf.d}" + local stream_suffix="${NGINX_ENVSUBST_STREAM_TEMPLATE_SUFFIX:-.stream-template}" + local stream_output_dir="${NGINX_ENVSUBST_STREAM_OUTPUT_DIR:-/etc/nginx/stream-conf.d}" + local filter="${NGINX_ENVSUBST_FILTER:-}" + + local template defined_envs relative_path output_path subdir + defined_envs=$(printf '${%s} ' $(awk "END { for (name in ENVIRON) { print ( name ~ /${filter}/ ) ? name : \"\" } }" < /dev/null )) + [ -d "$template_dir" ] || return 0 + if [ ! -w "$output_dir" ]; then + entrypoint_log "$ME: ERROR: $template_dir exists, but $output_dir is not writable" + return 0 + fi + find "$template_dir" -follow -type f -name "*$suffix" -print | while read -r template; do + relative_path="${template#$template_dir/}" + output_path="$output_dir/${relative_path%$suffix}" + subdir=$(dirname "$relative_path") + # create a subdirectory where the template file exists + mkdir -p "$output_dir/$subdir" + entrypoint_log "$ME: Running envsubst on $template to $output_path" + envsubst "$defined_envs" < "$template" > "$output_path" + done + + # Print the first file with the stream suffix, this will be false if there are none + if test -n "$(find "$template_dir" -name "*$stream_suffix" -print -quit)"; then + mkdir -p "$stream_output_dir" + if [ ! -w "$stream_output_dir" ]; then + entrypoint_log "$ME: ERROR: $template_dir exists, but $stream_output_dir is not writable" + return 0 + fi + add_stream_block + find "$template_dir" -follow -type f -name "*$stream_suffix" -print | while read -r template; do + relative_path="${template#$template_dir/}" + output_path="$stream_output_dir/${relative_path%$stream_suffix}" + subdir=$(dirname "$relative_path") + # create a subdirectory where the template file exists + mkdir -p "$stream_output_dir/$subdir" + entrypoint_log "$ME: Running envsubst on $template to $output_path" + envsubst "$defined_envs" < "$template" > "$output_path" + done + fi +} + +auto_envsubst + +exit 0 diff --git a/config/nginx/docker-entrypoint.d/30-tune-worker-processes.sh b/config/nginx/docker-entrypoint.d/30-tune-worker-processes.sh new file mode 100755 index 0000000..9aa42e9 --- /dev/null +++ b/config/nginx/docker-entrypoint.d/30-tune-worker-processes.sh @@ -0,0 +1,188 @@ +#!/bin/sh +# vim:sw=2:ts=2:sts=2:et + +set -eu + +LC_ALL=C +ME=$( basename "$0" ) +PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin + +[ "${NGINX_ENTRYPOINT_WORKER_PROCESSES_AUTOTUNE:-}" ] || exit 0 + +touch /etc/nginx/nginx.conf 2>/dev/null || { echo >&2 "$ME: error: can not modify /etc/nginx/nginx.conf (read-only file system?)"; exit 0; } + +ceildiv() { + num=$1 + div=$2 + echo $(( (num + div - 1) / div )) +} + +get_cpuset() { + cpusetroot=$1 + cpusetfile=$2 + ncpu=0 + [ -f "$cpusetroot/$cpusetfile" ] || return 1 + for token in $( tr ',' ' ' < "$cpusetroot/$cpusetfile" ); do + case "$token" in + *-*) + count=$( seq $(echo "$token" | tr '-' ' ') | wc -l ) + ncpu=$(( ncpu+count )) + ;; + *) + ncpu=$(( ncpu+1 )) + ;; + esac + done + echo "$ncpu" +} + +get_quota() { + cpuroot=$1 + ncpu=0 + [ -f "$cpuroot/cpu.cfs_quota_us" ] || return 1 + [ -f "$cpuroot/cpu.cfs_period_us" ] || return 1 + cfs_quota=$( cat "$cpuroot/cpu.cfs_quota_us" ) + cfs_period=$( cat "$cpuroot/cpu.cfs_period_us" ) + [ "$cfs_quota" = "-1" ] && return 1 + [ "$cfs_period" = "0" ] && return 1 + ncpu=$( ceildiv "$cfs_quota" "$cfs_period" ) + [ "$ncpu" -gt 0 ] || return 1 + echo "$ncpu" +} + +get_quota_v2() { + cpuroot=$1 + ncpu=0 + [ -f "$cpuroot/cpu.max" ] || return 1 + cfs_quota=$( cut -d' ' -f 1 < "$cpuroot/cpu.max" ) + cfs_period=$( cut -d' ' -f 2 < "$cpuroot/cpu.max" ) + [ "$cfs_quota" = "max" ] && return 1 + [ "$cfs_period" = "0" ] && return 1 + ncpu=$( ceildiv "$cfs_quota" "$cfs_period" ) + [ "$ncpu" -gt 0 ] || return 1 + echo "$ncpu" +} + +get_cgroup_v1_path() { + needle=$1 + found= + foundroot= + mountpoint= + + [ -r "/proc/self/mountinfo" ] || return 1 + [ -r "/proc/self/cgroup" ] || return 1 + + while IFS= read -r line; do + case "$needle" in + "cpuset") + case "$line" in + *cpuset*) + found=$( echo "$line" | cut -d ' ' -f 4,5 ) + break + ;; + esac + ;; + "cpu") + case "$line" in + *cpuset*) + ;; + *cpu,cpuacct*|*cpuacct,cpu|*cpuacct*|*cpu*) + found=$( echo "$line" | cut -d ' ' -f 4,5 ) + break + ;; + esac + esac + done << __EOF__ +$( grep -F -- '- cgroup ' /proc/self/mountinfo ) +__EOF__ + + while IFS= read -r line; do + controller=$( echo "$line" | cut -d: -f 2 ) + case "$needle" in + "cpuset") + case "$controller" in + cpuset) + mountpoint=$( echo "$line" | cut -d: -f 3 ) + break + ;; + esac + ;; + "cpu") + case "$controller" in + cpu,cpuacct|cpuacct,cpu|cpuacct|cpu) + mountpoint=$( echo "$line" | cut -d: -f 3 ) + break + ;; + esac + ;; + esac +done << __EOF__ +$( grep -F -- 'cpu' /proc/self/cgroup ) +__EOF__ + + case "${found%% *}" in + "/") + foundroot="${found##* }$mountpoint" + ;; + "$mountpoint") + foundroot="${found##* }" + ;; + esac + echo "$foundroot" +} + +get_cgroup_v2_path() { + found= + foundroot= + mountpoint= + + [ -r "/proc/self/mountinfo" ] || return 1 + [ -r "/proc/self/cgroup" ] || return 1 + + while IFS= read -r line; do + found=$( echo "$line" | cut -d ' ' -f 4,5 ) + done << __EOF__ +$( grep -F -- '- cgroup2 ' /proc/self/mountinfo ) +__EOF__ + + while IFS= read -r line; do + mountpoint=$( echo "$line" | cut -d: -f 3 ) +done << __EOF__ +$( grep -F -- '0::' /proc/self/cgroup ) +__EOF__ + + case "${found%% *}" in + "") + return 1 + ;; + "/") + foundroot="${found##* }$mountpoint" + ;; + "$mountpoint" | /../*) + foundroot="${found##* }" + ;; + esac + echo "$foundroot" +} + +ncpu_online=$( getconf _NPROCESSORS_ONLN ) +ncpu_cpuset= +ncpu_quota= +ncpu_cpuset_v2= +ncpu_quota_v2= + +cpuset=$( get_cgroup_v1_path "cpuset" ) && ncpu_cpuset=$( get_cpuset "$cpuset" "cpuset.effective_cpus" ) || ncpu_cpuset=$ncpu_online +cpu=$( get_cgroup_v1_path "cpu" ) && ncpu_quota=$( get_quota "$cpu" ) || ncpu_quota=$ncpu_online +cgroup_v2=$( get_cgroup_v2_path ) && ncpu_cpuset_v2=$( get_cpuset "$cgroup_v2" "cpuset.cpus.effective" ) || ncpu_cpuset_v2=$ncpu_online +cgroup_v2=$( get_cgroup_v2_path ) && ncpu_quota_v2=$( get_quota_v2 "$cgroup_v2" ) || ncpu_quota_v2=$ncpu_online + +ncpu=$( printf "%s\n%s\n%s\n%s\n%s\n" \ + "$ncpu_online" \ + "$ncpu_cpuset" \ + "$ncpu_quota" \ + "$ncpu_cpuset_v2" \ + "$ncpu_quota_v2" \ + | sort -n \ + | head -n 1 ) + +sed -i.bak -r 's/^(worker_processes)(.*)$/# Commented out by '"$ME"' on '"$(date)"'\n#\1\2\n\1 '"$ncpu"';/' /etc/nginx/nginx.conf diff --git a/config/nginx/docker-entrypoint.sh b/config/nginx/docker-entrypoint.sh new file mode 100755 index 0000000..721d30f --- /dev/null +++ b/config/nginx/docker-entrypoint.sh @@ -0,0 +1,47 @@ +#!/bin/sh +# vim:sw=4:ts=4:et + +set -e + +entrypoint_log() { + if [ -z "${NGINX_ENTRYPOINT_QUIET_LOGS:-}" ]; then + echo "$@" + fi +} + + + if /usr/bin/find "/docker-entrypoint.d/" -mindepth 1 -maxdepth 1 -type f -print -quit 2>/dev/null | read v; then + entrypoint_log "$0: /docker-entrypoint.d/ is not empty, will attempt to perform configuration" + + entrypoint_log "$0: Looking for shell scripts in /docker-entrypoint.d/" + find "/docker-entrypoint.d/" -follow -type f -print | sort -V | while read -r f; do + case "$f" in + *.envsh) + if [ -x "$f" ]; then + entrypoint_log "$0: Sourcing $f"; + . "$f" + else + # warn on shell scripts without exec bit + entrypoint_log "$0: Ignoring $f, not executable"; + fi + ;; + *.sh) + if [ -x "$f" ]; then + entrypoint_log "$0: Launching $f"; + "$f" + else + # warn on shell scripts without exec bit + entrypoint_log "$0: Ignoring $f, not executable"; + fi + ;; + *) entrypoint_log "$0: Ignoring $f";; + esac + done + + entrypoint_log "$0: Configuration complete; ready for start up" + else + entrypoint_log "$0: No files found in /docker-entrypoint.d/, skipping configuration" + fi + + +exec "$@" diff --git a/config/nginx/letsencrypt.conf b/config/nginx/letsencrypt.conf new file mode 100644 index 0000000..279d4b5 --- /dev/null +++ b/config/nginx/letsencrypt.conf @@ -0,0 +1,4 @@ +location ~ /.well-known/acme-challenge { + root "/letsencrypt"; + allow all; +} diff --git a/config/nginx/onlyoffice-proxy-ssl.conf b/config/nginx/onlyoffice-proxy-ssl.conf new file mode 100644 index 0000000..ee2ea34 --- /dev/null +++ b/config/nginx/onlyoffice-proxy-ssl.conf @@ -0,0 +1,76 @@ +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection $proxy_connection; +proxy_set_header Host $the_host; +proxy_set_header X-Forwarded-Host $the_host; +proxy_set_header X-Forwarded-Proto $the_scheme; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + +## HTTP host +server { + listen 0.0.0.0:80; + listen [::]:80 default_server; + server_name _; + + ## Redirects all traffic to the HTTPS host + root /nowhere; ## root doesn't have to be a valid path since we are redirecting + rewrite ^ https://$host$request_uri? permanent; +} + +server { + listen 127.0.0.1:80; + listen [::1]:80; + server_name localhost; + + client_max_body_size 4G; + + location / { + proxy_pass http://$router_host:8092; + } +} + +## HTTPS host +server { + listen 0.0.0.0:443 ssl; + listen [::]:443 ssl default_server; + root /usr/share/nginx/html; + + client_max_body_size 4G; + + ## Strong SSL Security + ## https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html + ssl_certificate /usr/local/share/ca-certificates/tls.crt; + ssl_certificate_key /etc/ssl/private/tls.key; + # Uncomment string below and specify the path to the file with the password if you use encrypted certificate key + # ssl_password_file $ssl_password_path; + ssl_verify_client off; + + ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH"; + + ssl_protocols TLSv1.2; + ssl_session_cache builtin:1000 shared:SSL:10m; + + ssl_prefer_server_ciphers on; + + add_header Strict-Transport-Security max-age=31536000; + # add_header X-Frame-Options SAMEORIGIN; + add_header X-Content-Type-Options nosniff; + + ## [Optional] If your certficate has OCSP, enable OCSP stapling to reduce the overhead and latency of running SSL. + ## Replace with your ssl_trusted_certificate. For more info see: + ## - https://medium.com/devops-programming/4445f4862461 + ## - https://www.ruby-forum.com/topic/4419319 + ## - https://www.digitalocean.com/community/tutorials/how-to-configure-ocsp-stapling-on-apache-and-nginx + # ssl_stapling on; + # ssl_stapling_verify on; + # ssl_trusted_certificate /etc/nginx/ssl/stapling.trusted.crt; + # resolver 208.67.222.222 208.67.222.220 valid=300s; # Can change to your DNS resolver if desired + # resolver_timeout 10s; + + ssl_dhparam /etc/ssl/certs/dhparam.pem; + + location / { + proxy_pass http://$router_host:8092; + } + + include includes/letsencrypt.conf; +} diff --git a/config/nginx/onlyoffice-proxy.conf b/config/nginx/onlyoffice-proxy.conf new file mode 100644 index 0000000..89fad8d --- /dev/null +++ b/config/nginx/onlyoffice-proxy.conf @@ -0,0 +1,19 @@ +proxy_set_header Upgrade $http_upgrade; +proxy_set_header Connection $proxy_connection; +proxy_set_header Host $the_host; +proxy_set_header X-Forwarded-Host $the_host; +proxy_set_header X-Forwarded-Proto $the_scheme; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + +server { + listen 0.0.0.0:80; + listen [::]:80 default_server; + + client_max_body_size 4G; + + location / { + proxy_pass http://$router_host:8092; + } + + include includes/letsencrypt.conf; +} diff --git a/config/nginx/templates/nginx.conf.template b/config/nginx/templates/nginx.conf.template new file mode 100644 index 0000000..d89e400 --- /dev/null +++ b/config/nginx/templates/nginx.conf.template @@ -0,0 +1,33 @@ +user nginx; +worker_processes 1; + +error_log /var/log/nginx/error.log warn; +pid /tmp/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + client_body_temp_path /tmp/client_temp; + proxy_temp_path /tmp/proxy_temp_path; + fastcgi_temp_path /tmp/fastcgi_temp; + uwsgi_temp_path /tmp/uwsgi_temp; + scgi_temp_path /tmp/scgi_temp; + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + + keepalive_timeout 65; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/config/nginx/templates/proxy.upstream.conf.template b/config/nginx/templates/proxy.upstream.conf.template new file mode 100644 index 0000000..2048b12 --- /dev/null +++ b/config/nginx/templates/proxy.upstream.conf.template @@ -0,0 +1,32 @@ +resolver 127.0.0.11 valid=30s; + +map $http_host $this_host { + "" $host; + default $http_host; +} + +map $http_x_forwarded_proto $the_scheme { + default $http_x_forwarded_proto; + "" $scheme; +} + +map $http_x_forwarded_host $the_host { + default $http_x_forwarded_host; + "" $host; +} + +map $http_x_forwarded_port $proxy_x_forwarded_port { + default $http_x_forwarded_port; + '' $server_port; +} + +map $http_upgrade $proxy_connection { + default upgrade; + "" close; +} + +map $ROUTER_HOST $router_host { + volatile; + default onlyoffice-router; + ~^(.*)$ $1; +} diff --git a/config/nginx/templates/upstream.conf.template b/config/nginx/templates/upstream.conf.template new file mode 100644 index 0000000..54e70d9 --- /dev/null +++ b/config/nginx/templates/upstream.conf.template @@ -0,0 +1,79 @@ +resolver $DNS_NAMESERVER valid=30s; + +map $SERVICE_LOGIN $service_login { + volatile; + "" 127.0.0.1:5011; + default $SERVICE_LOGIN; +} + +map $SERVICE_DOCEDITOR $service_doceditor { + volatile; + "" 127.0.0.1:5013; + default $SERVICE_DOCEDITOR; +} + +map $SERVICE_API_SYSTEM $service_api_system { + volatile; + "" 127.0.0.1:5010; + default $SERVICE_API_SYSTEM; +} + +map $SERVICE_BACKUP $service_backup { + volatile; + "" 127.0.0.1:5012; + default $SERVICE_BACKUP; +} + +map $SERVICE_FILES $service_files { + volatile; + "" 127.0.0.1:5007; + default $SERVICE_FILES; +} + +map $SERVICE_PEOPLE_SERVER $service_people_server { + volatile; + "" 127.0.0.1:5004; + default $SERVICE_PEOPLE_SERVER; +} + +map $SERVICE_SOCKET $service_socket { + volatile; + "" 127.0.0.1:9899; + default $SERVICE_SOCKET; +} + +map $SERVICE_API $service_api { + volatile; + "" 127.0.0.1:5000; + default $SERVICE_API; +} + +map $SERVICE_STUDIO $service_studio { + volatile; + "" 127.0.0.1:5003; + default $SERVICE_STUDIO; +} + +map $SERVICE_SSOAUTH $service_sso { + volatile; + "" 127.0.0.1:9834; + default $SERVICE_SSOAUTH; +} + +map $SERVICE_HELTHCHECKS $service_healthchecks { + volatile; + "" 127.0.0.1:5033; + default $SERVICE_HELTHCHECKS; +} + +map "$DOCUMENT_SERVER_URL_EXTERNAL" "$document_server" { + volatile; + default "$DOCUMENT_SERVER_URL_EXTERNAL"; + "" "http://$DOCUMENT_CONTAINER_NAME"; +} + +map $SERVICE_CLIENT $service_client { + volatile; + "" 127.0.0.1:5001; + default $SERVICE_CLIENT; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a226eac --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,383 @@ +version: "3.8" + +####### +x-healthcheck: + &x-healthcheck + test: curl --fail http://127.0.0.1 || exit 1 + interval: 60s + retries: 5 + start_period: 20s + timeout: 10s + +x-service: + &x-service-base + container_name: base + restart: always + expose: + - ${SERVICE_PORT} + environment: + MYSQL_CONTAINER_NAME: ${MYSQL_CONTAINER_NAME} + MYSQL_HOST: ${MYSQL_HOST} + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + DATABASE_MIGRATION: ${DATABASE_MIGRATION} + APP_DOTNET_ENV: ${APP_DOTNET_ENV} + APP_KNOWN_NETWORKS: ${APP_KNOWN_NETWORKS} + APP_KNOWN_PROXIES: ${APP_KNOWN_PROXIES} + APP_CORE_BASE_DOMAIN: ${APP_CORE_BASE_DOMAIN} + APP_CORE_MACHINEKEY: ${APP_CORE_MACHINEKEY} + APP_URL_PORTAL: ${APP_URL_PORTAL} + INSTALLATION_TYPE: ${INSTALLATION_TYPE} + OAUTH_REDIRECT_URL: ${OAUTH_REDIRECT_URL} + DOCUMENT_SERVER_JWT_SECRET: ${DOCUMENT_SERVER_JWT_SECRET} + DOCUMENT_SERVER_JWT_HEADER: ${DOCUMENT_SERVER_JWT_HEADER} + DOCUMENT_SERVER_URL_PUBLIC: ${DOCUMENT_SERVER_URL_PUBLIC} + DOCUMENT_CONTAINER_NAME: ${DOCUMENT_CONTAINER_NAME} + DOCUMENT_SERVER_URL_EXTERNAL: ${DOCUMENT_SERVER_URL_EXTERNAL} + # KAFKA_HOST: ${KAFKA_HOST} + ELK_CONTAINER_NAME: ${ELK_CONTAINER_NAME} + ELK_SHEME: ${ELK_SHEME} + ELK_HOST: ${ELK_HOST} + ELK_PORT: ${ELK_PORT} + REDIS_CONTAINER_NAME: ${REDIS_CONTAINER_NAME} + REDIS_HOST: ${REDIS_HOST} + REDIS_PORT: ${REDIS_PORT} + REDIS_USER_NAME: ${REDIS_USER_NAME} + REDIS_PASSWORD: ${REDIS_PASSWORD} + RABBIT_CONTAINER_NAME: ${RABBIT_CONTAINER_NAME} + RABBIT_HOST: ${RABBIT_HOST} + RABBIT_PORT: ${RABBIT_PORT} + RABBIT_VIRTUAL_HOST: ${RABBIT_VIRTUAL_HOST} + RABBIT_USER_NAME: ${RABBIT_USER_NAME} + RABBIT_PASSWORD: ${RABBIT_PASSWORD} + ROUTER_HOST: ${ROUTER_HOST} + LOG_LEVEL: ${LOG_LEVEL} + DEBUG_INFO: ${DEBUG_INFO} + volumes: + - ./data/app_data:/app/onlyoffice/data # changed + - files_data:/var/www/products/ASC.Files/server/ + - people_data:/var/www/products/ASC.People/server/ + # added + depends_on: + onlyoffice-migration-runner: + condition: service_completed_successfully + onlyoffice-mysql-server: + condition: service_healthy +####### + +####### +services: + onlyoffice-mysql-server: + image: ${MYSQL_IMAGE} + command: --default-authentication-plugin=caching_sha2_password + cap_add: + - SYS_NICE + container_name: ${MYSQL_CONTAINER_NAME} + restart: always +# tty: true + user: mysql + expose: + - "3306" + ports: + - 33060:3306 + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - ./data/mysql_data:/var/lib/mysql # changed + - ./config/mysql/conf.d/:/etc/mysql/conf.d + # added + healthcheck: + test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] + timeout: 20s + retries: 10 + + onlyoffice-migration-runner: + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-migration-runner:${DOCKER_TAG}" + container_name: ${MIGRATION_RUNNER_HOST} + restart: "no" + environment: + MYSQL_CONTAINER_NAME: ${MYSQL_CONTAINER_NAME} + MYSQL_HOST: ${MYSQL_HOST} + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + # added + depends_on: + onlyoffice-mysql-server: + condition: service_healthy +####### + + +####### + onlyoffice-rabbitmq: + image: rabbitmq:3 + container_name: ${RABBIT_CONTAINER_NAME} + restart: always + expose: + - "5672" + - "80" + + onlyoffice-redis: + image: redis:7 + container_name: ${REDIS_CONTAINER_NAME} + restart: always + expose: + - "6379" + + onlyoffice-elasticsearch: + image: onlyoffice/elasticsearch:${ELK_VERSION} + container_name: ${ELK_CONTAINER_NAME} + restart: always + environment: + - discovery.type=single-node + - bootstrap.memory_lock=true + - "ES_JAVA_OPTS=-Xms1g -Xmx1g -Dlog4j2.formatMsgNoLookups=true" # changed Xms4g > Xms1g + - "indices.fielddata.cache.size=30%" + - "indices.memory.index_buffer_size=30%" + - "ingest.geoip.downloader.enabled=false" + ulimits: +# memlock: # changed for LXC +# soft: -1 # changed for LXC +# hard: -1 # changed for LXC + nofile: + soft: 65535 + hard: 65535 + volumes: + - ./data/es_data:/usr/share/elasticsearch/data # changed + expose: + - "9200" + - "9300" +####### + +####### + onlyoffice-backup-background-tasks: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-backup-background:${DOCKER_TAG}" + container_name: ${BACKUP_BACKGRUOND_TASKS_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_BACKUP_BACKGRUOND_TASKS}/health/ || exit 1 + + onlyoffice-backup: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-backup:${DOCKER_TAG}" + container_name: ${BACKUP_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_BACKUP}/health/ || exit 1 + + onlyoffice-clear-events: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-clear-events:${DOCKER_TAG}" + container_name: ${CLEAR_EVENTS_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_CLEAR_EVENTS}/health/ || exit 1 + + onlyoffice-files: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-files:${DOCKER_TAG}" + container_name: ${FILES_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_FILES}/health/ || exit 1 + + onlyoffice-files-services: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-files-services:${DOCKER_TAG}" + container_name: ${FILES_SERVICES_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_FILES_SERVICES}/health/ || exit 1 + + onlyoffice-people-server: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-people-server:${DOCKER_TAG}" + container_name: ${PEOPLE_SERVER_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_PEOPLE_SERVER}/health/ || exit 1 + + onlyoffice-socket: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-socket:${DOCKER_TAG}" + container_name: ${SOCKET_HOST} + expose: + - ${SERVICE_PORT} + + onlyoffice-studio-notify: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-studio-notify:${DOCKER_TAG}" + container_name: ${STUDIO_NOTIFY_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_STUDIO_NOTIFY}/health/ || exit 1 + + onlyoffice-api: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-api:${DOCKER_TAG}" + container_name: ${API_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_API}/health/ || exit 1 + + onlyoffice-api-system: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-api-system:${DOCKER_TAG}" + container_name: ${API_SYSTEM_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_API_SYSTEM}/health/ || exit 1 + + onlyoffice-studio: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-studio:${DOCKER_TAG}" + container_name: ${STUDIO_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_STUDIO}/health/ || exit 1 + + onlyoffice-ssoauth: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-ssoauth:${DOCKER_TAG}" + container_name: ${SSOAUTH_HOST} + expose: + - ${SERVICE_PORT} + - "9834" + + onlyoffice-doceditor: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-doceditor:${DOCKER_TAG}" + container_name: ${DOCEDITOR_HOST} + expose: + - "5013" + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_DOCEDITOR}/health || exit 1 + + onlyoffice-login: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-login:${DOCKER_TAG}" + container_name: ${LOGIN_HOST} + expose: + - "5011" + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_LOGIN}/health || exit 1 +####### + +####### + onlyoffice-router: + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-router:${DOCKER_TAG}" + container_name: ${ROUTER_HOST} + restart: always + healthcheck: + <<: *x-healthcheck + test: nginx -t || exit 1 + expose: + - "8081" + - "8099" + - "8092" + depends_on: + - onlyoffice-backup-background-tasks + - onlyoffice-backup + - onlyoffice-clear-events + - onlyoffice-files + - onlyoffice-files-services + - onlyoffice-people-server + - onlyoffice-socket + - onlyoffice-studio-notify + - onlyoffice-api + - onlyoffice-api-system + - onlyoffice-studio + - onlyoffice-ssoauth + - onlyoffice-doceditor + - onlyoffice-login + environment: + - SERVICE_BACKUP=${SERVICE_BACKUP} + - SERVICE_FILES=${SERVICE_FILES} + - SERVICE_FILES_SERVICES=${SERVICE_FILES_SERVICES} + - SERVICE_CLEAR_EVENTS=${SERVICE_CLEAR_EVENTS} + - SERVICE_NOTIFY=${SERVICE_NOTIFY} + - SERVICE_PEOPLE_SERVER=${SERVICE_PEOPLE_SERVER} + - SERVICE_SOCKET=${SERVICE_SOCKET} + - SERVICE_STUDIO_NOTIFY=${SERVICE_STUDIO_NOTIFY} + - SERVICE_API=${SERVICE_API} + - SERVICE_API_SYSTEM=${SERVICE_API_SYSTEM} + - SERVICE_STUDIO=${SERVICE_STUDIO} + - SERVICE_SSOAUTH=${SERVICE_SSOAUTH} + - SERVICE_DOCEDITOR=${SERVICE_DOCEDITOR} + - SERVICE_LOGIN=${SERVICE_LOGIN} + - SERVICE_HELTHCHECKS=${SERVICE_HELTHCHECKS} + - WRONG_PORTAL_NAME_URL=${WRONG_PORTAL_NAME_URL} + - DOCUMENT_CONTAINER_NAME=${DOCUMENT_CONTAINER_NAME} + - DOCUMENT_SERVER_URL_EXTERNAL=${DOCUMENT_SERVER_URL_EXTERNAL} + - REDIS_CONTAINER_NAME=${REDIS_CONTAINER_NAME} + - REDIS_HOST=${REDIS_HOST} + - REDIS_PORT=${REDIS_PORT} + - REDIS_PASSWORD=${REDIS_PASSWORD} + - SERVICE_PORT=${SERVICE_PORT} + volumes: + - ./data/router_log:/var/log/nginx + + onlyoffice-proxy: + image: nginx + container_name: ${PROXY_HOST} + restart: always + healthcheck: + <<: *x-healthcheck + test: nginx -t || exit 1 + ports: + - ${EXTERNAL_PORT}:80 + # - 443:443 # for selfsigned ssl + environment: + - ROUTER_HOST=${ROUTER_HOST} + volumes: + - ./data/webroot_path:/letsencrypt # changed + - ./data/proxy_log:/var/log/nginx # changed + - ./config/nginx/templates/nginx.conf.template:/etc/nginx/nginx.conf + - ./config/nginx/letsencrypt.conf:/etc/nginx/includes/letsencrypt.conf + - ./config/nginx/templates/proxy.upstream.conf.template:/etc/nginx/templates/proxy.upstream.conf.template:ro + - ./config/nginx/onlyoffice-proxy.conf:/etc/nginx/conf.d/default.conf + # - ${CERTIFICATE_PATH}:/usr/local/share/ca-certificates/tls.crt # for selfsigned ssl + # - ${CERTIFICATE_KEY_PATH}:/etc/ssl/private/tls.key # for selfsigned ssl + # - ${DHPARAM_PATH}:/etc/ssl/certs/dhparam.pem # for selfsigned ssl + +####### + +####### + onlyoffice-notify: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-notify:${DOCKER_TAG}" + container_name: ${NOTIFY_HOST} + healthcheck: + <<: *x-healthcheck + test: curl --fail http://${SERVICE_NOTIFY}/health/ || exit 1 + + onlyoffice-health-checks-ui: + <<: *x-service-base + image: "${REPO}/${DOCKER_IMAGE_PREFIX}-healthchecks:${DOCKER_TAG}" + container_name: ${HELTHCHECKS_HOST} +####### + +networks: + onlyoffice: + +volumes: + files_data: + people_data: + # mysql_data: + # es_data: + # router_log: + # proxy_log: + # webroot_path: + # app_data: + # crm_data: + # project_data: + # calendar_data: + # mail_data: diff --git a/plugins/drawio.zip b/plugins/drawio.zip new file mode 100644 index 0000000000000000000000000000000000000000..3609bc1aaca1961944001fc1286c32467240e5ae GIT binary patch literal 41833 zcmeHw3w#vS^?yL527e0ps#;q&BaORdvf16dH>@E95R^zHyko@4?j#x6%xrgN^Ki3O zl!{Por4Ox&);HD`T3cI1t5(q#)FQOCzN$s7kNU2)KgFNg|M%QG&&?(R?f=()KEI$z zX6C-nJ@=e*&pG$pmQxQHH=&6By>QmDrB{qUZq@Efi;C!{7~fLKOqZet&({6^KfoSXjkTn;9z~Y}0l(kQt%&JB)_U_vej|`I(gWF~Ukj>o zzk&BaE-o4IZc0hxS1xDg*4?>{?zGl7V)i6uryIc7eK zvQJTULsH|i*6C9lOQIggCH;OarUj)`Dmg#^8yswF zM+PQ+?uhXNuii^p(4#Zc8?$_;9>!Uw8^b>MnY^pgd z0{=pEU{*S zrTPG27gZh$iyFQwu}CRpY0!e!==mE13(yuhtfW9`sS=D!Ng!uH33~PW<%}uaZLrJGBVFr6V+o<5C{@X zX(>NI6eOhKFHY8k+$m*}Mih`MA&}sIo-q(i%4(OVj}EnbEvK&W@Yl1D1(F4&yxtJOEE3s_GwC61)!NVhr))zAwp%V?XqS%vZG}? zH@#uenJ}ah0KSto;>j9m$JiMhGI}=<)oP|Kg1&qzgmQXJ=D1;ql8a;k`HkcDkRcUpQ4qS zwUqUsnljLp9zo=UPneZZ3|~T#y3!IpYHCwbiLZ1BAu$HdYBawIDDNMK+t@il`9YmT zCq)iq%{40XwPi0wUUyq1u)~B?P)3Jl^8SFQU`5fQ4V#OzEdr1bsrCojb42zT<;Uo( zkp@$0mmqd3Ntvixv`tWYq%K(i!Nn&5%UJGJ;xY)fkdXD2hL*xNN}2_V-6jM%xR9OI zgC@jCwUt*2pnb9;IyNR8sf_+quzH$y+4eVNc~(5d|FC^ z-6XXx;>RRtbb=Ey=B%5{7A(j0E!(FgjP9rq4u!rbh~2W%)ooaBDoRELL)MKli<9W- zcvKK_jA1J=UYJ_4itq~DU*g?LGLe?mXj?X}LE=zpXCf%Gsat}WqFzSbJ<4J4Av4ZZ zU^rQ@c8qNf1bI1)j)?qyhL#wJDtOUTNojzwN}-290p6KOCR^ia867AIF@ZqSWk(>e zxO`S(yI10*@g#DT@#4fXHFcgQC59HkKF=#Vm~W1tU{&tZ@^*#2f#I@3@Kx1%~JmyKH&h3`C$n|2S{rynL`RKR9$ zw%}q;k0A{yu#h!3ObOPPAw_2M{ti=yk7j5gk`vMU;Y#u3!o?N=Q(Wa}?hJ+wLEw<3 z;5Melga!us*x95vn#Fk~z?t0u-`LqqvXfxE&~^IO23g;3eb#L(Kz zM$ev%z;i1f9+Oa@#RL&`vr6%$K?n;`AtZPTVMSPl@R*j;imbbe;alNiSUq+*T1jT7 zl6WSiK9-<&B|(?XhN)_7br}E%>zq)kW3@G^iCO)M5u?r-UdVee-hsU>?pl(tS8#Sq z%xr-%TEVrI)-(;#!YQ^E4w_`Lde~YxQ#oe@Wm@9=v!JMoAy{g+3K9BK(!>cg9@poH z`I&^_yrgO~N$w}DsjEzlo#1Jqu03a}1JN3(((@F54D8e^>}Zs8WLoX8MOYfbh^4+~ z*@&VULHrg4{1TucXP^0!X=tkuE8C;cI`Wysh(MJnb4l2$uEMHLG1ZMC3RRh%vP}hm z80M8Qg;0X+gBm+wwVB0+94mnZi1sVaRtc)hL|ID1{;-D=Rj427)g-aetVw&Kyt{m@ zie}!ZSy5ClK`Qd`n3)-|5HXA>9}KO_Q%MJG<_5_$`(I@ZPL5?^$6|wM@XJB7pRv3x zol7~ZLt{#S$M%a9#{K{q_gv))jZcQX!yaL5FGmQ1Ze(=98af*X^!rU%FI}=ui!6ae zEunNOa)RtKP?nQ&*c@Rs8B=uAL>`zF6khLty#tX zOd+b+%lWe!8+r2T(?MEc7OI8Rd7@?Q@J_n{CBg&YK3b`@>3ml^~ zu^P)BW*QS1dK1)%S%@C}KaJ5sFD@%!S&CRn=G4|&$ZN06f{U-g@0b*ZU@c$xL)I(r zjAy}mbsOzKcNnv?xxjx0&U2K9L`|f!&IAlmJhFnoeg{h|0}kYs*^E43!8X89Ix@=q z3n-GxQJJVBdLtGfMZ-)1^eR}!8UcEw`SViM{7EudN5}*S*1v5la!Yafyu@L@+{j~! zKBlk-^DSoHL!Sw(e2F7*2$4=g2_`Tt5ur3E(q5^dAxw@3NHhh6f_Y4_#vyh>Mx`IGa+JM$gl3-Z}eX7l@T~iJ%AGg@Xk}&60gdN4?sOQb7=gp|+&8X+i zsOQaazDfUQJ#Snpy6R93yxX9n^TQV;AVQZ0uN#U0w zl;}~^R@op$l^)ANtr8Ww4Fe{<@^Yp@A)6I#t#mjy8sw#;c0~o+UBfx2vlBZV5lRc% z@NfYN&ZTLmIyg*{0n+4mF!n)-VwN`Z69-)&e>npa<${c=lDZ64atS(*ViKl8 zD3F*m>`8&2jORO5wnQc%R;{;Mgk1shI~BaC1? zFI6_X3@K~ag0}oIq_{O++3+Z}SWBKsAk|jPY73p5NBSOb&%F6*O56qJLjgNdeoUJ? z`#dOVN6L=rwX?(jP=RJrZuDS{Q<|%Y*(SEcQlplH8SbWIGByB4(A#&66;7{A@fwS% z4P0D{szLaQ1qx)Rp#t-@0!bJZmV#0s1p_d3P$hmSg(Wx}mX;>r^lG8Or;Wyd6i2fL z1u&Eo=9CofGf&41kk=^Bt91Ak_6+3owB)zC0EF@+IMd-mClB1rzIV#-Gaa@l*K{W} zII*@uGX;%#5G|3@o}W>PGWg-KTFWpbw*KKx3pE8$l8UNsCAl21)Oa>9R6=Zv2Bib)PzdoH^!X5R58TslR5)V4MW)Xur*^eV>4ZnN7tP2i z&zkIoM3S6_*!pu5EpqZk2G;_dNyZHa$B8s}3NS6PmET$+i`;b3DePF_`D*Y~#9D?6 zD~uOdD4ry0!$M+haIggWe``P^w8Z9$4cJCq8VsDR2{!?KcUd<&y1zR^cVVNub8ciD zuHfF_BGtL?sK|$>@@X@XjSrGprSi~ixc-fdZQh;@MK(%rHP?`hYuxLk0Q9gm759B# z>S*BE#B*jgILKb$9YaL9WiAmZz<|^oLurF2#SI7~kV?YzD;ZLwdkB0U9Zd+4u9ms5 zp<=~x_9~kOIC$8@l_xIy%g-pEI;C6`KwLOkX@ll3t@RJUjVYCe=W&0#UAnYXB|YaX z3GVZbO#y4vO_pRhtt(LVIxU=uCU^^0H>QG_SkJcoue4MsCoPnf&}Kb2j_eI^+$|T# z$r+YDtP^sTVm`eLDj-7~$PP$2|2Btit^^Jg%!QN9wRAX~%&dIoEJ?+o6L$WQk%a(i za9azGI}E4N7~asrMl~WTG5K*uQtR-yop^>it*4)ylYQM#pvQy)@}bXCfyabMC{j@t zsws=qEDTpiD(hOwE%b#$!BD*q zAF3YRnm^F&`#Stk! zuuAZe8P^=5aMBs7&o|8!gndeazAszQI&@2%!e^g7OE5n7@z0%*l36V&v~EANg7;H>f{#8k&#;(K8>!~++;t4e;p#fU z*CEAMcBQq9ih5Ky`3XMQrn^~Hdbfnhp+^P@AO1~CYnoxdv0*mJ$s_{~dS5CmaCQm~KtxvxH0w{1QMdyRoU z#XLtthe_JPVznsNpi3|#Fe>a9Y>MJqaH3F7o!85SfJJXO>Gf_qkS&uU)onSa6)SWY z>VT;*-DZ1aqgzWvg_ilP3(579d@A*5R%oI#8mdu7ew465(|DZW%%jvE4~~jhXA}Y~oe&uEyN*9NVU|Rr zy>biihzKcQY$`}C<0v3xSxs<05pL&J2HV`3##t+Vz5?W4hx4fp^(iK&5OC8$V&gQ- zA+TX1)a>>xgXysCTNX*^_3iel*`SiJEqW%omFymhmQx5ET>TQ}zXE9BH8i_dZDQseF4cK5fMsOJFy%I!!IjE#dM4wH$BjHXT)*K@>=FE2r=1+>1wzg~U?RDR z;V7$!Q)_^2TYsVilHm_ma*MwRI6&98I<{oY^Zanlmg>u&9paMZ^YF57va~D6jMgAW4$A$v)W@z9~@V#Jv84kePp7So|VP+ZCge7TB?ebCm zKJE}8Uw!$yHp$*=l=qD|Q+!BHFd#%eu;AS3WknobnZ0E}+P{DWb?zk#>ikp|^w0Ajb0h4s zqVJB(#h4M>YC+1EJ$tJ~*jlmegEVCOfUa;L za%NEoDZ;c1Fg;Iw7B_=W``(mvZzpuNovRl_!f0m#mWBGV1J_ zXctQ=O0H8h{U~{`lm%MfT(CbYib`2UB&hdVfmDcp6-q1=OXWW;fDE>~*51}7jtz++ zgZ)=+q-K;Ek22#?W;_~`4dL5N7rIeqJj#rRGUFaeQTYk#5g0O*wN3~&^8KPxWgYC` zwKoLYOjtsK3LHEY7nkLM%tm0Rj#CIsi+K)`2%I*=RKe|wWiaQ$TxJ_C_*cjRRsfu6 zmLDe(nwWXY2@S`NSTAM)5rxyBzccN^g4hd2u6Mc@{CF8E~ensO~0Lbb(&VMPMsRCGAan_>XV&(ZEAbKtRJ37 z<`MmFNuS@RlC6b;9`bOGvZ!Fg=QrAv_Lz)cw$2u(r0r<0@U9Z=E&!N$fnNU|K^d7U zy%9E}+-cx~As1j3%;v>PuQ-MW9j=}U_!qtKMkRZ|&baF8!FG6mQlE%#vO ziy6?Dhf)G+BRh}+Dt-*PLz|)XyXx^KSao^7Vv!}`Tq7sur^tU`m3|1S3Q(;?c>W1TzoUodK!c*xOshPqVtY!+QI?W6v!QAR73YipXJ>{ zL=(j(hx|mMa5q96421eJLx2)B7DSzVm*{*!3f_KFdVte&cLm-?94vQ-DdYUj2~(Dj zvE;Ks)?m}uTe zhiGylPf@U_%oCtSge8rw8!|ym^}8VKtDkGjzBzAo_)>uT-2gXiu!d znPeCIA|=&Mj7xT66Bc@M>$OgjQ|E&h%MV?Lg91*E5>C09)SO)^fdy=~HdD?Rdc50C z;-`ox$hj~Jr+%Cs%Bo#vmfDq6bXOUt6SMS`wDmSE>C~5Tie>bbQrhVr6ijv+)4)rk%93q;B0PDY{*0ms9X$kfAQ87(#?tBby6I0g6sqx6_qoscS$=+w~=#Qu7qV zFYZ+y`l*xHYgai>GT;I6&~?t7s!mT%li}Y7EKNB5MB`R(PSet^2}n7`TC}7a5mH83 zhcomRghI)5Wwzd``}{M~lCC5JcE3_ir`_53v*gN{c6zZu>)Cp}`rrk2C7p`h!`2M^ zAnXL)8FeedykO?+q+X}YLgo3p?t{n6V&(a}*BEvV!x_QCt-naG+`3V=(`0ApJHE5y zZv1!0j(c`&*>Ts7AKH1+&P?sRcGm^FR_$7~^RAtD?%ZPM(47``UbpMKo%it^c6w%| zJ+7TM?7Ry(e!T1Qo%f)oi&4{$?R?$NN_gV>C)Pi4%M({TajTsQ6tU*-$&F8~esar` zw>^2wQ|q4mzMZ4fSz=GEdU7MufBMYDcB;{Bm*2f*_3k?_-+kxxcH&BBTHgH5hSz?! z>5a>7w-e8H>V5h0n_k}V!&h&;#!fuPsrHr4=l%JfZ~ghsO)p=z#+6=X^w}-HvSrm@ ze(-H8OFP)6hm>YbB8VWN<`XVAaEn<^0(_+4Z|iKXApD!ge}j&MK@@YONXE%q%~Kbw zMfj%-4h~OfvCU)6x)`oqy|>GGVjz4QU49N8Vab-AOfdq|lPw8(>$C(7E+tv+fgXXw zjHPNyP?;gpb)E#}fWkU-fB+NQuA_6QF5<{H~@B<>RK`x+?Bqt^y zIL_4iR3{v1We8uDG-PRr(aWMjhenvnL^@Np)=30pTs0ocLcT-HHUbSIX8A(C2$P^# zU|ar?2e)-ZDz_LGH)VZG7W2X#T?HR@_`JJhhf9DnL(lT&I+HRaIQl))8bnlxYspLx zcvRtDxa-OvPH4D7Bzaoh&eBU%m}fdDp?OHAOL%nGYJihonRm4it|0l6B958CUvbug zz985aM{o-MN;5RfSmTtK6Cta(mcr3;}G1-*1>9P)U_ zr<2=H1DH*2>D){-i`Zp|I7g;Y>v)AAc1c7xK?0zjBSje~Y5{TvVYN#o1+flV2LDJI zvPWTBX1G)x7I`Y%O_RD>m^=)@Db$-h;);Vzf_QcY`+?4ZCVqS+DnQha%Viz1(TC{Y z2wk+-1NfH_V=G4ENYuD`2sMuE2Q|h@cGlYnfq?S$qxRAqRiW}*i}v$g8wf=fHOAvi zS&yd`rU8H!k+cB?6o7k0p>5si5Y<4T8ux-iqbDO%=t5ofQs|L$>?egTROx>Rg&tYS zTX=FEi0!>V{O_jFg{FX0Xr$z!^iY|*lBm`&3}J|zdg2mzGs96@_6ew)WKlZ$i$kMy zPaCBEURh*r)G*6&jW2F*OZw7sk7md|GP$sGddRCLe4RMGp21lL9CY=iV4uOR;Y46g zQo{uiCp1Zoic&R)_xgc?vH1`{zXbebT6qLWP^fI_Uh9X=-Dd0$aMzCnpg`yZ2IG zqL6{r5$i3uaz=qSjz{}XBlllVvQHdHu-#N#tlO>th0?#i__qc;Fsw+B1d`u<DrF91H@AYrnMjxq6U*k^HpAU7MG9_KXM0ZU zie=$t$$}?FyIJUT?mQGtxUF}#&krQ!VXh)nKK&{}7VOJ9M4rySkkC0!Y`_Q3ase$E z1&%?5x!IW_xs%^Bw*)*BZWsacr_0lB$KsYr_tP!6!!<(6^G~tdvWXbt80TncTm$wF z0MpFpYkvx3nnk0*KLlEcXR!Zpai&Q&E!qY{%zqXFxc6#r#)`%;Dm&Bxrjv&rZAoTe zfVHgYeq1U1CW(L|ov_kUCYJkJH>3@nlXAsYV?UO|rzQ=G)hg&ud&#Dawh6Uer z_ql)SBOJT|x_uNU^ZblQ#mP}|QsGE9Do(;_fApSeQ#LRacC^80(A&c8QS;0S5iN4M z$@$w5nya0zEJReNf3G;n%+++(pcV%UNFP~6Ck)7w7RO(?Cv8`9Lex-0lUWUl7C8KH> zqZ=m8bOCFY)eN(;v-l>>Qi^xLduRqj1zWHa=_GW4GF)10g5`vQbGn0h>{Un{GgG01 zN#+Tpyr{q2wXKz1b>_4NkpK!@`R&(AUtrj4A z%;z_G+EdN5^N&_GbR<8Ut0R2YosK|;z_fcSW`55*O;FbNw z8+CxavYxnVqZ2xLm~I2ks@-SC#Pa5Ijc24v*mM?JP!{Z@M1r}3^y4~0zNTEjA;9FX zI1*=ZzZ5)0aTk*_l)OrGrxw>Z^6MJDIPU%{R7GPtEe#-uqt*ktFu%3{4$Y-pf~zG< z#tcwtYfoo@SkqN?R@eD*%Wp@0!H4+ zKpYHYw3m;=TRv|Xd)Eaz8_CI}Jpk7X+C3|<0uN5%pbhWy{*=$v zBfwie&GX0YEjg!m;A3DWdGq2==0VWRWpx94Q75K`nU;qheq8&>Pc&I;hVr35jYF3r zZnX1k+yY`Bi^3h`%UKVSg+tJL zH4RtMlsgrbBe){>%Hx&s;y9nj%i_?s#t({9 zdwLv>9y)_q%UrS5)RT!Wj1?WCAQF-b6LQ{3q{+1YeFhIW9xNMrxi{11-+y=34VY#6 zInv3*k5rdI(4yEe)9~YC7E%N}M*#G0=2^|3Dd;GL~O^wqx^4(vECf^ zRWyZt;b3iDRb_?q*N5jhwR6IuCIrHdR98iO$P@}zh3n9HNL{41 zZeF;OzgO}1CcM|yRaX0|f|a!)s=Nx1H5HXLwe(oMxOQ1+f>pIuwZ7_k;gGK)SQ)O2gcsw*`nuRcj zQ)n$zSu+QBC!p`)rbr|hsjNXc{@oX0V*nbV{!l35yrVqIEso%32>ffZ@}ZuZnlSR2 z_24h1`^@_3JH5|A`;|a{>UTv1Xl~`Be&RdjXZ6_cyq(3=Z?sELV);(s8w^#| zvA^NE8jKU)!&O)S7zg?@574a)MKIn92Cq2)KC;)&!+WT<3gct>*qqf>5}ay-yq?Nn zRZW=H%iFQu^Tv<4W%=xWqn(;+*SIh*RF6G=h9i|tY+h@ttnYlh65KL*DSo}+ncKVr|PY8(HtZg~%$L_OBQ!XvhS-``=0#T(si58|Leq8uUH;xK0-Xd!FS0e zU)^-n`QJI}E0teLFE4(0S)l*L|N5-<_Sa+S?LX+%x}M8c=uMM8s($3qRW&ayonAe7 z*zdwU)3o(dYNFd?P46D}?o}@yzxbuK2k!j+VWIDS=1)IcwdUuK&)sq5w1s2KzHrP< z9Y=oDTRgaB`?d$pyJYdAo*S#5EPhVAzh=`r5C7=;&4-HHN)G(YbJbCo z&A7JrrnR-pinm=h|Nf^R)FxhW%C)B)GN|3PjaQeQUU^%| zn1-t#>iA&SANSmK2AU%h?H56-^#+k@Y_DJs7G6<5hj&Q2@MX?yS>hZk}=0`ChuObzF4Z{g^rW znTT8_%eO>}0+T%*xu&zpzE%11b9mY3$sWI$PxhP3S0;~Gbi^lbz468kcfa}bwo4~; zh#y?E{;WyGw>&?&=a+LXKBD=98`ggE&XIF3>b|J)rPF=izqWYY%=ksCK3H?@_1l`x zZ(Mu+gVS4zPyb-mg0jP=KmF{R>c+b(aMydq&gV<+uAl!v@BheGy><0Hdgog;?_aZL zN8RPCE=_*+^2kB+5BS9~i^lw+THNsb)G@0j zPZX+OIanwef8y>vXTJB=8RtE)>l^nUz2^9LdWwI1W$DbeC+ffO3+aU8nvB`?-`=@2 zb?&d;K7P!qo7YZy;e_2s&A9ywt6pBS>++kozji?JC%-vwPV5)wx3qp{*@Wruu3EkB zoP%GT@}B&QOxuy~ANy$6+SNwUnb!%K>Gv*q@8Z?#j+_xK-tvX_r!2hvut(*!t1qrU z>`P}(cx7AQ<)7@Bd-SB+rf*&|=he*Y2hJ0^H=lIk%-a5&4;Onc7?^od=D#*R z@#u|98*&3nN?-WZoLn>775 zCmg?X_lMg>{2ryYIlD?hyc+{QZ_wI!3jI&JEQ zcmHVIMSYL1s_oeD)Ma0PWAP=0^eId?a;th;pg z=f=!o>h6dnQJ*PdspI&0}wUeCcx^ulY^yo2#EX=-Fw-YGYN) z@l+S#`25LJmS6rY zrSH&Pq2?)x?crMw?0sI1UD)3?_52%3uX^*NwcjiG_$QNl*T4Pbnm4Z7)LEbYpWH>y z#XfiDFMZFSanP~rcbAP3FW&a;S+B16(HXzndC#6q?IVeF`l7FGpMC$dZ}tx!_?ru( z{bLq9IdjI&Q^y`X+fBpJXM}6+X6P|nF*~R0Z zJ+XiGd6SO6WZR8fI`7_@oA8&#_kT2P=FMfRbD~HUk&9r8uMDuSRZ~n^W_jYeOQ@>I@arsZ?D`QuO zR$sXOow0Y___Oijzx=Adf6S^mzyEm7_LJ^h9NMsPQS9y0-hbx)-CrnKSU0|S`gh`& z7SV(MPY+$yK6k?pF6(K(@_^JOk)NM(__Z%xcTL^+@@qHzP_CYYWA*W-~8>fi@T;jcFp_Ob>6*u!VzUR_rCVC?StD@d8-tA zZQu#qSo(o-^^Dc=0jIPW$Rt zH}qxSdwA<11AjPcrTTdFQIjsd^OpBltoiY#13HR@k~iLN-E!HF%U8em(oTQV#ob3A zl)8D`+SO05p7F?a@6JsHzq)pH_XT6aXFN6K&UFXAcggYLD~mgBNzYAv{=bfyQn@%- zJofRy=cXF6Ve^u337pMBUFo2Cq0_55KU%UwT9 zeSYJyN56j7v42WE@^tcJ;*B zxbv&X-_JQ!{QBE=yPn-$ehvA8pi{%n@8;D&Vqt#qe8Ivge&3aLaZAi-IrTu29s!}x O6x|HLbJKP7_WuCMn!v{Z literal 0 HcmV?d00001 diff --git a/plugins/pdf-converter.zip b/plugins/pdf-converter.zip new file mode 100644 index 0000000000000000000000000000000000000000..6cb2d93b3c2ee57a9915236bb2bea5365a279b6f GIT binary patch literal 28709 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 WIR4@%e?%32f`5O8`0npAD*X>m`>6x~ literal 0 HcmV?d00001