Compare commits

..
101 Commits
Author SHA1 Message Date
FreddleSpl0it 2e200bd887 Merge staging into nightly 2026-10-28 13:17:02 +01:00
FreddleSpl0it bab03ed88b [ACME][Dockerapi][Dovecot][Olefy][PHP][Rspamd][SOGo][Unbound][Watchdog][Netfilter] Update image tags (#7572)
* Update image tags
2026-10-28 13:08:22 +01:00
FreddleSpl0it 0cd71c7a00 Merge pull request #7569 from mailcow/feat/rspamd-4.2.3
[Rspamd] Update to 4.2.3
2026-10-28 12:22:43 +01:00
FreddleSpl0it 92ad6c5e6d [Rspamd] Update to 4.2.3 2026-10-28 12:19:53 +01:00
FreddleSpl0it aa7388810c Merge pull request #7568 from mailcow/feat/compose-update
[Web] Update composer libraries
2026-10-28 12:01:04 +01:00
FreddleSpl0it 4dd7aead73 [Web] Store PKCE data of OAuth2 authorization codes 2026-10-28 11:57:46 +01:00
FreddleSpl0it bd8c784118 [Web] Update composer libraries 2026-10-28 11:56:29 +01:00
FreddleSpl0it 9cc8ac2908 Merge pull request #7567 from mailcow/feat/netfilter-rework
[Netfilter] Rework: robust bans, no restart loops, identity provider outages are no failed logins
2026-10-28 11:24:29 +01:00
FreddleSpl0it 002400315d Merge pull request #7566 from mailcow/feat/groups
[Web][Dovecot][SOGo][DockerApi] Add mailbox groups, shared mailboxes and identity provider sync
2026-10-28 11:21:58 +01:00
FreddleSpl0it 8934a84d18 Merge pull request #7547 from mailcow/feat/apikey-rework
[Web] Rework API keys: multiple keys + locked service keys from mailcow.conf
2026-10-28 11:18:47 +01:00
FreddleSpl0it 5ce88b5795 Merge pull request #7546 from mailcow/feat/imapsync-rework
[Web][Dovecot] Rework imapsync: sync sources, OAuth2, Python runner
2026-10-28 11:17:09 +01:00
FreddleSpl0it e70af42aef Merge pull request #7564 from mailcow/staging
Automatic PR to nightly from 2026-10-28T05:05:56Z
2026-10-28 09:06:14 +01:00
FreddleSpl0it b2b21b1a85 Merge pull request #7551 from mailcow/renovate/tianon-gosu-1.x
Update dependency tianon/gosu to v1.20
2026-10-28 09:05:19 +01:00
FreddleSpl0it 4b1b9362f3 Merge pull request #7555 from mailcow/renovate/redis-7.x
Update redis Docker tag to v7.4.12
2026-10-28 09:04:55 +01:00
FreddleSpl0it fb32d8d345 Merge pull request #7563 from mailcow/feat/alpine3.24
Feat/alpine3.24
2026-10-28 09:04:34 +01:00
FreddleSpl0it af5d0e3d29 [Dovecot] Update base to Alpine 3.24, build Dovecot 2.3 from Alpine 3.21 packaging 2026-10-28 09:00:45 +01:00
FreddleSpl0it 4884ed0cbf [Olefy] Update base to Alpine 3.24, fix event loop for Python 3.14 2026-10-28 08:51:33 +01:00
FreddleSpl0it 8aaf68ffb5 [Unbound] Update to 1.26.2, base Alpine 3.24 2026-10-28 08:50:42 +01:00
FreddleSpl0it 503e64fe85 [ACME][Dockerapi][Netfilter][Watchdog] Update base to Alpine 3.24 2026-10-28 08:49:57 +01:00
FreddleSpl0it fe414e5341 [PHP][SOGo][Dovecot][Netfilter][Dockerapi] Update Images 2026-10-27 14:19:53 +01:00
FreddleSpl0it b95227d648 Merge pull request #7561 from mailcow/feat/netfilter-rework
[Netfilter] Rework: robust bans, no restart loops, identity provider outages are no failed logins
2026-10-27 14:16:51 +01:00
FreddleSpl0it 0c9335f868 Merge pull request #7560 from mailcow/feat/groups
[Web][Dovecot][SOGo][DockerApi] Add mailbox groups, shared mailboxes and identity provider sync
2026-10-27 14:13:32 +01:00
FreddleSpl0it a61851410e Merge pull request #7559 from mailcow/feat/apikey-rework
[Web] Rework API keys: multiple read-only/read-write keys, service keys from mailcow.conf
2026-10-27 14:09:37 +01:00
FreddleSpl0it 0bc0ef6424 Merge pull request #7558 from mailcow/feat/imapsync-rework
[Web][Dovecot] Rework imapsync: sync sources, XOAUTH2, Python runner
2026-10-27 14:03:31 +01:00
FreddleSpl0it ba034568e0 [Web] Add identity provider outage translation, remove unused needs_restart 2026-10-27 12:55:24 +01:00
FreddleSpl0it b7d24881b1 [Netfilter][Tests] Add netfilter unit tests and identity provider outage tests 2026-10-27 12:54:45 +01:00
FreddleSpl0it 1c7c328a2a [Web][Netfilter] Fix netban limits, no negative ban times, save lists atomically, check banlist id 2026-10-27 12:53:01 +01:00
FreddleSpl0it 7457fd59aa [Web][Dovecot] Do not count logins as failed while the identity provider is unavailable 2026-10-27 12:46:24 +01:00
FreddleSpl0it 60177720ed [Netfilter] Rework: keep bans across restarts, repair chain order instead of restarting, one isolation rule per port 2026-10-27 12:35:07 +01:00
FreddleSpl0itandClaude Opus 5.5 d3e0c1cbb2 Merge staging into nightly
nightly has the content of staging again, nightly-only changes are dropped.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 10:25:39 +01:00
renovate[bot] 0445f85d7d Update redis Docker tag to v7.4.12
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2026-10-27 00:27:59 +00:00
renovate[bot] 714ca5a3d8 Update dependency tianon/gosu to v1.20
Signed-off-by: milkmaker <milkmaker@mailcow.de>
2026-10-25 13:00:37 +00:00
FreddleSpl0it 7d27460fee Merge branch 'staging' into feat/imapsync-rework 2026-10-23 11:49:45 +02:00
milkmakerandciri-cuervo 82b1cf7198 [Web] Updated lang.es-es.json (#7542)
Co-authored-by: ciri-cuervo <nicolascirigliano@gmail.com>
2026-10-22 17:54:52 +02:00
FreddleSpl0it c0be3347f8 Merge pull request #7026 from mailcow/staging
Automatic PR to nightly from 2026-01-29T09:19:39Z
2026-01-29 10:33:28 +01:00
FreddleSpl0it 4f08c4ed7d Merge remote-tracking branch 'origin/staging' into nightly 2026-01-29 07:58:15 +01:00
DerLinkman 0e76396f01 reuse nightly images where needed 2026-01-20 08:46:04 +01:00
DerLinkman 9bbac9f171 Merge branch 'staging' into nightly 2026-01-19 12:17:11 +01:00
FreddleSpl0it e6f83853ae Merge remote-tracking branch 'origin/staging' into nightly 2025-10-15 11:17:07 +02:00
FreddleSpl0it 7da088c931 Merge branch 'staging' into nightly 2025-10-06 13:58:40 +02:00
FreddleSpl0it bb3c2fb4fe Merge pull request #6731 from mailcow/staging
Automatic PR to nightly from 2025-09-11T07:38:50Z
2025-09-12 11:43:02 +02:00
FreddleSpl0it eb84847a5b Merge branch 'staging' into nightly 2025-09-11 10:26:42 +02:00
DerLinkman 0cfcde673c Merge branch 'staging' into nightly 2025-08-28 10:21:38 +02:00
FreddleSpl0it ed5be5d7dc Merge branch 'feat/mailcow-adm' into nightly 2025-08-19 11:57:22 +02:00
FreddleSpl0it ac90ecaf4f Merge remote-tracking branch 'origin/staging' into nightly 2025-08-19 11:51:54 +02:00
FreddleSpl0it fed3fc9514 [Controller] Add HTTPS_PORT env var to base_url 2025-08-19 11:30:20 +02:00
FreddleSpl0it 35b9940db4 [Controller] Fix function description in SyncjobModel 2025-08-19 11:28:54 +02:00
FreddleSpl0it ece940b000 [Controller] Fix missing password2 assignment in from_dict 2025-08-19 11:28:08 +02:00
DerLinkman 4b5fd0b50a compose: bump nginx nightly 2025-08-05 16:42:24 +02:00
DerLinkman 5aa9498f65 Merge branch 'feat/remove-ip6nat' into nightly 2025-08-05 16:41:15 +02:00
DerLinkman 690d511e54 reuse DOCKER_MAJOR Variable in ip6_controller 2025-08-05 16:37:09 +02:00
DerLinkmanandCopilot e2a2b42139 Update _modules/scripts/new_options.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 16:36:29 +02:00
DerLinkmanandCopilot 4bbda8006d Update _modules/scripts/new_options.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 16:36:22 +02:00
DerLinkman a281746958 ip6_controller: moved docker major detection upwards 2025-08-05 16:25:59 +02:00
DerLinkman cec51b6162 improve detection of ENABLE_IPV6 2025-08-05 16:22:51 +02:00
DerLinkman 107c5d2e7d improve ENABLE_IPV6 check in nginx bootstrap 2025-08-05 16:18:29 +02:00
DerLinkmanandCopilot 00c025f31a Update _modules/scripts/core.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 16:12:11 +02:00
DerLinkmanandCopilot 9b6388d0d0 Update _modules/scripts/new_options.sh
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-08-05 16:11:39 +02:00
DerLinkman 2f25fcad77 removed unnecessary message on every call of function 2025-08-05 16:04:10 +02:00
DerLinkman 7067e2c714 move detect_major_update func to core submodule 2025-08-05 16:04:10 +02:00
DerLinkman 9f3cdfa713 adapted removal of ACME_CONTACT for nightly 2025-08-05 16:03:26 +02:00
DerLinkman 529acf5ff6 added error handling for blank daemon.json 2025-08-05 16:03:26 +02:00
DerLinkman 0371edcf5e reintegrated module loading (update.sh) 2025-08-05 16:03:25 +02:00
DerLinkman d20254d4ee improved _modules handling while running 2025-08-05 16:03:25 +02:00
DerLinkman befecfc31d fixed docker version check for daemon 2025-08-05 16:03:24 +02:00
DerLinkman 004fcf092b added jq as dependancy 2025-08-05 16:03:24 +02:00
DerLinkman a487fcd0bd fix broken EXIT_CODE var handling 2025-08-05 16:03:23 +02:00
DerLinkman 17e38a05f0 fixed/added comments for modules 2025-08-05 16:03:23 +02:00
DerLinkman c503abfe40 fixed missing fi in update.sh 2025-08-05 16:03:22 +02:00
DerLinkman 73929db796 rewrite to scripts after testing (improved error handling) 2025-08-05 16:03:22 +02:00
DerLinkman fb0685fa71 initial commit for script overhauls 2025-08-05 16:03:21 +02:00
DerLinkman df36670c7c nginx: renamed DISABLE_IPv6 to ENABLE_IPV6 to align 2025-08-05 16:02:41 +02:00
DerLinkman 3f9215678d ipv6: added ipv6 detection + removed ip6 nat container 2025-08-05 16:02:41 +02:00
FreddleSpl0it 0ac0e5c252 [DockerApi] Rename DockerApi to Controller and add mailcow-adm tool 2025-08-01 15:31:50 +02:00
DerLinkman af61c82077 adapted removal of ACME_CONTACT for nightly 2025-07-16 09:03:50 +02:00
DerLinkman c066273c79 Merge branch staging into nightly 2025-07-16 09:03:20 +02:00
DerLinkman 0c3e53e3a9 Merge branch 'feat/remove-ip6nat' into nightly 2025-05-27 16:32:08 +02:00
DerLinkman 5ca10d1cde added error handling for blank daemon.json 2025-05-27 16:31:22 +02:00
DerLinkman 7907d43af7 compose: bumped nginx container tag 2025-05-27 16:24:21 +02:00
DerLinkman d198f1d3f8 Merge branch 'feat/remove-ip6nat' into nightly 2025-05-27 16:19:42 +02:00
DerLinkman 102226723e reintegrated module loading (update.sh) 2025-05-27 16:18:55 +02:00
DerLinkman 2efaccf038 improved _modules handling while running 2025-05-27 16:16:23 +02:00
DerLinkman aa7b6fa4a9 improved _modules handling while running 2025-05-27 16:15:02 +02:00
DerLinkman 714727a129 Merge pull request #6561 from mailcow/feat/remove-ip6nat
core: rewrote ipv6 detection and core script splitting
2025-05-27 16:10:44 +02:00
DerLinkman 4e5e264e3e Merge branch 'staging' into nightly 2025-05-27 16:09:18 +02:00
DerLinkman 267c81b42e fixed docker version check for daemon 2025-05-27 15:55:58 +02:00
DerLinkman f2f3fbe497 added jq as dependancy 2025-05-27 15:54:58 +02:00
DerLinkman 6ba650820f fix broken EXIT_CODE var handling 2025-05-27 15:31:52 +02:00
DerLinkman baa6286471 fixed/added comments for modules 2025-05-27 15:30:56 +02:00
DerLinkman be8537d165 fixed missing fi in update.sh 2025-05-27 15:29:48 +02:00
DerLinkman 737fced7be rewrite to scripts after testing (improved error handling) 2025-05-27 15:26:08 +02:00
DerLinkman 5a532df8ce initial commit for script overhauls 2025-05-26 17:09:58 +02:00
DerLinkman f8ce7a71e6 nginx: renamed DISABLE_IPv6 to ENABLE_IPV6 to align 2025-05-26 17:09:37 +02:00
DerLinkman 2e876bda9a ipv6: added ipv6 detection + removed ip6 nat container 2025-05-26 15:19:42 +02:00
FreddleSpl0it d2e5926cce Merge pull request #6536 from mailcow/staging
Automatic PR to nightly from 2025-05-13T07:58:39Z
2025-05-13 11:22:48 +02:00
FreddleSpl0it e3b576be67 Merge pull request #6475 from mailcow/staging
Automatic PR to nightly from 2025-04-09T01:26:20Z
2025-05-13 09:52:49 +02:00
FreddleSpl0it 0f7e359686 Merge pull request #6467 from mailcow/staging
Automatic PR to nightly from 2025-04-07T05:55:15Z
2025-04-07 09:00:11 +02:00
FreddleSpl0it b9a0b2db6d Merge pull request #6456 from mailcow/staging
Automatic PR to nightly from 2025-04-03T11:26:19Z
2025-04-03 14:21:26 +02:00
FreddleSpl0it 93b876c473 Merge pull request #6446 from mailcow/staging
Automatic PR to nightly from 2025-04-01T13:48:33Z
2025-04-03 14:02:21 +02:00
FreddleSpl0it 92c2aa2023 Merge pull request #6420 from mailcow/staging
Automatic PR to nightly from 2025-03-27T05:23:47Z
2025-03-27 08:38:49 +01:00
FreddleSpl0it 9351cf24fe Merge pull request #6386 from mailcow/staging
Automatic PR to nightly from 2025-03-24T08:36:34Z
2025-03-27 07:45:25 +01:00
1346 changed files with 71975 additions and 23538 deletions

No files matched your search

+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.23 FROM alpine:3.24
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.23 FROM alpine:3.24
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
+43 -13
View File
@@ -1,12 +1,48 @@
FROM alpine:3.21 # Alpine >= 3.22 only ships Dovecot 2.4, Dovecot 2.3 is built from the Alpine 3.21 packaging
FROM alpine:3.24 AS builder
# Last commit of the aports 3.21-stable branch
ARG APORTS_COMMIT=55126a11a4b9dcd14de3068e3348e9951d144ca6
RUN apk add --no-cache alpine-sdk git \
&& git init -q /aports \
&& cd /aports \
&& git remote add origin https://github.com/alpinelinux/aports.git \
&& git sparse-checkout set main/dovecot community/dovecot-fts-flatcurve \
&& git fetch -q --depth 1 --filter=blob:none origin ${APORTS_COMMIT} \
&& git checkout -q FETCH_HEAD \
&& abuild-keygen -a -n \
&& cp /root/.abuild/*.rsa.pub /etc/apk/keys/ \
&& apk update \
&& cd /aports/main/dovecot \
# newer gettext does not overwrite the shipped config.rpath without -f
&& sed -i 's/autoreconf -vi/autoreconf -fvi/' APKBUILD \
&& abuild -F -r \
# flatcurve must be built against the Dovecot 2.3 headers
&& apk add --no-cache /root/packages/main/*/dovecot-2*.apk /root/packages/main/*/dovecot-dev-*.apk \
&& cd /aports/community/dovecot-fts-flatcurve \
&& abuild -F -r \
&& mkdir /packages \
&& cp /root/.abuild/*.rsa.pub /packages/ \
&& for pkg in dovecot dovecot-dev dovecot-lmtpd dovecot-lua dovecot-ldap dovecot-mysql dovecot-sql \
dovecot-submissiond dovecot-pigeonhole-plugin dovecot-pop3d dovecot-fts-flatcurve; do \
cp /root/packages/*/*/${pkg}-[0-9]*.apk /packages/; \
done
FROM alpine:3.24
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.19 ARG GOSU_VERSION=1.20
ENV LANG=C.UTF-8 ENV LANG=C.UTF-8
ENV LC_ALL=C.UTF-8 ENV LC_ALL=C.UTF-8
# MariaDB Connector/C >= 3.4 refuses DB servers without TLS, this covers Perl DBI, client.cnf covers Dovecot
ENV MARIADB_TLS_DISABLE_PEER_VERIFICATION=1
COPY --from=builder /packages /tmp/packages
# Add groups and users before installing Dovecot to not break compatibility # Add groups and users before installing Dovecot to not break compatibility
RUN addgroup -g 5000 vmail \ RUN addgroup -g 5000 vmail \
@@ -18,6 +54,7 @@ RUN addgroup -g 5000 vmail \
&& adduser -D -u 5000 -G vmail -h /var/vmail vmail \ && adduser -D -u 5000 -G vmail -h /var/vmail vmail \
&& adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \ && adduser -D -G dovecot -u 401 -h /dev/null -s /sbin/nologin dovecot \
&& adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \ && adduser -D -G dovenull -u 402 -h /dev/null -s /sbin/nologin dovenull \
&& cp /tmp/packages/*.rsa.pub /etc/apk/keys/ \
&& apk add --no-cache --update \ && apk add --no-cache --update \
bash \ bash \
bind-tools \ bind-tools \
@@ -98,22 +135,15 @@ RUN addgroup -g 5000 vmail \
supervisor \ supervisor \
tzdata \ tzdata \
wget \ wget \
dovecot \ /tmp/packages/*.apk \
dovecot-dev \ && rm -rf /tmp/packages \
dovecot-lmtpd \
dovecot-lua \
dovecot-ldap \
dovecot-mysql \
dovecot-sql \
dovecot-submissiond \
dovecot-pigeonhole-plugin \
dovecot-pop3d \
dovecot-fts-flatcurve \
&& arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \ && arch=$(arch | sed s/aarch64/arm64/ | sed s/x86_64/amd64/) \
&& wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \ && wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/$GOSU_VERSION/gosu-$arch" \
&& chmod +x /usr/local/bin/gosu \ && chmod +x /usr/local/bin/gosu \
&& gosu nobody true && gosu nobody true
# Dovecot's MySQL driver reads [client] from /etc/my.cnf, Connector/C ignores the !includedir there
COPY client.cnf /etc/my.cnf
COPY trim_logs.sh /usr/local/bin/trim_logs.sh COPY trim_logs.sh /usr/local/bin/trim_logs.sh
COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh COPY clean_q_aged.sh /usr/local/bin/clean_q_aged.sh
COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf COPY syslog-ng.conf /etc/syslog-ng/syslog-ng.conf
+3
View File
@@ -0,0 +1,3 @@
[client]
ssl = false
ssl-verify-server-cert = false
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.23 FROM alpine:3.24
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
@@ -17,8 +17,8 @@ if [ $nftables_found -gt $iptables_found ]; then
fi fi
if [ $nftables_found -eq 0 ] && [ $nftables_found -eq $iptables_found ]; then if [ $nftables_found -eq 0 ] && [ $nftables_found -eq $iptables_found ]; then
nftables_lines=$(nft list ruleset | wc -l) nftables_lines=$(nft list ruleset 2>/dev/null | wc -l)
iptables_lines=$(iptables-save | wc -l) iptables_lines=$(iptables-save 2>/dev/null | wc -l)
if [ $nftables_lines -gt $iptables_lines ]; then if [ $nftables_lines -gt $iptables_lines ]; then
backend=nftables backend=nftables
else else
+23 -514
View File
@@ -1,453 +1,41 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
DEBUG = False
import re
import os import os
import sys import sys
import time import time
import atexit
import signal import signal
import ipaddress
from collections import Counter
from random import randint
from threading import Thread
from threading import Lock
import redis import redis
import json
import dns.resolver
import dns.exception
import uuid
from modules.Logger import Logger from modules.Logger import Logger
from modules.IPTables import IPTables from modules.Netfilter import Netfilter
from modules.NFTables import NFTables
def logdebug(msg):
if DEBUG:
logger.logInfo("DEBUG: %s" % msg)
# Globals
WHITELIST = []
BLACKLIST = []
bans = {}
quit_now = False
exit_code = 0
lock = Lock()
chain_name = "MAILCOW" chain_name = "MAILCOW"
r = None
pubsub = None
clear_before_quit = False
def refreshF2boptions(): def connect_redis(logger):
global f2boptions if os.getenv('REDIS_SLAVEOF_IP', ''):
global quit_now host, port = os.getenv('REDIS_SLAVEOF_IP'), os.getenv('REDIS_SLAVEOF_PORT', '')
global exit_code
f2boptions = {}
if not r.get('F2B_OPTIONS'):
f2boptions['ban_time'] = r.get('F2B_BAN_TIME')
f2boptions['max_ban_time'] = r.get('F2B_MAX_BAN_TIME')
f2boptions['ban_time_increment'] = r.get('F2B_BAN_TIME_INCREMENT')
f2boptions['max_attempts'] = r.get('F2B_MAX_ATTEMPTS')
f2boptions['retry_window'] = r.get('F2B_RETRY_WINDOW')
f2boptions['netban_ipv4'] = r.get('F2B_NETBAN_IPV4')
f2boptions['netban_ipv6'] = r.get('F2B_NETBAN_IPV6')
else: else:
host, port = os.getenv('IPV4_NETWORK', '172.22.1') + '.249', 6379
while True:
try: try:
f2boptions = json.loads(r.get('F2B_OPTIONS')) # log lines are not always valid UTF-8
except ValueError as e: r = redis.StrictRedis(host=host, port=port, db=0, password=os.environ['REDISPASS'], decode_responses=True,
logger.logCrit( encoding_errors='replace', socket_connect_timeout=5, socket_timeout=30)
'Error loading F2B options: F2B_OPTIONS is not json. Exception: %s' % e) r.ping()
quit_now = True return r
exit_code = 2
verifyF2boptions(f2boptions)
r.set('F2B_OPTIONS', json.dumps(f2boptions, ensure_ascii=False))
def verifyF2boptions(f2boptions):
verifyF2boption(f2boptions, 'ban_time', 1800)
verifyF2boption(f2boptions, 'max_ban_time', 10000)
verifyF2boption(f2boptions, 'ban_time_increment', True)
verifyF2boption(f2boptions, 'max_attempts', 10)
verifyF2boption(f2boptions, 'retry_window', 600)
verifyF2boption(f2boptions, 'netban_ipv4', 32)
verifyF2boption(f2boptions, 'netban_ipv6', 128)
verifyF2boption(f2boptions, 'banlist_id', str(uuid.uuid4()))
verifyF2boption(f2boptions, 'manage_external', 0)
def verifyF2boption(f2boptions, f2boption, f2bdefault):
f2boptions[f2boption] = f2boptions[f2boption] if f2boption in f2boptions and f2boptions[f2boption] is not None else f2bdefault
def refreshF2bregex():
global f2bregex
global quit_now
global exit_code
if not r.get('F2B_REGEX'):
f2bregex = {}
f2bregex[1] = r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)'
f2bregex[2] = r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)'
f2bregex[3] = r'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+'
f2bregex[4] = r'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+'
f2bregex[5] = r'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+'
f2bregex[6] = r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): Password mismatch \(SHA1 of given password: [a-f0-9]+\)'
f2bregex[7] = r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): unknown user \(SHA1 of given password: [a-f0-9]+\)'
f2bregex[8] = r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked'
f2bregex[9] = r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+'
r.set('F2B_REGEX', json.dumps(f2bregex, ensure_ascii=False))
else:
try:
f2bregex = {}
f2bregex = json.loads(r.get('F2B_REGEX'))
except ValueError:
logger.logCrit('Error loading F2B options: F2B_REGEX is not json')
quit_now = True
exit_code = 2
def get_ip(address):
ip = ipaddress.ip_address(address)
if type(ip) is ipaddress.IPv6Address and ip.ipv4_mapped:
ip = ip.ipv4_mapped
if ip.is_private or ip.is_loopback:
return False
return ip
def ban(address):
global f2boptions
global lock
logdebug("ban() called with address=%s" % address)
refreshF2boptions()
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
RETRY_WINDOW = int(f2boptions['retry_window'])
NETBAN_IPV4 = '/' + str(f2boptions['netban_ipv4'])
NETBAN_IPV6 = '/' + str(f2boptions['netban_ipv6'])
ip = get_ip(address)
if not ip:
logdebug("No valid IP -- skipping ban()")
return
address = str(ip)
self_network = ipaddress.ip_network(address)
with lock:
temp_whitelist = set(WHITELIST)
logdebug("Checking if %s overlaps with any WHITELIST entries" % self_network)
if temp_whitelist:
for wl_key in temp_whitelist:
wl_net = ipaddress.ip_network(wl_key, False)
logdebug("Checking overlap between %s and %s" % (self_network, wl_net))
if wl_net.overlaps(self_network):
logger.logInfo(
'Address %s is allowlisted by rule %s' % (self_network, wl_net))
return
net = ipaddress.ip_network(
(address + (NETBAN_IPV4 if type(ip) is ipaddress.IPv4Address else NETBAN_IPV6)), strict=False)
net = str(net)
logdebug("Ban net: %s" % net)
if not net in bans:
bans[net] = {'attempts': 0, 'last_attempt': 0, 'ban_counter': 0}
logdebug("Initing new ban counter for %s" % net)
current_attempt = time.time()
logdebug("Current attempt ts=%s, previous: %s, retry_window: %s" %
(current_attempt, bans[net]['last_attempt'], RETRY_WINDOW))
if current_attempt - bans[net]['last_attempt'] > RETRY_WINDOW:
bans[net]['attempts'] = 0
logdebug("Ban counter for %s reset as window expired" % net)
bans[net]['attempts'] += 1
bans[net]['last_attempt'] = current_attempt
logdebug("%s attempts now %d" % (net, bans[net]['attempts']))
if bans[net]['attempts'] >= MAX_ATTEMPTS:
cur_time = int(round(time.time()))
NET_BAN_TIME = calcNetBanTime(bans[net]['ban_counter'])
logger.logCrit('Banning %s for %d minutes' % (net, NET_BAN_TIME / 60 ))
if type(ip) is ipaddress.IPv4Address and int(f2boptions['manage_external']) != 1:
with lock:
logdebug("Calling tables.banIPv4(%s)" % net)
tables.banIPv4(net)
elif int(f2boptions['manage_external']) != 1:
with lock:
logdebug("Calling tables.banIPv6(%s)" % net)
tables.banIPv6(net)
logdebug("Updating F2B_ACTIVE_BANS[%s]=%d" %
(net, cur_time + NET_BAN_TIME))
r.hset('F2B_ACTIVE_BANS', '%s' % net, cur_time + NET_BAN_TIME)
else:
logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
MAX_ATTEMPTS - bans[net]['attempts'], RETRY_WINDOW, net))
def unban(net):
global lock
logdebug("Calling unban() with net=%s" % net)
if not net in bans:
logger.logInfo(
'%s is not banned, skipping unban and deleting from queue (if any)' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
return
logger.logInfo('Unbanning %s' % net)
if type(ipaddress.ip_network(net)) is ipaddress.IPv4Network:
with lock:
logdebug("Calling tables.unbanIPv4(%s)" % net)
tables.unbanIPv4(net)
else:
with lock:
logdebug("Calling tables.unbanIPv6(%s)" % net)
tables.unbanIPv6(net)
r.hdel('F2B_ACTIVE_BANS', '%s' % net)
r.hdel('F2B_QUEUE_UNBAN', '%s' % net)
if net in bans:
logdebug("Unban for %s, setting attempts=0, ban_counter+=1" % net)
bans[net]['attempts'] = 0
bans[net]['ban_counter'] += 1
def permBan(net, unban=False):
global f2boptions
global lock
is_unbanned = False
is_banned = False
if type(ipaddress.ip_network(net, strict=False)) is ipaddress.IPv4Network:
with lock:
if unban:
is_unbanned = tables.unbanIPv4(net)
elif int(f2boptions['manage_external']) != 1:
is_banned = tables.banIPv4(net)
else:
with lock:
if unban:
is_unbanned = tables.unbanIPv6(net)
elif int(f2boptions['manage_external']) != 1:
is_banned = tables.banIPv6(net)
if is_unbanned:
r.hdel('F2B_PERM_BANS', '%s' % net)
logger.logCrit('Removed host/network %s from denylist' % net)
elif is_banned:
r.hset('F2B_PERM_BANS', '%s' % net, int(round(time.time())))
logger.logCrit('Added host/network %s to denylist' % net)
def clear():
global lock
logger.logInfo('Clearing all bans')
for net in bans.copy():
logdebug("Unbanning net: %s" % net)
unban(net)
with lock:
logdebug("Clearing IPv4/IPv6 table")
tables.clearIPv4Table()
tables.clearIPv6Table()
try:
if r is not None:
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
except Exception as ex: except Exception as ex:
logger.logWarn('Error clearing redis keys F2B_ACTIVE_BANS and F2B_PERM_BANS: %s' % ex) logger.logWarn('Redis connection failed: %s - trying again in 3 seconds' % ex)
time.sleep(3)
def watch():
global pubsub
global quit_now
global exit_code
logger.logInfo('Watching Redis channel F2B_CHANNEL')
pubsub.subscribe('F2B_CHANNEL')
while not quit_now:
try:
for item in pubsub.listen():
refreshF2bregex()
for rule_id, rule_regex in f2bregex.items():
if item['data'] and item['type'] == 'message':
try:
result = re.search(rule_regex, item['data'])
except re.error:
result = False
if result:
addr = result.group(1)
ip = ipaddress.ip_address(addr)
if ip.is_private or ip.is_loopback:
continue
logger.logWarn('%s matched rule id %s (%s)' % (addr, rule_id, item['data']))
ban(addr)
except Exception as ex:
logger.logWarn('Error reading log line from pubsub: %s' % ex)
pubsub = None
quit_now = True
exit_code = 2
def snat4(snat_target):
global lock
global quit_now
while not quit_now:
time.sleep(10)
with lock:
tables.snat4(snat_target, os.getenv('IPV4_NETWORK', '172.22.1') + '.0/24')
def snat6(snat_target):
global lock
global quit_now
while not quit_now:
time.sleep(10)
with lock:
tables.snat6(snat_target, os.getenv('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
def autopurge():
global f2boptions
logdebug("autopurge thread started")
while not quit_now:
logdebug("autopurge tick")
time.sleep(10)
refreshF2boptions()
MAX_ATTEMPTS = int(f2boptions['max_attempts'])
QUEUE_UNBAN = r.hgetall('F2B_QUEUE_UNBAN')
logdebug("QUEUE_UNBAN: %s" % QUEUE_UNBAN)
if QUEUE_UNBAN:
for net in QUEUE_UNBAN:
logdebug("Autopurge: unbanning queued net: %s" % net)
unban(str(net))
# Only check expiry for actively banned IPs:
active_bans = r.hgetall('F2B_ACTIVE_BANS')
now = time.time()
for net_str, expire_str in active_bans.items():
logdebug("Checking ban expiry for (actively banned): %s" % net_str)
# Defensive: always process if timer missing or expired
try:
expire = float(expire_str)
except Exception:
logdebug("Invalid expire time for %s; unbanning" % net_str)
unban(net_str)
continue
time_left = expire - now
logdebug("Time left for %s: %.1f seconds" % (net_str, time_left))
if time_left <= 0:
logdebug("Ban expired for %s" % net_str)
unban(net_str)
def mailcowChainOrder():
global lock
global quit_now
global exit_code
while not quit_now:
time.sleep(10)
with lock:
quit_now, exit_code = tables.checkIPv4ChainOrder()
if quit_now: return
quit_now, exit_code = tables.checkIPv6ChainOrder()
def calcNetBanTime(ban_counter):
global f2boptions
BAN_TIME = int(f2boptions['ban_time'])
MAX_BAN_TIME = int(f2boptions['max_ban_time'])
BAN_TIME_INCREMENT = bool(f2boptions['ban_time_increment'])
NET_BAN_TIME = BAN_TIME if not BAN_TIME_INCREMENT else BAN_TIME * 2 ** ban_counter
NET_BAN_TIME = max([BAN_TIME, min([NET_BAN_TIME, MAX_BAN_TIME])])
return NET_BAN_TIME
def isIpNetwork(address):
try:
ipaddress.ip_network(address, False)
except ValueError:
return False
return True
def genNetworkList(list):
resolver = dns.resolver.Resolver()
hostnames = []
networks = []
for key in list:
if isIpNetwork(key):
networks.append(key)
else:
hostnames.append(key)
for hostname in hostnames:
hostname_ips = []
for rdtype in ['A', 'AAAA']:
try:
answer = resolver.resolve(qname=hostname, rdtype=rdtype, lifetime=3)
except dns.exception.Timeout:
logger.logInfo('Hostname %s timedout on resolve' % hostname)
break
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
continue
except dns.exception.DNSException as dnsexception:
logger.logInfo('%s' % dnsexception)
continue
for rdata in answer:
hostname_ips.append(rdata.to_text())
networks.extend(hostname_ips)
return set(networks)
def whitelistUpdate():
global lock
global quit_now
global WHITELIST
while not quit_now:
start_time = time.time()
list = r.hgetall('F2B_WHITELIST')
new_whitelist = []
if list:
new_whitelist = genNetworkList(list)
with lock:
if Counter(new_whitelist) != Counter(WHITELIST):
WHITELIST = new_whitelist
logger.logInfo('Allowlist was changed, it has %s entries' % len(WHITELIST))
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def blacklistUpdate():
global quit_now
global BLACKLIST
while not quit_now:
start_time = time.time()
list = r.hgetall('F2B_BLACKLIST')
new_blacklist = []
if list:
new_blacklist = genNetworkList(list)
if Counter(new_blacklist) != Counter(BLACKLIST):
addban = set(new_blacklist).difference(BLACKLIST)
delban = set(BLACKLIST).difference(new_blacklist)
BLACKLIST = new_blacklist
logger.logInfo('Denylist was changed, it has %s entries' % len(BLACKLIST))
if addban:
for net in addban:
permBan(net=net)
if delban:
for net in delban:
permBan(net=net, unban=True)
time.sleep(60.0 - ((time.time() - start_time) % 60.0))
def sigterm_quit(signum, frame):
global clear_before_quit
logdebug("SIGTERM received, setting clear_before_quit to True and exiting")
clear_before_quit = True
sys.exit(exit_code)
def before_quit():
logdebug("before_quit called, clear_before_quit=%s" % clear_before_quit)
if clear_before_quit:
clear()
if pubsub is not None:
pubsub.unsubscribe()
if __name__ == '__main__': if __name__ == '__main__':
logger = Logger() logger = Logger()
logdebug("Sys.argv: %s" % sys.argv)
atexit.register(before_quit)
signal.signal(signal.SIGTERM, sigterm_quit)
backend = sys.argv[1] backend = sys.argv[1]
logdebug("Backend: %s" % backend)
if backend == "nftables": if backend == "nftables":
from modules.NFTables import NFTables
logger.logInfo('Using NFTables backend') logger.logInfo('Using NFTables backend')
tables = NFTables(chain_name, logger) tables = NFTables(chain_name, logger)
else: else:
from modules.IPTables import IPTables
logger.logInfo('Using IPTables backend') logger.logInfo('Using IPTables backend')
logger.logWarn( logger.logWarn(
"DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. " "DEPRECATION: iptables-legacy is deprecated and will be removed in future releases. "
@@ -456,93 +44,14 @@ if __name__ == '__main__':
time.sleep(5) time.sleep(5)
tables = IPTables(chain_name, logger) tables = IPTables(chain_name, logger)
clear() r = connect_redis(logger)
logger.logInfo("Initializing mailcow netfilter chain")
tables.initChainIPv4()
tables.initChainIPv6()
if os.getenv("DISABLE_NETFILTER_ISOLATION_RULE", "").lower() in ("y", "yes"):
logger.logInfo(f"Skipping {chain_name} isolation")
else:
logger.logInfo(f"Setting {chain_name} isolation")
tables.create_mailcow_isolation_rule("br-mailcow", [3306, 6379, 8983, 12345], os.getenv("MAILCOW_REPLICA_IP"))
# connect to redis
while True:
try:
redis_slaveof_ip = os.getenv('REDIS_SLAVEOF_IP', '')
redis_slaveof_port = os.getenv('REDIS_SLAVEOF_PORT', '')
logdebug(
"Connecting redis (SLAVEOF_IP:%s, PORT:%s)" % (redis_slaveof_ip, redis_slaveof_port))
if "".__eq__(redis_slaveof_ip):
r = redis.StrictRedis(
host=os.getenv('IPV4_NETWORK', '172.22.1') + '.249', decode_responses=True, port=6379, db=0, password=os.environ['REDISPASS'])
else:
r = redis.StrictRedis(
host=redis_slaveof_ip, decode_responses=True, port=redis_slaveof_port, db=0, password=os.environ['REDISPASS'])
r.ping()
pubsub = r.pubsub()
except Exception as ex:
logdebug(
'Redis connection failed: %s - trying again in 3 seconds' % (ex))
time.sleep(3)
else:
break
logger.set_redis(r) logger.set_redis(r)
logdebug("Redis connection established, setting up F2B keys")
if r.exists('F2B_LOG'): netfilter = Netfilter(r, tables, logger, os.environ)
logdebug("Renaming F2B_LOG to NETFILTER_LOG") signal.signal(signal.SIGTERM, lambda signum, frame: netfilter.stop.set())
r.rename('F2B_LOG', 'NETFILTER_LOG') signal.signal(signal.SIGINT, lambda signum, frame: netfilter.stop.set())
r.delete('F2B_ACTIVE_BANS')
r.delete('F2B_PERM_BANS')
refreshF2boptions() netfilter.setup()
netfilter.run()
watch_thread = Thread(target=watch) netfilter.shutdown()
watch_thread.daemon = True sys.exit(0)
watch_thread.start()
if os.getenv('SNAT_TO_SOURCE') and os.getenv('SNAT_TO_SOURCE') != 'n':
try:
snat_ip = os.getenv('SNAT_TO_SOURCE')
snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv4Address:
snat4_thread = Thread(target=snat4, args=(snat_ip,))
snat4_thread.daemon = True
snat4_thread.start()
except ValueError:
print(os.getenv('SNAT_TO_SOURCE') + ' is not a valid IPv4 address')
if os.getenv('SNAT6_TO_SOURCE') and os.getenv('SNAT6_TO_SOURCE') != 'n':
try:
snat_ip = os.getenv('SNAT6_TO_SOURCE')
snat_ipo = ipaddress.ip_address(snat_ip)
if type(snat_ipo) is ipaddress.IPv6Address:
snat6_thread = Thread(target=snat6,args=(snat_ip,))
snat6_thread.daemon = True
snat6_thread.start()
except ValueError:
print(os.getenv('SNAT6_TO_SOURCE') + ' is not a valid IPv6 address')
autopurge_thread = Thread(target=autopurge)
autopurge_thread.daemon = True
autopurge_thread.start()
mailcowchainwatch_thread = Thread(target=mailcowChainOrder)
mailcowchainwatch_thread.daemon = True
mailcowchainwatch_thread.start()
blacklistupdate_thread = Thread(target=blacklistUpdate)
blacklistupdate_thread.daemon = True
blacklistupdate_thread.start()
whitelistupdate_thread = Thread(target=whitelistUpdate)
whitelistupdate_thread.daemon = True
whitelistupdate_thread.start()
while not quit_now:
time.sleep(0.5)
logdebug("Exiting with code %s" % exit_code)
sys.exit(exit_code)
@@ -0,0 +1,236 @@
import ipaddress
import time
class BanManager:
"""Failed attempts and automatic bans (Redis F2B_ACTIVE_BANS: network => expiry), the denylist (F2B_PERM_BANS)
and the allowlist. Every firewall change runs under lock, which also guards the other firewall work of netfilter
(chains, SNAT), because the backends must not run in parallel."""
# Seconds failed logins of an address are not counted after the auth backend was unavailable for it
EXCUSE_TIME = 60
def __init__(self, r, tables, options, logger, lock, clock=time.time):
self.r = r
self.tables = tables
self.options = options
self.logger = logger
self.lock = lock
self.clock = clock
self.counters = {}
self.banned = set()
self.denied = {}
self.allowlist = []
self.excused = {}
def excuse(self, ip):
"""The auth backend could not check a login of ip: its failed logins are not counted for EXCUSE_TIME"""
with self.lock:
now = self.clock()
if self.excused.get(ip, 0) <= now:
self.logger.logWarn('Auth backend unavailable for %s, its failed logins of the next %d seconds are not counted' % (
ip, self.EXCUSE_TIME))
self.excused[ip] = now + self.EXCUSE_TIME
def attempt(self, ip):
"""Counts a failed attempt of ip and bans its network after max_attempts within retry_window"""
options = self.options.values
prefix = options['netban_ipv4'] if ip.version == 4 else options['netban_ipv6']
net = str(ipaddress.ip_network('%s/%d' % (ip, prefix), strict=False))
with self.lock:
now = self.clock()
allowed = self.allowed_by(ip)
if allowed is not None:
self.logger.logInfo('Address %s is allowlisted by rule %s' % (ip, allowed))
return
if self.excused.get(ip, 0) > now:
self.logger.logInfo('Not counting the failed login of %s, the auth backend was unavailable' % ip)
return
if net in self.banned:
# log lines of connections from before the ban
return
counter = self.counter(net)
if now - counter['last_attempt'] > options['retry_window']:
counter['attempts'] = 0
counter['attempts'] += 1
counter['last_attempt'] = now
if counter['attempts'] < options['max_attempts']:
self.logger.logWarn('%d more attempts in the next %d seconds until %s is banned' % (
options['max_attempts'] - counter['attempts'], options['retry_window'], net))
return
ban_time = self.options.ban_time(counter['ban_counter'])
self.logger.logCrit('Banning %s for %d minutes' % (net, ban_time / 60))
# Redis first: a firewall rule without expiry would never be lifted
self.r.hset('F2B_ACTIVE_BANS', net, int(round(now + ban_time)))
self.banned.add(net)
if options['manage_external'] != 1:
self.apply([net])
def unban(self, net):
"""Lifts the automatic ban of a network; the rule stays while the denylist bans the same network"""
with self.lock:
normalized = self.normalize(net)
if normalized is None:
self.logger.logWarn('Removing invalid network %s from the active bans' % net)
else:
self.logger.logInfo('Unbanning %s' % net)
if normalized not in self.denied:
self.remove([normalized])
self.r.hdel('F2B_ACTIVE_BANS', net)
self.r.hdel('F2B_QUEUE_UNBAN', net)
if net in self.banned:
self.banned.discard(net)
counter = self.counter(net)
counter['attempts'] = 0
counter['ban_counter'] += 1
def unban_queued(self, entry):
"""Unban request of the UI: a banned network, or a network that contains banned ones (allowlisting)"""
network = self.network(entry)
if network is None:
self.logger.logWarn('Removing invalid network %s from the unban queue' % entry)
self.r.hdel('F2B_QUEUE_UNBAN', entry)
return
with self.lock:
banned = set(self.r.hkeys('F2B_ACTIVE_BANS')) | self.banned
targets = [net for net in banned if self.overlaps(net, network)]
for net in targets:
self.unban(net)
if not targets and str(network) not in self.denied:
# a rule without known ban, e.g. left by an older netfilter
if self.remove([str(network)]):
self.logger.logInfo('Unbanning %s' % network)
else:
self.logger.logInfo('%s is not banned, removing it from the unban queue' % network)
self.r.hdel('F2B_QUEUE_UNBAN', entry)
def purge(self):
"""Processes the unban queue of the UI and lifts expired bans"""
for entry in self.r.hkeys('F2B_QUEUE_UNBAN'):
self.unban_queued(entry)
now = self.clock()
for net, expires in self.r.hgetall('F2B_ACTIVE_BANS').items():
try:
expired = self.network(net) is None or float(expires) <= now
except (TypeError, ValueError):
expired = True
if expired:
self.unban(net)
with self.lock:
# bans whose Redis entry is gone, e.g. deleted by hand
for net in self.banned - set(self.r.hkeys('F2B_ACTIVE_BANS')):
self.unban(net)
self.forget_idle(now)
def restore(self):
"""Applies the unexpired automatic bans of Redis again after a start of netfilter and drops the expired ones"""
now = self.clock()
restored = []
with self.lock:
for net, expires in self.r.hgetall('F2B_ACTIVE_BANS').items():
try:
valid = self.network(net) is not None and float(expires) > now
except (TypeError, ValueError):
valid = False
if not valid:
self.r.hdel('F2B_ACTIVE_BANS', net)
continue
self.banned.add(net)
self.counter(net)
restored.append(net)
if restored and self.options.values['manage_external'] != 1:
self.apply(restored)
if restored:
self.logger.logInfo('Restored %d active bans' % len(restored))
def reapply(self):
"""Applies all bans again, e.g. after the MAILCOW chain had to be recreated"""
with self.lock:
if self.options.values['manage_external'] != 1:
self.apply(list(self.banned) + list(self.denied))
def set_denylist(self, entries):
"""Bans the networks of the denylist permanently and lifts the bans of removed entries"""
networks = {}
for entry in entries:
net = self.normalize(entry)
if net is None:
self.logger.logWarn('Skipping invalid denylist entry %s' % entry)
continue
networks[net] = entry
with self.lock:
if networks.keys() == self.denied.keys():
return
self.logger.logInfo('Denylist was changed, it has %d entries' % len(networks))
added = [net for net in networks if net not in self.denied]
removed = [net for net in self.denied if net not in networks]
if added and self.options.values['manage_external'] != 1:
self.apply(added)
for net in added:
self.r.hset('F2B_PERM_BANS', networks[net], int(round(self.clock())))
self.logger.logCrit('Added host/network %s to denylist' % networks[net])
# an automatic ban of the same network keeps its rule
auto_banned = {self.normalize(net) for net in self.banned}
self.remove([net for net in removed if net not in auto_banned])
for net in removed:
self.r.hdel('F2B_PERM_BANS', self.denied[net])
self.logger.logCrit('Removed host/network %s from denylist' % self.denied[net])
self.denied = networks
def set_allowlist(self, entries):
networks = []
for entry in entries:
network = self.network(entry)
if network is None:
self.logger.logWarn('Skipping invalid allowlist entry %s' % entry)
continue
networks.append(network)
with self.lock:
if set(networks) != set(self.allowlist):
self.allowlist = networks
self.logger.logInfo('Allowlist was changed, it has %d entries' % len(networks))
def allowed_by(self, ip):
address = ipaddress.ip_network(ip)
for network in self.allowlist:
if network.version == address.version and network.overlaps(address):
return network
return None
def counter(self, net):
return self.counters.setdefault(net, {'attempts': 0, 'last_attempt': 0.0, 'ban_counter': 0})
def forget_idle(self, now):
"""Drops counters of networks that were never banned and had no attempt within the retry window"""
window = self.options.values['retry_window']
with self.lock:
for net in [net for net, counter in self.counters.items()
if net not in self.banned and counter['ban_counter'] == 0 and now - counter['last_attempt'] > window]:
del self.counters[net]
for ip in [ip for ip, until in self.excused.items() if until <= now]:
del self.excused[ip]
def apply(self, nets):
for net in self.tables.ban(nets):
self.logger.logWarn('Could not add the firewall rule for %s' % net)
def remove(self, nets):
return self.tables.unban(nets) if nets else []
@staticmethod
def network(entry):
try:
return ipaddress.ip_network(entry, strict=False)
except (TypeError, ValueError):
return None
@classmethod
def normalize(cls, entry):
network = cls.network(entry)
return str(network) if network is not None else None
@classmethod
def overlaps(cls, net, network):
other = cls.network(net)
return other is not None and other.version == network.version and other.overlaps(network)
+128 -112
View File
@@ -1,6 +1,7 @@
import iptc import iptc
import time import time
import os import ipaddress
import contextlib
class IPTables: class IPTables:
def __init__(self, chain_name, logger): def __init__(self, chain_name, logger):
@@ -8,62 +9,10 @@ class IPTables:
self.logger = logger self.logger = logger
def initChainIPv4(self): def initChainIPv4(self):
if not iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) in iptc.Table(iptc.Table.FILTER).chains: self.init_chain(iptc.Table(iptc.Table.FILTER))
iptc.Table(iptc.Table.FILTER).create_chain(self.chain_name)
for c in ['FORWARD', 'INPUT']:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), c)
rule = iptc.Rule()
rule.src = '0.0.0.0/0'
rule.dst = '0.0.0.0/0'
target = iptc.Target(rule, self.chain_name)
rule.target = target
if rule not in chain.rules:
chain.insert_rule(rule)
def initChainIPv6(self): def initChainIPv6(self):
if not iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) in iptc.Table6(iptc.Table6.FILTER).chains: self.init_chain(iptc.Table6(iptc.Table6.FILTER))
iptc.Table6(iptc.Table6.FILTER).create_chain(self.chain_name)
for c in ['FORWARD', 'INPUT']:
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), c)
rule = iptc.Rule6()
rule.src = '::/0'
rule.dst = '::/0'
target = iptc.Target(rule, self.chain_name)
rule.target = target
if rule not in chain.rules:
chain.insert_rule(rule)
def checkIPv4ChainOrder(self):
filter_table = iptc.Table(iptc.Table.FILTER)
filter_table.refresh()
return self.checkChainOrder(filter_table)
def checkIPv6ChainOrder(self):
filter_table = iptc.Table6(iptc.Table6.FILTER)
filter_table.refresh()
return self.checkChainOrder(filter_table)
def checkChainOrder(self, filter_table):
err = False
exit_code = None
forward_chain = iptc.Chain(filter_table, 'FORWARD')
input_chain = iptc.Chain(filter_table, 'INPUT')
for chain in [forward_chain, input_chain]:
target_found = False
for position, item in enumerate(chain.rules):
if item.target.name == self.chain_name:
target_found = True
if position > 2:
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
err = True
exit_code = 2
if not target_found:
self.logger.logCrit('Error in %s chain: %s target not found, restarting container' % (chain.name, self.chain_name))
err = True
exit_code = 2
return err, exit_code
def clearIPv4Table(self): def clearIPv4Table(self):
self.clearTable(iptc.Table(iptc.Table.FILTER)) self.clearTable(iptc.Table(iptc.Table.FILTER))
@@ -71,68 +20,133 @@ class IPTables:
def clearIPv6Table(self): def clearIPv6Table(self):
self.clearTable(iptc.Table6(iptc.Table6.FILTER)) self.clearTable(iptc.Table6(iptc.Table6.FILTER))
def clearTable(self, filter_table): @contextlib.contextmanager
filter_table.autocommit = False def transaction(self, table):
forward_chain = iptc.Chain(filter_table, "FORWARD") # One commit for all changes, autocommit rewrites the whole table for every change
input_chain = iptc.Chain(filter_table, "INPUT") table.autocommit = False
mailcow_chain = iptc.Chain(filter_table, self.chain_name) table.refresh()
if mailcow_chain in filter_table.chains: try:
for rule in mailcow_chain.rules: yield
mailcow_chain.delete_rule(rule) table.commit()
for rule in forward_chain.rules: finally:
if rule.target.name == self.chain_name: table.refresh()
forward_chain.delete_rule(rule) table.autocommit = True
for rule in input_chain.rules:
if rule.target.name == self.chain_name: def new_rule(self, table):
input_chain.delete_rule(rule) return iptc.Rule6() if isinstance(table, iptc.Table6) else iptc.Rule()
filter_table.delete_chain(self.chain_name)
filter_table.commit() def family(self, table):
return 'IPv6' if isinstance(table, iptc.Table6) else 'IPv4'
def init_chain(self, filter_table):
# Empty MAILCOW chain with one jump at the top of the input and forward chains
filter_table.refresh() filter_table.refresh()
filter_table.autocommit = True if filter_table.is_chain(self.chain_name):
filter_table.flush_entries(self.chain_name)
else:
filter_table.create_chain(self.chain_name)
self.ensure_jumps(filter_table)
def banIPv4(self, source): def ensure_chains(self):
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) # Recreates missing MAILCOW chains and moves the jumps back to the top, True if a chain was recreated
rule = iptc.Rule() recreated = False
rule.src = source for filter_table in [iptc.Table(iptc.Table.FILTER), iptc.Table6(iptc.Table6.FILTER)]:
target = iptc.Target(rule, "REJECT") filter_table.refresh()
rule.target = target if not filter_table.is_chain(self.chain_name):
if rule in chain.rules: filter_table.create_chain(self.chain_name)
return False self.logger.logWarn(f'MAILCOW {self.family(filter_table)} chain was missing and has been recreated')
chain.insert_rule(rule) recreated = True
return True self.ensure_jumps(filter_table)
return recreated
def banIPv6(self, source): def ensure_jumps(self, filter_table):
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) # Exactly one jump to MAILCOW as first rule: docker inserts its jump (DOCKER-USER) at the top
rule = iptc.Rule6() for name in ['INPUT', 'FORWARD']:
rule.src = source chain = iptc.Chain(filter_table, name)
target = iptc.Target(rule, "REJECT") positions = [position for position, rule in enumerate(chain.rules) if rule.target.name == self.chain_name]
rule.target = target if positions == [0]: continue
if rule in chain.rules:
return False
chain.insert_rule(rule)
return True
def unbanIPv4(self, source): with self.transaction(filter_table):
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) for rule in [rule for rule in chain.rules if rule.target.name == self.chain_name]:
rule = iptc.Rule()
rule.src = source
target = iptc.Target(rule, "REJECT")
rule.target = target
if rule not in chain.rules:
return False
chain.delete_rule(rule) chain.delete_rule(rule)
return True rule = self.new_rule(filter_table)
rule.target = iptc.Target(rule, self.chain_name)
chain.insert_rule(rule)
if positions:
self.logger.logInfo(f'Moved MAILCOW jump to the top of the {self.family(filter_table)} {name} chain, it was at position {positions[0] + 1}')
else:
self.logger.logInfo(f'Added MAILCOW jump to the {self.family(filter_table)} {name} chain')
def unbanIPv6(self, source): def clearTable(self, filter_table):
chain = iptc.Chain(iptc.Table6(iptc.Table6.FILTER), self.chain_name) try:
rule = iptc.Rule6() with self.transaction(filter_table):
rule.src = source if not filter_table.is_chain(self.chain_name):
target = iptc.Target(rule, "REJECT") return
rule.target = target for chain in filter_table.chains:
if rule not in chain.rules: for rule in chain.rules:
return False if rule.target.name == self.chain_name:
chain.delete_rule(rule) chain.delete_rule(rule)
return True filter_table.flush_entries(self.chain_name)
filter_table.delete_chain(self.chain_name)
self.logger.logInfo(f'Clear completed: {self.family(filter_table)}')
except Exception as ex:
self.logger.logWarn(f'Error clearing the {self.family(filter_table)} MAILCOW chain: {ex}')
def ban_rule_network(self, rule):
# Source network of a ban rule, None for every other rule
if rule.target.name not in ('REJECT', 'DROP') or rule.matches or rule.in_interface or rule.out_interface:
return None
try:
return ipaddress.ip_network(rule.src, strict=False)
except ValueError:
return None
def split_tables(self, sources):
networks = {4: [], 6: []}
for source in sources:
network = ipaddress.ip_network(source, strict=False)
networks[network.version].append(network)
tables = [(iptc.Table(iptc.Table.FILTER), networks[4]), (iptc.Table6(iptc.Table6.FILTER), networks[6])]
return [(table, table_networks) for table, table_networks in tables if table_networks]
def ban(self, sources):
# One REJECT rule per network, existing rules are kept. Returns the networks that could not be banned
failed = []
for filter_table, networks in self.split_tables(sources):
try:
with self.transaction(filter_table):
chain = iptc.Chain(filter_table, self.chain_name)
existing = {self.ban_rule_network(rule) for rule in chain.rules}
for network in dict.fromkeys(networks):
if network in existing: continue
rule = self.new_rule(filter_table)
rule.src = str(network)
rule.target = iptc.Target(rule, "REJECT")
chain.insert_rule(rule)
except Exception as ex:
self.logger.logWarn(f'iptables: {ex}')
failed += networks
return [str(network) for network in failed]
def unban(self, sources):
# Removes every rule of the networks, also duplicates of older versions. Returns the unbanned networks
removed = []
for filter_table, networks in self.split_tables(sources):
targets = set(networks)
try:
with self.transaction(filter_table):
chain = iptc.Chain(filter_table, self.chain_name)
found = set()
for rule in chain.rules:
network = self.ban_rule_network(rule)
if network in targets:
chain.delete_rule(rule)
found.add(network)
except Exception as ex:
self.logger.logWarn(f'iptables: {ex}')
continue
removed += found
return [str(network) for network in removed]
def snat4(self, snat_target, source): def snat4(self, snat_target, source):
try: try:
@@ -215,7 +229,9 @@ class IPTables:
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""): def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
try: try:
chain = iptc.Chain(iptc.Table(iptc.Table.FILTER), self.chain_name) table = iptc.Table(iptc.Table.FILTER)
table.refresh()
chain = iptc.Chain(table, self.chain_name)
# insert mailcow isolation rule # insert mailcow isolation rule
rule = iptc.Rule() rule = iptc.Rule()
@@ -231,7 +247,7 @@ class IPTables:
chain.insert_rule(rule, position=0) chain.insert_rule(rule, position=0)
# insert mailcow isolation exception rule # insert mailcow isolation exception rule
if _allow != "": if _allow:
rule = iptc.Rule() rule = iptc.Rule()
rule.src = _allow rule.src = _allow
rule.in_interface = f'!{_interface}' rule.in_interface = f'!{_interface}'
+228 -424
View File
@@ -1,8 +1,9 @@
import nftables import nftables
import ipaddress import ipaddress
import os
class NFTables: class NFTables:
ISOLATION_COMMENTS = ('mailcow isolation', 'mailcow isolation allow')
def __init__(self, chain_name, logger): def __init__(self, chain_name, logger):
self.chain_name = chain_name self.chain_name = chain_name
self.logger = logger self.logger = logger
@@ -12,41 +13,15 @@ class NFTables:
self.nft.set_handle_output(True) self.nft.set_handle_output(True)
self.nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} }, self.nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} },
'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } } 'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } }
self.mailcow_chains = set()
self.search_current_chains() self.search_current_chains()
def initChainIPv4(self): def initChainIPv4(self):
self.insert_mailcow_chains("ip") self.init_chain("ip")
def initChainIPv6(self): def initChainIPv6(self):
self.insert_mailcow_chains("ip6") self.init_chain("ip6")
def checkIPv4ChainOrder(self):
return self.checkChainOrder("ip")
def checkIPv6ChainOrder(self):
return self.checkChainOrder("ip6")
def checkChainOrder(self, filter_table):
err = False
exit_code = None
for chain in ['input', 'forward']:
chain_position = self.check_mailcow_chains(filter_table, chain)
if chain_position is None: continue
if chain_position is False:
self.logger.logCrit(f'MAILCOW target not found in {filter_table} {chain} table, restarting container to fix it...')
err = True
exit_code = 2
if chain_position > 0:
chain_position += 1
self.logger.logCrit(f'MAILCOW target is in position {chain_position} in the {filter_table} {chain} table, restarting container to fix it...')
err = True
exit_code = 2
return err, exit_code
def clearIPv4Table(self): def clearIPv4Table(self):
self.clearTable("ip") self.clearTable("ip")
@@ -54,84 +29,19 @@ class NFTables:
def clearIPv6Table(self): def clearIPv6Table(self):
self.clearTable("ip6") self.clearTable("ip6")
def clearTable(self, _family):
is_empty_dict = True
json_command = self.get_base_dict()
chain_handle = self.get_chain_handle(_family, "filter", self.chain_name)
# if no handle, the chain doesn't exists
if chain_handle is not None:
is_empty_dict = False
# flush chain
mailcow_chain = {'family': _family, 'table': 'filter', 'name': self.chain_name}
flush_chain = {'flush': {'chain': mailcow_chain}}
json_command["nftables"].append(flush_chain)
# remove rule in forward chain
# remove rule in input chain
chains_family = [self.nft_chain_names[_family]['filter']['input'],
self.nft_chain_names[_family]['filter']['forward'] ]
for chain_base in chains_family:
if not chain_base: continue
rules_handle = self.get_rules_handle(_family, "filter", chain_base)
if rules_handle is not None:
for r_handle in rules_handle:
is_empty_dict = False
mailcow_rule = {'family':_family,
'table': 'filter',
'chain': chain_base,
'handle': r_handle }
delete_rules = {'delete': {'rule': mailcow_rule} }
json_command["nftables"].append(delete_rules)
# remove chain
# after delete all rules referencing this chain
if chain_handle is not None:
mc_chain_handle = {'family':_family,
'table': 'filter',
'name': self.chain_name,
'handle': chain_handle }
delete_chain = {'delete': {'chain': mc_chain_handle} }
json_command["nftables"].append(delete_chain)
if is_empty_dict == False:
if self.nft_exec_dict(json_command):
self.logger.logInfo(f"Clear completed: {_family}")
def banIPv4(self, source):
ban_dict = self.get_ban_ip_dict(source, "ip")
return self.nft_exec_dict(ban_dict)
def banIPv6(self, source):
ban_dict = self.get_ban_ip_dict(source, "ip6")
return self.nft_exec_dict(ban_dict)
def unbanIPv4(self, source):
unban_dict = self.get_unban_ip_dict(source, "ip")
if not unban_dict:
return False
return self.nft_exec_dict(unban_dict)
def unbanIPv6(self, source):
unban_dict = self.get_unban_ip_dict(source, "ip6")
if not unban_dict:
return False
return self.nft_exec_dict(unban_dict)
def snat4(self, snat_target, source): def snat4(self, snat_target, source):
self.snat_rule("ip", snat_target, source) self.snat_rule("ip", snat_target, source)
def snat6(self, snat_target, source): def snat6(self, snat_target, source):
self.snat_rule("ip6", snat_target, source) self.snat_rule("ip6", snat_target, source)
def nft_exec_dict(self, query: dict, quiet: bool = False):
def nft_exec_dict(self, query: dict):
if not query: return False if not query: return False
rc, output, error = self.nft.json_cmd(query) rc, output, error = self.nft.json_cmd(query)
if rc != 0: if rc != 0:
#self.logger.logCrit(f"Nftables Error: {error}") if not quiet:
self.logger.logWarn(f"nftables: {str(error).strip() or f'error {rc}'}")
return False return False
# Prevent returning False or empty string on commands that do not produce output # Prevent returning False or empty string on commands that do not produce output
@@ -144,15 +54,20 @@ class NFTables:
return {'nftables': [{ 'metainfo': { 'json_schema_version': 1} } ] } return {'nftables': [{ 'metainfo': { 'json_schema_version': 1} } ] }
def search_current_chains(self): def search_current_chains(self):
nft_chain_names = {'ip': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} },
'ip6': {'filter': {'input': '', 'forward': ''}, 'nat': {'postrouting': ''} } }
nft_chain_priority = {'ip': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} }, nft_chain_priority = {'ip': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} },
'ip6': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} } } 'ip6': {'filter': {'input': None, 'forward': None}, 'nat': {'postrouting': None} } }
mailcow_chains = set()
# Command: 'nft list chains' # Command: 'nft list chains'
_list = {'list' : {'chains': 'null'} } _list = {'list' : {'chains': 'null'} }
command = self.get_base_dict() command = self.get_base_dict()
command['nftables'].append(_list) command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command) kernel_ruleset = self.nft_exec_dict(command)
if kernel_ruleset: if not kernel_ruleset:
return
for _object in kernel_ruleset['nftables']: for _object in kernel_ruleset['nftables']:
chain = _object.get("chain") chain = _object.get("chain")
if not chain: continue if not chain: continue
@@ -163,9 +78,11 @@ class NFTables:
_priority = chain.get("prio") _priority = chain.get("prio")
_name = chain['name'] _name = chain['name']
if _family not in self.nft_chain_names: continue if _family not in nft_chain_names: continue
if _table not in self.nft_chain_names[_family]: continue if _table not in nft_chain_names[_family]: continue
if _hook not in self.nft_chain_names[_family][_table]: continue if _table == 'filter' and _name == self.chain_name:
mailcow_chains.add(_family)
if _hook not in nft_chain_names[_family][_table]: continue
if _priority is None: continue if _priority is None: continue
_saved_priority = nft_chain_priority[_family][_table][_hook] _saved_priority = nft_chain_priority[_family][_table][_hook]
@@ -174,19 +91,24 @@ class NFTables:
# hook and priority set # hook and priority set
# and it has the lowest priority # and it has the lowest priority
nft_chain_priority[_family][_table][_hook] = _priority nft_chain_priority[_family][_table][_hook] = _priority
self.nft_chain_names[_family][_table][_hook] = _name nft_chain_names[_family][_table][_hook] = _name
def search_for_chain(self, kernel_ruleset: dict, chain_name: str): self.nft_chain_names = nft_chain_names
found = False self.mailcow_chains = mailcow_chains
for _object in kernel_ruleset["nftables"]:
chain = _object.get("chain") def has_filter_chains(self, _family: str):
if not chain: # no base chains: the filter table does not exist, e.g. ip6 if docker has ipv6 disabled
continue return any(self.nft_chain_names[_family]['filter'].values())
ch_name = chain.get("name")
if ch_name == chain_name: def list_rules(self, _family: str, _table: str, _chain: str):
found = True # Command: 'nft list chain {family} {table} {chain}', None if the chain does not exist
break _list = {'list': {'chain': {'family': _family, 'table': _table, 'name': _chain}}}
return found command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command, quiet=True)
if not kernel_ruleset or not isinstance(kernel_ruleset, dict):
return None
return [_object["rule"] for _object in kernel_ruleset["nftables"] if _object.get("rule")]
def get_chain_dict(self, _family: str, _name: str): def get_chain_dict(self, _family: str, _name: str):
# nft (add | create) chain [<family>] <table> <name> # nft (add | create) chain [<family>] <table> <name>
@@ -196,66 +118,101 @@ class NFTables:
final_chain["nftables"].append(_add) final_chain["nftables"].append(_add)
return final_chain return final_chain
def get_mailcow_jump_rule_dict(self, _family: str, _chain: str): def get_mailcow_jump_rule(self, _family: str, _chain: str):
_jump_rule = self.get_base_dict() _expr_opt = [{'counter': {'family': _family, 'table': 'filter', 'packets': 0, 'bytes': 0}},
_expr_opt=[] {'jump': {'target': self.chain_name}}]
_expr_counter = {'family': _family, 'table': 'filter', 'packets': 0, 'bytes': 0}
_counter_dict = {'counter': _expr_counter}
_expr_opt.append(_counter_dict)
_jump_opts = {'jump': {'target': self.chain_name} }
_expr_opt.append(_jump_opts)
_rule_params = {'family': _family, _rule_params = {'family': _family,
'table': 'filter', 'table': 'filter',
'chain': _chain, 'chain': _chain,
'expr': _expr_opt, 'expr': _expr_opt,
'comment': "mailcow" } 'comment': "mailcow" }
return {'insert': {'rule': _rule_params} }
_add_rule = {'insert': {'rule': _rule_params} } def is_mailcow_jump(self, rule: dict):
# by target, jumps restored by iptables-restore carry no nft comment
return any(isinstance(expr, dict) and isinstance(expr.get('jump'), dict) and expr['jump'].get('target') == self.chain_name
for expr in rule.get('expr', []))
_jump_rule["nftables"].append(_add_rule) def init_chain(self, _family: str):
# Empty MAILCOW chain with one jump at the top of the input and forward chains
if not self.has_filter_chains(_family): return
return _jump_rule if self.list_rules(_family, 'filter', self.chain_name) is None:
if self.nft_exec_dict(self.get_chain_dict(_family, self.chain_name)):
self.logger.logInfo(f"MAILCOW {_family} chain created successfully.")
else:
flush_chain = {'flush': {'chain': {'family': _family, 'table': 'filter', 'name': self.chain_name}}}
command = self.get_base_dict()
command['nftables'].append(flush_chain)
self.nft_exec_dict(command)
def insert_mailcow_chains(self, _family: str): self.ensure_jumps(_family)
nft_input_chain = self.nft_chain_names[_family]['filter']['input']
nft_forward_chain = self.nft_chain_names[_family]['filter']['forward'] def ensure_chains(self):
# Command: 'nft list table <family> filter' # Recreates missing MAILCOW chains and moves the jumps back to the top, True if a chain was recreated
_table_opts = {'family': _family, 'name': 'filter'} self.search_current_chains()
_list = {'list': {'table': _table_opts} } recreated = False
for _family in ['ip', 'ip6']:
if not self.has_filter_chains(_family): continue
if _family not in self.mailcow_chains:
if self.nft_exec_dict(self.get_chain_dict(_family, self.chain_name)):
self.logger.logWarn(f"MAILCOW {_family} chain was missing and has been recreated")
recreated = True
self.ensure_jumps(_family)
return recreated
def ensure_jumps(self, _family: str):
# Exactly one jump to MAILCOW as first rule: docker inserts its jumps (DOCKER-USER, DOCKER-FORWARD) at the top
for hook in ['input', 'forward']:
base_chain = self.nft_chain_names[_family]['filter'][hook]
if not base_chain: continue
rules = self.list_rules(_family, 'filter', base_chain)
if rules is None: continue
positions = [position for position, rule in enumerate(rules) if self.is_mailcow_jump(rule)]
if positions == [0]: continue
# delete and insert in one transaction, the chain is never without jump
command = self.get_base_dict()
for position in positions:
_rule_opts = {'family': _family, 'table': 'filter', 'chain': base_chain, 'handle': rules[position]['handle']}
command['nftables'].append({'delete': {'rule': _rule_opts}})
command['nftables'].append(self.get_mailcow_jump_rule(_family, base_chain))
if self.nft_exec_dict(command):
if positions:
self.logger.logInfo(f"Moved MAILCOW jump to the top of the {_family} {base_chain} chain, it was at position {positions[0] + 1}")
else:
self.logger.logInfo(f"Added MAILCOW jump to the {_family} {base_chain} chain")
def clearTable(self, _family: str):
# Jumps first, the chain can only be deleted without references. Separate steps, so one failing step
# does not roll back the others
_list = {'list': {'table': {'family': _family, 'name': 'filter'}}}
command = self.get_base_dict() command = self.get_base_dict()
command['nftables'].append(_list) command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command) kernel_ruleset = self.nft_exec_dict(command, quiet=True)
if kernel_ruleset: if not kernel_ruleset or not isinstance(kernel_ruleset, dict):
# chain return
if not self.search_for_chain(kernel_ruleset, self.chain_name):
cadena = self.get_chain_dict(_family, self.chain_name)
if self.nft_exec_dict(cadena):
self.logger.logInfo(f"MAILCOW {_family} chain created successfully.")
input_jump_found, forward_jump_found = False, False
delete_jumps = self.get_base_dict()
for _object in kernel_ruleset["nftables"]: for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"): rule = _object.get("rule")
continue if rule and self.is_mailcow_jump(rule):
_rule_opts = {'family': _family, 'table': 'filter', 'chain': rule['chain'], 'handle': rule['handle']}
delete_jumps['nftables'].append({'delete': {'rule': _rule_opts}})
if len(delete_jumps['nftables']) > 1:
self.nft_exec_dict(delete_jumps)
rule = _object["rule"] if self.list_rules(_family, 'filter', self.chain_name) is None:
if nft_input_chain and rule["chain"] == nft_input_chain: return
if rule.get("comment") and rule["comment"] == "mailcow": mailcow_chain = {'family': _family, 'table': 'filter', 'name': self.chain_name}
input_jump_found = True command = self.get_base_dict()
if nft_forward_chain and rule["chain"] == nft_forward_chain: command['nftables'].append({'flush': {'chain': mailcow_chain}})
if rule.get("comment") and rule["comment"] == "mailcow": command['nftables'].append({'delete': {'chain': mailcow_chain}})
forward_jump_found = True if self.nft_exec_dict(command):
self.logger.logInfo(f"Clear completed: {_family}")
if not input_jump_found:
command = self.get_mailcow_jump_rule_dict(_family, nft_input_chain)
self.nft_exec_dict(command)
if not forward_jump_found:
command = self.get_mailcow_jump_rule_dict(_family, nft_forward_chain)
self.nft_exec_dict(command)
def delete_nat_rule(self, _family:str, _chain: str, _handle:str): def delete_nat_rule(self, _family:str, _chain: str, _handle:str):
delete_command = self.get_base_dict() delete_command = self.get_base_dict()
@@ -285,23 +242,14 @@ class NFTables:
# no postrouting chain, may occur if docker has ipv6 disabled. # no postrouting chain, may occur if docker has ipv6 disabled.
if not chain_name: return if not chain_name: return
# Command: nft list chain <family> nat <chain_name> rules = self.list_rules(_family, 'nat', chain_name)
_chain_opts = {'family': _family, 'table': 'nat', 'name': chain_name} if rules is None:
_list = {'list':{'chain': _chain_opts} }
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
if not kernel_ruleset:
return return
rule_position = 0 rule_position = 0
rule_handle = None rule_handle = None
rule_found = False rule_found = False
for _object in kernel_ruleset["nftables"]: for rule in rules:
if not _object.get("rule"):
continue
rule = _object["rule"]
if not rule.get("comment") or not rule["comment"] == "mailcow": if not rule.get("comment") or not rule["comment"] == "mailcow":
rule_position +=1 rule_position +=1
continue continue
@@ -314,6 +262,7 @@ class NFTables:
target_net = ipaddress.ip_network(snat_target, strict=False) target_net = ipaddress.ip_network(snat_target, strict=False)
if rule_found: if rule_found:
try:
saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"] saddr_ip = rule["expr"][0]["match"]["right"]["prefix"]["addr"]
saddr_len = int(rule["expr"][0]["match"]["right"]["prefix"]["len"]) saddr_len = int(rule["expr"][0]["match"]["right"]["prefix"]["len"])
@@ -331,6 +280,10 @@ class NFTables:
dest_net == daddr_net, dest_net == daddr_net,
target_net == current_target_net target_net == current_target_net
)) ))
except (KeyError, IndexError, TypeError, ValueError):
# rule of another format, replace it
saddr_net = dest_net
match = False
try: try:
if rule_position == 0: if rule_position == 0:
if not match: if not match:
@@ -377,49 +330,13 @@ class NFTables:
except: except:
self.logger.logCrit(f"Error running SNAT on {_family}, retrying...") self.logger.logCrit(f"Error running SNAT on {_family}, retrying...")
def get_chain_handle(self, _family: str, _table: str, chain_name: str):
chain_handle = None
# Command: 'nft list chains {family}'
_list = {'list': {'chains': {'family': _family} } }
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("chain"):
continue
chain = _object["chain"]
if chain["family"] == _family and chain["table"] == _table and chain["name"] == chain_name:
chain_handle = chain["handle"]
break
return chain_handle
def get_rules_handle(self, _family: str, _table: str, chain_name: str, _comment_filter = "mailcow"): def get_rules_handle(self, _family: str, _table: str, chain_name: str, _comment_filter = "mailcow"):
rule_handle = [] rules = self.list_rules(_family, _table, chain_name) or []
# Command: 'nft list chain {family} {table} {chain_name}' return [rule["handle"] for rule in rules if rule.get("comment") == _comment_filter]
_chain_opts = {'family': _family, 'table': _table, 'name': chain_name}
_list = {'list': {'chain': _chain_opts} }
command = self.get_base_dict()
command['nftables'].append(_list)
kernel_ruleset = self.nft_exec_dict(command)
if kernel_ruleset:
for _object in kernel_ruleset["nftables"]:
if not _object.get("rule"):
continue
rule = _object["rule"]
if rule["family"] == _family and rule["table"] == _table and rule["chain"] == chain_name:
if rule.get("comment") and rule["comment"] == _comment_filter:
rule_handle.append(rule["handle"])
return rule_handle
def get_ban_ip_dict(self, ipaddr: str, _family: str):
json_command = self.get_base_dict()
def get_ban_rule(self, network, _family: str):
expr_opt = [] expr_opt = []
ipaddr_net = ipaddress.ip_network(ipaddr, strict=False) right_dict = {'prefix': {'addr': str(network.network_address), 'len': int(network.prefixlen) } }
right_dict = {'prefix': {'addr': str(ipaddr_net.network_address), 'len': int(ipaddr_net.prefixlen) } }
left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} } left_dict = {'payload': {'protocol': _family, 'field': 'saddr'} }
match_dict = {'op': '==', 'left': left_dict, 'right': right_dict } match_dict = {'op': '==', 'left': left_dict, 'right': right_dict }
@@ -432,228 +349,115 @@ class NFTables:
rule_dict = {'family': _family, 'table': "filter", 'chain': self.chain_name, 'expr': expr_opt} rule_dict = {'family': _family, 'table': "filter", 'chain': self.chain_name, 'expr': expr_opt}
base_dict = {'insert': {'rule': rule_dict} } return {'insert': {'rule': rule_dict} }
json_command["nftables"].append(base_dict)
return json_command def ban_rule_network(self, rule: dict, _family: str):
# Source network of a ban rule (saddr match and drop), None for every other rule
if rule.get("comment") in self.ISOLATION_COMMENTS:
return None
exprs = [expr for expr in rule.get("expr", []) if isinstance(expr, dict)]
if not any('drop' in expr or 'reject' in expr for expr in exprs):
return None
for expr in exprs:
match = expr.get("match")
if not isinstance(match, dict) or match.get("op") != "==":
continue
left = match.get("left")
payload = left.get("payload") if isinstance(left, dict) else None
if not isinstance(payload, dict) or payload.get("protocol") != _family or payload.get("field") != "saddr":
continue
right = match.get("right")
try:
if isinstance(right, dict) and isinstance(right.get("prefix"), dict):
return ipaddress.ip_network(f'{right["prefix"]["addr"]}/{right["prefix"]["len"]}', strict=False)
if isinstance(right, str):
return ipaddress.ip_network(right, strict=False)
except (KeyError, ValueError):
return None
return None
def split_families(self, sources):
families = {}
for source in sources:
network = ipaddress.ip_network(source, strict=False)
families.setdefault('ip' if network.version == 4 else 'ip6', []).append(network)
return families
def ban(self, sources):
# One drop rule per network, existing rules are kept. Returns the networks that could not be banned
failed = []
for _family, networks in self.split_families(sources).items():
rules = self.list_rules(_family, 'filter', self.chain_name)
if rules is None:
failed += networks
continue
existing = {self.ban_rule_network(rule, _family) for rule in rules}
missing = [network for network in dict.fromkeys(networks) if network not in existing]
if not missing: continue
def get_unban_ip_dict(self, ipaddr:str, _family: str):
json_command = self.get_base_dict()
# Command: 'nft list chain {s_family} filter MAILCOW'
_chain_opts = {'family': _family, 'table': 'filter', 'name': self.chain_name}
_list = {'list': {'chain': _chain_opts} }
command = self.get_base_dict() command = self.get_base_dict()
command['nftables'].append(_list) for network in missing:
kernel_ruleset = self.nft_exec_dict(command) command['nftables'].append(self.get_ban_rule(network, _family))
rule_handle = None if not self.nft_exec_dict(command):
if kernel_ruleset: failed += missing
for _object in kernel_ruleset["nftables"]: return [str(network) for network in failed]
if not _object.get("rule"):
continue
rule = _object["rule"]["expr"][0]["match"] def unban(self, sources):
if not "payload" in rule["left"]: # Removes every rule of the networks, also duplicates of older versions. Returns the unbanned networks
continue removed = []
left_opt = rule["left"]["payload"] for _family, networks in self.split_families(sources).items():
if not left_opt["protocol"] == _family: rules = self.list_rules(_family, 'filter', self.chain_name)
continue if not rules: continue
if not left_opt["field"] =="saddr":
continue
# ip currently banned targets = set(networks)
rule_right = rule["right"]
if isinstance(rule_right, dict):
current_rule_ip = rule_right["prefix"]["addr"] + '/' + str(rule_right["prefix"]["len"])
else:
current_rule_ip = rule_right
current_rule_net = ipaddress.ip_network(current_rule_ip)
# ip to ban
candidate_net = ipaddress.ip_network(ipaddr, strict=False)
if current_rule_net == candidate_net:
rule_handle = _object["rule"]["handle"]
break
if rule_handle is not None:
mailcow_rule = {'family': _family, 'table': 'filter', 'chain': self.chain_name, 'handle': rule_handle}
delete_rule = {'delete': {'rule': mailcow_rule} }
json_command["nftables"].append(delete_rule)
else:
return False
return json_command
def check_mailcow_chains(self, family: str, chain: str):
position = 0
rule_found = False
chain_name = self.nft_chain_names[family]['filter'][chain]
if not chain_name: return None
_chain_opts = {'family': family, 'table': 'filter', 'name': chain_name}
_list = {'list': {'chain': _chain_opts}}
command = self.get_base_dict() command = self.get_base_dict()
command['nftables'].append(_list) found = set()
kernel_ruleset = self.nft_exec_dict(command) for rule in rules:
if kernel_ruleset: network = self.ban_rule_network(rule, _family)
for _object in kernel_ruleset["nftables"]: if network in targets:
if not _object.get("rule"): _rule_opts = {'family': _family, 'table': 'filter', 'chain': self.chain_name, 'handle': rule['handle']}
continue command['nftables'].append({'delete': {'rule': _rule_opts}})
rule = _object["rule"] found.add(network)
if rule.get("comment") and rule["comment"] == "mailcow": if found and self.nft_exec_dict(command):
rule_found = True removed += found
break return [str(network) for network in removed]
position+=1
return position if rule_found else False
def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""): def create_mailcow_isolation_rule(self, _interface:str, _dports:list, _allow:str = ""):
family = "ip" # One rule per port: iptables-nft cannot show an anonymous set, a host that saves and restores the ruleset
table = "filter" # with iptables-save (e.g. netfilter-persistent) would get a drop for all ports
comment_filter_drop = "mailcow isolation" for comment in self.ISOLATION_COMMENTS:
comment_filter_allow = "mailcow isolation allow" for handle in self.get_rules_handle("ip", "filter", self.chain_name, comment):
json_command = self.get_base_dict() self.delete_filter_rule("ip", self.chain_name, handle)
# Delete old mailcow isolation rules if not self.nft_exec_dict(self.get_isolation_dict(_interface, _dports, self.ISOLATION_COMMENTS[0], {"drop": None})):
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_drop)
for handle in handles:
self.delete_filter_rule(family, self.chain_name, handle)
handles = self.get_rules_handle(family, table, self.chain_name, comment_filter_allow)
for handle in handles:
self.delete_filter_rule(family, self.chain_name, handle)
# insert mailcow isolation rule
_match_dict_drop = [
{
"match": {
"op": "!=",
"left": {
"meta": {
"key": "iifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"meta": {
"key": "oifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "tcp",
"field": "dport"
}
},
"right": {
"set": _dports
}
}
},
{
"counter": {
"packets": 0,
"bytes": 0
}
},
{
"drop": None
}
]
rule_drop = { "insert": { "rule": {
"family": family,
"table": table,
"chain": self.chain_name,
"comment": comment_filter_drop,
"expr": _match_dict_drop
}}}
json_command["nftables"].append(rule_drop)
# insert mailcow isolation allow rule
if _allow != "":
_match_dict_allow = [
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "ip",
"field": "saddr"
}
},
"right": _allow
}
},
{
"match": {
"op": "!=",
"left": {
"meta": {
"key": "iifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"meta": {
"key": "oifname"
}
},
"right": _interface
}
},
{
"match": {
"op": "==",
"left": {
"payload": {
"protocol": "tcp",
"field": "dport"
}
},
"right": {
"set": _dports
}
}
},
{
"counter": {
"packets": 0,
"bytes": 0
}
},
{
"accept": None
}
]
rule_allow = { "insert": { "rule": {
"family": family,
"table": table,
"chain": self.chain_name,
"comment": comment_filter_allow,
"expr": _match_dict_allow
}}}
json_command["nftables"].append(rule_allow)
success = self.nft_exec_dict(json_command)
if success == False:
self.logger.logCrit(f"Error adding {self.chain_name} isolation") self.logger.logCrit(f"Error adding {self.chain_name} isolation")
return False return False
# separate, an invalid address must not prevent the drop rules. Inserted last, so they are above them
if _allow and not self.nft_exec_dict(self.get_isolation_dict(_interface, _dports, self.ISOLATION_COMMENTS[1], {"accept": None}, _allow)):
self.logger.logCrit(f"Error adding {self.chain_name} isolation exception for {_allow}")
return False
return True return True
def get_isolation_dict(self, _interface:str, _dports:list, _comment:str, _verdict:dict, _saddr:str = ""):
json_command = self.get_base_dict()
for port in _dports:
expr = []
if _saddr:
expr.append({"match": {"op": "==", "left": {"payload": {"protocol": "ip", "field": "saddr"}}, "right": _saddr}})
expr += [
{"match": {"op": "!=", "left": {"meta": {"key": "iifname"}}, "right": _interface}},
{"match": {"op": "==", "left": {"meta": {"key": "oifname"}}, "right": _interface}},
{"match": {"op": "==", "left": {"payload": {"protocol": "tcp", "field": "dport"}}, "right": int(port)}},
{"counter": {"packets": 0, "bytes": 0}},
_verdict
]
json_command["nftables"].append({"insert": {"rule": {
"family": "ip",
"table": "filter",
"chain": self.chain_name,
"comment": _comment,
"expr": expr
}}})
return json_command
@@ -0,0 +1,200 @@
import ipaddress
import threading
import dns.exception
import dns.resolver
from modules.BanManager import BanManager
from modules.Options import Options
from modules.Regexes import Regexes
class Netfilter:
"""Runs the workers of netfilter. Every worker is a loop that logs errors and goes on, so neither a broken log
line nor a Redis restart can stop the expiry of bans."""
ISOLATION_INTERFACE = 'br-mailcow'
ISOLATION_PORTS = [3306, 6379, 8983, 12345]
def __init__(self, r, tables, logger, env):
self.r = r
self.tables = tables
self.logger = logger
self.env = env
# reentrant, bans.reapply() runs within the chain check
self.lock = threading.RLock()
self.stop = threading.Event()
self.options = Options(logger)
self.regexes = Regexes(logger)
self.bans = BanManager(r, tables, self.options, logger, self.lock)
self.snat4_target = self.snat_target('SNAT_TO_SOURCE', 4)
self.snat6_target = self.snat_target('SNAT6_TO_SOURCE', 6)
self.resolved = {}
self.workers = {
'watch': (self.watch, 3),
'purge': (self.purge, 10),
'chains': (self.check_chains, 10),
'lists': (self.update_lists, 60),
}
if self.snat4_target or self.snat6_target:
self.workers['snat'] = (self.snat, 10)
self.threads = {}
def setup(self):
if self.r.exists('F2B_LOG'):
self.r.rename('F2B_LOG', 'NETFILTER_LOG')
# filled again by the lists worker
self.r.delete('F2B_PERM_BANS')
self.options.refresh(self.r)
self.regexes.refresh(self.r)
with self.lock:
self.tables.clearIPv4Table()
self.tables.clearIPv6Table()
self.logger.logInfo('Initializing mailcow netfilter chain')
self.tables.initChainIPv4()
self.tables.initChainIPv6()
self.isolate()
self.bans.restore()
def run(self):
for name in self.workers:
self.start(name)
while not self.stop.wait(5):
for name, thread in list(self.threads.items()):
if not thread.is_alive():
self.logger.logCrit('Worker %s stopped unexpectedly, restarting it' % name)
self.start(name)
def shutdown(self):
"""Removes the firewall rules, the active bans stay in Redis and are restored on the next start"""
self.stop.set()
for thread in self.threads.values():
thread.join(timeout=5)
with self.lock:
self.logger.logInfo('Clearing all bans')
self.tables.clearIPv4Table()
self.tables.clearIPv6Table()
try:
self.r.delete('F2B_PERM_BANS')
except Exception as ex:
self.logger.logWarn('Error clearing redis key F2B_PERM_BANS: %s' % ex)
def start(self, name):
task, interval = self.workers[name]
thread = threading.Thread(target=self.loop, args=(name, task, interval), name=name, daemon=True)
self.threads[name] = thread
thread.start()
def loop(self, name, task, interval):
while not self.stop.is_set():
try:
task()
except Exception as ex:
self.logger.logWarn('Error in worker %s: %s' % (name, ex))
self.stop.wait(interval)
def watch(self):
pubsub = self.r.pubsub(ignore_subscribe_messages=True)
try:
pubsub.subscribe('F2B_CHANNEL')
self.logger.logInfo('Watching Redis channel F2B_CHANNEL')
while not self.stop.is_set():
message = pubsub.get_message(timeout=1.0)
if message and message['type'] == 'message' and message['data']:
self.handle_line(message['data'])
finally:
try:
pubsub.close()
except Exception:
pass
def handle_line(self, line):
# a broken line must not end the watcher
try:
excused = Regexes.temporary_failure(line)
if excused is not None:
ip = Regexes.parse_ip(excused)
if self.external(ip):
self.bans.excuse(ip)
return
result = self.regexes.match(line)
if result is None:
return
rule_id, address = result
ip = Regexes.parse_ip(address)
if ip is None:
self.logger.logWarn('Rule id %s matched an invalid address %s (%s)' % (rule_id, address, line))
return
if not self.external(ip):
return
self.logger.logWarn('%s matched rule id %s (%s)' % (ip, rule_id, line))
self.bans.attempt(ip)
except Exception as ex:
self.logger.logWarn('Error processing log line %r: %s' % (line, ex))
@staticmethod
def external(ip):
# private and loopback addresses are never banned
return ip is not None and not (ip.is_private or ip.is_loopback)
def purge(self):
self.options.refresh(self.r)
self.regexes.refresh(self.r)
self.bans.purge()
def check_chains(self):
with self.lock:
if self.tables.ensure_chains():
self.isolate()
self.bans.reapply()
def snat(self):
with self.lock:
if self.snat4_target:
self.tables.snat4(self.snat4_target, self.env.get('IPV4_NETWORK', '172.22.1') + '.0/24')
if self.snat6_target:
self.tables.snat6(self.snat6_target, self.env.get('IPV6_NETWORK', 'fd4d:6169:6c63:6f77::/64'))
def update_lists(self):
self.bans.set_allowlist(self.resolve(self.r.hkeys('F2B_WHITELIST')))
self.bans.set_denylist(self.resolve(self.r.hkeys('F2B_BLACKLIST')))
def isolate(self):
if self.env.get('DISABLE_NETFILTER_ISOLATION_RULE', '').lower() in ('y', 'yes'):
self.logger.logInfo('Skipping MAILCOW isolation')
return
self.logger.logInfo('Setting MAILCOW isolation')
self.tables.create_mailcow_isolation_rule(self.ISOLATION_INTERFACE, self.ISOLATION_PORTS, self.env.get('MAILCOW_REPLICA_IP', ''))
def resolve(self, entries):
"""Networks of the entries; hostnames are resolved, on DNS errors to their last known addresses"""
resolver = dns.resolver.Resolver()
networks = set()
for entry in entries:
if BanManager.network(entry) is not None:
networks.add(entry)
continue
addresses = set()
for rdtype in ['A', 'AAAA']:
try:
answer = resolver.resolve(qname=entry, rdtype=rdtype, lifetime=3)
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer):
continue
except dns.exception.DNSException as ex:
self.logger.logInfo('Could not resolve %s: %s' % (entry, ex))
addresses = self.resolved.get(entry, set())
break
addresses.update(rdata.to_text() for rdata in answer)
self.resolved[entry] = addresses
networks |= addresses
return networks
def snat_target(self, name, version):
value = self.env.get(name, '')
if not value or value == 'n':
return None
try:
if ipaddress.ip_address(value).version == version:
return value
except ValueError:
pass
self.logger.logWarn('%s is not a valid IPv%d address' % (value, version))
return None
@@ -0,0 +1,120 @@
import json
import uuid
class Options:
"""Fail2ban options of the mailcow UI (Redis F2B_OPTIONS). The values are replaced as a whole and never changed
in place, so a thread that took them (options.values) keeps a consistent set."""
DEFAULTS = {
'ban_time': 1800,
'max_ban_time': 10000,
'ban_time_increment': True,
'max_attempts': 10,
'retry_window': 600,
'netban_ipv4': 32,
'netban_ipv6': 128,
'manage_external': 0,
}
# Single keys of old installations, read while F2B_OPTIONS does not exist
LEGACY_KEYS = {
'ban_time': 'F2B_BAN_TIME',
'max_ban_time': 'F2B_MAX_BAN_TIME',
'ban_time_increment': 'F2B_BAN_TIME_INCREMENT',
'max_attempts': 'F2B_MAX_ATTEMPTS',
'retry_window': 'F2B_RETRY_WINDOW',
'netban_ipv4': 'F2B_NETBAN_IPV4',
'netban_ipv6': 'F2B_NETBAN_IPV6',
}
def __init__(self, logger):
self.logger = logger
self.values = self.normalize({})
def refresh(self, r):
"""Reads the options of the UI; options it lacks are added to Redis once, existing ones are never overwritten"""
raw = r.get('F2B_OPTIONS')
if raw:
try:
stored = json.loads(raw)
except ValueError as ex:
self.logger.logWarn('F2B_OPTIONS is no valid JSON, keeping the current options: %s' % ex)
return
if not isinstance(stored, dict):
self.logger.logWarn('F2B_OPTIONS is no JSON object, keeping the current options')
return
else:
stored = {key: r.get(name) for key, name in self.LEGACY_KEYS.items()}
stored = {key: value for key, value in stored.items() if value is not None}
values = self.normalize(stored)
self.values = values
# First start or old installation: store the complete set, else only what the UI has not saved yet
missing = {key: value for key, value in values.items() if not raw or stored.get(key) is None}
if missing:
self.add_missing(r, missing)
def ban_time(self, ban_counter):
"""Seconds of the next ban of a network that was banned ban_counter times before"""
values = self.values
ban_time = values['ban_time']
if values['ban_time_increment']:
# the exponent is capped, max_ban_time limits the result anyway
ban_time = ban_time * 2 ** min(ban_counter, 32)
return max(values['ban_time'], min(ban_time, values['max_ban_time']))
@classmethod
def normalize(cls, stored):
values = dict(stored)
for key, default in cls.DEFAULTS.items():
if isinstance(default, bool):
values[key] = cls.to_bool(stored.get(key), default)
else:
values[key] = cls.to_int(stored.get(key), default)
values['ban_time'] = max(1, values['ban_time'])
values['max_ban_time'] = max(1, values['max_ban_time'])
values['max_attempts'] = max(1, values['max_attempts'])
values['retry_window'] = max(1, values['retry_window'])
# A short prefix would ban whole regions or everything (/0)
values['netban_ipv4'] = min(32, max(8, values['netban_ipv4']))
values['netban_ipv6'] = min(128, max(8, values['netban_ipv6']))
values['manage_external'] = 1 if values['manage_external'] == 1 else 0
if not values.get('banlist_id'):
values['banlist_id'] = str(uuid.uuid4())
return values
@staticmethod
def to_int(value, default):
try:
return int(value)
except (TypeError, ValueError):
return default
@staticmethod
def to_bool(value, default):
if isinstance(value, bool):
return value
if isinstance(value, (int, float)):
return value != 0
if isinstance(value, str):
return value.strip().lower() in ('1', 'true', 'yes', 'on')
return default
def add_missing(self, r, missing):
# WATCH keeps a save of the UI between reading and writing
def fill(pipe):
raw = pipe.get('F2B_OPTIONS')
current = json.loads(raw) if raw else {}
if not isinstance(current, dict):
return
added = {key: value for key, value in missing.items() if current.get(key) is None}
if not added:
return
current.update(added)
pipe.multi()
pipe.set('F2B_OPTIONS', json.dumps(current, ensure_ascii=False))
try:
r.transaction(fill, 'F2B_OPTIONS')
except Exception as ex:
self.logger.logWarn('Could not add default options to F2B_OPTIONS: %s' % ex)
@@ -0,0 +1,88 @@
import ipaddress
import json
import re
class Regexes:
"""Rules for log lines (Redis F2B_REGEX): rule id => regex whose first group captures the client address"""
DEFAULTS = {
1: r'mailcow UI: Invalid password for .+ by ([0-9a-f\.:]+)',
2: r'Rspamd UI: Invalid password by ([0-9a-f\.:]+)',
3: r'warning: .*\[([0-9a-f\.:]+)\]: SASL .+ authentication failed: (?!.*Connection lost to authentication server).+',
4: r'warning: non-SMTP command from .*\[([0-9a-f\.:]+)]:.+',
5: r'NOQUEUE: reject: RCPT from \[([0-9a-f\.:]+)].+Protocol error.+',
6: r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): Password mismatch \(SHA1 of given password: [a-f0-9]+\)',
7: r'\w+\([^,]+,([0-9a-f\.:]+),<[^>]+>\): unknown user \(SHA1 of given password: [a-f0-9]+\)',
8: r'SOGo.+ Login from \'([0-9a-f\.:]+)\' for user .+ might not have worked',
9: r'([0-9a-f\.:]+) \"GET \/SOGo\/.* HTTP.+\" 403 .+',
}
# Dovecot could not ask the auth backend (identity provider down, mailcowauth.php unreachable). Postfix logs
# such a temporary failure like a wrong password, so failed logins of the address are not counted for a while.
TEMPORARY_FAILURE = re.compile(r'lua\([^,]+,([0-9a-f\.:]+)(?:,<[^>]+>)?\): passdb-lua: Upstream (?:unavailable|unreachable)')
def __init__(self, logger):
self.logger = logger
self.rules = []
self.source = None
def refresh(self, r):
"""Compiles the rules of the UI again when they changed; invalid rules are skipped"""
raw = r.get('F2B_REGEX')
if not raw:
raw = json.dumps(self.DEFAULTS, ensure_ascii=False)
r.set('F2B_REGEX', raw, nx=True)
if raw == self.source:
return
try:
stored = json.loads(raw)
except ValueError as ex:
self.logger.logWarn('F2B_REGEX is no valid JSON, keeping the current rules: %s' % ex)
return
if not isinstance(stored, dict):
self.logger.logWarn('F2B_REGEX is no JSON object, keeping the current rules')
return
rules = []
for rule_id, regex in sorted(stored.items(), key=lambda item: self.sort_key(item[0])):
try:
rules.append((str(rule_id), re.compile(regex)))
except (re.error, TypeError) as ex:
self.logger.logWarn('Skipping regex of rule id %s: %s' % (rule_id, ex))
self.rules = rules
self.source = raw
def match(self, line):
"""(rule id, captured address) of the first rule that matches the line, None if no rule matches"""
for rule_id, regex in self.rules:
result = regex.search(line)
if result and result.lastindex and result.group(1):
return rule_id, result.group(1)
return None
@classmethod
def temporary_failure(cls, line):
"""Address of a login that failed because the auth backend was unavailable, None for other lines"""
result = cls.TEMPORARY_FAILURE.search(line)
return result.group(1) if result else None
@staticmethod
def parse_ip(text):
"""IP address of a captured address, IPv4-mapped IPv6 as IPv4; a trailing colon (e.g. "1.2.3.4:") is
tolerated. None if the text is no address."""
for candidate in (text, text.rstrip(':')):
try:
ip = ipaddress.ip_address(candidate)
except ValueError:
continue
if ip.version == 6 and ip.ipv4_mapped:
return ip.ipv4_mapped
return ip
return None
@staticmethod
def sort_key(rule_id):
try:
return (0, int(rule_id))
except (TypeError, ValueError):
return (1, str(rule_id))
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.21 FROM alpine:3.24
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
+4 -3
View File
@@ -67,7 +67,7 @@ logger.debug('olefy listen address string: {} (type {})'.format(olefy_listen_add
if not olefy_listen_addr_string: if not olefy_listen_addr_string:
olefy_listen_addr = "" olefy_listen_addr = ""
else: else:
addr_re = re.compile('[\[" \]]') addr_re = re.compile(r'[\[" \]]')
olefy_listen_addr = addr_re.sub('', olefy_listen_addr_string.replace("'", "")).split(',') olefy_listen_addr = addr_re.sub('', olefy_listen_addr_string.replace("'", "")).split(',')
# log runtime variables # log runtime variables
@@ -206,8 +206,9 @@ class AIO(asyncio.Protocol):
self.transport.close() self.transport.close()
# start the listeners # start the listeners, Python >= 3.14 no longer creates a loop in get_event_loop()
loop = asyncio.get_event_loop() loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
# each client connection will create a new protocol instance # each client connection will create a new protocol instance
coro = loop.create_server(AIO, olefy_listen_addr, olefy_listen_port) coro = loop.create_server(AIO, olefy_listen_addr, olefy_listen_port)
server = loop.run_until_complete(coro) server = loop.run_until_complete(coro)
+1 -1
View File
@@ -2,7 +2,7 @@ FROM debian:trixie-slim
LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer="The Infrastructure Company GmbH <info@servercow.de>"
ARG DEBIAN_FRONTEND=noninteractive ARG DEBIAN_FRONTEND=noninteractive
ARG RSPAMD_VER=rspamd_4.2.1-1~b7a16ae ARG RSPAMD_VER=rspamd_4.2.3-1~50bde75
ARG CODENAME=trixie ARG CODENAME=trixie
ENV LC_ALL=C ENV LC_ALL=C
+1 -1
View File
@@ -17,7 +17,7 @@ ARG SOPE_VERSION=SOPE-5.12.12
# Security patches to apply (space-separated commit hashes) # Security patches to apply (space-separated commit hashes)
ARG SOGO_SECURITY_PATCHES="" ARG SOGO_SECURITY_PATCHES=""
# renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$ # renovate: datasource=github-releases depName=tianon/gosu versioning=semver-coerced extractVersion=^(?<version>.*)$
ARG GOSU_VERSION=1.19 ARG GOSU_VERSION=1.20
ENV LC_ALL=C ENV LC_ALL=C
# Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size) # Install dependencies, build SOPE and SOGo, then clean up (all in one layer to minimize image size)
+2 -2
View File
@@ -1,10 +1,10 @@
FROM alpine:3.23 FROM alpine:3.24
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
# install unbound from alpine:edge to get security patches # install unbound from alpine:edge to get security patches
# minimum version is pinned # minimum version is pinned
RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main "unbound>=1.26.1-r0" RUN apk add --no-cache --repository=https://dl-cdn.alpinelinux.org/alpine/edge/main "unbound>=1.26.2-r0"
# install other packages from regular alpine stable repo # install other packages from regular alpine stable repo
RUN apk add --update --no-cache \ RUN apk add --update --no-cache \
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.23 FROM alpine:3.24
LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>" LABEL maintainer = "The Infrastructure Company GmbH <info@servercow.de>"
+9
View File
@@ -101,7 +101,16 @@ if ($result === false){
// Init Identity Provider // Init Identity Provider
$iam_provider = identity_provider('init'); $iam_provider = identity_provider('init');
$iam_settings = identity_provider('get'); $iam_settings = identity_provider('get');
try {
$result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service'])); $result = user_login($post['username'], $post['password'], array('is_internal' => true, 'service' => $post['service']));
} catch (\Mailcow\Auth\IdpUnavailable $e) {
// temporary failure for Dovecot, no failed login
error_log("MAILCOWAUTH: Identity provider unavailable for user " . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip'] . ": " . $e->getMessage());
http_response_code(503); // Service Unavailable
echo json_encode($return);
session_destroy();
exit;
}
if ($result) { if ($result) {
error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']); error_log('MAILCOWAUTH: User auth for user ' . $post['username'] . " with service " . $post['service'] . " from IP " . $post['real_rip']);
set_sasl_log($post['username'], $post['real_rip'], $post['service']); set_sasl_log($post['username'], $post['real_rip'], $post['service']);
+6
View File
@@ -41,6 +41,12 @@ function auth_password_verify(request, password)
return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unreachable" return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unreachable"
end end
-- 5xx: the backend or the identity provider cannot answer, a temporary failure and no failed login
if c >= 500 then
dovecot.i_info("HTTP request to auth backend failed with " .. tostring(c) .. " for user " .. request.user)
return dovecot.auth.PASSDB_RESULT_INTERNAL_FAILURE, "Upstream unavailable"
end
if c ~= 200 and c ~= 401 then if c ~= 200 and c ~= 401 then
dovecot.i_info("HTTP request failed with " .. tostring(c) .. " for user " .. request.user) dovecot.i_info("HTTP request failed with " .. tostring(c) .. " for user " .. request.user)
return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error" return dovecot.auth.PASSDB_RESULT_PASSWORD_MISMATCH, "Upstream error"
+3 -1
View File
@@ -67,9 +67,11 @@ passdb {
} }
# check for regular password - if empty (e.g. force-passwd-reset), previous pass=yes passdbs also fail # check for regular password - if empty (e.g. force-passwd-reset), previous pass=yes passdbs also fail
# a return of the following passdb is mandatory # a return of the following passdb is mandatory
# cache_key with password: the lua default (%u) would let a temporary failure fall back to the negative entry
# of a passdb lookup (master logins) and log "unknown user", which netfilter counts as failed login
passdb { passdb {
driver = lua driver = lua
args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes args = file=/etc/dovecot/auth/passwd-verify.lua blocking=yes cache_key=%s:%u:%w
} }
# Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing) # Set doveadm_password=your-secret-password in data/conf/dovecot/extra.conf (create if missing)
service doveadm { service doveadm {
+24 -29
View File
@@ -36,7 +36,18 @@ function check_login($user, $pass, $extra = null) {
// Try validate user // Try validate user
if (!isset($role) || $role == "user") { if (!isset($role) || $role == "user") {
try {
$result = user_login($user, $pass, $extra); $result = user_login($user, $pass, $extra);
} catch (\Mailcow\Auth\IdpUnavailable $e) {
// no failed login, netfilter must not count it
error_log("mailcow UI: Identity provider unavailable for " . $user . " by " . $_SERVER['REMOTE_ADDR'] . ": " . $e->getMessage());
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
'msg' => 'iam_unavailable'
);
return false;
}
if ($result !== false) { if ($result !== false) {
$real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']); $real_rip = ($_SERVER['HTTP_X_REAL_IP'] ?? $_SERVER['REMOTE_ADDR']);
set_sasl_log($user, $real_rip, $extra['service']); set_sasl_log($user, $real_rip, $extra['service']);
@@ -516,11 +527,14 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
return false; return false;
} }
if (!$iam_provider) { if (!$iam_provider) {
return false; throw new \Mailcow\Auth\IdpUnavailable('Keycloak is not configured completely');
} }
// get access_token for service account of mailcow client // get access_token for service account of mailcow client
$admin_token = identity_provider("get-keycloak-admin-token"); $admin_token = identity_provider("get-keycloak-admin-token");
if (!$admin_token) {
throw new \Mailcow\Auth\IdpUnavailable('Could not get an admin token from Keycloak');
}
// get the mailcow_password attribute from keycloak user // get the mailcow_password attribute from keycloak user
$url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users"; $url = "{$iam_settings['server_url']}/admin/realms/{$iam_settings['realm']}/users";
@@ -541,13 +555,9 @@ function keycloak_mbox_login_rest($user, $pass, $extra = null){
$users = json_decode(curl_exec($curl), true); $users = json_decode(curl_exec($curl), true);
$code = curl_getinfo($curl, CURLINFO_HTTP_CODE); $code = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl); curl_close($curl);
// the password is checked below, a failed request is never a wrong password
if ($code != 200) { if ($code != 200) {
$_SESSION['return'][] = array( throw new \Mailcow\Auth\IdpUnavailable('Keycloak returned HTTP ' . $code);
'type' => 'danger',
'log' => array(__FUNCTION__, $user, '*', 'Identity Provider returned HTTP ' . $code),
'msg' => 'generic_server_error'
);
return false;
} }
// exact=true can still return several users with that email // exact=true can still return several users with that email
if (!is_array($users) || count($users) != 1) { if (!is_array($users) || count($users) != 1) {
@@ -687,7 +697,8 @@ function ldap_mbox_login($user, $pass, $extra = null){
return false; return false;
} }
if (!$iam_provider) { if (!$iam_provider) {
return false; // identity_provider('init') could not connect
throw new \Mailcow\Auth\IdpUnavailable('No connection to the LDAP server');
} }
try { try {
@@ -700,33 +711,17 @@ function ldap_mbox_login($user, $pass, $extra = null){
\Mailcow\Identity\Directory\LdapDirectory::idAttributes($iam_settings['id_attribute'] ?? ''))))); \Mailcow\Identity\Directory\LdapDirectory::idAttributes($iam_settings['id_attribute'] ?? '')))));
$user_res = $ldap_query->firstOrFail(); $user_res = $ldap_query->firstOrFail();
} catch (Exception $e) { } catch (\LdapRecord\Query\ObjectNotFoundException $e) {
// clear $_SESSION['return'] to not leak data
$_SESSION['return'] = array();
$_SESSION['return'][] = array(
'type' => 'danger',
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
'msg' => 'generic_server_error'
);
return false; return false;
}
try {
if (!$iam_provider->auth()->attempt($user_res['dn'], $pass)) {
return false;
}
} catch (Exception $e) { } catch (Exception $e) {
// clear $_SESSION['return'] to not leak data throw new \Mailcow\Auth\IdpUnavailable('LDAP search failed: ' . $e->getMessage(), 0, $e);
$_SESSION['return'] = array(); }
$_SESSION['return'][] = array( $idp_login = \Mailcow\Auth\AuthModule::fromGlobals()->idp;
'type' => 'danger', if (!$idp_login->verifyLdapPassword($iam_provider, (string)$user_res['dn'], (string)$pass)) {
'log' => array(__FUNCTION__, $user, '*', $e->getMessage()),
'msg' => 'generic_server_error'
);
return false; return false;
} }
// get mapped template // get mapped template
$idp_login = \Mailcow\Auth\AuthModule::fromGlobals()->idp;
$mapped_template = $idp_login->mappedTemplate($user_res[$iam_settings['attribute_field']] ?? array()); $mapped_template = $idp_login->mappedTemplate($user_res[$iam_settings['attribute_field']] ?? array());
$external_id = \Mailcow\Identity\Directory\LdapDirectory::entryId($user_res, $iam_settings['id_attribute'] ?? ''); $external_id = \Mailcow\Identity\Directory\LdapDirectory::entryId($user_res, $iam_settings['id_attribute'] ?? '');
+37 -38
View File
@@ -60,13 +60,14 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$queue_unban = $redis->hGetAll('F2B_QUEUE_UNBAN'); $queue_unban = $redis->hGetAll('F2B_QUEUE_UNBAN');
if (is_array($active_bans)) { if (is_array($active_bans)) {
foreach ($active_bans as $network => $banned_until) { foreach ($active_bans as $network => $banned_until) {
$queued_for_unban = (isset($queue_unban[$network]) && $queue_unban[$network] == 1) ? 1 : 0; // Expired bans are lifted by netfilter within seconds
$difference = $banned_until - time(); $difference = max(0, intval($banned_until) - time());
$queued_for_unban = ($difference == 0 || (isset($queue_unban[$network]) && $queue_unban[$network] == 1)) ? 1 : 0;
$f2b_options['active_bans'][] = array( $f2b_options['active_bans'][] = array(
'queued_for_unban' => $queued_for_unban, 'queued_for_unban' => $queued_for_unban,
'network' => $network, 'network' => $network,
'ip' => strtok($network,'/'), 'ip' => strtok($network,'/'),
'banned_until' => sprintf('%02dh %02dm %02ds', ($difference/3600), ($difference/60%60), $difference%60) 'banned_until' => sprintf('%02dh %02dm %02ds', intdiv($difference, 3600), intdiv($difference, 60) % 60, $difference % 60)
); );
} }
} }
@@ -108,16 +109,15 @@ function fail2ban($_action, $_data = null, $_extra = null) {
); );
return false; return false;
} }
// Rules will also be recreated on log events, but rules may seem empty for a second in the UI // netfilter recreates the default rules within 10 seconds
docker('post', 'netfilter-mailcow', 'restart');
$fail_count = 0; $fail_count = 0;
$regex_result = json_decode($redis->Get('F2B_REGEX'), true); $regex_result = json_decode($redis->Get('F2B_REGEX'), true);
while (empty($regex_result) && $fail_count < 10) { while (empty($regex_result) && $fail_count < 15) {
$regex_result = json_decode($redis->Get('F2B_REGEX'), true); $regex_result = json_decode($redis->Get('F2B_REGEX'), true);
$fail_count++; $fail_count++;
sleep(1); sleep(1);
} }
if ($fail_count >= 10) { if (empty($regex_result)) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'danger', 'type' => 'danger',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
@@ -141,7 +141,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
'type' => 'success', 'type' => 'success',
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('object_modified', htmlspecialchars($network)) 'msg' => 'f2b_modified'
); );
return true; return true;
} }
@@ -172,7 +172,7 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if (valid_network($network)) { if (valid_network($network)) {
try { try {
$redis->hSet('F2B_WHITELIST', $network, 1); $redis->hSet('F2B_WHITELIST', $network, 1);
$redis->hDel('F2B_BLACKLIST', $network, 1); $redis->hDel('F2B_BLACKLIST', $network);
$redis->hSet('F2B_QUEUE_UNBAN', $network, 1); $redis->hSet('F2B_QUEUE_UNBAN', $network, 1);
} }
catch (RedisException $e) { catch (RedisException $e) {
@@ -199,14 +199,13 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if (valid_network($network) && !in_array($network, array( if (valid_network($network) && !in_array($network, array(
'0.0.0.0', '0.0.0.0',
'0.0.0.0/0', '0.0.0.0/0',
getenv('IPV4_NETWORK') . '0/24', getenv('IPV4_NETWORK') . '.0/24',
getenv('IPV4_NETWORK') . '0', getenv('IPV4_NETWORK') . '.0',
getenv('IPV6_NETWORK') getenv('IPV6_NETWORK')
))) { ))) {
try { try {
$redis->hSet('F2B_BLACKLIST', $network, 1); $redis->hSet('F2B_BLACKLIST', $network, 1);
$redis->hDel('F2B_WHITELIST', $network, 1); $redis->hDel('F2B_WHITELIST', $network);
//$response = docker('post', 'netfilter-mailcow', 'restart');
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -261,25 +260,17 @@ function fail2ban($_action, $_data = null, $_extra = null) {
$f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time; $f2b_options['ban_time'] = ($ban_time < 60) ? 60 : $ban_time;
$f2b_options['ban_time_increment'] = ($ban_time_increment == 1) ? true : false; $f2b_options['ban_time_increment'] = ($ban_time_increment == 1) ? true : false;
$f2b_options['max_ban_time'] = ($max_ban_time < 60) ? 60 : $max_ban_time; $f2b_options['max_ban_time'] = ($max_ban_time < 60) ? 60 : $max_ban_time;
$f2b_options['netban_ipv4'] = ($netban_ipv4 < 8) ? 8 : $netban_ipv4; $f2b_options['netban_ipv4'] = min(32, max(8, $netban_ipv4));
$f2b_options['netban_ipv6'] = ($netban_ipv6 < 8) ? 8 : $netban_ipv6; $f2b_options['netban_ipv6'] = min(128, max(8, $netban_ipv6));
$f2b_options['netban_ipv4'] = ($netban_ipv4 > 32) ? 32 : $netban_ipv4;
$f2b_options['netban_ipv6'] = ($netban_ipv6 > 128) ? 128 : $netban_ipv6;
$f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts; $f2b_options['max_attempts'] = ($max_attempts < 1) ? 1 : $max_attempts;
$f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window; $f2b_options['retry_window'] = ($retry_window < 1) ? 1 : $retry_window;
$f2b_options['banlist_id'] = $is_now['banlist_id']; $f2b_options['banlist_id'] = (!empty($is_now['banlist_id'])) ? $is_now['banlist_id'] : uuid4();
$f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0; $f2b_options['manage_external'] = ($manage_external > 0) ? 1 : 0;
try { $wl_items = array();
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
$redis->Del('F2B_WHITELIST');
$redis->Del('F2B_BLACKLIST');
if (!empty($wl)) { if (!empty($wl)) {
$wl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $wl)); foreach (array_filter(array_map('trim', preg_split("/( |,|;|\n)/", $wl))) as $wl_item) {
$wl_array = array_filter($wl_array);
if (is_array($wl_array)) {
foreach ($wl_array as $wl_item) {
if (valid_network($wl_item) || valid_hostname($wl_item)) { if (valid_network($wl_item) || valid_hostname($wl_item)) {
$redis->hSet('F2B_WHITELIST', $wl_item, 1); $wl_items[$wl_item] = 1;
} }
else { else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -287,24 +278,20 @@ function fail2ban($_action, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('network_host_invalid', $wl_item) 'msg' => array('network_host_invalid', $wl_item)
); );
continue;
}
} }
} }
} }
$bl_items = array();
if (!empty($bl)) { if (!empty($bl)) {
$bl_array = array_map('trim', preg_split( "/( |,|;|\n)/", $bl)); foreach (array_filter(array_map('trim', preg_split("/( |,|;|\n)/", $bl))) as $bl_item) {
$bl_array = array_filter($bl_array);
if (is_array($bl_array)) {
foreach ($bl_array as $bl_item) {
if (valid_network($bl_item) && !in_array($bl_item, array( if (valid_network($bl_item) && !in_array($bl_item, array(
'0.0.0.0', '0.0.0.0',
'0.0.0.0/0', '0.0.0.0/0',
getenv('IPV4_NETWORK') . '0/24', getenv('IPV4_NETWORK') . '.0/24',
getenv('IPV4_NETWORK') . '0', getenv('IPV4_NETWORK') . '.0',
getenv('IPV6_NETWORK') getenv('IPV6_NETWORK')
))) { ))) {
$redis->hSet('F2B_BLACKLIST', $bl_item, 1); $bl_items[$bl_item] = 1;
} }
else { else {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -312,11 +299,21 @@ function fail2ban($_action, $_data = null, $_extra = null) {
'log' => array(__FUNCTION__, $_action, $_data_log), 'log' => array(__FUNCTION__, $_action, $_data_log),
'msg' => array('network_host_invalid', $bl_item) 'msg' => array('network_host_invalid', $bl_item)
); );
continue;
} }
} }
} }
try {
// One transaction, netfilter must never see empty lists in between
$redis->multi();
$redis->Set('F2B_OPTIONS', json_encode($f2b_options));
$redis->Del('F2B_WHITELIST', 'F2B_BLACKLIST');
if (!empty($wl_items)) {
$redis->hMSet('F2B_WHITELIST', $wl_items);
} }
if (!empty($bl_items)) {
$redis->hMSet('F2B_BLACKLIST', $bl_items);
}
$redis->exec();
} }
catch (RedisException $e) { catch (RedisException $e) {
$_SESSION['return'][] = array( $_SESSION['return'][] = array(
@@ -348,7 +345,9 @@ function fail2ban($_action, $_data = null, $_extra = null) {
if (is_array($_extra)) { if (is_array($_extra)) {
$_extra = $_extra[0]; $_extra = $_extra[0];
} }
if ($_extra != $f2b_options['banlist_id']){ // Timing safe, a missing or empty id never matches
$banlist_id = (string)($f2b_options['banlist_id'] ?? '');
if ($banlist_id === '' || !is_string($_extra) || !hash_equals($banlist_id, $_extra)) {
http_response_code(404); http_response_code(404);
return false; return false;
} }
+5
View File
@@ -1292,8 +1292,13 @@ function set_tfa($_data) {
$row = $stmt->fetch(PDO::FETCH_ASSOC); $row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) { if ($row) {
if ($row['authsource'] == 'ldap'){ if ($row['authsource'] == 'ldap'){
try {
if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true; if (!ldap_mbox_login($username, $_data["confirm_password"], $iam_settings)) $access_denied = true;
else $access_denied = false; else $access_denied = false;
} catch (\Mailcow\Auth\IdpUnavailable $e) {
$_SESSION['return'][] = array('type' => 'danger', 'log' => array(__FUNCTION__, $_data_log, $e->getMessage()), 'msg' => 'iam_unavailable');
return false;
}
} else { } else {
if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true; if (!verify_hash($row['password'], $_data["confirm_password"])) $access_denied = true;
else $access_denied = false; else $access_denied = false;
+3 -1
View File
@@ -1604,7 +1604,9 @@ function init_db_schema()
"redirect_uri" => "VARCHAR(2000)", "redirect_uri" => "VARCHAR(2000)",
"expires" => "TIMESTAMP NOT NULL", "expires" => "TIMESTAMP NOT NULL",
"scope" => "VARCHAR(4000)", "scope" => "VARCHAR(4000)",
"id_token" => "VARCHAR(1000)" "id_token" => "VARCHAR(1000)",
"code_challenge" => "VARCHAR(1000)",
"code_challenge_method" => "VARCHAR(20)"
), ),
"keys" => array( "keys" => array(
"primary" => array( "primary" => array(
+1 -1
View File
@@ -10,7 +10,7 @@
"mustangostang/spyc": "^0.6.3", "mustangostang/spyc": "^0.6.3",
"directorytree/ldaprecord": "^3.3", "directorytree/ldaprecord": "^3.3",
"twig/twig": "^3.0", "twig/twig": "^3.0",
"stevenmaguire/oauth2-keycloak": "^4.0", "stevenmaguire/oauth2-keycloak": "^6.1",
"league/oauth2-client": "^2.7", "league/oauth2-client": "^2.7",
"bacon/bacon-qr-code": "^2.0" "bacon/bacon-qr-code": "^2.0"
}, },
+825 -472
View File
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Mailcow\Auth;
use RuntimeException;
/**
* The identity provider could not answer a login (unreachable, timeout, server error). No failed login:
* callers report a temporary failure and must not count it as an attempt for netfilter.
*/
class IdpUnavailable extends RuntimeException
{
}
@@ -4,6 +4,10 @@ declare(strict_types=1);
namespace Mailcow\Auth\Service; namespace Mailcow\Auth\Service;
use LdapRecord\Auth\BindException;
use LdapRecord\Connection;
use LdapRecord\LdapRecordException;
use Mailcow\Auth\IdpUnavailable;
use Mailcow\Identity\Model\Settings; use Mailcow\Identity\Model\Settings;
use Mailcow\Identity\Service\LinkService; use Mailcow\Identity\Service\LinkService;
use Mailcow\MailboxGroup\Service\TemplateService; use Mailcow\MailboxGroup\Service\TemplateService;
@@ -19,6 +23,37 @@ class IdpLoginService
{ {
} }
/**
* Binds as the user to check the password, then again as the configured user. False for wrong credentials,
* IdpUnavailable when the server cannot answer (client side codes are negative: server down, timeout, ...).
*/
public function verifyLdapPassword(Connection $ldap, string $dn, string $password): bool
{
// an empty password would be an unauthenticated bind, which succeeds
if ($dn === '' || $password === '') {
return false;
}
$guard = $ldap->auth();
try {
$guard->bind($dn, $password);
$bound = true;
} catch (BindException $e) {
$code = $e->getDetailedError()?->getErrorCode();
if ($code !== null && ($code < 0 || in_array($code, [51, 52], true))) {
throw new IdpUnavailable('LDAP bind failed: ' . $e->getMessage(), 0, $e);
}
$bound = false;
} catch (LdapRecordException $e) {
throw new IdpUnavailable('LDAP bind failed: ' . $e->getMessage(), 0, $e);
}
try {
$guard->bindAsConfiguredUser();
} catch (LdapRecordException $e) {
// the login needs no further LDAP access
}
return $bound;
}
/** Template of the first attribute mapping matching one of the IdP attribute values, null if none */ /** Template of the first attribute mapping matching one of the IdP attribute values, null if none */
public function mappedTemplate(mixed $values): ?string public function mappedTemplate(mixed $values): ?string
{ {
+1 -4
View File
@@ -14,10 +14,7 @@ if (PHP_VERSION_ID < 50600) {
echo $err; echo $err;
} }
} }
trigger_error( throw new RuntimeException($err);
$err,
E_USER_ERROR
);
} }
require_once __DIR__ . '/composer/autoload_real.php'; require_once __DIR__ . '/composer/autoload_real.php';
+34 -12
View File
@@ -12,6 +12,7 @@
namespace Composer; namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php'; $GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) { if (PHP_VERSION_ID < 80000) {
@@ -23,18 +24,17 @@ if (PHP_VERSION_ID < 80000) {
{ {
private $handle; private $handle;
private $position; private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path) public function stream_open($path, $mode, $options, &$opened_path)
{ {
// get rid of composer-bin-proxy:// prefix for __FILE__ & __DIR__ resolution // get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 21); $opened_path = substr($path, 17);
$opened_path = realpath($opened_path) ?: $opened_path; $this->realpath = realpath($opened_path) ?: $opened_path;
$this->handle = fopen($opened_path, $mode); $opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0; $this->position = 0;
// remove all traces of this stream wrapper once it has been used
stream_wrapper_unregister('composer-bin-proxy');
return (bool) $this->handle; return (bool) $this->handle;
} }
@@ -66,6 +66,16 @@ if (PHP_VERSION_ID < 80000) {
return $operation ? flock($this->handle, $operation) : true; return $operation ? flock($this->handle, $operation) : true;
} }
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell() public function stream_tell()
{ {
return $this->position; return $this->position;
@@ -78,20 +88,32 @@ if (PHP_VERSION_ID < 80000) {
public function stream_stat() public function stream_stat()
{ {
return fstat($this->handle); return array();
} }
public function stream_set_option($option, $arg1, $arg2) public function stream_set_option($option, $arg1, $arg2)
{ {
return true; return true;
} }
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
} }
} }
if (function_exists('stream_wrapper_register') && stream_wrapper_register('composer-bin-proxy', 'Composer\BinProxyWrapper')) { if (
include("composer-bin-proxy://" . __DIR__ . '/..'.'/matthiasmullie/minify/bin/minifycss'); (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
exit(0); || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/matthiasmullie/minify/bin/minifycss');
} }
} }
include __DIR__ . '/..'.'/matthiasmullie/minify/bin/minifycss'; return include __DIR__ . '/..'.'/matthiasmullie/minify/bin/minifycss';
+34 -12
View File
@@ -12,6 +12,7 @@
namespace Composer; namespace Composer;
$GLOBALS['_composer_bin_dir'] = __DIR__;
$GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php'; $GLOBALS['_composer_autoload_path'] = __DIR__ . '/..'.'/autoload.php';
if (PHP_VERSION_ID < 80000) { if (PHP_VERSION_ID < 80000) {
@@ -23,18 +24,17 @@ if (PHP_VERSION_ID < 80000) {
{ {
private $handle; private $handle;
private $position; private $position;
private $realpath;
public function stream_open($path, $mode, $options, &$opened_path) public function stream_open($path, $mode, $options, &$opened_path)
{ {
// get rid of composer-bin-proxy:// prefix for __FILE__ & __DIR__ resolution // get rid of phpvfscomposer:// prefix for __FILE__ & __DIR__ resolution
$opened_path = substr($path, 21); $opened_path = substr($path, 17);
$opened_path = realpath($opened_path) ?: $opened_path; $this->realpath = realpath($opened_path) ?: $opened_path;
$this->handle = fopen($opened_path, $mode); $opened_path = $this->realpath;
$this->handle = fopen($this->realpath, $mode);
$this->position = 0; $this->position = 0;
// remove all traces of this stream wrapper once it has been used
stream_wrapper_unregister('composer-bin-proxy');
return (bool) $this->handle; return (bool) $this->handle;
} }
@@ -66,6 +66,16 @@ if (PHP_VERSION_ID < 80000) {
return $operation ? flock($this->handle, $operation) : true; return $operation ? flock($this->handle, $operation) : true;
} }
public function stream_seek($offset, $whence)
{
if (0 === fseek($this->handle, $offset, $whence)) {
$this->position = ftell($this->handle);
return true;
}
return false;
}
public function stream_tell() public function stream_tell()
{ {
return $this->position; return $this->position;
@@ -78,20 +88,32 @@ if (PHP_VERSION_ID < 80000) {
public function stream_stat() public function stream_stat()
{ {
return fstat($this->handle); return array();
} }
public function stream_set_option($option, $arg1, $arg2) public function stream_set_option($option, $arg1, $arg2)
{ {
return true; return true;
} }
public function url_stat($path, $flags)
{
$path = substr($path, 17);
if (file_exists($path)) {
return stat($path);
}
return false;
}
} }
} }
if (function_exists('stream_wrapper_register') && stream_wrapper_register('composer-bin-proxy', 'Composer\BinProxyWrapper')) { if (
include("composer-bin-proxy://" . __DIR__ . '/..'.'/matthiasmullie/minify/bin/minifyjs'); (function_exists('stream_get_wrappers') && in_array('phpvfscomposer', stream_get_wrappers(), true))
exit(0); || (function_exists('stream_wrapper_register') && stream_wrapper_register('phpvfscomposer', 'Composer\BinProxyWrapper'))
) {
return include("phpvfscomposer://" . __DIR__ . '/..'.'/matthiasmullie/minify/bin/minifyjs');
} }
} }
include __DIR__ . '/..'.'/matthiasmullie/minify/bin/minifyjs'; return include __DIR__ . '/..'.'/matthiasmullie/minify/bin/minifyjs';
@@ -0,0 +1,74 @@
name: Test Suite
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
services:
redis:
image: redis
ports:
- 6379:6379
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
mongodb:
image: mongo
ports:
- 27017:27017
myriadb:
image: mariadb
env:
MYSQL_ROOT_PASSWORD: root
ports:
- 3808:3808
- 3306:3306
postgres:
image: postgres
env:
POSTGRES_DB: oauth2_server_php
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
ports:
- 5432:5432
options: --health-cmd="pg_isready -h localhost" --health-interval=10s --health-timeout=5s --health-retries=5
strategy:
matrix:
php: [ 7.2, 7.3, 7.4, "8.0", 8.1, 8.2 ]
name: "PHP ${{ matrix.php }} Unit Test"
steps:
- uses: actions/checkout@v2
- uses: codecov/codecov-action@v1
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: mongodb, mbstring, intl, redis, pdo_mysql
- name: Install composer dependencies
uses: nick-invision/retry@v1
with:
timeout_minutes: 10
max_attempts: 3
command: composer install
- name: Run PHPUnit
run: vendor/bin/phpunit -v
phpstan:
name: "PHPStan"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
- name: Install composer dependencies
uses: nick-invision/retry@v1
with:
timeout_minutes: 10
max_attempts: 3
command: composer install
- name: Run PHPStan
run: |
composer require phpstan/phpstan
vendor/bin/phpstan analyse --level=0 src/
@@ -1,8 +1,6 @@
oauth2-server-php oauth2-server-php
================= =================
[![Build Status](https://travis-ci.org/bshaffer/oauth2-server-php.svg?branch=master)](https://travis-ci.org/bshaffer/oauth2-server-php) [![Total Downloads](https://poser.pugx.org/bshaffer/oauth2-server-php/downloads)](https://packagist.org/packages/bshaffer/oauth2-server-php)
[![Total Downloads](https://poser.pugx.org/bshaffer/oauth2-server-php/downloads.png)](https://packagist.org/packages/bshaffer/oauth2-server-php)
View the [complete documentation](https://bshaffer.github.io/oauth2-server-php-docs/) View the [complete documentation](https://bshaffer.github.io/oauth2-server-php-docs/)
@@ -16,21 +16,21 @@
"psr-0": { "OAuth2": "src/" } "psr-0": { "OAuth2": "src/" }
}, },
"require":{ "require":{
"php":">=5.3.9" "php":">=7.2"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^4.0", "phpunit/phpunit": "^7.5||^8.0",
"aws/aws-sdk-php": "~2.8", "aws/aws-sdk-php": "^2.8",
"firebase/php-jwt": "~2.2", "firebase/php-jwt": "^6.4",
"predis/predis": "dev-master", "predis/predis": "^1.1",
"thobbs/phpcassa": "dev-master", "thobbs/phpcassa": "dev-master",
"mongodb/mongodb": "^1.1" "yoast/phpunit-polyfills": "^1.0"
}, },
"suggest": { "suggest": {
"predis/predis": "Required to use Redis storage", "predis/predis": "Required to use Redis storage",
"thobbs/phpcassa": "Required to use Cassandra storage", "thobbs/phpcassa": "Required to use Cassandra storage",
"aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage", "aws/aws-sdk-php": "~2.8 is required to use DynamoDB storage",
"firebase/php-jwt": "~2.2 is required to use JWT features", "firebase/php-jwt": "~v6.4 is required to use JWT features",
"mongodb/mongodb": "^1.1 is required to use MongoDB storage" "mongodb/mongodb": "^1.1 is required to use MongoDB storage"
} }
} }
@@ -39,7 +39,7 @@ class Autoloader
* Handles autoloading of classes. * Handles autoloading of classes.
* *
* @param string $class - A class name. * @param string $class - A class name.
* @return boolean - Returns true if the class has been loaded * @return void
*/ */
public function autoload($class) public function autoload($class)
{ {
@@ -112,7 +112,7 @@ class HttpBasic implements ClientAssertionTypeInterface
* *
* @ingroup oauth2_section_2 * @ingroup oauth2_section_2
*/ */
public function getClientCredentials(RequestInterface $request, ResponseInterface $response = null) public function getClientCredentials(RequestInterface $request, ?ResponseInterface $response = null)
{ {
if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) { if (!is_null($request->headers('PHP_AUTH_USER')) && !is_null($request->headers('PHP_AUTH_PW'))) {
return array('client_id' => $request->headers('PHP_AUTH_USER'), 'client_secret' => $request->headers('PHP_AUTH_PW')); return array('client_id' => $request->headers('PHP_AUTH_USER'), 'client_secret' => $request->headers('PHP_AUTH_PW'));
@@ -78,7 +78,7 @@ class AuthorizeController implements AuthorizeControllerInterface
* ); * );
* @endcode * @endcode
*/ */
public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ScopeInterface $scopeUtil = null) public function __construct(ClientInterface $clientStorage, array $responseTypes = array(), array $config = array(), ?ScopeInterface $scopeUtil = null)
{ {
$this->clientStorage = $clientStorage; $this->clientStorage = $clientStorage;
$this->responseTypes = $responseTypes; $this->responseTypes = $responseTypes;
@@ -87,6 +87,7 @@ class AuthorizeController implements AuthorizeControllerInterface
'enforce_state' => true, 'enforce_state' => true,
'require_exact_redirect_uri' => true, 'require_exact_redirect_uri' => true,
'redirect_status_code' => 302, 'redirect_status_code' => 302,
'enforce_pkce' => false,
), $config); ), $config);
if (is_null($scopeUtil)) { if (is_null($scopeUtil)) {
@@ -47,7 +47,7 @@ class ResourceController implements ResourceControllerInterface
* @param array $config * @param array $config
* @param ScopeInterface $scopeUtil * @param ScopeInterface $scopeUtil
*/ */
public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, $config = array(), ScopeInterface $scopeUtil = null) public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, $config = array(), ?ScopeInterface $scopeUtil = null)
{ {
$this->tokenType = $tokenType; $this->tokenType = $tokenType;
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;
@@ -13,7 +13,7 @@ use OAuth2\ResponseInterface;
* @code * @code
* if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) { * if (!$resourceController->verifyResourceRequest(OAuth2\Request::createFromGlobals(), $response = new OAuth2\Response())) {
* $response->send(); // authorization failed * $response->send(); // authorization failed
* exit(); * die();
* } * }
* return json_encode($resource); // valid token! Send the stuff! * return json_encode($resource); // valid token! Send the stuff!
* @endcode * @endcode
@@ -54,7 +54,7 @@ class TokenController implements TokenControllerInterface
* @param ScopeInterface $scopeUtil * @param ScopeInterface $scopeUtil
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ClientAssertionTypeInterface $clientAssertionType = null, ScopeInterface $scopeUtil = null) public function __construct(AccessTokenInterface $accessToken, ClientInterface $clientStorage, array $grantTypes = array(), ?ClientAssertionTypeInterface $clientAssertionType = null, ?ScopeInterface $scopeUtil = null)
{ {
if (is_null($clientAssertionType)) { if (is_null($clientAssertionType)) {
foreach ($grantTypes as $grantType) { foreach ($grantTypes as $grantType) {
@@ -2,6 +2,9 @@
namespace OAuth2\Encryption; namespace OAuth2\Encryption;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
/** /**
* Bridge file to use the firebase/php-jwt package for JWT encoding and decoding. * Bridge file to use the firebase/php-jwt package for JWT encoding and decoding.
* @author Francis Chuang <francis.chuang@gmail.com> * @author Francis Chuang <francis.chuang@gmail.com>
@@ -10,26 +13,36 @@ class FirebaseJwt implements EncryptionInterface
{ {
public function __construct() public function __construct()
{ {
if (!class_exists('\JWT')) { if (!class_exists(JWT::class)) {
throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"'); throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"');
} }
} }
public function encode($payload, $key, $alg = 'HS256', $keyId = null) public function encode($payload, $key, $alg = 'HS256', $keyId = null)
{ {
return \JWT::encode($payload, $key, $alg, $keyId); return JWT::encode($payload, $key, $alg, $keyId);
} }
public function decode($jwt, $key = null, $allowedAlgorithms = null) public function decode($jwt, $key = null, $allowedAlgorithms = null)
{ {
try { try {
//Maintain BC: Do not verify if no algorithms are passed in. //Maintain BC: Do not verify if no algorithms are passed in.
if (!$allowedAlgorithms) { if (!$allowedAlgorithms) {
$key = null; $tks = \explode('.', $jwt);
if (\count($tks) === 3) {
[$headb64] = $tks;
$headerRaw = JWT::urlsafeB64Decode($headb64);
if (($header = JWT::jsonDecode($headerRaw))) {
$key = new Key($key, $header->alg);
}
}
} elseif(is_array($allowedAlgorithms)) {
$key = new Key($key, $allowedAlgorithms[0]);
} else {
$key = new Key($key, $allowedAlgorithms);
} }
return (array)\JWT::decode($jwt, $key, $allowedAlgorithms); return (array) JWT::decode($jwt, $key);
} catch (\Exception $e) { } catch (\Exception $e) {
return false; return false;
} }
@@ -37,11 +50,11 @@ class FirebaseJwt implements EncryptionInterface
public function urlSafeB64Encode($data) public function urlSafeB64Encode($data)
{ {
return \JWT::urlsafeB64Encode($data); return JWT::urlsafeB64Encode($data);
} }
public function urlSafeB64Decode($b64) public function urlSafeB64Decode($b64)
{ {
return \JWT::urlsafeB64Decode($b64); return JWT::urlsafeB64Decode($b64);
} }
} }
@@ -84,6 +84,41 @@ class AuthorizationCode implements GrantTypeInterface
return false; return false;
} }
if (isset($authCode['code_challenge']) && $authCode['code_challenge']) {
if (!($code_verifier = $request->request('code_verifier'))) {
$response->setError(400, 'code_verifier_missing', "The PKCE code verifier parameter is required.");
return false;
}
// Validate code_verifier according to RFC-7636
// @see: https://tools.ietf.org/html/rfc7636#section-4.1
if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $code_verifier) !== 1) {
$response->setError(400, 'code_verifier_invalid', "The PKCE code verifier parameter is invalid.");
return false;
}
$code_verifier = $request->request('code_verifier');
switch ($authCode['code_challenge_method']) {
case 'S256':
$code_verifier_hashed = strtr(rtrim(base64_encode(hash('sha256', $code_verifier, true)), '='), '+/', '-_');
break;
case 'plain':
$code_verifier_hashed = $code_verifier;
break;
default:
$response->setError(400, 'code_challenge_method_invalid', "Unknown PKCE code challenge method.");
return FALSE;
}
if ($code_verifier_hashed !== $authCode['code_challenge']) {
$response->setError(400, 'code_verifier_mismatch', "The PKCE code verifier parameter does not match the code challenge.");
return FALSE;
}
}
if (!isset($authCode['code'])) { if (!isset($authCode['code'])) {
$authCode['code'] = $code; // used to expire the code after the access token is granted $authCode['code'] = $code; // used to expire the code after the access token is granted
} }
@@ -37,7 +37,7 @@ class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface
* @param EncryptionInterface|JWT $jwtUtil - OPTONAL The class used to decode, encode and verify JWTs. * @param EncryptionInterface|JWT $jwtUtil - OPTONAL The class used to decode, encode and verify JWTs.
* @param array $config * @param array $config
*/ */
public function __construct(JwtBearerInterface $storage, $audience, EncryptionInterface $jwtUtil = null, array $config = array()) public function __construct(JwtBearerInterface $storage, $audience, ?EncryptionInterface $jwtUtil = null, array $config = array())
{ {
$this->storage = $storage; $this->storage = $storage;
$this->audience = $audience; $this->audience = $audience;
@@ -46,13 +46,13 @@ class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface
$jwtUtil = new Jwt(); $jwtUtil = new Jwt();
} }
$this->config = array_merge(array( $config = array_merge(array(
'allowed_algorithms' => array('RS256', 'RS384', 'RS512') 'allowed_algorithms' => array('RS256', 'RS384', 'RS512')
), $config); ), $config);
$this->jwtUtil = $jwtUtil; $this->jwtUtil = $jwtUtil;
$this->allowedAlgorithms = $this->config['allowed_algorithms']; $this->allowedAlgorithms = $config['allowed_algorithms'];
} }
/** /**
@@ -127,7 +127,7 @@ class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface
} }
// Check expiration // Check expiration
if (ctype_digit($jwt['exp'])) { if (ctype_digit((string)$jwt['exp'])) {
if ($jwt['exp'] <= time()) { if ($jwt['exp'] <= time()) {
$response->setError(400, 'invalid_grant', "JWT has expired"); $response->setError(400, 'invalid_grant', "JWT has expired");
@@ -141,7 +141,7 @@ class JwtBearer implements GrantTypeInterface, ClientAssertionTypeInterface
// Check the not before time // Check the not before time
if ($notBefore = $jwt['nbf']) { if ($notBefore = $jwt['nbf']) {
if (ctype_digit($notBefore)) { if (ctype_digit((string)$notBefore)) {
if ($notBefore > time()) { if ($notBefore > time()) {
$response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time"); $response->setError(400, 'invalid_grant', "JWT cannot be used before the Not Before (nbf) time");
@@ -16,6 +16,16 @@ class AuthorizeController extends BaseAuthorizeController implements AuthorizeCo
*/ */
private $nonce; private $nonce;
/**
* @var mixed
*/
protected $code_challenge;
/**
* @var mixed
*/
protected $code_challenge_method;
/** /**
* Set not authorized response * Set not authorized response
* *
@@ -65,6 +75,10 @@ class AuthorizeController extends BaseAuthorizeController implements AuthorizeCo
// add the nonce to return with the redirect URI // add the nonce to return with the redirect URI
$params['nonce'] = $this->nonce; $params['nonce'] = $this->nonce;
// Add PKCE code challenge.
$params['code_challenge'] = $this->code_challenge;
$params['code_challenge_method'] = $this->code_challenge_method;
return $params; return $params;
} }
@@ -90,6 +104,32 @@ class AuthorizeController extends BaseAuthorizeController implements AuthorizeCo
$this->nonce = $nonce; $this->nonce = $nonce;
$code_challenge = $request->query('code_challenge');
$code_challenge_method = $request->query('code_challenge_method');
if ($this->config['enforce_pkce']) {
if (!$code_challenge) {
$response->setError(400, 'missing_code_challenge', 'This application requires you provide a PKCE code challenge');
return false;
}
if (preg_match('/^[A-Za-z0-9-._~]{43,128}$/', $code_challenge) !== 1) {
$response->setError(400, 'invalid_code_challenge', 'The PKCE code challenge supplied is invalid');
return false;
}
if (!in_array($code_challenge_method, array('plain', 'S256'), true)) {
$response->setError(400, 'missing_code_challenge_method', 'This application requires you specify a PKCE code challenge method');
return false;
}
}
$this->code_challenge = $code_challenge;
$this->code_challenge_method = $code_challenge_method;
return true; return true;
} }
@@ -30,7 +30,7 @@ class UserInfoController extends ResourceController implements UserInfoControlle
* @param array $config * @param array $config
* @param ScopeInterface $scopeUtil * @param ScopeInterface $scopeUtil
*/ */
public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, UserClaimsInterface $userClaimsStorage, $config = array(), ScopeInterface $scopeUtil = null) public function __construct(TokenTypeInterface $tokenType, AccessTokenInterface $tokenStorage, UserClaimsInterface $userClaimsStorage, $config = array(), ?ScopeInterface $scopeUtil = null)
{ {
parent::__construct($tokenType, $tokenStorage, $config, $scopeUtil); parent::__construct($tokenType, $tokenStorage, $config, $scopeUtil);
@@ -31,9 +31,9 @@ class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCo
// build the URL to redirect to // build the URL to redirect to
$result = array('query' => array()); $result = array('query' => array());
$params += array('scope' => null, 'state' => null, 'id_token' => null); $params += array('scope' => null, 'state' => null, 'id_token' => null, 'code_challenge' => null, 'code_challenge_method' => null);
$result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token']); $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['id_token'], $params['code_challenge'], $params['code_challenge_method']);
if (isset($params['state'])) { if (isset($params['state'])) {
$result['query']['state'] = $params['state']; $result['query']['state'] = $params['state'];
@@ -56,10 +56,10 @@ class AuthorizationCode extends BaseAuthorizationCode implements AuthorizationCo
* @see http://tools.ietf.org/html/rfc6749#section-4 * @see http://tools.ietf.org/html/rfc6749#section-4
* @ingroup oauth2_section_4 * @ingroup oauth2_section_4
*/ */
public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null) public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
$code = $this->generateAuthorizationCode(); $code = $this->generateAuthorizationCode();
$this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token); $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, $id_token, $code_challenge, $code_challenge_method);
return $code; return $code;
} }
@@ -38,7 +38,7 @@ class IdToken implements IdTokenInterface
* @param EncryptionInterface $encryptionUtil * @param EncryptionInterface $encryptionUtil
* @throws LogicException * @throws LogicException
*/ */
public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, array $config = array(), EncryptionInterface $encryptionUtil = null) public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, array $config = array(), ?EncryptionInterface $encryptionUtil = null)
{ {
$this->userClaimsStorage = $userClaimsStorage; $this->userClaimsStorage = $userClaimsStorage;
$this->publicKeyStorage = $publicKeyStorage; $this->publicKeyStorage = $publicKeyStorage;
@@ -33,5 +33,5 @@ interface AuthorizationCodeInterface extends BaseAuthorizationCodeInterface
* *
* @ingroup oauth2_section_4 * @ingroup oauth2_section_4
*/ */
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null); public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null);
} }
@@ -34,7 +34,7 @@ class Request implements RequestInterface
* *
* @api * @api
*/ */
public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null) public function __construct(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, ?array $headers = null)
{ {
$this->initialize($query, $request, $attributes, $cookies, $files, $server, $content, $headers); $this->initialize($query, $request, $attributes, $cookies, $files, $server, $content, $headers);
} }
@@ -55,7 +55,7 @@ class Request implements RequestInterface
* *
* @api * @api
*/ */
public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, array $headers = null) public function initialize(array $query = array(), array $request = array(), array $attributes = array(), array $cookies = array(), array $files = array(), array $server = array(), $content = null, ?array $headers = null)
{ {
$this->request = $request; $this->request = $request;
$this->query = $query; $this->query = $query;
@@ -140,11 +140,10 @@ class Response implements ResponseInterface
} }
/** /**
* @param int $statusCode
* @param string $text * @param string $text
* @throws InvalidArgumentException * @throws InvalidArgumentException
*/ */
public function setStatusCode($statusCode, $text = null) public function setStatusCode(int $statusCode, $text = null)
{ {
$this->statusCode = (int) $statusCode; $this->statusCode = (int) $statusCode;
if ($this->isInvalid()) { if ($this->isInvalid()) {
@@ -20,10 +20,7 @@ interface ResponseInterface
*/ */
public function addHttpHeaders(array $httpHeaders); public function addHttpHeaders(array $httpHeaders);
/** public function setStatusCode(int $statusCode);
* @param int $statusCode
*/
public function setStatusCode($statusCode);
/** /**
* @param int $statusCode * @param int $statusCode
@@ -38,7 +38,7 @@ class AccessToken implements AccessTokenInterface
* ); * );
* @endcode * @endcode
*/ */
public function __construct(AccessTokenStorageInterface $tokenStorage, RefreshTokenInterface $refreshStorage = null, array $config = array()) public function __construct(AccessTokenStorageInterface $tokenStorage, ?RefreshTokenInterface $refreshStorage = null, array $config = array())
{ {
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;
$this->refreshStorage = $refreshStorage; $this->refreshStorage = $refreshStorage;
@@ -26,9 +26,9 @@ class AuthorizationCode implements AuthorizationCodeInterface
// build the URL to redirect to // build the URL to redirect to
$result = array('query' => array()); $result = array('query' => array());
$params += array('scope' => null, 'state' => null); $params += array('scope' => null, 'state' => null, 'code_challenge' => null, 'code_challenge_method' => null);
$result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope']); $result['query']['code'] = $this->createAuthorizationCode($params['client_id'], $user_id, $params['redirect_uri'], $params['scope'], $params['code_challenge'], $params['code_challenge_method']);
if (isset($params['state'])) { if (isset($params['state'])) {
$result['query']['state'] = $params['state']; $result['query']['state'] = $params['state'];
@@ -53,10 +53,10 @@ class AuthorizationCode implements AuthorizationCodeInterface
* @see http://tools.ietf.org/html/rfc6749#section-4 * @see http://tools.ietf.org/html/rfc6749#section-4
* @ingroup oauth2_section_4 * @ingroup oauth2_section_4
*/ */
public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null) public function createAuthorizationCode($client_id, $user_id, $redirect_uri, $scope = null, $code_challenge = null, $code_challenge_method = null)
{ {
$code = $this->generateAuthorizationCode(); $code = $this->generateAuthorizationCode();
$this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope); $this->storage->setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, time() + $this->config['auth_code_lifetime'], $scope, null, $code_challenge, $code_challenge_method);
return $code; return $code;
} }
@@ -26,7 +26,7 @@ class JwtAccessToken extends AccessToken
* or just the token ID is stored * or just the token ID is stored
* @param EncryptionInterface $encryptionUtil - * @param EncryptionInterface $encryptionUtil -
*/ */
public function __construct(PublicKeyInterface $publicKeyStorage = null, AccessTokenStorageInterface $tokenStorage = null, RefreshTokenInterface $refreshStorage = null, array $config = array(), EncryptionInterface $encryptionUtil = null) public function __construct(?PublicKeyInterface $publicKeyStorage = null, ?AccessTokenStorageInterface $tokenStorage = null, ?RefreshTokenInterface $refreshStorage = null, array $config = array(), ?EncryptionInterface $encryptionUtil = null)
{ {
$this->publicKeyStorage = $publicKeyStorage; $this->publicKeyStorage = $publicKeyStorage;
$config = array_merge(array( $config = array_merge(array(
@@ -150,7 +150,7 @@ class Server implements ResourceControllerInterface,
* *
* @ingroup oauth2_section_7 * @ingroup oauth2_section_7
*/ */
public function __construct($storage = array(), array $config = array(), array $grantTypes = array(), array $responseTypes = array(), TokenTypeInterface $tokenType = null, ScopeInterface $scopeUtil = null, ClientAssertionTypeInterface $clientAssertionType = null) public function __construct($storage = array(), array $config = array(), array $grantTypes = array(), array $responseTypes = array(), ?TokenTypeInterface $tokenType = null, ?ScopeInterface $scopeUtil = null, ?ClientAssertionTypeInterface $clientAssertionType = null)
{ {
$storage = is_array($storage) ? $storage : array($storage); $storage = is_array($storage) ? $storage : array($storage);
$this->storages = array(); $this->storages = array();
@@ -172,6 +172,7 @@ class Server implements ResourceControllerInterface,
'enforce_state' => true, 'enforce_state' => true,
'require_exact_redirect_uri' => true, 'require_exact_redirect_uri' => true,
'allow_implicit' => false, 'allow_implicit' => false,
'enforce_pkce' => false,
'allow_credentials_in_request_body' => true, 'allow_credentials_in_request_body' => true,
'allow_public_clients' => true, 'allow_public_clients' => true,
'always_issue_new_refresh_token' => false, 'always_issue_new_refresh_token' => false,
@@ -288,7 +289,7 @@ class Server implements ResourceControllerInterface,
* *
* @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo * @see http://openid.net/specs/openid-connect-core-1_0.html#UserInfo
*/ */
public function handleUserInfoRequest(RequestInterface $request, ResponseInterface $response = null) public function handleUserInfoRequest(RequestInterface $request, ?ResponseInterface $response = null)
{ {
$this->response = is_null($response) ? new Response() : $response; $this->response = is_null($response) ? new Response() : $response;
$this->getUserInfoController()->handleUserInfoRequest($request, $this->response); $this->getUserInfoController()->handleUserInfoRequest($request, $this->response);
@@ -314,7 +315,7 @@ class Server implements ResourceControllerInterface,
* *
* @ingroup oauth2_section_4 * @ingroup oauth2_section_4
*/ */
public function handleTokenRequest(RequestInterface $request, ResponseInterface $response = null) public function handleTokenRequest(RequestInterface $request, ?ResponseInterface $response = null)
{ {
$this->response = is_null($response) ? new Response() : $response; $this->response = is_null($response) ? new Response() : $response;
$this->getTokenController()->handleTokenRequest($request, $this->response); $this->getTokenController()->handleTokenRequest($request, $this->response);
@@ -327,7 +328,7 @@ class Server implements ResourceControllerInterface,
* @param ResponseInterface $response - Response object * @param ResponseInterface $response - Response object
* @return mixed * @return mixed
*/ */
public function grantAccessToken(RequestInterface $request, ResponseInterface $response = null) public function grantAccessToken(RequestInterface $request, ?ResponseInterface $response = null)
{ {
$this->response = is_null($response) ? new Response() : $response; $this->response = is_null($response) ? new Response() : $response;
$value = $this->getTokenController()->grantAccessToken($request, $this->response); $value = $this->getTokenController()->grantAccessToken($request, $this->response);
@@ -345,7 +346,7 @@ class Server implements ResourceControllerInterface,
* @param ResponseInterface $response * @param ResponseInterface $response
* @return Response|ResponseInterface * @return Response|ResponseInterface
*/ */
public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response = null) public function handleRevokeRequest(RequestInterface $request, ?ResponseInterface $response = null)
{ {
$this->response = is_null($response) ? new Response() : $response; $this->response = is_null($response) ? new Response() : $response;
$this->getTokenController()->handleRevokeRequest($request, $this->response); $this->getTokenController()->handleRevokeRequest($request, $this->response);
@@ -407,7 +408,7 @@ class Server implements ResourceControllerInterface,
* *
* @ingroup oauth2_section_3 * @ingroup oauth2_section_3
*/ */
public function validateAuthorizeRequest(RequestInterface $request, ResponseInterface $response = null) public function validateAuthorizeRequest(RequestInterface $request, ?ResponseInterface $response = null)
{ {
$this->response = is_null($response) ? new Response() : $response; $this->response = is_null($response) ? new Response() : $response;
$value = $this->getAuthorizeController()->validateAuthorizeRequest($request, $this->response); $value = $this->getAuthorizeController()->validateAuthorizeRequest($request, $this->response);
@@ -421,7 +422,7 @@ class Server implements ResourceControllerInterface,
* @param string $scope - Scope * @param string $scope - Scope
* @return mixed * @return mixed
*/ */
public function verifyResourceRequest(RequestInterface $request, ResponseInterface $response = null, $scope = null) public function verifyResourceRequest(RequestInterface $request, ?ResponseInterface $response = null, $scope = null)
{ {
$this->response = is_null($response) ? new Response() : $response; $this->response = is_null($response) ? new Response() : $response;
$value = $this->getResourceController()->verifyResourceRequest($request, $this->response, $scope); $value = $this->getResourceController()->verifyResourceRequest($request, $this->response, $scope);
@@ -434,7 +435,7 @@ class Server implements ResourceControllerInterface,
* @param ResponseInterface $response - Response object * @param ResponseInterface $response - Response object
* @return mixed * @return mixed
*/ */
public function getAccessTokenData(RequestInterface $request, ResponseInterface $response = null) public function getAccessTokenData(RequestInterface $request, ?ResponseInterface $response = null)
{ {
$this->response = is_null($response) ? new Response() : $response; $this->response = is_null($response) ? new Response() : $response;
$value = $this->getResourceController()->getAccessTokenData($request, $this->response); $value = $this->getResourceController()->getAccessTokenData($request, $this->response);
@@ -577,7 +578,7 @@ class Server implements ResourceControllerInterface,
} }
} }
$config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri'))); $config = array_intersect_key($this->config, array_flip(explode(' ', 'allow_implicit enforce_state require_exact_redirect_uri enforce_pkce')));
if ($this->config['use_openid_connect']) { if ($this->config['use_openid_connect']) {
return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil()); return new OpenIDAuthorizeController($this->storages['client'], $this->responseTypes, $config, $this->getScopeUtil());
@@ -191,11 +191,11 @@ class Cassandra implements AuthorizationCodeInterface,
* @param string $id_token * @param string $id_token
* @return bool * @return bool
*/ */
public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
return $this->setValue( return $this->setValue(
$this->config['code_key'] . $authorization_code, $this->config['code_key'] . $authorization_code,
compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'),
$expires $expires
); );
} }
@@ -2,6 +2,7 @@
namespace OAuth2\Storage; namespace OAuth2\Storage;
use Couchbase;
use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface;
/** /**
@@ -27,14 +28,18 @@ class CouchbaseDB implements AuthorizationCodeInterface,
public function __construct($connection, $config = array()) public function __construct($connection, $config = array())
{ {
if ($connection instanceof \Couchbase) { if (!class_exists(Couchbase::class)) {
throw new \RuntimeException('Missing Couchbase');
}
if ($connection instanceof Couchbase) {
$this->db = $connection; $this->db = $connection;
} else { } else {
if (!is_array($connection) || !is_array($connection['servers'])) { if (!is_array($connection) || !is_array($connection['servers'])) {
throw new \InvalidArgumentException('First argument to OAuth2\Storage\CouchbaseDB must be an instance of Couchbase or a configuration array containing a server array'); throw new \InvalidArgumentException('First argument to OAuth2\Storage\CouchbaseDB must be an instance of Couchbase or a configuration array containing a server array');
} }
$this->db = new \Couchbase($connection['servers'], (!isset($connection['username'])) ? '' : $connection['username'], (!isset($connection['password'])) ? '' : $connection['password'], $connection['bucket'], false); $this->db = new Couchbase($connection['servers'], (!isset($connection['username'])) ? '' : $connection['username'], (!isset($connection['password'])) ? '' : $connection['password'], $connection['bucket'], false);
} }
$this->config = array_merge(array( $this->config = array_merge(array(
@@ -173,7 +178,7 @@ class CouchbaseDB implements AuthorizationCodeInterface,
return is_null($code) ? false : $code; return is_null($code) ? false : $code;
} }
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
// if it exists, update it. // if it exists, update it.
if ($this->getAuthorizationCode($code)) { if ($this->getAuthorizationCode($code)) {
@@ -185,6 +190,8 @@ class CouchbaseDB implements AuthorizationCodeInterface,
'expires' => $expires, 'expires' => $expires,
'scope' => $scope, 'scope' => $scope,
'id_token' => $id_token, 'id_token' => $id_token,
'code_challenge' => $code_challenge,
'code_challenge_method' => $code_challenge_method,
)); ));
} else { } else {
$this->setObjectByType('code_table',$code,array( $this->setObjectByType('code_table',$code,array(
@@ -195,6 +202,8 @@ class CouchbaseDB implements AuthorizationCodeInterface,
'expires' => $expires, 'expires' => $expires,
'scope' => $scope, 'scope' => $scope,
'id_token' => $id_token, 'id_token' => $id_token,
'code_challenge' => $code_challenge,
'code_challenge_method' => $code_challenge_method,
)); ));
} }
@@ -213,12 +213,12 @@ class DynamoDB implements
} }
public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
// convert expires to datestring // convert expires to datestring
$expires = date('Y-m-d H:i:s', $expires); $expires = date('Y-m-d H:i:s', $expires);
$clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope'); $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method');
$clientData = array_filter($clientData, 'self::isNotEmpty'); $clientData = array_filter($clientData, 'self::isNotEmpty');
$result = $this->client->putItem(array( $result = $this->client->putItem(array(
@@ -21,7 +21,7 @@ class JwtAccessToken implements JwtAccessTokenInterface
* is not necessary when using this grant type. * is not necessary when using this grant type.
* @param OAuth2\Encryption\EncryptionInterface $encryptionUtil OPTIONAL class to use for "encode" and "decode" functions. * @param OAuth2\Encryption\EncryptionInterface $encryptionUtil OPTIONAL class to use for "encode" and "decode" functions.
*/ */
public function __construct(PublicKeyInterface $publicKeyStorage, AccessTokenInterface $tokenStorage = null, EncryptionInterface $encryptionUtil = null) public function __construct(PublicKeyInterface $publicKeyStorage, ?AccessTokenInterface $tokenStorage = null, ?EncryptionInterface $encryptionUtil = null)
{ {
$this->publicKeyStorage = $publicKeyStorage; $this->publicKeyStorage = $publicKeyStorage;
$this->tokenStorage = $tokenStorage; $this->tokenStorage = $tokenStorage;
@@ -74,9 +74,9 @@ class Memory implements AuthorizationCodeInterface,
), $this->authorizationCodes[$code]); ), $this->authorizationCodes[$code]);
} }
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
$this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'); $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method');
return true; return true;
} }
@@ -179,7 +179,7 @@ class Mongo implements AuthorizationCodeInterface,
return is_null($code) ? false : $code; return is_null($code) ? false : $code;
} }
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
// if it exists, update it. // if it exists, update it.
if ($this->getAuthorizationCode($code)) { if ($this->getAuthorizationCode($code)) {
@@ -192,6 +192,8 @@ class Mongo implements AuthorizationCodeInterface,
'expires' => $expires, 'expires' => $expires,
'scope' => $scope, 'scope' => $scope,
'id_token' => $id_token, 'id_token' => $id_token,
'code_challenge' => $code_challenge,
'code_challenge_method' => $code_challenge_method,
)) ))
); );
} else { } else {
@@ -203,6 +205,8 @@ class Mongo implements AuthorizationCodeInterface,
'expires' => $expires, 'expires' => $expires,
'scope' => $scope, 'scope' => $scope,
'id_token' => $id_token, 'id_token' => $id_token,
'code_challenge' => $code_challenge,
'code_challenge_method' => $code_challenge_method,
); );
$this->collection('code_table')->insert($token); $this->collection('code_table')->insert($token);
} }
@@ -32,6 +32,9 @@ class MongoDB implements AuthorizationCodeInterface,
public function __construct($connection, $config = array()) public function __construct($connection, $config = array())
{ {
if (!class_exists(Database::class) || !class_exists(Client::class)) {
throw new \LogicException('Missing MongoDB php extension. Please install mongodb.so');
}
if ($connection instanceof Database) { if ($connection instanceof Database) {
$this->db = $connection; $this->db = $connection;
} else { } else {
@@ -167,7 +170,7 @@ class MongoDB implements AuthorizationCodeInterface,
return is_null($code) ? false : $code; return is_null($code) ? false : $code;
} }
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
// if it exists, update it. // if it exists, update it.
if ($this->getAuthorizationCode($code)) { if ($this->getAuthorizationCode($code)) {
@@ -180,6 +183,8 @@ class MongoDB implements AuthorizationCodeInterface,
'expires' => $expires, 'expires' => $expires,
'scope' => $scope, 'scope' => $scope,
'id_token' => $id_token, 'id_token' => $id_token,
'code_challenge' => $code_challenge,
'code_challenge_method' => $code_challenge_method,
)) ))
); );
return $result->getMatchedCount() > 0; return $result->getMatchedCount() > 0;
@@ -192,6 +197,8 @@ class MongoDB implements AuthorizationCodeInterface,
'expires' => $expires, 'expires' => $expires,
'scope' => $scope, 'scope' => $scope,
'id_token' => $id_token, 'id_token' => $id_token,
'code_challenge' => $code_challenge,
'code_challenge_method' => $code_challenge_method,
); );
$result = $this->collection('code_table')->insertOne($token); $result = $this->collection('code_table')->insertOne($token);
return $result->getInsertedCount() > 0; return $result->getInsertedCount() > 0;
@@ -247,7 +247,7 @@ class Pdo implements
* @param string $id_token * @param string $id_token
* @return bool|mixed * @return bool|mixed
*/ */
public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
if (func_num_args() > 6) { if (func_num_args() > 6) {
// we are calling with an id token // we are calling with an id token
@@ -259,12 +259,12 @@ class Pdo implements
// if it exists, update it. // if it exists, update it.
if ($this->getAuthorizationCode($code)) { if ($this->getAuthorizationCode($code)) {
$stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope where authorization_code=:code', $this->config['code_table'])); $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, code_challenge=:code_challenge, code_challenge_method=:code_challenge_method where authorization_code=:code', $this->config['code_table']));
} else { } else {
$stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope)', $this->config['code_table'])); $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, code_challenge, code_challenge_method) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :code_challenge, :code_challenge_method)', $this->config['code_table']));
} }
return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope')); return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'code_challenge', 'code_challenge_method'));
} }
/** /**
@@ -277,19 +277,19 @@ class Pdo implements
* @param string $id_token * @param string $id_token
* @return bool * @return bool
*/ */
private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) private function setAuthorizationCodeWithIdToken($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
// convert expires to datestring // convert expires to datestring
$expires = date('Y-m-d H:i:s', $expires); $expires = date('Y-m-d H:i:s', $expires);
// if it exists, update it. // if it exists, update it.
if ($this->getAuthorizationCode($code)) { if ($this->getAuthorizationCode($code)) {
$stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token where authorization_code=:code', $this->config['code_table'])); $stmt = $this->db->prepare($sql = sprintf('UPDATE %s SET client_id=:client_id, user_id=:user_id, redirect_uri=:redirect_uri, expires=:expires, scope=:scope, id_token =:id_token, code_challenge=:code_challenge, code_challenge_method=:code_challenge_method where authorization_code=:code', $this->config['code_table']));
} else { } else {
$stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token)', $this->config['code_table'])); $stmt = $this->db->prepare(sprintf('INSERT INTO %s (authorization_code, client_id, user_id, redirect_uri, expires, scope, id_token, code_challenge, code_challenge_method) VALUES (:code, :client_id, :user_id, :redirect_uri, :expires, :scope, :id_token, :code_challenge, :code_challenge_method)', $this->config['code_table']));
} }
return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token')); return $stmt->execute(compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'));
} }
/** /**
@@ -676,6 +676,8 @@ class Pdo implements
expires TIMESTAMP NOT NULL, expires TIMESTAMP NOT NULL,
scope VARCHAR(4000), scope VARCHAR(4000),
id_token VARCHAR(1000), id_token VARCHAR(1000),
code_challenge VARCHAR(1000),
code_challenge_method VARCHAR(20),
PRIMARY KEY (authorization_code) PRIMARY KEY (authorization_code)
); );
@@ -95,11 +95,11 @@ class Redis implements AuthorizationCodeInterface,
return $this->getValue($this->config['code_key'] . $code); return $this->getValue($this->config['code_key'] . $code);
} }
public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null, $code_challenge = null, $code_challenge_method = null)
{ {
return $this->setValue( return $this->setValue(
$this->config['code_key'] . $authorization_code, $this->config['code_key'] . $authorization_code,
compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'), compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token', 'code_challenge', 'code_challenge_method'),
$expires $expires
); );
} }
@@ -109,7 +109,7 @@ class Bearer implements TokenTypeInterface
} }
$contentType = $request->server('CONTENT_TYPE'); $contentType = $request->server('CONTENT_TYPE');
if (false !== $pos = strpos($contentType, ';')) { if (false !== $pos = strpos((string) $contentType, ';')) {
$contentType = substr($contentType, 0, $pos); $contentType = substr($contentType, 0, $pos);
} }
@@ -133,7 +133,7 @@ class AuthorizeControllerTest extends TestCase
$server->handleAuthorizeRequest($request, $response = new Response(), true); $server->handleAuthorizeRequest($request, $response = new Response(), true);
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$this->assertNotContains('error', $response->getHttpHeader('Location')); $this->assertStringNotContainsString('error', $response->getHttpHeader('Location'));
} }
public function testEnforceScope() public function testEnforceScope()
@@ -161,7 +161,7 @@ class AuthorizeControllerTest extends TestCase
$server->handleAuthorizeRequest($request, $response = new Response(), true); $server->handleAuthorizeRequest($request, $response = new Response(), true);
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$this->assertNotContains('error', $response->getHttpHeader('Location')); $this->assertStringNotContainsString('error', $response->getHttpHeader('Location'));
} }
public function testInvalidRedirectUri() public function testInvalidRedirectUri()
@@ -227,7 +227,7 @@ class AuthorizeControllerTest extends TestCase
$server->handleAuthorizeRequest($request, $response = new Response(), true); $server->handleAuthorizeRequest($request, $response = new Response(), true);
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$this->assertContains('code', $response->getHttpHeader('Location')); $this->assertStringContainsString('code', $response->getHttpHeader('Location'));
} }
public function testRedirectUriWithDifferentQueryAndExactMatchRequired() public function testRedirectUriWithDifferentQueryAndExactMatchRequired()
@@ -263,7 +263,7 @@ class AuthorizeControllerTest extends TestCase
$server->handleAuthorizeRequest($request, $response = new Response(), true); $server->handleAuthorizeRequest($request, $response = new Response(), true);
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$this->assertContains('code', $response->getHttpHeader('Location')); $this->assertStringContainsString('code', $response->getHttpHeader('Location'));
} }
public function testMultipleRedirectUris() public function testMultipleRedirectUris()
@@ -278,14 +278,14 @@ class AuthorizeControllerTest extends TestCase
$server->handleAuthorizeRequest($request, $response = new Response(), true); $server->handleAuthorizeRequest($request, $response = new Response(), true);
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$this->assertContains('code', $response->getHttpHeader('Location')); $this->assertStringContainsString('code', $response->getHttpHeader('Location'));
// call again with different (but still valid) redirect URI // call again with different (but still valid) redirect URI
$request->query['redirect_uri'] = 'http://morehazards.com'; $request->query['redirect_uri'] = 'http://morehazards.com';
$server->handleAuthorizeRequest($request, $response = new Response(), true); $server->handleAuthorizeRequest($request, $response = new Response(), true);
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$this->assertContains('code', $response->getHttpHeader('Location')); $this->assertStringContainsString('code', $response->getHttpHeader('Location'));
} }
/** /**
@@ -303,7 +303,7 @@ class AuthorizeControllerTest extends TestCase
$server->handleAuthorizeRequest($request, $response = new Response(), true); $server->handleAuthorizeRequest($request, $response = new Response(), true);
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$this->assertContains('state', $response->getHttpHeader('Location')); $this->assertStringContainsString('state', $response->getHttpHeader('Location'));
$this->assertStringStartsWith('http://brentertainment.com?code=', $response->getHttpHeader('Location')); $this->assertStringStartsWith('http://brentertainment.com?code=', $response->getHttpHeader('Location'));
$parts = parse_url($response->getHttpHeader('Location')); $parts = parse_url($response->getHttpHeader('Location'));
@@ -421,7 +421,7 @@ class AuthorizeControllerTest extends TestCase
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$location = $response->getHttpHeader('Location'); $location = $response->getHttpHeader('Location');
$this->assertNotContains('error', $location); $this->assertStringNotContainsString('error', $location);
$parts = parse_url($location); $parts = parse_url($location);
$this->assertFalse(isset($parts['fake'])); $this->assertFalse(isset($parts['fake']));
@@ -9,7 +9,7 @@ class FirebaseJwtTest extends TestCase
{ {
private $privateKey; private $privateKey;
public function setUp() public function setUp(): void
{ {
$this->privateKey = <<<EOD $this->privateKey = <<<EOD
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
@@ -9,7 +9,7 @@ class JwtTest extends TestCase
{ {
private $privateKey; private $privateKey;
public function setUp() public function setUp(): void
{ {
$this->privateKey = <<<EOD $this->privateKey = <<<EOD
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
@@ -13,7 +13,7 @@ class JwtBearerTest extends TestCase
{ {
private $privateKey; private $privateKey;
public function setUp() public function setUp(): void
{ {
$this->privateKey = <<<EOD $this->privateKey = <<<EOD
-----BEGIN RSA PRIVATE KEY----- -----BEGIN RSA PRIVATE KEY-----
@@ -29,7 +29,7 @@ class CodeIdTokenTest extends TestCase
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$location = $response->getHttpHeader('Location'); $location = $response->getHttpHeader('Location');
$this->assertNotContains('error', $location); $this->assertStringNotContainsString('error', $location);
$parts = parse_url($location); $parts = parse_url($location);
$this->assertArrayHasKey('query', $parts); $this->assertArrayHasKey('query', $parts);
@@ -87,7 +87,7 @@ class CodeIdTokenTest extends TestCase
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$location = $response->getHttpHeader('Location'); $location = $response->getHttpHeader('Location');
$this->assertNotContains('error', $location); $this->assertStringNotContainsString('error', $location);
$parts = parse_url($location); $parts = parse_url($location);
$this->assertArrayHasKey('query', $parts); $this->assertArrayHasKey('query', $parts);
@@ -133,7 +133,7 @@ class CodeIdTokenTest extends TestCase
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$location = $response->getHttpHeader('Location'); $location = $response->getHttpHeader('Location');
$this->assertNotContains('error', $location); $this->assertStringNotContainsString('error', $location);
$parts = parse_url($location); $parts = parse_url($location);
$this->assertArrayHasKey('query', $parts); $this->assertArrayHasKey('query', $parts);
@@ -52,7 +52,7 @@ class IdTokenTest extends TestCase
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$location = $response->getHttpHeader('Location'); $location = $response->getHttpHeader('Location');
$this->assertNotContains('error', $location); $this->assertStringNotContainsString('error', $location);
$parts = parse_url($location); $parts = parse_url($location);
$this->assertArrayHasKey('fragment', $parts); $this->assertArrayHasKey('fragment', $parts);
@@ -110,7 +110,7 @@ class IdTokenTest extends TestCase
{ {
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$location = $response->getHttpHeader('Location'); $location = $response->getHttpHeader('Location');
$this->assertNotContains('error', $location); $this->assertStringNotContainsString('error', $location);
$parts = parse_url($location); $parts = parse_url($location);
$this->assertArrayHasKey('fragment', $parts); $this->assertArrayHasKey('fragment', $parts);
@@ -31,7 +31,7 @@ class IdTokenTokenTest extends TestCase
$this->assertEquals($response->getStatusCode(), 302); $this->assertEquals($response->getStatusCode(), 302);
$location = $response->getHttpHeader('Location'); $location = $response->getHttpHeader('Location');
$this->assertNotContains('error', $location); $this->assertStringNotContainsString('error', $location);
$parts = parse_url($location); $parts = parse_url($location);
$this->assertArrayHasKey('fragment', $parts); $this->assertArrayHasKey('fragment', $parts);
@@ -14,7 +14,7 @@ class ResponseTest extends TestCase
)); ));
$string = $response->getResponseBody('xml'); $string = $response->getResponseBody('xml');
$this->assertContains('<response><foo>bar</foo><halland>oates</halland></response>', $string); $this->assertStringContainsString('<response><foo>bar</foo><halland>oates</halland></response>', $string);
} }
public function testSetRedirect() public function testSetRedirect()
@@ -162,7 +162,7 @@ class JwtAccessTokenTest extends TestCase
$memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); $memoryStorage = Bootstrap::getInstance()->getMemoryStorage();
$storage = array( $storage = array(
'access_token' => new JwtAccessTokenStorage($memoryStorage), 'access_token' => new JwtAccessTokenStorage($memoryStorage, $memoryStorage),
'client' => $memoryStorage, 'client' => $memoryStorage,
'client_credentials' => $memoryStorage, 'client_credentials' => $memoryStorage,
); );
@@ -6,27 +6,26 @@ use OAuth2\Request\TestRequest;
use OAuth2\ResponseType\AuthorizationCode; use OAuth2\ResponseType\AuthorizationCode;
use OAuth2\Storage\Bootstrap; use OAuth2\Storage\Bootstrap;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Yoast\PHPUnitPolyfills\Polyfills\ExpectPHPException;
class ServerTest extends TestCase class ServerTest extends TestCase
{ {
/** use ExpectPHPException;
* @expectedException LogicException OAuth2\Storage\ClientInterface
**/
public function testGetAuthorizeControllerWithNoClientStorageThrowsException() public function testGetAuthorizeControllerWithNoClientStorageThrowsException()
{ {
$this->expectExceptionMessage('OAuth2\Storage\ClientInterface');
// must set Client Storage // must set Client Storage
$server = new Server(); $server = new Server();
$server->getAuthorizeController(); $server->getAuthorizeController();
} }
/**
* @expectedException LogicException OAuth2\Storage\AccessTokenInterface
**/
public function testGetAuthorizeControllerWithNoAccessTokenStorageThrowsException() public function testGetAuthorizeControllerWithNoAccessTokenStorageThrowsException()
{ {
$this->expectExceptionMessage('OAuth2\Storage\AccessTokenInterface');
// must set AccessToken or AuthorizationCode // must set AccessToken or AuthorizationCode
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientInterface'));
$server->getAuthorizeController(); $server->getAuthorizeController();
} }
@@ -34,8 +33,8 @@ class ServerTest extends TestCase
{ {
// must set AccessToken or AuthorizationCode // must set AccessToken or AuthorizationCode
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientInterface'));
$server->addResponseType($this->getMock('OAuth2\ResponseType\AccessTokenInterface')); $server->addResponseType($this->createMock('OAuth2\ResponseType\AccessTokenInterface'));
$this->assertNotNull($server->getAuthorizeController()); $this->assertNotNull($server->getAuthorizeController());
} }
@@ -44,21 +43,19 @@ class ServerTest extends TestCase
{ {
// must set AccessToken or AuthorizationCode // must set AccessToken or AuthorizationCode
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientInterface'));
$server->addResponseType($this->getMock('OAuth2\ResponseType\AuthorizationCodeInterface')); $server->addResponseType($this->createMock('OAuth2\ResponseType\AuthorizationCodeInterface'));
$this->assertNotNull($server->getAuthorizeController()); $this->assertNotNull($server->getAuthorizeController());
} }
/**
* @expectedException LogicException allow_implicit
**/
public function testGetAuthorizeControllerWithClientStorageAndAccessTokenStorageThrowsException() public function testGetAuthorizeControllerWithClientStorageAndAccessTokenStorageThrowsException()
{ {
// must set AuthorizationCode or AccessToken / implicit // must set AuthorizationCode or AccessToken / implicit
$this->expectExceptionMessage('allow_implicit');
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientInterface'));
$server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface'));
$this->assertNotNull($server->getAuthorizeController()); $this->assertNotNull($server->getAuthorizeController());
} }
@@ -67,8 +64,8 @@ class ServerTest extends TestCase
{ {
// must set AuthorizationCode or AccessToken / implicit // must set AuthorizationCode or AccessToken / implicit
$server = new Server(array(), array('allow_implicit' => true)); $server = new Server(array(), array('allow_implicit' => true));
$server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientInterface'));
$server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface'));
$this->assertNotNull($server->getAuthorizeController()); $this->assertNotNull($server->getAuthorizeController());
} }
@@ -77,63 +74,55 @@ class ServerTest extends TestCase
{ {
// must set AccessToken or AuthorizationCode // must set AccessToken or AuthorizationCode
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\ClientInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientInterface'));
$server->addStorage($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')); $server->addStorage($this->createMock('OAuth2\Storage\AuthorizationCodeInterface'));
$this->assertNotNull($server->getAuthorizeController()); $this->assertNotNull($server->getAuthorizeController());
} }
/**
* @expectedException LogicException grant_types
**/
public function testGetTokenControllerWithGrantTypeStorageThrowsException() public function testGetTokenControllerWithGrantTypeStorageThrowsException()
{ {
$this->expectExceptionMessage('grant_types');
$server = new Server(); $server = new Server();
$server->getTokenController(); $server->getTokenController();
} }
/**
* @expectedException LogicException OAuth2\Storage\ClientCredentialsInterface
**/
public function testGetTokenControllerWithNoClientCredentialsStorageThrowsException() public function testGetTokenControllerWithNoClientCredentialsStorageThrowsException()
{ {
$this->expectExceptionMessage('OAuth2\Storage\ClientCredentialsInterface');
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\UserCredentialsInterface')); $server->addStorage($this->createMock('OAuth2\Storage\UserCredentialsInterface'));
$server->getTokenController(); $server->getTokenController();
} }
/**
* @expectedException LogicException OAuth2\Storage\AccessTokenInterface
**/
public function testGetTokenControllerWithNoAccessTokenStorageThrowsException() public function testGetTokenControllerWithNoAccessTokenStorageThrowsException()
{ {
$this->expectExceptionMessage('OAuth2\Storage\AccessTokenInterface');
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientCredentialsInterface'));
$server->getTokenController(); $server->getTokenController();
} }
public function testGetTokenControllerWithAccessTokenAndClientCredentialsStorage() public function testGetTokenControllerWithAccessTokenAndClientCredentialsStorage()
{ {
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface'));
$server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientCredentialsInterface'));
$server->getTokenController(); $server->getTokenController();
} }
public function testGetTokenControllerAccessTokenStorageAndClientCredentialsStorageAndGrantTypes() public function testGetTokenControllerAccessTokenStorageAndClientCredentialsStorageAndGrantTypes()
{ {
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface'));
$server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientCredentialsInterface'));
$server->addGrantType($this->getMockBuilder('OAuth2\GrantType\AuthorizationCode')->disableOriginalConstructor()->getMock()); $server->addGrantType($this->createMock('OAuth2\GrantType\AuthorizationCode'));
$server->getTokenController(); $server->getTokenController();
} }
/**
* @expectedException LogicException OAuth2\Storage\AccessTokenInterface
**/
public function testGetResourceControllerWithNoAccessTokenStorageThrowsException() public function testGetResourceControllerWithNoAccessTokenStorageThrowsException()
{ {
$this->expectExceptionMessage('OAuth2\Storage\AccessTokenInterface');
$server = new Server(); $server = new Server();
$server->getResourceController(); $server->getResourceController();
} }
@@ -141,41 +130,35 @@ class ServerTest extends TestCase
public function testGetResourceControllerWithAccessTokenStorage() public function testGetResourceControllerWithAccessTokenStorage()
{ {
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface')); $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface'));
$server->getResourceController(); $server->getResourceController();
} }
/**
* @expectedException InvalidArgumentException OAuth2\Storage\AccessTokenInterface
**/
public function testAddingStorageWithInvalidClass() public function testAddingStorageWithInvalidClass()
{ {
$this->expectExceptionMessage('OAuth2\Storage\AccessTokenInterface');
$server = new Server(); $server = new Server();
$server->addStorage(new \StdClass()); $server->addStorage(new \StdClass());
} }
/**
* @expectedException InvalidArgumentException access_token
**/
public function testAddingStorageWithInvalidKey() public function testAddingStorageWithInvalidKey()
{ {
$this->expectExceptionMessage('access_token');
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'), 'nonexistant_storage'); $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface'), 'nonexistant_storage');
} }
/**
* @expectedException InvalidArgumentException OAuth2\Storage\AuthorizationCodeInterface
**/
public function testAddingStorageWithInvalidKeyStorageCombination() public function testAddingStorageWithInvalidKeyStorageCombination()
{ {
$this->expectExceptionMessage('OAuth2\Storage\AuthorizationCodeInterface');
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\AccessTokenInterface'), 'authorization_code'); $server->addStorage($this->createMock('OAuth2\Storage\AccessTokenInterface'), 'authorization_code');
} }
public function testAddingStorageWithValidKeyOnlySetsThatKey() public function testAddingStorageWithValidKeyOnlySetsThatKey()
{ {
$server = new Server(); $server = new Server();
$server->addStorage($this->getMock('OAuth2\Storage\Memory'), 'access_token'); $server->addStorage($this->createMock('OAuth2\Storage\Memory'), 'access_token');
$reflection = new \ReflectionClass($server); $reflection = new \ReflectionClass($server);
$prop = $reflection->getProperty('storages'); $prop = $reflection->getProperty('storages');
@@ -191,7 +174,7 @@ class ServerTest extends TestCase
public function testAddingClientStorageSetsClientCredentialsStorageByDefault() public function testAddingClientStorageSetsClientCredentialsStorageByDefault()
{ {
$server = new Server(); $server = new Server();
$memory = $this->getMock('OAuth2\Storage\Memory'); $memory = $this->createMock('OAuth2\Storage\Memory');
$server->addStorage($memory, 'client'); $server->addStorage($memory, 'client');
$client_credentials = $server->getStorage('client_credentials'); $client_credentials = $server->getStorage('client_credentials');
@@ -202,7 +185,7 @@ class ServerTest extends TestCase
public function testAddStorageWithNullValue() public function testAddStorageWithNullValue()
{ {
$memory = $this->getMock('OAuth2\Storage\Memory'); $memory = $this->createMock('OAuth2\Storage\Memory');
$server = new Server($memory); $server = new Server($memory);
$server->addStorage(null, 'refresh_token'); $server->addStorage(null, 'refresh_token');
@@ -218,7 +201,7 @@ class ServerTest extends TestCase
public function testNewServerWithNullStorageValue() public function testNewServerWithNullStorageValue()
{ {
$memory = $this->getMock('OAuth2\Storage\Memory'); $memory = $this->createMock('OAuth2\Storage\Memory');
$server = new Server(array( $server = new Server(array(
'client_credentials' => $memory, 'client_credentials' => $memory,
'refresh_token' => null, 'refresh_token' => null,
@@ -237,7 +220,7 @@ class ServerTest extends TestCase
public function testAddingClientCredentialsStorageSetsClientStorageByDefault() public function testAddingClientCredentialsStorageSetsClientStorageByDefault()
{ {
$server = new Server(); $server = new Server();
$memory = $this->getMock('OAuth2\Storage\Memory'); $memory = $this->createMock('OAuth2\Storage\Memory');
$server->addStorage($memory, 'client_credentials'); $server->addStorage($memory, 'client_credentials');
$client = $server->getStorage('client'); $client = $server->getStorage('client');
@@ -249,10 +232,9 @@ class ServerTest extends TestCase
public function testSettingClientStorageByDefaultDoesNotOverrideSetStorage() public function testSettingClientStorageByDefaultDoesNotOverrideSetStorage()
{ {
$server = new Server(); $server = new Server();
$pdo = $this->getMockBuilder('OAuth2\Storage\Pdo') $pdo = $this->createMock('OAuth2\Storage\Pdo');
->disableOriginalConstructor()->getMock();
$memory = $this->getMock('OAuth2\Storage\Memory'); $memory = $this->createMock('OAuth2\Storage\Memory');
$server->addStorage($pdo, 'client'); $server->addStorage($pdo, 'client');
$server->addStorage($memory, 'client_credentials'); $server->addStorage($memory, 'client_credentials');
@@ -266,7 +248,7 @@ class ServerTest extends TestCase
public function testAddingResponseType() public function testAddingResponseType()
{ {
$storage = $this->getMock('OAuth2\Storage\Memory'); $storage = $this->createMock('OAuth2\Storage\Memory');
$storage $storage
->expects($this->any()) ->expects($this->any())
->method('getClientDetails') ->method('getClientDetails')
@@ -323,7 +305,7 @@ class ServerTest extends TestCase
'code' => 'testcode', 'code' => 'testcode',
)); ));
// verify the mock clientAssertionType was called as expected // verify the mock clientAssertionType was called as expected
$clientAssertionType = $this->getMock('OAuth2\ClientAssertionType\ClientAssertionTypeInterface', array('validateRequest', 'getClientId')); $clientAssertionType = $this->createMock('OAuth2\ClientAssertionType\ClientAssertionTypeInterface');
$clientAssertionType $clientAssertionType
->expects($this->once()) ->expects($this->once())
->method('validateRequest') ->method('validateRequest')
@@ -420,98 +402,84 @@ class ServerTest extends TestCase
$this->assertFalse($used_token, 'the refresh token used is no longer valid'); $this->assertFalse($used_token, 'the refresh token used is no longer valid');
} }
/**
* @expectedException InvalidArgumentException OAuth2\ResponseType\AuthorizationCodeInterface
**/
public function testAddingUnknownResponseTypeThrowsException() public function testAddingUnknownResponseTypeThrowsException()
{ {
$this->expectExceptionMessage('OAuth2\ResponseType\AuthorizationCodeInterface');
$server = new Server(); $server = new Server();
$server->addResponseType($this->getMock('OAuth2\ResponseType\ResponseTypeInterface')); $server->addResponseType($this->createMock('OAuth2\ResponseType\ResponseTypeInterface'));
} }
/**
* @expectedException LogicException OAuth2\Storage\PublicKeyInterface
**/
public function testUsingJwtAccessTokensWithoutPublicKeyStorageThrowsException() public function testUsingJwtAccessTokensWithoutPublicKeyStorageThrowsException()
{ {
$this->expectExceptionMessage('OAuth2\Storage\PublicKeyInterface');
$server = new Server(array(), array('use_jwt_access_tokens' => true)); $server = new Server(array(), array('use_jwt_access_tokens' => true));
$server->addGrantType($this->getMock('OAuth2\GrantType\GrantTypeInterface')); $server->addGrantType($this->createMock('OAuth2\GrantType\GrantTypeInterface'));
$server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientCredentialsInterface'));
$server->addStorage($this->getMock('OAuth2\Storage\ClientCredentialsInterface')); $server->addStorage($this->createMock('OAuth2\Storage\ClientCredentialsInterface'));
$server->getTokenController(); $server->getTokenController();
} }
public function testUsingJustJwtAccessTokenStorageWithResourceControllerIsOkay() public function testUsingJustJwtAccessTokenStorageWithResourceControllerIsOkay()
{ {
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true));
$this->assertNotNull($server->getResourceController()); $this->assertNotNull($server->getResourceController());
$this->assertInstanceOf('OAuth2\Storage\PublicKeyInterface', $server->getStorage('public_key')); $this->assertInstanceOf('OAuth2\Storage\PublicKeyInterface', $server->getStorage('public_key'));
} }
/**
* @expectedException LogicException OAuth2\Storage\ClientInterface
**/
public function testUsingJustJwtAccessTokenStorageWithAuthorizeControllerThrowsException() public function testUsingJustJwtAccessTokenStorageWithAuthorizeControllerThrowsException()
{ {
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $this->expectExceptionMessage('OAuth2\Storage\ClientInterface');
$pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true));
$this->assertNotNull($server->getAuthorizeController()); $this->assertNotNull($server->getAuthorizeController());
} }
/**
* @expectedException LogicException grant_types
**/
public function testUsingJustJwtAccessTokenStorageWithTokenControllerThrowsException() public function testUsingJustJwtAccessTokenStorageWithTokenControllerThrowsException()
{ {
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $this->expectExceptionMessage('grant_types');
$pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$server = new Server(array($pubkey), array('use_jwt_access_tokens' => true)); $server = new Server(array($pubkey), array('use_jwt_access_tokens' => true));
$server->getTokenController(); $server->getTokenController();
} }
public function testUsingJwtAccessTokenAndClientStorageWithAuthorizeControllerIsOkay() public function testUsingJwtAccessTokenAndClientStorageWithAuthorizeControllerIsOkay()
{ {
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$client = $this->getMock('OAuth2\Storage\ClientInterface'); $client = $this->createMock('OAuth2\Storage\ClientInterface');
$server = new Server(array($pubkey, $client), array('use_jwt_access_tokens' => true, 'allow_implicit' => true)); $server = new Server(array($pubkey, $client), array('use_jwt_access_tokens' => true, 'allow_implicit' => true));
$this->assertNotNull($server->getAuthorizeController()); $this->assertNotNull($server->getAuthorizeController());
$this->assertInstanceOf('OAuth2\ResponseType\JwtAccessToken', $server->getResponseType('token')); $this->assertInstanceOf('OAuth2\ResponseType\JwtAccessToken', $server->getResponseType('token'));
} }
/**
* @expectedException LogicException UserClaims
**/
public function testUsingOpenIDConnectWithoutUserClaimsThrowsException() public function testUsingOpenIDConnectWithoutUserClaimsThrowsException()
{ {
$client = $this->getMock('OAuth2\Storage\ClientInterface'); $this->expectExceptionMessage('UserClaims');
$client = $this->createMock('OAuth2\Storage\ClientInterface');
$server = new Server($client, array('use_openid_connect' => true)); $server = new Server($client, array('use_openid_connect' => true));
$server->getAuthorizeController(); $server->getAuthorizeController();
} }
/**
* @expectedException LogicException PublicKeyInterface
**/
public function testUsingOpenIDConnectWithoutPublicKeyThrowsException() public function testUsingOpenIDConnectWithoutPublicKeyThrowsException()
{ {
$client = $this->getMock('OAuth2\Storage\ClientInterface'); $this->expectExceptionMessage('PublicKeyInterface');
$userclaims = $this->getMock('OAuth2\OPenID\Storage\UserClaimsInterface'); $client = $this->createMock('OAuth2\Storage\ClientInterface');
$userclaims = $this->createMock('OAuth2\OPenID\Storage\UserClaimsInterface');
$server = new Server(array($client, $userclaims), array('use_openid_connect' => true)); $server = new Server(array($client, $userclaims), array('use_openid_connect' => true));
$server->getAuthorizeController(); $server->getAuthorizeController();
} }
/**
* @expectedException LogicException issuer
**/
public function testUsingOpenIDConnectWithoutIssuerThrowsException() public function testUsingOpenIDConnectWithoutIssuerThrowsException()
{ {
$client = $this->getMock('OAuth2\Storage\ClientInterface'); $this->expectExceptionMessage('issuer');
$userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $client = $this->createMock('OAuth2\Storage\ClientInterface');
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $userclaims = $this->createMock('OAuth2\OpenID\Storage\UserClaimsInterface');
$pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$server = new Server(array($client, $userclaims, $pubkey), array('use_openid_connect' => true)); $server = new Server(array($client, $userclaims, $pubkey), array('use_openid_connect' => true));
$server->getAuthorizeController(); $server->getAuthorizeController();
@@ -519,9 +487,9 @@ class ServerTest extends TestCase
public function testUsingOpenIDConnectWithIssuerPublicKeyAndUserClaimsIsOkay() public function testUsingOpenIDConnectWithIssuerPublicKeyAndUserClaimsIsOkay()
{ {
$client = $this->getMock('OAuth2\Storage\ClientInterface'); $client = $this->createMock('OAuth2\Storage\ClientInterface');
$userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $userclaims = $this->createMock('OAuth2\OpenID\Storage\UserClaimsInterface');
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$server = new Server(array($client, $userclaims, $pubkey), array( $server = new Server(array($client, $userclaims, $pubkey), array(
'use_openid_connect' => true, 'use_openid_connect' => true,
'issuer' => 'someguy', 'issuer' => 'someguy',
@@ -533,14 +501,12 @@ class ServerTest extends TestCase
$this->assertNull($server->getResponseType('id_token token')); $this->assertNull($server->getResponseType('id_token token'));
} }
/**
* @expectedException LogicException OAuth2\ResponseType\AccessTokenInterface
**/
public function testUsingOpenIDConnectWithAllowImplicitWithoutTokenStorageThrowsException() public function testUsingOpenIDConnectWithAllowImplicitWithoutTokenStorageThrowsException()
{ {
$client = $this->getMock('OAuth2\Storage\ClientInterface'); $this->expectErrorMessage('OAuth2\ResponseType\AccessTokenInterface');
$userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $client = $this->createMock('OAuth2\Storage\ClientInterface');
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $userclaims = $this->createMock('OAuth2\OpenID\Storage\UserClaimsInterface');
$pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$server = new Server(array($client, $userclaims, $pubkey), array( $server = new Server(array($client, $userclaims, $pubkey), array(
'use_openid_connect' => true, 'use_openid_connect' => true,
'issuer' => 'someguy', 'issuer' => 'someguy',
@@ -552,9 +518,9 @@ class ServerTest extends TestCase
public function testUsingOpenIDConnectWithAllowImplicitAndUseJwtAccessTokensIsOkay() public function testUsingOpenIDConnectWithAllowImplicitAndUseJwtAccessTokensIsOkay()
{ {
$client = $this->getMock('OAuth2\Storage\ClientInterface'); $client = $this->createMock('OAuth2\Storage\ClientInterface');
$userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $userclaims = $this->createMock('OAuth2\OpenID\Storage\UserClaimsInterface');
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$server = new Server(array($client, $userclaims, $pubkey), array( $server = new Server(array($client, $userclaims, $pubkey), array(
'use_openid_connect' => true, 'use_openid_connect' => true,
'issuer' => 'someguy', 'issuer' => 'someguy',
@@ -570,10 +536,10 @@ class ServerTest extends TestCase
public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenStorageIsOkay() public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenStorageIsOkay()
{ {
$client = $this->getMock('OAuth2\Storage\ClientInterface'); $client = $this->createMock('OAuth2\Storage\ClientInterface');
$userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $userclaims = $this->createMock('OAuth2\OpenID\Storage\UserClaimsInterface');
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); $token = $this->createMock('OAuth2\Storage\AccessTokenInterface');
$server = new Server(array($client, $userclaims, $pubkey, $token), array( $server = new Server(array($client, $userclaims, $pubkey, $token), array(
'use_openid_connect' => true, 'use_openid_connect' => true,
'issuer' => 'someguy', 'issuer' => 'someguy',
@@ -588,17 +554,17 @@ class ServerTest extends TestCase
public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenResponseTypeIsOkay() public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenResponseTypeIsOkay()
{ {
$client = $this->getMock('OAuth2\Storage\ClientInterface'); $client = $this->createMock('OAuth2\Storage\ClientInterface');
$userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $userclaims = $this->createMock('OAuth2\OpenID\Storage\UserClaimsInterface');
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
// $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); // $token = $this->createMock('OAuth2\Storage\AccessTokenInterface');
$server = new Server(array($client, $userclaims, $pubkey), array( $server = new Server(array($client, $userclaims, $pubkey), array(
'use_openid_connect' => true, 'use_openid_connect' => true,
'issuer' => 'someguy', 'issuer' => 'someguy',
'allow_implicit' => true, 'allow_implicit' => true,
)); ));
$token = $this->getMock('OAuth2\ResponseType\AccessTokenInterface'); $token = $this->createMock('OAuth2\ResponseType\AccessTokenInterface');
$server->addResponseType($token, 'token'); $server->addResponseType($token, 'token');
$server->getAuthorizeController(); $server->getAuthorizeController();
@@ -607,17 +573,15 @@ class ServerTest extends TestCase
$this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token')); $this->assertInstanceOf('OAuth2\OpenID\ResponseType\IdTokenTokenInterface', $server->getResponseType('id_token token'));
} }
/**
* @expectedException LogicException OAuth2\OpenID\Storage\AuthorizationCodeInterface
**/
public function testUsingOpenIDConnectWithAuthorizationCodeStorageThrowsException() public function testUsingOpenIDConnectWithAuthorizationCodeStorageThrowsException()
{ {
$client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface'); $client = $this->createMock('OAuth2\Storage\ClientCredentialsInterface');
$userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $userclaims = $this->createMock('OAuth2\OpenID\Storage\UserClaimsInterface');
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); $token = $this->createMock('OAuth2\Storage\AccessTokenInterface');
$authcode = $this->getMock('OAuth2\Storage\AuthorizationCodeInterface'); $authcode = $this->createMock('OAuth2\Storage\AuthorizationCodeInterface');
$this->expectErrorMessage('OAuth2\OpenID\Storage\AuthorizationCodeInterface');
$server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array( $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array(
'use_openid_connect' => true, 'use_openid_connect' => true,
'issuer' => 'someguy' 'issuer' => 'someguy'
@@ -630,11 +594,11 @@ class ServerTest extends TestCase
public function testUsingOpenIDConnectWithOpenIDAuthorizationCodeStorageCreatesOpenIDAuthorizationCodeGrantType() public function testUsingOpenIDConnectWithOpenIDAuthorizationCodeStorageCreatesOpenIDAuthorizationCodeGrantType()
{ {
$client = $this->getMock('OAuth2\Storage\ClientCredentialsInterface'); $client = $this->createMock('OAuth2\Storage\ClientCredentialsInterface');
$userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $userclaims = $this->createMock('OAuth2\OpenID\Storage\UserClaimsInterface');
$pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $pubkey = $this->createMock('OAuth2\Storage\PublicKeyInterface');
$token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); $token = $this->createMock('OAuth2\Storage\AccessTokenInterface');
$authcode = $this->getMock('OAuth2\OpenID\Storage\AuthorizationCodeInterface'); $authcode = $this->createMock('OAuth2\OpenID\Storage\AuthorizationCodeInterface');
$server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array( $server = new Server(array($client, $userclaims, $pubkey, $token, $authcode), array(
'use_openid_connect' => true, 'use_openid_connect' => true,
@@ -648,7 +612,7 @@ class ServerTest extends TestCase
public function testMultipleValuedResponseTypeOrderDoesntMatter() public function testMultipleValuedResponseTypeOrderDoesntMatter()
{ {
$responseType = $this->getMock('OAuth2\OpenID\ResponseType\IdTokenTokenInterface'); $responseType = $this->createMock('OAuth2\OpenID\ResponseType\IdTokenTokenInterface');
$server = new Server(array(), array(), array(), array( $server = new Server(array(), array(), array(), array(
'token id_token' => $responseType, 'token id_token' => $responseType,
)); ));
@@ -659,7 +623,7 @@ class ServerTest extends TestCase
public function testAddGrantTypeWithoutKey() public function testAddGrantTypeWithoutKey()
{ {
$server = new Server(); $server = new Server();
$server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface'))); $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->createMock('OAuth2\Storage\AuthorizationCodeInterface')));
$grantTypes = $server->getGrantTypes(); $grantTypes = $server->getGrantTypes();
$this->assertEquals('authorization_code', key($grantTypes)); $this->assertEquals('authorization_code', key($grantTypes));
@@ -668,7 +632,7 @@ class ServerTest extends TestCase
public function testAddGrantTypeWithKey() public function testAddGrantTypeWithKey()
{ {
$server = new Server(); $server = new Server();
$server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 'ac'); $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->createMock('OAuth2\Storage\AuthorizationCodeInterface')), 'ac');
$grantTypes = $server->getGrantTypes(); $grantTypes = $server->getGrantTypes();
$this->assertEquals('ac', key($grantTypes)); $this->assertEquals('ac', key($grantTypes));
@@ -677,7 +641,7 @@ class ServerTest extends TestCase
public function testAddGrantTypeWithKeyNotString() public function testAddGrantTypeWithKeyNotString()
{ {
$server = new Server(); $server = new Server();
$server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->getMock('OAuth2\Storage\AuthorizationCodeInterface')), 42); $server->addGrantType(new \OAuth2\GrantType\AuthorizationCode($this->createMock('OAuth2\Storage\AuthorizationCodeInterface')), 42);
$grantTypes = $server->getGrantTypes(); $grantTypes = $server->getGrantTypes();
$this->assertEquals('authorization_code', key($grantTypes)); $this->assertEquals('authorization_code', key($grantTypes));
@@ -2,8 +2,12 @@
namespace OAuth2\Storage; namespace OAuth2\Storage;
use Yoast\PHPUnitPolyfills\Polyfills\ExpectPHPException;
class PdoTest extends BaseTest class PdoTest extends BaseTest
{ {
use ExpectPHPException;
public function testCreatePdoStorageUsingPdoClass() public function testCreatePdoStorageUsingPdoClass()
{ {
$dsn = sprintf('sqlite:%s', Bootstrap::getInstance()->getSqliteDir()); $dsn = sprintf('sqlite:%s', Bootstrap::getInstance()->getSqliteDir());
@@ -30,11 +34,9 @@ class PdoTest extends BaseTest
$this->assertNotNull($storage->getClientDetails('oauth_test_client')); $this->assertNotNull($storage->getClientDetails('oauth_test_client'));
} }
/**
* @expectedException InvalidArgumentException dsn
*/
public function testCreatePdoStorageWithoutDSNThrowsException() public function testCreatePdoStorageWithoutDSNThrowsException()
{ {
$this->expectErrorMessage('dsn');
$config = array('username' => 'brent', 'password' => 'brentisaballer'); $config = array('username' => 'brent', 'password' => 'brentisaballer');
$storage = new Pdo($config); $storage = new Pdo($config);
} }
@@ -35,6 +35,17 @@ class BearerTest extends TestCase
$this->assertEquals($response->getParameter('error_description'), 'The content type for POST requests must be "application/x-www-form-urlencoded"'); $this->assertEquals($response->getParameter('error_description'), 'The content type for POST requests must be "application/x-www-form-urlencoded"');
} }
public function testMissingContentTypeExpectsToBeCorrectContent()
{
$bearer = new Bearer();
$request = TestRequest::createPost(array(
'access_token' => 'ThisIsMyAccessToken'
));
$param = $bearer->getAccessTokenParameter($request, $response = new Response());
$this->assertEquals($param, 'ThisIsMyAccessToken');
}
public function testValidRequestUsingAuthorizationHeader() public function testValidRequestUsingAuthorizationHeader()
{ {
$bearer = new Bearer(); $bearer = new Bearer();
@@ -1,15 +0,0 @@
<?php
require_once(dirname(__FILE__).'/../src/OAuth2/Autoloader.php');
OAuth2\Autoloader::register();
// register test classes
OAuth2\Autoloader::register(dirname(__FILE__).'/lib');
// register vendors if possible
if (file_exists(__DIR__.'/../vendor/autoload.php')) {
require_once(__DIR__.'/../vendor/autoload.php');
}
// remove the dynamoDB database that was created for this build
OAuth2\Storage\Bootstrap::getInstance()->cleanupTravisDynamoDb();
@@ -68,7 +68,7 @@ class Bootstrap
public function getPostgresDriver() public function getPostgresDriver()
{ {
try { try {
$pdo = new \PDO('pgsql:host=localhost;dbname=oauth2_server_php', 'postgres'); $pdo = new \PDO('pgsql:host=localhost;dbname=oauth2_server_php', 'postgres', 'postgres');
return $pdo; return $pdo;
} catch (\PDOException $e) { } catch (\PDOException $e) {
@@ -118,7 +118,7 @@ class Bootstrap
if (!$this->mysql) { if (!$this->mysql) {
$pdo = null; $pdo = null;
try { try {
$pdo = new \PDO('mysql:host=localhost;', 'root'); $pdo = new \PDO('mysql:host=localhost;', 'root', 'root');
} catch (\PDOException $e) { } catch (\PDOException $e) {
$this->mysql = new NullStorage('MySQL', 'Unable to connect to MySQL on root@localhost'); $this->mysql = new NullStorage('MySQL', 'Unable to connect to MySQL on root@localhost');
} }
@@ -352,11 +352,11 @@ class Bootstrap
private function createPostgresDb() private function createPostgresDb()
{ {
if (!`psql postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='postgres'"`) { if (!`PGPASSWORD=postgres psql postgres -tAc "SELECT 1 FROM pg_roles WHERE rolname='postgres'" -h localhost -U postgres`) {
`createuser -s -r postgres`; `PGPASSWORD=postgres createuser -s -r postgres -h localhost -U postgres`;
} }
`createdb -O postgres oauth2_server_php`; `PGPASSWORD=postgres createdb -O postgres oauth2_server_php -h localhost -U postgres`;
} }
private function populatePostgresDb(\PDO $pdo) private function populatePostgresDb(\PDO $pdo)
@@ -366,8 +366,8 @@ class Bootstrap
private function removePostgresDb() private function removePostgresDb()
{ {
if (trim(`psql -l | grep oauth2_server_php | wc -l`)) { if (trim(`PGPASSWORD=postgres psql -l -h localhost -U postgres | grep oauth2_server_php | wc -l`)) {
`dropdb oauth2_server_php`; `PGPASSWORD=postgres dropdb oauth2_server_php -h localhost -U postgres`;
} }
} }
@@ -945,21 +945,6 @@ class Bootstrap
)); ));
} }
public function cleanupTravisDynamoDb($prefix = null)
{
if (is_null($prefix)) {
// skip this when not applicable
if (!$this->getEnvVar('TRAVIS') || self::DYNAMODB_PHP_VERSION != $this->getEnvVar('TRAVIS_PHP_VERSION')) {
return;
}
$prefix = sprintf('build_%s_', $this->getEnvVar('TRAVIS_JOB_NUMBER'));
}
$client = $this->getDynamoDbClient();
$this->deleteDynamoDb($client, $prefix);
}
private function getEnvVar($var, $default = null) private function getEnvVar($var, $default = null)
{ {
return isset($_SERVER[$var]) ? $_SERVER[$var] : (getenv($var) ?: $default); return isset($_SERVER[$var]) ? $_SERVER[$var] : (getenv($var) ?: $default);
@@ -4,7 +4,7 @@ Types to use Carbon in Doctrine
## Documentation ## Documentation
[Check how to use in the official Carbon documentation](https://carbon.nesbot.com/symfony/) [Check how to use in the official Carbon documentation](https://carbon.nesbot.com/guide/getting-started/symfony.html)
This package is an externalization of [src/Carbon/Doctrine](https://github.com/briannesbitt/Carbon/tree/2.71.0/src/Carbon/Doctrine) This package is an externalization of [src/Carbon/Doctrine](https://github.com/briannesbitt/Carbon/tree/2.71.0/src/Carbon/Doctrine)
from `nestbot/carbon` package. from `nestbot/carbon` package.
@@ -32,5 +32,10 @@
"email": "kylekatarnls@gmail.com" "email": "kylekatarnls@gmail.com"
} }
], ],
"minimum-stability": "dev" "minimum-stability": "dev",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
}
} }
@@ -4,13 +4,14 @@ declare(strict_types=1);
namespace Carbon\Doctrine; namespace Carbon\Doctrine;
use Carbon\CarbonInterface;
use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Platforms\AbstractPlatform;
interface CarbonDoctrineType interface CarbonDoctrineType
{ {
public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform); public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string;
public function convertToPHPValue(mixed $value, AbstractPlatform $platform); public function convertToPHPValue(mixed $value, AbstractPlatform $platform): ?CarbonInterface;
public function convertToDatabaseValue($value, AbstractPlatform $platform); public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string;
} }
@@ -62,7 +62,7 @@ trait CarbonTypeConverter
/** /**
* @SuppressWarnings(PHPMD.UnusedFormalParameter) * @SuppressWarnings(PHPMD.UnusedFormalParameter)
*/ */
public function convertToDatabaseValue($value, AbstractPlatform $platform): ?string public function convertToDatabaseValue(mixed $value, AbstractPlatform $platform): ?string
{ {
if ($value === null) { if ($value === null) {
return $value; return $value;
@@ -79,11 +79,11 @@ trait CarbonTypeConverter
); );
} }
private function doConvertToPHPValue(mixed $value) private function doConvertToPHPValue(mixed $value): ?CarbonInterface
{ {
$class = $this->getCarbonClassName(); $class = $this->getCarbonClassName();
if ($value === null || is_a($value, $class)) { if ($value === null || (is_object($value) && is_a($value, $class))) {
return $value; return $value;
} }
+19 -1
View File
@@ -26,6 +26,12 @@ use Composer\Semver\VersionParser;
*/ */
class InstalledVersions class InstalledVersions
{ {
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/** /**
* @var mixed[]|null * @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null * @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
@@ -322,6 +328,18 @@ class InstalledVersions
self::$installedIsLocalDir = false; self::$installedIsLocalDir = false;
} }
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/** /**
* @return array[] * @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}> * @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
@@ -336,7 +354,7 @@ class InstalledVersions
$copiedLocalDir = false; $copiedLocalDir = false;
if (self::$canGetVendors) { if (self::$canGetVendors) {
$selfDir = strtr(__DIR__, '\\', '/'); $selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) { foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/'); $vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) { if (isset(self::$installedByVendor[$vendorDir])) {
+35 -2
View File
@@ -6,12 +6,45 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir); $baseDir = dirname($vendorDir);
return array( return array(
'AllowDynamicProperties' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php',
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', 'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'CURLStringFile' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'DateError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
'DateException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
'DateInvalidOperationException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
'DateInvalidTimeZoneException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php',
'DateMalformedIntervalStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php',
'DateMalformedPeriodStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php',
'DateMalformedStringException' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php',
'DateObjectError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php',
'DateRangeError' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php',
'DelayedTargetValidation' => $vendorDir . '/symfony/polyfill-php85/Resources/stubs/DelayedTargetValidation.php',
'Deprecated' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php',
'Filter\\FilterException' => $vendorDir . '/symfony/polyfill-php85/Resources/stubs/Filter/FilterException.php',
'Filter\\FilterFailedException' => $vendorDir . '/symfony/polyfill-php85/Resources/stubs/Filter/FilterFailedException.php',
'NoDiscard' => $vendorDir . '/symfony/polyfill-php85/Resources/stubs/NoDiscard.php',
'Override' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/Override.php',
'Pdo\\Dblib' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Pdo/Dblib.php',
'Pdo\\Firebird' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Pdo/Firebird.php',
'Pdo\\Mysql' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Pdo/Mysql.php',
'Pdo\\Odbc' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Pdo/Odbc.php',
'Pdo\\Pgsql' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Pdo/Pgsql.php',
'Pdo\\Sqlite' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/Pdo/Sqlite.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', 'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'ReturnTypeWillChange' => $vendorDir . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php', 'Random\\BrokenRandomEngineError' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php',
'Random\\CryptoSafeEngine' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php',
'Random\\Engine' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/Engine.php',
'Random\\Engine\\Secure' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php',
'Random\\RandomError' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php',
'Random\\RandomException' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php',
'ReflectionConstant' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
'RoundingMode' => $vendorDir . '/symfony/polyfill-php84/Resources/stubs/RoundingMode.php',
'SQLite3Exception' => $vendorDir . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
'SensitiveParameter' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php',
'SensitiveParameterValue' => $vendorDir . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'Time\\Duration' => $vendorDir . '/symfony/polyfill-time/Resources/stubs/Time/Duration.php',
'Time\\TimeException' => $vendorDir . '/symfony/polyfill-time/Resources/stubs/Time/TimeException.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', 'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', 'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
); );
+7 -5
View File
@@ -6,16 +6,18 @@ $vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir); $baseDir = dirname($vendorDir);
return array( return array(
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
'5897ea0ac4cccf14d323035e65887801' => $vendorDir . '/symfony/polyfill-php82/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php', '6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => $vendorDir . '/symfony/polyfill-mbstring/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => $vendorDir . '/ralouphie/getallheaders/src/getallheaders.php', '662a729f963d39afe703c9d9b7ab4a8c' => $vendorDir . '/symfony/polyfill-php83/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => $vendorDir . '/guzzlehttp/promises/src/functions_include.php', '2203a247e6fda86070a5e4e07aed533a' => $vendorDir . '/symfony/clock/Resources/now.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => $vendorDir . '/guzzlehttp/guzzle/src/functions_include.php', '9d2b9fc6db0f153a0a149fefb182415e' => $vendorDir . '/symfony/polyfill-php84/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php', '606a39d89246991a373564698c2d8383' => $vendorDir . '/symfony/polyfill-php85/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php', 'a1105708a18b76903365ca1c4aa61b02' => $vendorDir . '/symfony/translation/Resources/functions.php',
'23f09fe3194f8c2f70923f90d6702129' => $vendorDir . '/illuminate/collections/functions.php',
'60799491728b879e74601d83e38b2cad' => $vendorDir . '/illuminate/collections/helpers.php', '60799491728b879e74601d83e38b2cad' => $vendorDir . '/illuminate/collections/helpers.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => $vendorDir . '/symfony/polyfill-ctype/bootstrap.php',
'23c18046f52bef3eea034657bafda50f' => $vendorDir . '/symfony/polyfill-php81/bootstrap.php',
'04c6c5c2f7095ccf6c481d3e53e1776f' => $vendorDir . '/mustangostang/spyc/Spyc.php', '04c6c5c2f7095ccf6c481d3e53e1776f' => $vendorDir . '/mustangostang/spyc/Spyc.php',
'89efb1254ef2d1c5d80096acd12c4098' => $vendorDir . '/twig/twig/src/Resources/core.php', '89efb1254ef2d1c5d80096acd12c4098' => $vendorDir . '/twig/twig/src/Resources/core.php',
'ffecb95d45175fd40f75be8a23b34f90' => $vendorDir . '/twig/twig/src/Resources/debug.php', 'ffecb95d45175fd40f75be8a23b34f90' => $vendorDir . '/twig/twig/src/Resources/debug.php',
+6 -1
View File
@@ -7,12 +7,17 @@ $baseDir = dirname($vendorDir);
return array( return array(
'Twig\\' => array($vendorDir . '/twig/twig/src'), 'Twig\\' => array($vendorDir . '/twig/twig/src'),
'Symfony\\Polyfill\\Php81\\' => array($vendorDir . '/symfony/polyfill-php81'), 'Symfony\\Polyfill\\Time\\' => array($vendorDir . '/symfony/polyfill-time'),
'Symfony\\Polyfill\\Php85\\' => array($vendorDir . '/symfony/polyfill-php85'),
'Symfony\\Polyfill\\Php84\\' => array($vendorDir . '/symfony/polyfill-php84'),
'Symfony\\Polyfill\\Php83\\' => array($vendorDir . '/symfony/polyfill-php83'),
'Symfony\\Polyfill\\Php82\\' => array($vendorDir . '/symfony/polyfill-php82'),
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'), 'Symfony\\Polyfill\\Mbstring\\' => array($vendorDir . '/symfony/polyfill-mbstring'),
'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'), 'Symfony\\Polyfill\\Ctype\\' => array($vendorDir . '/symfony/polyfill-ctype'),
'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'), 'Symfony\\Contracts\\Translation\\' => array($vendorDir . '/symfony/translation-contracts'),
'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'), 'Symfony\\Component\\Translation\\' => array($vendorDir . '/symfony/translation'),
'Symfony\\Component\\Clock\\' => array($vendorDir . '/symfony/clock'),
'Stevenmaguire\\OAuth2\\Client\\' => array($vendorDir . '/stevenmaguire/oauth2-keycloak/src'), 'Stevenmaguire\\OAuth2\\Client\\' => array($vendorDir . '/stevenmaguire/oauth2-keycloak/src'),
'RobThree\\Auth\\' => array($vendorDir . '/robthree/twofactorauth/lib'), 'RobThree\\Auth\\' => array($vendorDir . '/robthree/twofactorauth/lib'),
'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'), 'Psr\\SimpleCache\\' => array($vendorDir . '/psr/simple-cache/src'),
+70 -10
View File
@@ -7,16 +7,18 @@ namespace Composer\Autoload;
class ComposerStaticInit873464e4bd965a3168f133248b1b218b class ComposerStaticInit873464e4bd965a3168f133248b1b218b
{ {
public static $files = array ( public static $files = array (
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
'5897ea0ac4cccf14d323035e65887801' => __DIR__ . '/..' . '/symfony/polyfill-php82/bootstrap.php',
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php', '6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php', '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . '/symfony/polyfill-mbstring/bootstrap.php',
'7b11c4dc42b3b3023073cb14e519683c' => __DIR__ . '/..' . '/ralouphie/getallheaders/src/getallheaders.php', '662a729f963d39afe703c9d9b7ab4a8c' => __DIR__ . '/..' . '/symfony/polyfill-php83/bootstrap.php',
'c964ee0ededf28c96ebd9db5099ef910' => __DIR__ . '/..' . '/guzzlehttp/promises/src/functions_include.php', '2203a247e6fda86070a5e4e07aed533a' => __DIR__ . '/..' . '/symfony/clock/Resources/now.php',
'37a3dc5111fe8f707ab4c132ef1dbc62' => __DIR__ . '/..' . '/guzzlehttp/guzzle/src/functions_include.php', '9d2b9fc6db0f153a0a149fefb182415e' => __DIR__ . '/..' . '/symfony/polyfill-php84/bootstrap.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php', '606a39d89246991a373564698c2d8383' => __DIR__ . '/..' . '/symfony/polyfill-php85/bootstrap.php',
'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php', 'a1105708a18b76903365ca1c4aa61b02' => __DIR__ . '/..' . '/symfony/translation/Resources/functions.php',
'23f09fe3194f8c2f70923f90d6702129' => __DIR__ . '/..' . '/illuminate/collections/functions.php',
'60799491728b879e74601d83e38b2cad' => __DIR__ . '/..' . '/illuminate/collections/helpers.php', '60799491728b879e74601d83e38b2cad' => __DIR__ . '/..' . '/illuminate/collections/helpers.php',
'320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php', '320cde22f66dd4f5d3fd621d3e88b98f' => __DIR__ . '/..' . '/symfony/polyfill-ctype/bootstrap.php',
'23c18046f52bef3eea034657bafda50f' => __DIR__ . '/..' . '/symfony/polyfill-php81/bootstrap.php',
'04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php', '04c6c5c2f7095ccf6c481d3e53e1776f' => __DIR__ . '/..' . '/mustangostang/spyc/Spyc.php',
'89efb1254ef2d1c5d80096acd12c4098' => __DIR__ . '/..' . '/twig/twig/src/Resources/core.php', '89efb1254ef2d1c5d80096acd12c4098' => __DIR__ . '/..' . '/twig/twig/src/Resources/core.php',
'ffecb95d45175fd40f75be8a23b34f90' => __DIR__ . '/..' . '/twig/twig/src/Resources/debug.php', 'ffecb95d45175fd40f75be8a23b34f90' => __DIR__ . '/..' . '/twig/twig/src/Resources/debug.php',
@@ -31,12 +33,17 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
), ),
'S' => 'S' =>
array ( array (
'Symfony\\Polyfill\\Php81\\' => 23, 'Symfony\\Polyfill\\Time\\' => 22,
'Symfony\\Polyfill\\Php85\\' => 23,
'Symfony\\Polyfill\\Php84\\' => 23,
'Symfony\\Polyfill\\Php83\\' => 23,
'Symfony\\Polyfill\\Php82\\' => 23,
'Symfony\\Polyfill\\Php80\\' => 23, 'Symfony\\Polyfill\\Php80\\' => 23,
'Symfony\\Polyfill\\Mbstring\\' => 26, 'Symfony\\Polyfill\\Mbstring\\' => 26,
'Symfony\\Polyfill\\Ctype\\' => 23, 'Symfony\\Polyfill\\Ctype\\' => 23,
'Symfony\\Contracts\\Translation\\' => 30, 'Symfony\\Contracts\\Translation\\' => 30,
'Symfony\\Component\\Translation\\' => 30, 'Symfony\\Component\\Translation\\' => 30,
'Symfony\\Component\\Clock\\' => 24,
'Stevenmaguire\\OAuth2\\Client\\' => 28, 'Stevenmaguire\\OAuth2\\Client\\' => 28,
), ),
'R' => 'R' =>
@@ -105,9 +112,25 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
array ( array (
0 => __DIR__ . '/..' . '/twig/twig/src', 0 => __DIR__ . '/..' . '/twig/twig/src',
), ),
'Symfony\\Polyfill\\Php81\\' => 'Symfony\\Polyfill\\Time\\' =>
array ( array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php81', 0 => __DIR__ . '/..' . '/symfony/polyfill-time',
),
'Symfony\\Polyfill\\Php85\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php85',
),
'Symfony\\Polyfill\\Php84\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php84',
),
'Symfony\\Polyfill\\Php83\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php83',
),
'Symfony\\Polyfill\\Php82\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php82',
), ),
'Symfony\\Polyfill\\Php80\\' => 'Symfony\\Polyfill\\Php80\\' =>
array ( array (
@@ -129,6 +152,10 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
array ( array (
0 => __DIR__ . '/..' . '/symfony/translation', 0 => __DIR__ . '/..' . '/symfony/translation',
), ),
'Symfony\\Component\\Clock\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/clock',
),
'Stevenmaguire\\OAuth2\\Client\\' => 'Stevenmaguire\\OAuth2\\Client\\' =>
array ( array (
0 => __DIR__ . '/..' . '/stevenmaguire/oauth2-keycloak/src', 0 => __DIR__ . '/..' . '/stevenmaguire/oauth2-keycloak/src',
@@ -253,12 +280,45 @@ class ComposerStaticInit873464e4bd965a3168f133248b1b218b
); );
public static $classMap = array ( public static $classMap = array (
'AllowDynamicProperties' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/AllowDynamicProperties.php',
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php', 'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'CURLStringFile' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/CURLStringFile.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'DateError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateError.php',
'DateException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateException.php',
'DateInvalidOperationException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidOperationException.php',
'DateInvalidTimeZoneException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateInvalidTimeZoneException.php',
'DateMalformedIntervalStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedIntervalStringException.php',
'DateMalformedPeriodStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedPeriodStringException.php',
'DateMalformedStringException' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateMalformedStringException.php',
'DateObjectError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateObjectError.php',
'DateRangeError' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/DateRangeError.php',
'DelayedTargetValidation' => __DIR__ . '/..' . '/symfony/polyfill-php85/Resources/stubs/DelayedTargetValidation.php',
'Deprecated' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Deprecated.php',
'Filter\\FilterException' => __DIR__ . '/..' . '/symfony/polyfill-php85/Resources/stubs/Filter/FilterException.php',
'Filter\\FilterFailedException' => __DIR__ . '/..' . '/symfony/polyfill-php85/Resources/stubs/Filter/FilterFailedException.php',
'NoDiscard' => __DIR__ . '/..' . '/symfony/polyfill-php85/Resources/stubs/NoDiscard.php',
'Override' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/Override.php',
'Pdo\\Dblib' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Pdo/Dblib.php',
'Pdo\\Firebird' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Pdo/Firebird.php',
'Pdo\\Mysql' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Pdo/Mysql.php',
'Pdo\\Odbc' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Pdo/Odbc.php',
'Pdo\\Pgsql' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Pdo/Pgsql.php',
'Pdo\\Sqlite' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/Pdo/Sqlite.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php', 'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'ReturnTypeWillChange' => __DIR__ . '/..' . '/symfony/polyfill-php81/Resources/stubs/ReturnTypeWillChange.php', 'Random\\BrokenRandomEngineError' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/BrokenRandomEngineError.php',
'Random\\CryptoSafeEngine' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/CryptoSafeEngine.php',
'Random\\Engine' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/Engine.php',
'Random\\Engine\\Secure' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/Engine/Secure.php',
'Random\\RandomError' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/RandomError.php',
'Random\\RandomException' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/Random/RandomException.php',
'ReflectionConstant' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/ReflectionConstant.php',
'RoundingMode' => __DIR__ . '/..' . '/symfony/polyfill-php84/Resources/stubs/RoundingMode.php',
'SQLite3Exception' => __DIR__ . '/..' . '/symfony/polyfill-php83/Resources/stubs/SQLite3Exception.php',
'SensitiveParameter' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameter.php',
'SensitiveParameterValue' => __DIR__ . '/..' . '/symfony/polyfill-php82/Resources/stubs/SensitiveParameterValue.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php', 'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'Time\\Duration' => __DIR__ . '/..' . '/symfony/polyfill-time/Resources/stubs/Time/Duration.php',
'Time\\TimeException' => __DIR__ . '/..' . '/symfony/polyfill-time/Resources/stubs/Time/TimeException.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php', 'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php', 'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
); );
File diff suppressed because it is too large. Load diff
+145 -118
View File
@@ -29,18 +29,18 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'bshaffer/oauth2-server-php' => array( 'bshaffer/oauth2-server-php' => array(
'pretty_version' => 'v1.11.1', 'pretty_version' => 'v1.14.2',
'version' => '1.11.1.0', 'version' => '1.14.2.0',
'reference' => '5a0c8000d4763b276919e2106f54eddda6bc50fa', 'reference' => '1c715c2d2f71254d1c683b8c89a0f16b61ccd0d6',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../bshaffer/oauth2-server-php', 'install_path' => __DIR__ . '/../bshaffer/oauth2-server-php',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'carbonphp/carbon-doctrine-types' => array( 'carbonphp/carbon-doctrine-types' => array(
'pretty_version' => '3.2.0', 'pretty_version' => '3.2.1',
'version' => '3.2.0.0', 'version' => '3.2.1.0',
'reference' => '18ba5ddfec8976260ead6e866180bd5d2f71aa1d', 'reference' => '5fa5eacafd9ef47c8c6ab9143fc901ba0194d3dc',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../carbonphp/carbon-doctrine-types', 'install_path' => __DIR__ . '/../carbonphp/carbon-doctrine-types',
'aliases' => array(), 'aliases' => array(),
@@ -56,18 +56,18 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ddeboer/imap' => array( 'ddeboer/imap' => array(
'pretty_version' => '1.13.1', 'pretty_version' => '1.19.0',
'version' => '1.13.1.0', 'version' => '1.19.0.0',
'reference' => '8b772d04b1deadb5df13782fb78c4b648f77496e', 'reference' => '30800b1cfeacc4add5bb418e40a8b6e95a8a04ac',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../ddeboer/imap', 'install_path' => __DIR__ . '/../ddeboer/imap',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'directorytree/ldaprecord' => array( 'directorytree/ldaprecord' => array(
'pretty_version' => 'v3.8.5', 'pretty_version' => 'v3.8.6',
'version' => '3.8.5.0', 'version' => '3.8.6.0',
'reference' => '00e5f088f8c4028d5f398783cccc2e8119a27a65', 'reference' => 'a1cab9f078eb4756fc6fad58caea1e3d11cad0ec',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../directorytree/ldaprecord', 'install_path' => __DIR__ . '/../directorytree/ldaprecord',
'aliases' => array(), 'aliases' => array(),
@@ -80,90 +80,90 @@
), ),
), ),
'firebase/php-jwt' => array( 'firebase/php-jwt' => array(
'pretty_version' => 'v6.5.0', 'pretty_version' => 'v7.2.1',
'version' => '6.5.0.0', 'version' => '7.2.1.0',
'reference' => 'e94e7353302b0c11ec3cfff7180cd0b1743975d2', 'reference' => '0501f5729b9e2448516674aff07c7d1727869b8e',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../firebase/php-jwt', 'install_path' => __DIR__ . '/../firebase/php-jwt',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'guzzlehttp/guzzle' => array( 'guzzlehttp/guzzle' => array(
'pretty_version' => '7.5.0', 'pretty_version' => '8.3.1',
'version' => '7.5.0.0', 'version' => '8.3.1.0',
'reference' => 'b50a2a1251152e43f6a37f0fa053e730a67d25ba', 'reference' => '7d9b1f430bb65ac664e120af227bf9a795ad6333',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/guzzle', 'install_path' => __DIR__ . '/../guzzlehttp/guzzle',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'guzzlehttp/promises' => array( 'guzzlehttp/promises' => array(
'pretty_version' => '1.5.2', 'pretty_version' => '3.1.0',
'version' => '1.5.2.0', 'version' => '3.1.0.0',
'reference' => 'b94b2807d85443f9719887892882d0329d1e2598', 'reference' => '094d0ec418eb3b1411ea271a180ec95cae6ae933',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/promises', 'install_path' => __DIR__ . '/../guzzlehttp/promises',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'guzzlehttp/psr7' => array( 'guzzlehttp/psr7' => array(
'pretty_version' => '2.4.5', 'pretty_version' => '3.2.0',
'version' => '2.4.5.0', 'version' => '3.2.0.0',
'reference' => '0454e12ef0cd597ccd2adb036f7bda4e7fface66', 'reference' => 'cdc139396207a5eba10c242d241fb014cb098390',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../guzzlehttp/psr7', 'install_path' => __DIR__ . '/../guzzlehttp/psr7',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'illuminate/collections' => array( 'illuminate/collections' => array(
'pretty_version' => 'v10.49.0', 'pretty_version' => 'v12.70.2',
'version' => '10.49.0.0', 'version' => '12.70.2.0',
'reference' => '6ae9c74fa92d4e1824d1b346cd435e8eacdc3232', 'reference' => '83313b009c4afb6f02dbc090bdb67809756eefa2',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/collections', 'install_path' => __DIR__ . '/../illuminate/collections',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'illuminate/conditionable' => array( 'illuminate/conditionable' => array(
'pretty_version' => 'v10.49.0', 'pretty_version' => 'v12.70.2',
'version' => '10.49.0.0', 'version' => '12.70.2.0',
'reference' => '47c700320b7a419f0d188d111f3bbed978fcbd3f', 'reference' => 'ec677967c1f2faf90b8428919124d2184a4c9b49',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/conditionable', 'install_path' => __DIR__ . '/../illuminate/conditionable',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'illuminate/contracts' => array( 'illuminate/contracts' => array(
'pretty_version' => 'v10.44.0', 'pretty_version' => 'v12.70.2',
'version' => '10.44.0.0', 'version' => '12.70.2.0',
'reference' => '8d7152c4a1f5d9cf7da3e8b71f23e4556f6138ac', 'reference' => 'c16fd7ba7d8e6b8f336639ceb6b7e1ff6ed0efb5',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/contracts', 'install_path' => __DIR__ . '/../illuminate/contracts',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'illuminate/macroable' => array( 'illuminate/macroable' => array(
'pretty_version' => 'v10.49.0', 'pretty_version' => 'v12.70.2',
'version' => '10.49.0.0', 'version' => '12.70.2.0',
'reference' => 'dff667a46ac37b634dcf68909d9d41e94dc97c27', 'reference' => 'e295d62d89dcdb87e2b1bd70dd14d074a0ed73cc',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../illuminate/macroable', 'install_path' => __DIR__ . '/../illuminate/macroable',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'league/oauth2-client' => array( 'league/oauth2-client' => array(
'pretty_version' => '2.7.0', 'pretty_version' => '2.9.1',
'version' => '2.7.0.0', 'version' => '2.9.1.0',
'reference' => '160d6274b03562ebeb55ed18399281d8118b76c8', 'reference' => '8cedfef9d01a8d1fd2ecfabb41734b17592c4b71',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../league/oauth2-client', 'install_path' => __DIR__ . '/../league/oauth2-client',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'matthiasmullie/minify' => array( 'matthiasmullie/minify' => array(
'pretty_version' => '1.3.66', 'pretty_version' => '1.3.75',
'version' => '1.3.66.0', 'version' => '1.3.75.0',
'reference' => '45fd3b0f1dfa2c965857c6d4a470bea52adc31a6', 'reference' => '76ba4a5f555fd7bf4aa408af608e991569076671',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../matthiasmullie/minify', 'install_path' => __DIR__ . '/../matthiasmullie/minify',
'aliases' => array(), 'aliases' => array(),
@@ -194,36 +194,27 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'nesbot/carbon' => array( 'nesbot/carbon' => array(
'pretty_version' => '2.72.3', 'pretty_version' => '3.15.1',
'version' => '2.72.3.0', 'version' => '3.15.1.0',
'reference' => '0c6fd108360c562f6e4fd1dedb8233b423e91c83', 'reference' => 'ce0e5636fae7d1e99856eb70cae4c719a284aa08',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../nesbot/carbon', 'install_path' => __DIR__ . '/../nesbot/carbon',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'paragonie/random_compat' => array(
'pretty_version' => 'v9.99.100',
'version' => '9.99.100.0',
'reference' => '996434e5492cb4c3edcb9168db6fbb1359ef965a',
'type' => 'library',
'install_path' => __DIR__ . '/../paragonie/random_compat',
'aliases' => array(),
'dev_requirement' => false,
),
'php-mime-mail-parser/php-mime-mail-parser' => array( 'php-mime-mail-parser/php-mime-mail-parser' => array(
'pretty_version' => '7.0.0', 'pretty_version' => '7.1.2',
'version' => '7.0.0.0', 'version' => '7.1.2.0',
'reference' => '9d09a017f3f103fec8456211a4a538b80e0eca0d', 'reference' => '81d84bbc6a73b913110e155e98059526825c7ee3',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../php-mime-mail-parser/php-mime-mail-parser', 'install_path' => __DIR__ . '/../php-mime-mail-parser/php-mime-mail-parser',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'phpmailer/phpmailer' => array( 'phpmailer/phpmailer' => array(
'pretty_version' => 'v6.6.0', 'pretty_version' => 'v6.12.0',
'version' => '6.6.0.0', 'version' => '6.12.0.0',
'reference' => 'e43bac82edc26ca04b36143a48bde1c051cfd5b1', 'reference' => 'd1ac35d784bf9f5e61b424901d5a014967f15b12',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../phpmailer/phpmailer', 'install_path' => __DIR__ . '/../phpmailer/phpmailer',
'aliases' => array(), 'aliases' => array(),
@@ -254,9 +245,9 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'psr/http-client' => array( 'psr/http-client' => array(
'pretty_version' => '1.0.1', 'pretty_version' => '1.0.3',
'version' => '1.0.1.0', 'version' => '1.0.3.0',
'reference' => '2dfb5f6c5eff0e91e20e913f8c5452ed95b86621', 'reference' => 'bb5906edc1c324c9a05aa0873d40117941e5fa90',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-client', 'install_path' => __DIR__ . '/../psr/http-client',
'aliases' => array(), 'aliases' => array(),
@@ -269,9 +260,9 @@
), ),
), ),
'psr/http-factory' => array( 'psr/http-factory' => array(
'pretty_version' => '1.0.1', 'pretty_version' => '1.1.0',
'version' => '1.0.1.0', 'version' => '1.1.0.0',
'reference' => '12ac7fcd07e5b077433f5f2bee95b3a771bf61be', 'reference' => '2b4765fddfe3b508ac62f829e852b1501d3f6e8a',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-factory', 'install_path' => __DIR__ . '/../psr/http-factory',
'aliases' => array(), 'aliases' => array(),
@@ -280,13 +271,13 @@
'psr/http-factory-implementation' => array( 'psr/http-factory-implementation' => array(
'dev_requirement' => false, 'dev_requirement' => false,
'provided' => array( 'provided' => array(
0 => '1.0', 0 => '1.1',
), ),
), ),
'psr/http-message' => array( 'psr/http-message' => array(
'pretty_version' => '1.0.1', 'pretty_version' => '2.0',
'version' => '1.0.1.0', 'version' => '2.0.0.0',
'reference' => 'f6561bf28d520154e4b0ec72be95418abe6d9363', 'reference' => '402d35bcb92c70c026d1a6a9883f06b2ead23d71',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../psr/http-message', 'install_path' => __DIR__ . '/../psr/http-message',
'aliases' => array(), 'aliases' => array(),
@@ -295,40 +286,31 @@
'psr/http-message-implementation' => array( 'psr/http-message-implementation' => array(
'dev_requirement' => false, 'dev_requirement' => false,
'provided' => array( 'provided' => array(
0 => '1.0', 0 => '2.0',
), ),
), ),
'psr/log' => array( 'psr/log' => array(
'pretty_version' => '3.0.0', 'pretty_version' => '3.0.2',
'version' => '3.0.0.0', 'version' => '3.0.2.0',
'reference' => 'fe5ea303b0887d5caefd3d431c3e61ad47037001', 'reference' => 'f16e1d5863e37f8d8c2a01719f5b34baa2b714d3',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../psr/log', 'install_path' => __DIR__ . '/../psr/log',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'psr/simple-cache' => array( 'psr/simple-cache' => array(
'pretty_version' => '2.0.0', 'pretty_version' => '3.0.0',
'version' => '2.0.0.0', 'version' => '3.0.0.0',
'reference' => '8707bf3cea6f710bf6ef05491234e3ab06f6432a', 'reference' => '764e0b3939f5ca87cb904f570ef9be2d78a07865',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../psr/simple-cache', 'install_path' => __DIR__ . '/../psr/simple-cache',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'ralouphie/getallheaders' => array(
'pretty_version' => '3.0.3',
'version' => '3.0.3.0',
'reference' => '120b605dfeb996808c31b6477290a714d356e822',
'type' => 'library',
'install_path' => __DIR__ . '/../ralouphie/getallheaders',
'aliases' => array(),
'dev_requirement' => false,
),
'robthree/twofactorauth' => array( 'robthree/twofactorauth' => array(
'pretty_version' => '1.8.1', 'pretty_version' => '1.8.2',
'version' => '1.8.1.0', 'version' => '1.8.2.0',
'reference' => '5afcb45282f1c75562a48d479ecd1732c9bdb11b', 'reference' => '65681de5a324eae05140ac58b08648a60212afc0',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../robthree/twofactorauth', 'install_path' => __DIR__ . '/../robthree/twofactorauth',
'aliases' => array(), 'aliases' => array(),
@@ -344,72 +326,117 @@
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'stevenmaguire/oauth2-keycloak' => array( 'stevenmaguire/oauth2-keycloak' => array(
'pretty_version' => '4.0.0', 'pretty_version' => '6.1.1',
'version' => '4.0.0.0', 'version' => '6.1.1.0',
'reference' => '05ead6bb6bcd2b6f96dfae87c769dcd3e5f6129d', 'reference' => '31bb3b1fa15b444212ed43facc898fafc7c2707a',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../stevenmaguire/oauth2-keycloak', 'install_path' => __DIR__ . '/../stevenmaguire/oauth2-keycloak',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/clock' => array(
'pretty_version' => 'v7.4.21',
'version' => '7.4.21.0',
'reference' => '07437f612dca89c1894c1db04491002b1e798f03',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/clock',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array( 'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.5.0', 'pretty_version' => 'v3.7.1',
'version' => '3.5.0.0', 'version' => '3.7.1.0',
'reference' => '0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1', 'reference' => 'f3202fa1b5097b0af062dc978b32ecf63404e31d',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts', 'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-ctype' => array( 'symfony/polyfill-ctype' => array(
'pretty_version' => 'v1.24.0', 'pretty_version' => 'v1.37.0',
'version' => '1.24.0.0', 'version' => '1.37.0.0',
'reference' => '30885182c981ab175d4d034db0f6f469898070ab', 'reference' => '141046a8f9477948ff284fa65be2095baafb94f2',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-ctype', 'install_path' => __DIR__ . '/../symfony/polyfill-ctype',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-mbstring' => array( 'symfony/polyfill-mbstring' => array(
'pretty_version' => 'v1.29.0', 'pretty_version' => 'v1.43.3',
'version' => '1.29.0.0', 'version' => '1.43.3.0',
'reference' => '9773676c8a1bb1f8d4340a62efe641cf76eda7ec', 'reference' => '122889ed24dc000165bcbfd77ec412f15cbdfc6a',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-mbstring', 'install_path' => __DIR__ . '/../symfony/polyfill-mbstring',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-php80' => array( 'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.29.0', 'pretty_version' => 'v1.43.0',
'version' => '1.29.0.0', 'version' => '1.43.0.0',
'reference' => '87b68208d5c1188808dd7839ee1e6c8ec3b02f1b', 'reference' => '9c6a5d6b01ca51d486c813e9a9c0ed55f208bb74',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80', 'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/polyfill-php81' => array( 'symfony/polyfill-php82' => array(
'pretty_version' => 'v1.31.0', 'pretty_version' => 'v1.43.0',
'version' => '1.31.0.0', 'version' => '1.43.0.0',
'reference' => '4a4cfc2d253c21a5ad0e53071df248ed48c6ce5c', 'reference' => '843bbe8b2a7934307b749751eede0f111f5d1cfc',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php81', 'install_path' => __DIR__ . '/../symfony/polyfill-php82',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php83' => array(
'pretty_version' => 'v1.43.0',
'version' => '1.43.0.0',
'reference' => '80ccff923a8d61f73ebe9da0aa94e25232f004ce',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php83',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php84' => array(
'pretty_version' => 'v1.43.0',
'version' => '1.43.0.0',
'reference' => '22c6548499cbe5c5bb072fc0c42312dff062a015',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php84',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php85' => array(
'pretty_version' => 'v1.43.0',
'version' => '1.43.0.0',
'reference' => '86a42cbcc07adc331481a5ab310a4024bfc227f6',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php85',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-time' => array(
'pretty_version' => 'v1.42.0',
'version' => '1.42.0.0',
'reference' => 'ac9f1ed802dd58f37e720388130c088434aa09fc',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-time',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/translation' => array( 'symfony/translation' => array(
'pretty_version' => 'v6.4.3', 'pretty_version' => 'v7.4.17',
'version' => '6.4.3.0', 'version' => '7.4.17.0',
'reference' => '637c51191b6b184184bbf98937702bcf554f7d04', 'reference' => '2ee1e4a3b32a528a642babe041ff7c440b213b4b',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/translation', 'install_path' => __DIR__ . '/../symfony/translation',
'aliases' => array(), 'aliases' => array(),
'dev_requirement' => false, 'dev_requirement' => false,
), ),
'symfony/translation-contracts' => array( 'symfony/translation-contracts' => array(
'pretty_version' => 'v3.4.1', 'pretty_version' => 'v3.7.1',
'version' => '3.4.1.0', 'version' => '3.7.1.0',
'reference' => '06450585bf65e978026bda220cdebca3f867fde7', 'reference' => 'ccb206b98faccc511ebae8e5fad50f2dc0b30621',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../symfony/translation-contracts', 'install_path' => __DIR__ . '/../symfony/translation-contracts',
'aliases' => array(), 'aliases' => array(),
@@ -422,9 +449,9 @@
), ),
), ),
'twig/twig' => array( 'twig/twig' => array(
'pretty_version' => 'v3.14.0', 'pretty_version' => 'v3.30.0',
'version' => '3.14.0.0', 'version' => '3.30.0.0',
'reference' => '126b2c97818dbff0cdf3fbfc881aedb3d40aae72', 'reference' => '8c737079b726af72ff8ef3c595be9f6a810ea1ef',
'type' => 'library', 'type' => 'library',
'install_path' => __DIR__ . '/../twig/twig', 'install_path' => __DIR__ . '/../twig/twig',
'aliases' => array(), 'aliases' => array(),
+4 -5
View File
@@ -4,8 +4,8 @@
$issues = array(); $issues = array();
if (!(PHP_VERSION_ID >= 80100)) { if (!(PHP_VERSION_ID >= 80200)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; $issues[] = 'Your Composer dependencies require a PHP version ">= 8.2.0". You are running ' . PHP_VERSION . '.';
} }
if ($issues) { if ($issues) {
@@ -19,8 +19,7 @@ if ($issues) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
} }
} }
trigger_error( throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues), 'Composer detected issues in your platform: ' . implode(' ', $issues)
E_USER_ERROR
); );
} }
+3 -3
View File
@@ -17,7 +17,7 @@ return (new PhpCsFixer\Config())
'binary_operator_spaces' => ['default' => 'align_single_space'], 'binary_operator_spaces' => ['default' => 'align_single_space'],
'blank_line_before_statement' => true, 'blank_line_before_statement' => true,
'class_definition' => ['single_item_single_line' => true], 'class_definition' => ['single_item_single_line' => true],
'compact_nullable_typehint' => true, 'compact_nullable_type_declaration' => true,
'concat_space' => ['spacing' => 'one'], 'concat_space' => ['spacing' => 'one'],
'echo_tag_syntax' => ['format' => 'long'], 'echo_tag_syntax' => ['format' => 'long'],
'error_suppression' => false, 'error_suppression' => false,
@@ -39,7 +39,7 @@ return (new PhpCsFixer\Config())
'no_null_property_initialization' => true, 'no_null_property_initialization' => true,
'no_php4_constructor' => true, 'no_php4_constructor' => true,
'no_superfluous_elseif' => true, 'no_superfluous_elseif' => true,
'no_unneeded_curly_braces' => true, 'no_unneeded_braces' => true,
'no_unneeded_final_method' => true, 'no_unneeded_final_method' => true,
'no_unreachable_default_argument_value' => true, 'no_unreachable_default_argument_value' => true,
'no_useless_else' => true, 'no_useless_else' => true,
@@ -49,7 +49,7 @@ return (new PhpCsFixer\Config())
'php_unit_set_up_tear_down_visibility' => true, 'php_unit_set_up_tear_down_visibility' => true,
'php_unit_strict' => true, 'php_unit_strict' => true,
'php_unit_test_annotation' => true, 'php_unit_test_annotation' => true,
'php_unit_test_case_static_method_calls' => true, 'php_unit_test_case_static_method_calls' => false,
'php_unit_test_class_requires_covers' => false, 'php_unit_test_class_requires_covers' => false,
'phpdoc_add_missing_param_annotation' => true, 'phpdoc_add_missing_param_annotation' => true,
'phpdoc_order' => true, 'phpdoc_order' => true,
+20 -22
View File
@@ -2,12 +2,12 @@
[![Latest Stable Version](https://img.shields.io/packagist/v/ddeboer/imap.svg)](https://packagist.org/packages/ddeboer/imap) [![Latest Stable Version](https://img.shields.io/packagist/v/ddeboer/imap.svg)](https://packagist.org/packages/ddeboer/imap)
[![Downloads](https://img.shields.io/packagist/dt/ddeboer/imap.svg)](https://packagist.org/packages/ddeboer/imap) [![Downloads](https://img.shields.io/packagist/dt/ddeboer/imap.svg)](https://packagist.org/packages/ddeboer/imap)
[![Integrate](https://github.com/ddeboer/imap/workflows/Integrate/badge.svg?branch=master)](https://github.com/ddeboer/imap/actions) [![Integrate](https://github.com/ddeboer/imap/workflows/CI/badge.svg)](https://github.com/ddeboer/imap/actions)
[![Code Coverage](https://codecov.io/gh/ddeboer/imap/coverage.svg?branch=master)](https://codecov.io/gh/ddeboer/imap?branch=master) [![Code Coverage](https://codecov.io/gh/ddeboer/imap/coverage.svg?branch=master)](https://codecov.io/gh/ddeboer/imap?branch=master)
A PHP IMAP library to read and process e-mails over IMAP protocol, built with robust Object-Oriented architecture. A PHP IMAP library to read and process e-mails over IMAP protocol, built with robust Object-Oriented architecture.
This library requires PHP >= 7.4 with [IMAP](https://www.php.net/manual/en/book.imap.php), This library requires PHP >= 8.2 with [IMAP](https://www.php.net/manual/en/book.imap.php),
[iconv](https://www.php.net/manual/en/book.iconv.php) and [iconv](https://www.php.net/manual/en/book.iconv.php) and
[Multibyte String](https://www.php.net/manual/en/book.mbstring.php) extensions installed. [Multibyte String](https://www.php.net/manual/en/book.mbstring.php) extensions installed.
@@ -157,7 +157,7 @@ By the way most of the common search criteria are available and functioning, bro
References: References:
1. https://stackoverflow.com/questions/36356715/imap-search-unknown-search-criterion-or 1. https://stackoverflow.com/questions/36356715/imap-search-unknown-search-criterion-or
1. imap-2007f.tar.gz: `./src/c-client/mail.c` and `./docs/internal.txt` 2. imap-2007f.tar.gz: `./src/c-client/mail.c` and `./docs/internal.txt`
#### Message Properties and Operations #### Message Properties and Operations
@@ -182,19 +182,29 @@ $message->isDraft();
$message->isSeen(); $message->isSeen();
``` ```
Get message headers as a [\Ddeboer\Imap\Message\Headers](/src/Ddeboer/Imap/Message/Headers.php) object: Get message headers as a [\Ddeboer\Imap\Message\Headers](/src/Message/Headers.php) object:
```php ```php
$message->getHeaders(); $message->getHeaders();
``` ```
Get message body as HTML or plain text: Get message body as HTML or plain text (only first part):
```php ```php
$message->getBodyHtml(); // Content of text/html part, if present $message->getBodyHtml(); // Content of text/html part, if present
$message->getBodyText(); // Content of text/plain part, if present $message->getBodyText(); // Content of text/plain part, if present
``` ```
Get complete body (all parts):
```php
$body = $message->getCompleteBodyHtml(); // Content of text/html part, if present
if ($body === null) { // If body is null, there are no HTML parts, so let's try getting the text body
$body = $message->getCompleteBodyText(); // Content of text/plain part, if present
}
```
Reading the message body keeps the message as unseen. Reading the message body keeps the message as unseen.
If you want to mark the message as seen: If you want to mark the message as seen:
@@ -284,24 +294,12 @@ Mockability is granted by interfaces present for each API.
Dig into [MockabilityTest](tests/MockabilityTest.php) for an example of a Dig into [MockabilityTest](tests/MockabilityTest.php) for an example of a
mocked workflow. mocked workflow.
## Running the Tests ## Contributing: run the build locally
This library is functionally tested on [Travis CI](https://travis-ci.org/ddeboer/imap) Docker is needed to run the build on your computer.
against a local Dovecot server.
If you have your own IMAP (test) account, you can run the tests locally by First command you need to run is `make start-imap-server`, which starts an IMAP server locally.
providing your IMAP credentials:
```bash Then the local build can be triggered with a bare `make`.
$ composer install
$ IMAP_SERVER_NAME="my.imap.server.com" IMAP_SERVER_PORT="60993" IMAP_USERNAME="johndoe" IMAP_PASSWORD="p4ssword" vendor/bin/phpunit
```
You can also copy `phpunit.xml.dist` file to a custom `phpunit.xml` and put When you finish the development, stop the local IMAP server with `make stop-imap-server`.
these environment variables in it.
**WARNING** Tests create new mailboxes without removing them.
```
$ docker-compose run tests
```
+9 -13
View File
@@ -22,19 +22,20 @@
} }
], ],
"require": { "require": {
"php": "^8.0.1", "php": "~8.2.0 || ~8.3.0",
"ext-dom": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-imap": "*", "ext-imap": "*",
"ext-libxml": "*",
"ext-mbstring": "*" "ext-mbstring": "*"
}, },
"require-dev": { "require-dev": {
"friendsofphp/php-cs-fixer": "^v3.4.0", "friendsofphp/php-cs-fixer": "^3.38.2",
"laminas/laminas-mail": "^2.15.1", "laminas/laminas-mail": "^2.25.1",
"malukenho/mcbumpface": "^1.1.5", "phpstan/phpstan": "^1.10.43",
"phpstan/phpstan": "^1.3.3", "phpstan/phpstan-phpunit": "^1.3.15",
"phpstan/phpstan-phpunit": "^1.0.0", "phpstan/phpstan-strict-rules": "^1.5.2",
"phpstan/phpstan-strict-rules": "^1.1.0", "phpunit/phpunit": "^10.4.2"
"phpunit/phpunit": "^9.5.11"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
@@ -45,10 +46,5 @@
"psr-4": { "psr-4": {
"Ddeboer\\Imap\\Tests\\": "tests/" "Ddeboer\\Imap\\Tests\\": "tests/"
} }
},
"config": {
"allow-plugins": {
"malukenho/mcbumpface": true
}
} }
} }
+3 -1
View File
@@ -50,9 +50,11 @@ final class Connection implements ConnectionInterface
public function close(int $flag = 0): bool public function close(int $flag = 0): bool
{ {
$stream = $this->resource->getStream();
$this->resource->clearLastMailboxUsedCache(); $this->resource->clearLastMailboxUsedCache();
return \imap_close($this->resource->getStream(), $flag); return \imap_close($stream, $flag);
} }
public function getQuota(string $root = 'INBOX'): array public function getQuota(string $root = 'INBOX'): array
@@ -6,7 +6,6 @@ namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\CreateMailboxException; use Ddeboer\Imap\Exception\CreateMailboxException;
use Ddeboer\Imap\Exception\DeleteMailboxException; use Ddeboer\Imap\Exception\DeleteMailboxException;
use Ddeboer\Imap\Exception\InvalidResourceException;
use Ddeboer\Imap\Exception\MailboxDoesNotExistException; use Ddeboer\Imap\Exception\MailboxDoesNotExistException;
/** /**
@@ -31,8 +30,6 @@ interface ConnectionInterface extends \Countable
/** /**
* Check if the connection is still active. * Check if the connection is still active.
*
* @throws InvalidResourceException If connection was closed
*/ */
public function ping(): bool; public function ping(): bool;
@@ -4,6 +4,6 @@ declare(strict_types=1);
namespace Ddeboer\Imap\Exception; namespace Ddeboer\Imap\Exception;
final class InvalidResourceException extends AbstractException final class RenameMailboxException extends AbstractException
{ {
} }
+5 -20
View File
@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Ddeboer\Imap; namespace Ddeboer\Imap;
use Ddeboer\Imap\Exception\InvalidResourceException;
use Ddeboer\Imap\Exception\ReopenMailboxException; use Ddeboer\Imap\Exception\ReopenMailboxException;
use IMAP\Connection; use IMAP\Connection;
@@ -13,33 +12,21 @@ use IMAP\Connection;
*/ */
final class ImapResource implements ImapResourceInterface final class ImapResource implements ImapResourceInterface
{ {
/** private Connection $resource;
* @var resource private ?MailboxInterface $mailbox;
*/
private $resource;
private ?MailboxInterface $mailbox = null;
private static ?string $lastMailboxUsedCache = null; private static ?string $lastMailboxUsedCache = null;
/** /**
* Constructor. * Constructor.
*
* @param Connection|resource $resource
*/ */
public function __construct($resource, MailboxInterface $mailbox = null) public function __construct(Connection $resource, MailboxInterface $mailbox = null)
{ {
$this->resource = $resource; $this->resource = $resource;
$this->mailbox = $mailbox; $this->mailbox = $mailbox;
} }
public function getStream() public function getStream(): Connection
{ {
if (
!$this->resource instanceof Connection
&& (false === \is_resource($this->resource) || 'imap' !== \get_resource_type($this->resource))
) {
throw new InvalidResourceException('Supplied resource is not a valid imap resource');
}
$this->initMailbox(); $this->initMailbox();
return $this->resource; return $this->resource;
@@ -76,10 +63,8 @@ final class ImapResource implements ImapResourceInterface
/** /**
* Check whether the current mailbox is open. * Check whether the current mailbox is open.
*
* @param resource $resource
*/ */
private static function isMailboxOpen(MailboxInterface $mailbox, $resource): bool private static function isMailboxOpen(MailboxInterface $mailbox, Connection $resource): bool
{ {
$currentMailboxName = $mailbox->getFullEncodedName(); $currentMailboxName = $mailbox->getFullEncodedName();
if ($currentMailboxName === self::$lastMailboxUsedCache) { if ($currentMailboxName === self::$lastMailboxUsedCache) {
Loaded 100 of 1346 files, more files were not shown because too many files have changed in this diff. Show more