diff --git a/bin/quickstart.sh b/bin/quickstart.sh index 01c9f1df..565eb97d 100755 --- a/bin/quickstart.sh +++ b/bin/quickstart.sh @@ -1,132 +1,133 @@ #!/bin/bash set -e function die() { echo "$1" exit 1 } rpm -qv docker-compose >/dev/null 2>&1 || \ test ! -z "$(which docker-compose 2>/dev/null)" || \ die "Is docker-compose installed?" test ! -z "$(grep 'systemd.unified_cgroup_hierarchy=0' /proc/cmdline)" || \ die "systemd containers only work with cgroupv1 (use 'grubby --update-kernel=ALL --args=\"systemd.unified_cgroup_hierarchy=0\"' and a reboot to fix)" base_dir=$(dirname $(dirname $0)) export DOCKER_BUILDKIT=0 docker-compose down -t 1 --remove-orphans docker volume rm kolab_mariadb || : docker volume rm kolab_imap || : docker volume rm kolab_ldap || : +docker volume rm kolab_minio || : # We can't use the following artisan commands because it will just block if redis is unavailable: # src/artisan octane:stop >/dev/null 2>&1 || : # src/artisan horizon:terminate >/dev/null 2>&1 || : # we therefore just kill all artisan processes running. pkill -9 -f artisan || : pkill -9 -f swoole || : bin/regen-certs docker-compose build coturn ldap kolab mariadb meet pdns proxy redis haproxy roundcube -docker-compose up -d coturn ldap kolab mariadb meet pdns redis roundcube +docker-compose up -d coturn ldap kolab mariadb meet pdns redis roundcube minio # Workaround until we have docker-compose --wait (https://github.com/docker/compose/pull/8777) function wait_for_container { container_id="$1" container_name="$(docker inspect "${container_id}" --format '{{ .Name }}')" echo "Waiting for container: ${container_name} [${container_id}]" waiting_done="false" while [[ "${waiting_done}" != "true" ]]; do container_state="$(docker inspect "${container_id}" --format '{{ .State.Status }}')" if [[ "${container_state}" == "running" ]]; then health_status="$(docker inspect "${container_id}" --format '{{ .State.Health.Status }}')" echo "${container_name}: container_state=${container_state}, health_status=${health_status}" if [[ ${health_status} == "healthy" ]]; then waiting_done="true" fi else echo "${container_name}: container_state=${container_state}" waiting_done="true" fi sleep 1; done; } if [ "$1" == "--nodev" ]; then echo "starting everything in containers" docker-compose -f docker-compose.build.yml build swoole docker-compose build webapp docker-compose up -d webapp wait_for_container 'kolab-webapp' docker-compose up --no-deps -d proxy haproxy exit 0 fi echo "Starting the development environment" rpm -qv composer >/dev/null 2>&1 || \ test ! -z "$(which composer 2>/dev/null)" || \ die "Is composer installed?" rpm -qv npm >/dev/null 2>&1 || \ test ! -z "$(which npm 2>/dev/null)" || \ die "Is npm installed?" rpm -qv php >/dev/null 2>&1 || \ test ! -z "$(which php 2>/dev/null)" || \ die "Is php installed?" rpm -qv php-ldap >/dev/null 2>&1 || \ test ! -z "$(php --ini | grep ldap)" || \ die "Is php-ldap installed?" rpm -qv php-mysqlnd >/dev/null 2>&1 || \ test ! -z "$(php --ini | grep mysql)" || \ die "Is php-mysqlnd installed?" test ! -z "$(php --modules | grep swoole)" || \ die "Is swoole installed?" # Ensure the containers we depend on are fully started wait_for_container 'kolab' wait_for_container 'kolab-redis' pushd ${base_dir}/src/ rm -rf vendor/ composer.lock php -dmemory_limit=-1 $(which composer) install npm install find bootstrap/cache/ -type f ! -name ".gitignore" -delete ./artisan key:generate ./artisan clear-compiled ./artisan cache:clear ./artisan horizon:install if rpm -qv chromium 2>/dev/null; then chver=$(rpmquery --queryformat="%{VERSION}" chromium | awk -F'.' '{print $1}') ./artisan dusk:chrome-driver ${chver} fi if [ ! -f 'resources/countries.php' ]; then ./artisan data:countries fi npm run dev popd pushd ${base_dir}/src/ rm -rf database/database.sqlite ./artisan db:ping --wait php -dmemory_limit=512M ./artisan migrate:refresh --seed ./artisan data:import || : nohup ./artisan octane:start --host=$(grep OCTANE_HTTP_HOST .env | tail -n1 | sed "s/OCTANE_HTTP_HOST=//") > octane.out & nohup ./artisan horizon > horizon.out & popd docker-compose up --no-deps -d proxy haproxy diff --git a/config.demo/src/.env b/config.demo/src/.env index 92c8a534..2a56740b 100644 --- a/config.demo/src/.env +++ b/config.demo/src/.env @@ -1,196 +1,200 @@ APP_NAME=Kolab APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=https://{{ host }} APP_PASSPHRASE=simple123 APP_PUBLIC_URL=https://{{ host }} APP_DOMAIN={{ host }} APP_WEBSITE_DOMAIN={{ host }} APP_THEME=default APP_TENANT_ID=5 APP_LOCALE=en APP_LOCALES= APP_WITH_ADMIN=1 APP_WITH_RESELLER=1 APP_WITH_SERVICES=1 APP_WITH_FILES=1 APP_LDAP=1 APP_IMAP=1 APP_HEADER_CSP="connect-src 'self'; child-src 'self'; font-src 'self'; form-action 'self' data:; frame-ancestors 'self'; img-src blob: data: 'self' *; media-src 'self'; object-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; default-src 'self';" APP_HEADER_XFO=sameorigin SIGNUP_LIMIT_EMAIL=0 SIGNUP_LIMIT_IP=0 ASSET_URL=https://{{ host }} WEBMAIL_URL=/roundcubemail/ SUPPORT_URL=/support SUPPORT_EMAIL=support@example.com LOG_CHANNEL=stdout LOG_SLOW_REQUESTS=5 LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug DB_CONNECTION=mysql DB_DATABASE=kolabdev DB_HOST=mariadb DB_PASSWORD=kolab DB_ROOT_PASSWORD=Welcome2KolabSystems DB_PORT=3306 DB_USERNAME=kolabdev BROADCAST_DRIVER=redis CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=file SESSION_LIFETIME=120 OPENEXCHANGERATES_API_KEY="from openexchangerates.org" MFA_DSN=mysql://roundcube:kolab@mariadb/roundcube MFA_TOTP_DIGITS=6 MFA_TOTP_INTERVAL=30 MFA_TOTP_DIGEST=sha1 IMAP_URI=ssl://kolab:11993 IMAP_HOST=172.18.0.5 IMAP_ADMIN_LOGIN=cyrus-admin IMAP_ADMIN_PASSWORD=Welcome2KolabSystems IMAP_VERIFY_HOST=false IMAP_VERIFY_PEER=false LDAP_BASE_DN="dc=mgmt,dc=com" LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com" LDAP_HOSTS=ldap LDAP_PORT=389 LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_SERVICE_BIND_PW="Welcome2KolabSystems" LDAP_USE_SSL=false LDAP_USE_TLS=false # Administrative LDAP_ADMIN_BIND_DN="cn=Directory Manager" LDAP_ADMIN_BIND_PW="Welcome2KolabSystems" LDAP_ADMIN_ROOT_DN="dc=mgmt,dc=com" # Hosted (public registration) LDAP_HOSTED_BIND_DN="uid=hosted-kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_HOSTED_BIND_PW="Welcome2KolabSystems" LDAP_HOSTED_ROOT_DN="dc=hosted,dc=com" COTURN_PUBLIC_IP='{{ public_ip }}' COTURN_STATIC_SECRET="Welcome2KolabSystems" MEET_WEBHOOK_TOKEN=Welcome2KolabSystems MEET_SERVER_TOKEN=Welcome2KolabSystems MEET_SERVER_URLS=https://{{ host }}/meetmedia/api/ MEET_SERVER_VERIFY_TLS=false MEET_WEBRTC_LISTEN_IP='172.18.0.1' MEET_PUBLIC_DOMAIN={{ host }} MEET_TURN_SERVER='turn:172.18.0.1:3478' MEET_LISTENING_HOST=172.18.0.1 PGP_ENABLE=true PGP_BINARY=/usr/bin/gpg PGP_AGENT=/usr/bin/gpg-agent PGP_GPGCONF=/usr/bin/gpgconf PGP_LENGTH= # Set these to IP addresses you serve WOAT with. # Have the domain owner point _woat. NS RRs refer to ns0{1,2}. WOAT_NS1=ns01.domain.tld WOAT_NS2=ns02.domain.tld REDIS_HOST=redis REDIS_PASSWORD=null REDIS_PORT=6379 OCTANE_HTTP_HOST=0.0.0.0 SWOOLE_PACKAGE_MAX_LENGTH=10485760 PAYMENT_PROVIDER= MOLLIE_KEY= STRIPE_KEY= STRIPE_PUBLIC_KEY= STRIPE_WEBHOOK_SECRET= MAIL_DRIVER=log MAIL_MAILER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="noreply@example.com" MAIL_FROM_NAME="Example.com" MAIL_REPLYTO_ADDRESS="replyto@example.com" MAIL_REPLYTO_NAME=null DNS_TTL=3600 DNS_SPF="v=spf1 mx -all" DNS_STATIC="%s. MX 10 ext-mx01.mykolab.com." DNS_COPY_FROM=null AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= AWS_DEFAULT_REGION=us-east-1 AWS_BUCKET= AWS_USE_PATH_STYLE_ENDPOINT=false PUSHER_APP_ID= PUSHER_APP_KEY= PUSHER_APP_SECRET= PUSHER_APP_CLUSTER=mt1 MIX_ASSET_PATH='/' MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" PASSWORD_POLICY= COMPANY_NAME=kolab.org COMPANY_ADDRESS= COMPANY_DETAILS= COMPANY_EMAIL= COMPANY_LOGO= COMPANY_FOOTER= VAT_COUNTRIES=CH,LI VAT_RATE=7.7 KB_ACCOUNT_DELETE= KB_ACCOUNT_SUSPENDED= KB_PAYMENT_SYSTEM= KOLAB_SSL_CERTIFICATE=/etc/pki/tls/certs/kolab.hosted.com.cert KOLAB_SSL_CERTIFICATE_FULLCHAIN=/etc/pki/tls/certs/kolab.hosted.com.chain.pem KOLAB_SSL_CERTIFICATE_KEY=/etc/pki/tls/certs/kolab.hosted.com.key PROXY_SSL_CERTIFICATE=/etc/certs/imap.hosted.com.cert PROXY_SSL_CERTIFICATE_KEY=/etc/certs/imap.hosted.com.key APP_KEY=base64:FG6ECzyAMSmyX+eYwO/FW3bwnarbKkBhqtO65vlMb1E= COTURN_STATIC_SECRET=uzYguvIl9tpZFMuQOE78DpOi6Jc7VFSD0UAnvgMsg5n4e74MgIf6vQvbc6LWzZjz MOLLIE_KEY="from mollie" STRIPE_KEY="from stripe" STRIPE_PUBLIC_KEY="from stripe" STRIPE_WEBHOOK_SECRET="from stripe" OX_API_KEY="from openexchange" FIREBASE_API_KEY="from firebase" #Generated by php artisan passport:client --password, but can be left hardcoded (the seeder will pick it up) PASSPORT_PROXY_OAUTH_CLIENT_ID=942edef5-3dbd-4a14-8e3e-d5d59b727bee PASSPORT_PROXY_OAUTH_CLIENT_SECRET=L6L0n56ecvjjK0cJMjeeV1pPAeffUBO0YSSH63wf +MINIO_USER=minio +MINIO_PASSWORD=W3lcom32@ph3lia +MINIO_BUCKET=kolab +FILESYSTEM_DISK=minio diff --git a/config.prod/src/.env b/config.prod/src/.env index 4992e040..4a8a7729 100644 --- a/config.prod/src/.env +++ b/config.prod/src/.env @@ -1,153 +1,158 @@ APP_NAME=Kolab APP_ENV=local APP_KEY= APP_DEBUG=true APP_URL=https://{{ host }} APP_PUBLIC_URL=https://{{ host }} APP_DOMAIN={{ host }} APP_WEBSITE_DOMAIN={{ host }} APP_THEME=default APP_TENANT_ID=5 APP_LOCALE=en APP_LOCALES= APP_WITH_ADMIN=1 APP_WITH_RESELLER=1 APP_WITH_SERVICES=1 APP_WITH_FILES=1 APP_WITH_WALLET=0 APP_WITH_SIGNUP=0 APP_LDAP=1 APP_IMAP=1 APP_HEADER_CSP="connect-src 'self'; child-src 'self'; font-src 'self'; form-action 'self' data:; frame-ancestors 'self'; img-src blob: data: 'self' *; media-src 'self'; object-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-eval' 'unsafe-inline'; default-src 'self';" APP_HEADER_XFO=sameorigin ASSET_URL=https://{{ host }} WEBMAIL_URL=/roundcubemail/ SUPPORT_URL=/support LOG_CHANNEL=stdout LOG_SLOW_REQUESTS=5 LOG_DEPRECATIONS_CHANNEL=null LOG_LEVEL=debug DB_CONNECTION=mysql DB_DATABASE=kolabdev DB_HOST=mariadb DB_PASSWORD={{ admin_password }} DB_ROOT_PASSWORD={{ admin_password }} DB_PORT=3306 DB_USERNAME=kolabdev BROADCAST_DRIVER=redis CACHE_DRIVER=redis QUEUE_CONNECTION=redis SESSION_DRIVER=file SESSION_LIFETIME=120 OPENEXCHANGERATES_API_KEY="from openexchangerates.org" MFA_DSN=mysql://roundcube:{{ admin_password }}@mariadb/roundcube MFA_TOTP_DIGITS=6 MFA_TOTP_INTERVAL=30 MFA_TOTP_DIGEST=sha1 IMAP_URI=ssl://kolab:11993 IMAP_HOST=172.18.0.5 IMAP_ADMIN_LOGIN=cyrus-admin IMAP_ADMIN_PASSWORD={{ admin_password }} IMAP_VERIFY_HOST=false IMAP_VERIFY_PEER=false LDAP_BASE_DN="dc=mgmt,dc=com" LDAP_DOMAIN_BASE_DN="ou=Domains,dc=mgmt,dc=com" LDAP_HOSTS=ldap LDAP_PORT=389 LDAP_SERVICE_BIND_DN="uid=kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_SERVICE_BIND_PW="{{ admin_password }}" LDAP_USE_SSL=false LDAP_USE_TLS=false # Administrative LDAP_ADMIN_BIND_DN="cn=Directory Manager" LDAP_ADMIN_BIND_PW="{{ admin_password }}" LDAP_ADMIN_ROOT_DN="dc=mgmt,dc=com" # Hosted (public registration) LDAP_HOSTED_BIND_DN="uid=hosted-kolab-service,ou=Special Users,dc=mgmt,dc=com" LDAP_HOSTED_BIND_PW="{{ admin_password }}" LDAP_HOSTED_ROOT_DN="dc=hosted,dc=com" COTURN_PUBLIC_IP='{{ public_ip }}' MEET_SERVER_URLS=https://{{ host }}/meetmedia/api/ MEET_SERVER_VERIFY_TLS=false MEET_WEBRTC_LISTEN_IP='172.18.0.1' MEET_PUBLIC_DOMAIN={{ host }} MEET_TURN_SERVER='turn:172.18.0.1:3478' MEET_LISTENING_HOST=172.18.0.1 PGP_ENABLE=true PGP_BINARY=/usr/bin/gpg PGP_AGENT=/usr/bin/gpg-agent PGP_GPGCONF=/usr/bin/gpgconf PGP_LENGTH= REDIS_HOST=redis REDIS_PASSWORD=null REDIS_PORT=6379 OCTANE_HTTP_HOST={{ host }} SWOOLE_PACKAGE_MAX_LENGTH=10485760 MAIL_DRIVER=log MAIL_MAILER=smtp MAIL_HOST=smtp.mailtrap.io MAIL_PORT=2525 MAIL_USERNAME=null MAIL_PASSWORD=null MAIL_ENCRYPTION=null MAIL_FROM_ADDRESS="noreply@example.com" MAIL_FROM_NAME="Example.com" MAIL_REPLYTO_ADDRESS="replyto@example.com" MAIL_REPLYTO_NAME=null DNS_TTL=3600 DNS_SPF="v=spf1 mx -all" DNS_STATIC="%s. MX 10 ext-mx01.mykolab.com." DNS_COPY_FROM=null MIX_ASSET_PATH='/' PASSWORD_POLICY= COMPANY_NAME=kolab.org COMPANY_ADDRESS= COMPANY_DETAILS= COMPANY_EMAIL= COMPANY_LOGO= COMPANY_FOOTER= VAT_COUNTRIES=CH,LI VAT_RATE=7.7 KB_ACCOUNT_DELETE= KB_ACCOUNT_SUSPENDED= KB_PAYMENT_SYSTEM= KOLAB_SSL_CERTIFICATE=/etc/pki/tls/certs/kolab.hosted.com.cert KOLAB_SSL_CERTIFICATE_FULLCHAIN=/etc/pki/tls/certs/kolab.hosted.com.chain.pem KOLAB_SSL_CERTIFICATE_KEY=/etc/pki/tls/certs/kolab.hosted.com.key PROXY_SSL_CERTIFICATE=/etc/certs/imap.hosted.com.cert PROXY_SSL_CERTIFICATE_KEY=/etc/certs/imap.hosted.com.key OPENEXCHANGERATES_API_KEY={{ openexchangerates_api_key }} FIREBASE_API_KEY={{ firebase_api_key }} + +MINIO_USER=minio +MINIO_PASSWORD=W3lcom32@ph3lia +MINIO_BUCKET=kolab +FILESYSTEM_DISK=minio diff --git a/docker-compose.yml b/docker-compose.yml index 67011649..1da7df77 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,349 +1,373 @@ version: '3' services: coturn: build: context: ./docker/coturn/ container_name: kolab-coturn healthcheck: interval: 10s test: "kill -0 $$(cat /tmp/turnserver.pid)" timeout: 5s retries: 30 environment: - TURN_PUBLIC_IP=${COTURN_PUBLIC_IP} - TURN_LISTEN_PORT=3478 - TURN_STATIC_SECRET=${COTURN_STATIC_SECRET} hostname: sturn.mgmt.com image: kolab-coturn network_mode: host restart: on-failure kolab: build: context: ./docker/kolab/ args: DB_KOLAB_DATABASE: kolab DB_KOLAB_USERNAME: kolab DB_KOLAB_PASSWORD: ${DB_PASSWORD:?"DB_PASSWORD is missing"} LDAP_HOST: ldap LDAP_ADMIN_BIND_DN: ${LDAP_ADMIN_BIND_DN} LDAP_ADMIN_BIND_PW: ${LDAP_ADMIN_BIND_PW} LDAP_SERVICE_BIND_PW: ${LDAP_SERVICE_BIND_PW} IMAP_ADMIN_LOGIN: ${IMAP_ADMIN_LOGIN} IMAP_ADMIN_PASSWORD: ${IMAP_ADMIN_PASSWORD} container_name: kolab privileged: true restart: on-failure tty: true depends_on: mariadb: condition: service_healthy pdns: condition: service_healthy ldap: condition: service_healthy extra_hosts: - "kolab.mgmt.com:127.0.0.1" - "services.${APP_DOMAIN}:172.18.0.4" environment: - APP_DOMAIN=${APP_DOMAIN} - LDAP_HOST=ldap - LDAP_ADMIN_BIND_DN=${LDAP_ADMIN_BIND_DN} - LDAP_ADMIN_BIND_PW=${LDAP_ADMIN_BIND_PW} - LDAP_SERVICE_BIND_PW=${LDAP_SERVICE_BIND_PW} - DB_HOST=mariadb - DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - DB_HKCCP_DATABASE=${DB_DATABASE} - DB_HKCCP_USERNAME=${DB_USERNAME} - DB_HKCCP_PASSWORD=${DB_PASSWORD:?"DB_PASSWORD is missing"} - DB_KOLAB_DATABASE=kolab - DB_KOLAB_USERNAME=kolab - DB_KOLAB_PASSWORD=${DB_PASSWORD:?"DB_PASSWORD is missing"} - SSL_CERTIFICATE=${KOLAB_SSL_CERTIFICATE:?"KOLAB_SSL_CERTIFICATE is missing"} - SSL_CERTIFICATE_FULLCHAIN=${KOLAB_SSL_CERTIFICATE_FULLCHAIN:?"KOLAB_SSL_CERTIFICATE_FULLCHAIN is missing"} - SSL_CERTIFICATE_KEY=${KOLAB_SSL_CERTIFICATE_KEY:?"KOLAB_SSL_CERTIFICATE_KEY is missing"} - IMAP_HOST=127.0.0.1 - IMAP_PORT=11993 - IMAP_ADMIN_LOGIN=${IMAP_ADMIN_LOGIN} - IMAP_ADMIN_PASSWORD=${IMAP_ADMIN_PASSWORD} - MAIL_HOST=127.0.0.1 - MAIL_PORT=10587 healthcheck: interval: 10s test: "systemctl is-active kolab-init || exit 1" timeout: 5s retries: 30 start_period: 5m # This makes docker's dns, resolve via pdns for this container. # Please note it does not affect /etc/resolv.conf dns: 172.18.0.11 hostname: kolab.mgmt.com image: kolab networks: kolab: ipv4_address: 172.18.0.5 ports: - "12143:12143" tmpfs: - /run - /tmp - /var/run - /var/tmp volumes: - ./ext/:/src/:ro - /etc/letsencrypt/:/etc/letsencrypt/:ro - ./docker/certs/ca.cert:/etc/pki/tls/certs/ca.cert:ro - ./docker/certs/ca.cert:/etc/pki/ca-trust/source/anchors/ca.cert:ro - ./docker/certs/kolab.hosted.com.cert:${KOLAB_SSL_CERTIFICATE:?err} - ./docker/certs/kolab.hosted.com.chain.pem:${KOLAB_SSL_CERTIFICATE_FULLCHAIN:?err} - ./docker/certs/kolab.hosted.com.key:${KOLAB_SSL_CERTIFICATE_KEY:?err} - ./docker/kolab/utils:/root/utils:ro - /sys/fs/cgroup:/sys/fs/cgroup:ro - imap:/imapdata ldap: build: context: ./docker/ldap/ container_name: kolab-ldap restart: on-failure tty: true hostname: ldap privileged: true environment: - APP_DOMAIN=${APP_DOMAIN} - LDAP_ADMIN_ROOT_DN=${LDAP_ADMIN_ROOT_DN} - LDAP_ADMIN_BIND_DN=${LDAP_ADMIN_BIND_DN} - LDAP_ADMIN_BIND_PW=${LDAP_ADMIN_BIND_PW} - LDAP_SERVICE_BIND_PW=${LDAP_SERVICE_BIND_PW} - LDAP_HOSTED_BIND_PW=${LDAP_HOSTED_BIND_PW} - IMAP_ADMIN_PASSWORD=${IMAP_ADMIN_PASSWORD} healthcheck: interval: 10s test: "systemctl status dirsrv@kolab || exit 1" timeout: 5s retries: 30 start_period: 5m image: kolab-ldap networks: kolab: ipv4_address: 172.18.0.12 tmpfs: - /run - /tmp - /var/run - /var/tmp volumes: - /sys/fs/cgroup:/sys/fs/cgroup:ro - ldap:/ldapdata roundcube: build: context: ./docker/roundcube/ container_name: kolab-roundcube hostname: roundcube.hosted.com restart: on-failure depends_on: mariadb: condition: service_healthy pdns: condition: service_healthy kolab: condition: service_healthy environment: - APP_DOMAIN=${APP_DOMAIN} - LDAP_HOST=ldap - LDAP_ADMIN_BIND_DN=${LDAP_ADMIN_BIND_DN} - LDAP_ADMIN_BIND_PW=${LDAP_ADMIN_BIND_PW} - LDAP_SERVICE_BIND_PW=${LDAP_SERVICE_BIND_PW} - LDAP_HOSTED_BIND_PW=${LDAP_HOSTED_BIND_PW} - DB_HOST=mariadb - DB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - DB_RC_DATABASE=roundcube - DB_RC_USERNAME=roundcube - DB_RC_PASSWORD=${DB_PASSWORD:?"DB_PASSWORD is missing"} - IMAP_HOST=tls://haproxy - IMAP_PORT=145 - IMAP_ADMIN_LOGIN=${IMAP_ADMIN_LOGIN} - IMAP_ADMIN_PASSWORD=${IMAP_ADMIN_PASSWORD} - MAIL_HOST=tls://kolab - MAIL_PORT=10587 healthcheck: interval: 10s test: "kill -0 $$(cat /run/httpd/httpd.pid)" timeout: 5s retries: 30 # This makes docker's dns, resolve via pdns for this container. # Please note it does not affect /etc/resolv.conf dns: 172.18.0.11 image: roundcube networks: kolab: ipv4_address: 172.18.0.9 ports: - "8001:80" tmpfs: - /run - /tmp - /var/run - /var/tmp volumes: - ./ext/:/src.orig/:ro mariadb: container_name: kolab-mariadb restart: on-failure environment: - MARIADB_ROOT_PASSWORD=${DB_ROOT_PASSWORD} - TZ="+02:00" - DB_HKCCP_DATABASE=${DB_DATABASE} - DB_HKCCP_USERNAME=${DB_USERNAME} - DB_HKCCP_PASSWORD=${DB_PASSWORD} healthcheck: interval: 10s test: test -e /var/run/mysqld/mysqld.sock timeout: 5s retries: 30 image: mariadb:latest networks: kolab: ipv4_address: 172.18.0.3 volumes: - ./docker/mariadb/mysql-init/:/docker-entrypoint-initdb.d/ - mariadb:/var/lib/mysql haproxy: build: context: ./docker/haproxy/ healthcheck: interval: 10s test: "kill -0 $$(cat /var/run/haproxy.pid)" timeout: 5s retries: 30 container_name: kolab-haproxy restart: on-failure hostname: haproxy.hosted.com image: kolab-haproxy networks: kolab: ipv4_address: 172.18.0.6 tmpfs: - /run - /tmp - /var/run - /var/tmp volumes: - ./docker/certs/:/etc/certs/:ro - /etc/letsencrypt/:/etc/letsencrypt/:ro pdns: build: context: ./docker/pdns/ args: DB_HOST: mariadb DB_DATABASE: ${DB_DATABASE:?DB_DATABASE} DB_USERNAME: ${DB_USERNAME:?DB_USERNAME} DB_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD} container_name: kolab-pdns restart: on-failure tty: true hostname: pdns depends_on: mariadb: condition: service_healthy healthcheck: interval: 10s test: "systemctl status pdns || exit 1" timeout: 5s retries: 30 image: kolab-pdns networks: kolab: ipv4_address: 172.18.0.11 tmpfs: - /run - /tmp - /var/run - /var/tmp volumes: - /sys/fs/cgroup:/sys/fs/cgroup:ro redis: build: context: ./docker/redis/ healthcheck: interval: 10s test: "redis-cli ping || exit 1" timeout: 5s retries: 30 container_name: kolab-redis restart: on-failure hostname: redis image: redis networks: - kolab volumes: - ./docker/redis/redis.conf:/usr/local/etc/redis/redis.conf:ro webapp: build: context: ./docker/webapp/ args: GIT_REF: ${KOLAB_GIT_REF:-master} container_name: kolab-webapp restart: on-failure image: kolab-webapp healthcheck: interval: 10s test: "/src/kolabsrc/artisan octane:status || exit 1" timeout: 5s retries: 30 start_period: 5m depends_on: kolab: condition: service_healthy redis: condition: service_healthy roundcube: condition: service_healthy networks: kolab: ipv4_address: 172.18.0.4 volumes: - ./src:/src/kolabsrc.orig:ro ports: - "8000:8000" meet: build: context: ./docker/meet/ args: GIT_REF: ${KOLAB_GIT_REF:-master} container_name: kolab-meet restart: on-failure healthcheck: interval: 10s test: "curl --insecure -H 'X-AUTH-TOKEN: ${MEET_SERVER_TOKEN}' --fail https://${MEET_LISTENING_HOST}:12443/meetmedia/api/health || exit 1" timeout: 5s retries: 30 start_period: 5m environment: - WEBRTC_LISTEN_IP=${MEET_WEBRTC_LISTEN_IP:?err} - PUBLIC_DOMAIN=${MEET_PUBLIC_DOMAIN:?err} - LISTENING_HOST=${MEET_LISTENING_HOST:?err} - LISTENING_PORT=12443 - TURN_SERVER=${MEET_TURN_SERVER} - TURN_STATIC_SECRET=${COTURN_STATIC_SECRET} - AUTH_TOKEN=${MEET_SERVER_TOKEN:?err} - WEBHOOK_TOKEN=${MEET_WEBHOOK_TOKEN:?err} - WEBHOOK_URL=${APP_PUBLIC_URL:?err}/api/webhooks/meet - SSL_CERT=/etc/pki/tls/certs/meet.${APP_WEBSITE_DOMAIN:?err}.cert - SSL_KEY=/etc/pki/tls/private/meet.${APP_WEBSITE_DOMAIN:?err}.key network_mode: host container_name: kolab-meet image: kolab-meet volumes: - ./meet/server:/src/meet/:ro - ./docker/certs/meet.${APP_WEBSITE_DOMAIN}.cert:/etc/pki/tls/certs/meet.${APP_WEBSITE_DOMAIN}.cert - ./docker/certs/meet.${APP_WEBSITE_DOMAIN}.key:/etc/pki/tls/private/meet.${APP_WEBSITE_DOMAIN}.key + minio: + container_name: kolab-minio + restart: on-failure + healthcheck: + interval: 10s + test: "curl -f http://127.0.0.1:9000/minio/health/live || exit 1" + timeout: 5s + retries: 30 + start_period: 5m + environment: + - MINIO_ROOT_USER=${MINIO_USER} + - MINIO_ROOT_PASSWORD=${MINIO_PASSWORD} + image: minio/minio + networks: + kolab: + ipv4_address: 172.18.0.14 + ports: + - "9000:9000" + - "9001:9001" + entrypoint: sh + command: -c 'mkdir -p /data/${MINIO_BUCKET} && minio server /data --console-address ":9001"' + volumes: + - minio:/data networks: kolab: driver: bridge ipam: config: - subnet: "172.18.0.0/24" volumes: mariadb: imap: ldap: + minio: diff --git a/src/app/Backends/Storage.php b/src/app/Backends/Storage.php index 346e58e9..5513477f 100644 --- a/src/app/Backends/Storage.php +++ b/src/app/Backends/Storage.php @@ -1,293 +1,293 @@ path . '/' . $file->id; // TODO: Deleting files might be slow, consider marking as deleted and async job $disk->deleteDirectory($path); $file->forceDelete(); } /** * Delete a file chunk. * * @param \App\Fs\Chunk $chunk File chunk object * * @throws \Exception */ public static function fileChunkDelete(Chunk $chunk): void { - $disk = LaravelStorage::disk('files'); + $disk = LaravelStorage::disk(\config('filesystems.default')); $path = self::chunkLocation($chunk->chunk_id, $chunk->item); $disk->delete($path); $chunk->forceDelete(); } /** * File download handler. * * @param \App\Fs\Item $file File object * * @throws \Exception */ public static function fileDownload(Item $file): StreamedResponse { $response = new StreamedResponse(); $props = $file->getProperties(['name', 'size', 'mimetype']); // Prepare the file name for the Content-Disposition header $extension = pathinfo($props['name'], \PATHINFO_EXTENSION) ?: 'file'; $fallbackName = str_replace('%', '', Str::ascii($props['name'])) ?: "file.{$extension}"; $disposition = $response->headers->makeDisposition('attachment', $props['name'], $fallbackName); $response->headers->replace([ 'Content-Type' => $props['mimetype'], 'Content-Disposition' => $disposition, ]); $response->setCallback(function () use ($file) { $file->chunks()->orderBy('sequence')->get()->each(function ($chunk) use ($file) { - $disk = LaravelStorage::disk('files'); + $disk = LaravelStorage::disk(\config('filesystems.default')); $path = Storage::chunkLocation($chunk->chunk_id, $file); $stream = $disk->readStream($path); fpassthru($stream); fclose($stream); }); }); return $response; } /** * File upload handler * * @param resource $stream File input stream * @param array $params Request parameters * @param ?\App\Fs\Item $file The file object * * @return array File/Response attributes * @throws \Exception */ public static function fileInput($stream, array $params, Item $file = null): array { if (!empty($params['uploadId'])) { return self::fileInputResumable($stream, $params, $file); } - $disk = LaravelStorage::disk('files'); + $disk = LaravelStorage::disk(\config('filesystems.default')); $chunkId = \App\Utils::uuidStr(); $path = self::chunkLocation($chunkId, $file); $disk->writeStream($path, $stream); $fileSize = $disk->size($path); if ($file->type & Item::TYPE_INCOMPLETE) { $file->type -= Item::TYPE_INCOMPLETE; $file->save(); } // Update the file type and size information $file->setProperties([ 'size' => $fileSize, 'mimetype' => self::mimetype($path), ]); // Assign the node to the file, "unlink" any old nodes of this file $file->chunks()->delete(); $file->chunks()->create([ 'chunk_id' => $chunkId, 'sequence' => 0, 'size' => $fileSize, ]); return ['id' => $file->id]; } /** * Resumable file upload handler * * @param resource $stream File input stream * @param array $params Request parameters * @param ?\App\Fs\Item $file The file object * * @return array File/Response attributes * @throws \Exception */ protected static function fileInputResumable($stream, array $params, Item $file = null): array { // Initial request, save file metadata, return uploadId if ($params['uploadId'] == 'resumable') { if (empty($params['size']) || empty($file)) { throw new \Exception("Missing parameters of resumable file upload."); } $params['uploadId'] = \App\Utils::uuidStr(); $upload = [ 'fileId' => $file->id, 'size' => $params['size'], 'uploaded' => 0, ]; if (!Cache::add('upload:' . $params['uploadId'], $upload, self::UPLOAD_TTL)) { throw new \Exception("Failed to create cache entry for resumable file upload."); } return [ 'uploadId' => $params['uploadId'], 'uploaded' => 0, 'maxChunkSize' => (\config('octane.swoole.options.package_max_length') ?: 10 * 1024 * 1024) - 8192, ]; } $upload = Cache::get('upload:' . $params['uploadId']); if (empty($upload)) { throw new \Exception("Cache entry for resumable file upload does not exist."); } $file = Item::find($upload['fileId']); if (!$file) { throw new \Exception("Invalid fileId for resumable file upload."); } $from = $params['from'] ?? 0; // Sanity checks on the input parameters // TODO: Support uploading again a chunk that already has been uploaded? if ($from < $upload['uploaded'] || $from > $upload['uploaded'] || $from > $upload['size']) { throw new \Exception("Invalid 'from' parameter for resumable file upload."); } - $disk = LaravelStorage::disk('files'); + $disk = LaravelStorage::disk(\config('filesystems.default')); $chunkId = \App\Utils::uuidStr(); $path = self::chunkLocation($chunkId, $file); // Save the file chunk $disk->writeStream($path, $stream); // Detect file type using the first chunk if ($from == 0) { $upload['mimetype'] = self::mimetype($path); $upload['chunks'] = []; } $chunkSize = $disk->size($path); // Create the chunk record $file->chunks()->create([ 'chunk_id' => $chunkId, 'sequence' => count($upload['chunks']), 'size' => $chunkSize, 'deleted_at' => \now(), // not yet active chunk ]); $upload['chunks'][] = $chunkId; $upload['uploaded'] += $chunkSize; // Update the file metadata after the upload of all chunks is completed if ($upload['uploaded'] >= $upload['size']) { if ($file->type & Item::TYPE_INCOMPLETE) { $file->type -= Item::TYPE_INCOMPLETE; $file->save(); } // Update file metadata $file->setProperties([ 'size' => $upload['uploaded'], 'mimetype' => $upload['mimetype'] ?: 'application/octet-stream', ]); // Assign uploaded chunks to the file, "unlink" any old chunks of this file $file->chunks()->delete(); $file->chunks()->whereIn('chunk_id', $upload['chunks'])->restore(); // TODO: Create a "cron" job to remove orphaned nodes from DB and the storage. // I.e. all with deleted_at set and older than UPLOAD_TTL // Delete the upload cache record Cache::forget('upload:' . $params['uploadId']); return ['id' => $file->id]; } // Update the upload metadata Cache::put('upload:' . $params['uploadId'], $upload, self::UPLOAD_TTL); return ['uploadId' => $params['uploadId'], 'uploaded' => $upload['uploaded']]; } /** * Get the file mime type. * * @param string $path File location * * @return string File mime type */ protected static function mimetype(string $path): string { - $disk = LaravelStorage::disk('files'); + $disk = LaravelStorage::disk(\config('filesystems.default')); $mimetype = $disk->mimeType($path); // The mimetype may contain e.g. "; charset=UTF-8", remove this if ($mimetype) { return explode(';', $mimetype)[0]; } return 'application/octet-stream'; } /** * Node location in the storage * * @param string $chunkId Chunk identifier * @param \App\Fs\Item $file File the chunk belongs to * * @return string Chunk location */ public static function chunkLocation(string $chunkId, Item $file): string { return $file->path . '/' . $file->id . '/' . $chunkId; } } diff --git a/src/composer.json b/src/composer.json index 23dae1e8..47448f29 100644 --- a/src/composer.json +++ b/src/composer.json @@ -1,85 +1,86 @@ { "name": "kolab/kolab4", "type": "project", "description": "Kolab 4", "keywords": [ "framework", "laravel" ], "license": "MIT", "repositories": [ { "type": "vcs", "url": "https://git.kolab.org/diffusion/PNL/php-net_ldap3.git" } ], "require": { "php": "^8.1", "bacon/bacon-qr-code": "^2.0", "barryvdh/laravel-dompdf": "^2.0.0", "doctrine/dbal": "^3.3.2", "dyrynda/laravel-nullable-fields": "^4.2.0", "guzzlehttp/guzzle": "^7.4.1", "kolab/net_ldap3": "dev-master", "laravel/framework": "^9.2", "laravel/horizon": "^5.9", "laravel/octane": "^1.2", "laravel/passport": "^11.3", "laravel/tinker": "^2.7", + "league/flysystem-aws-s3-v3": "^3.0", "mlocati/spf-lib": "^3.1", "mollie/laravel-mollie": "^2.19", "pear/crypt_gpg": "^1.6.6", "predis/predis": "^2.0", "sabre/vobject": "^4.5", "spatie/laravel-translatable": "^6.3", "spomky-labs/otphp": "~10.0.0", "stripe/stripe-php": "^10.7" }, "require-dev": { "code-lts/doctum": "^5.5.1", "laravel/dusk": "~7.5.0", "mockery/mockery": "^1.5", "nunomaduro/larastan": "^2.0", "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^9", "squizlabs/php_codesniffer": "^3.6" }, "config": { "optimize-autoloader": true, "preferred-install": "dist", "sort-packages": true }, "extra": { "laravel": { "dont-discover": [] } }, "autoload": { "psr-4": { "App\\": "app/" }, "classmap": [ "database/seeds", "include" ] }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, "minimum-stability": "stable", "prefer-stable": true, "scripts": { "post-autoload-dump": [ "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", "@php artisan package:discover --ansi" ], "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force" ], "post-create-project-cmd": [ "@php artisan key:generate --ansi" ] } } diff --git a/src/config/filesystems.php b/src/config/filesystems.php index 6d5f46e4..d1c4c1e3 100644 --- a/src/config/filesystems.php +++ b/src/config/filesystems.php @@ -1,83 +1,93 @@ env('FILESYSTEM_DISK', 'local'), /* |-------------------------------------------------------------------------- | Filesystem Disks |-------------------------------------------------------------------------- | | Here you may configure as many filesystem "disks" as you wish, and you | may even configure multiple disks of the same driver. Defaults have | been setup for each driver as an example of the required options. | | Supported Drivers: "local", "ftp", "sftp", "s3" | */ 'disks' => [ 'local' => [ 'driver' => 'local', 'root' => storage_path('app'), ], 'files' => [ 'driver' => 'local', 'root' => storage_path('app/files'), ], 'pgp' => [ 'driver' => 'local', 'root' => storage_path('app/keys'), ], 'public' => [ 'driver' => 'local', 'root' => storage_path('app/public'), 'url' => env('APP_URL') . '/storage', 'visibility' => 'public', ], + 'minio' => [ + 'driver' => 's3', + 'key' => env('MINIO_USER'), + 'secret' => env('MINIO_PASSWORD'), + 'region' => 'local', + 'bucket' => env('MINIO_BUCKET'), + 'endpoint' => 'http://minio:9000', + 'use_path_style_endpoint' => true, + ], + 's3' => [ 'driver' => 's3', 'key' => env('AWS_ACCESS_KEY_ID'), 'secret' => env('AWS_SECRET_ACCESS_KEY'), 'region' => env('AWS_DEFAULT_REGION'), 'bucket' => env('AWS_BUCKET'), 'url' => env('AWS_URL'), 'endpoint' => env('AWS_ENDPOINT'), 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), ], ], /* |-------------------------------------------------------------------------- | Symbolic Links |-------------------------------------------------------------------------- | | Here you may configure the symbolic links that will be created when the | `storage:link` Artisan command is executed. The array keys should be | the locations of the links and the values should be their targets. | */ 'links' => [ public_path('storage') => storage_path('app/public'), ], ];