diff --git a/.gitignore b/.gitignore index 6ccd199d..c61097e9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,4 @@ data/conf/nginx/server_name.active data/conf/postfix/sql data/conf/dovecot/sql data/web/inc/vars.local.inc.php -site/ data/assets/ssl diff --git a/README.md b/README.md index 025dc825..29857408 100644 --- a/README.md +++ b/README.md @@ -2,4 +2,4 @@ [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) -Please see [the official documentation](https://andryyy.github.io/mailcow-dockerized/) for instructions. +Please see [the official documentation](https://mailcow.github.io/mailcow-dockerized-docs/) for instructions. diff --git a/data/Dockerfiles/clamav/Dockerfile b/data/Dockerfiles/clamav/Dockerfile index 5fc44d9a..170b7d8f 100755 --- a/data/Dockerfiles/clamav/Dockerfile +++ b/data/Dockerfiles/clamav/Dockerfile @@ -1,8 +1,8 @@ -FROM debian:latest +FROM debian:stretch-slim MAINTAINER https://m-ko.de Markus Kosmal <code@cnfg.io> # Debian Base to use -ENV DEBIAN_VERSION jessie +ENV DEBIAN_VERSION stretch # initial install of av daemon RUN echo "deb http://http.debian.net/debian/ $DEBIAN_VERSION main contrib non-free" > /etc/apt/sources.list && \ @@ -13,15 +13,14 @@ RUN echo "deb http://http.debian.net/debian/ $DEBIAN_VERSION main contrib non-fr clamav-daemon \ clamav-freshclam \ libclamunrar7 \ - wget && \ + curl && \ apt-get clean && \ rm -rf /var/lib/apt/lists/* # initial update of av databases -RUN wget -O /var/lib/clamav/main.cvd http://db.local.clamav.net/main.cvd && \ - wget -O /var/lib/clamav/daily.cvd http://db.local.clamav.net/daily.cvd && \ - wget -O /var/lib/clamav/bytecode.cvd http://db.local.clamav.net/bytecode.cvd && \ - chown clamav:clamav /var/lib/clamav/*.cvd +COPY dl_files.sh /dl_files.sh +RUN chmod +x /dl_files.sh +RUN /dl_files.sh # permission juggling RUN mkdir /var/run/clamav && \ @@ -33,9 +32,6 @@ RUN sed -i 's/^Foreground .*$/Foreground true/g' /etc/clamav/clamd.conf && \ echo "TCPSocket 3310" >> /etc/clamav/clamd.conf && \ sed -i 's/^Foreground .*$/Foreground true/g' /etc/clamav/freshclam.conf -# volume provision -VOLUME ["/var/lib/clamav"] - # port provision EXPOSE 3310 diff --git a/data/Dockerfiles/clamav/bootstrap.sh b/data/Dockerfiles/clamav/bootstrap.sh index 635e93ea..bc5d1b32 100755 --- a/data/Dockerfiles/clamav/bootstrap.sh +++ b/data/Dockerfiles/clamav/bootstrap.sh @@ -1,35 +1,7 @@ #!/bin/bash -# bootstrap clam av service and clam av database updater shell script -# presented by mko (Markus Kosmal<code@cnfg.io>) -set -m +trap "kill 0" SIGINT -# start clam service itself and the updater in background as daemon freshclam -d & clamd & -# recognize PIDs -pidlist=`jobs -p` - -# initialize latest result var -latest_exit=0 - -# define shutdown helper -function shutdown() { - trap "" SUBS - - for single in $pidlist; do - if ! kill -0 $pidlist 2>/dev/null; then - wait $pidlist - exitcode=$? - fi - done - - kill $pidlist 2>/dev/null -} - -# run shutdown -trap terminate SUBS -wait - -# return received result -exit $latest_exit +sleep inf diff --git a/data/Dockerfiles/clamav/dl_files.sh b/data/Dockerfiles/clamav/dl_files.sh new file mode 100755 index 00000000..09d61241 --- /dev/null +++ b/data/Dockerfiles/clamav/dl_files.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +declare -a DB_MIRRORS=( + "switch.clamav.net" + "clamavdb.heanet.ie" + "clamav.iol.cz" + "clamav.univ-nantes.fr" + "clamav.easynet.fr" + "clamav.begi.net" +) +declare -a DB_MIRRORS=( $(shuf -e "${DB_MIRRORS[@]}") ) + +DB_FILES=( + "bytecode.cvd" + "daily.cvd" + "main.cvd" +) + +for i in "${DB_MIRRORS[@]}"; do + for j in "${DB_FILES[@]}"; do + [[ -f "/var/lib/clamav/${j}" && -s "/var/lib/clamav/${j}" ]] && continue; + if [[ $(curl -o /dev/null --connect-timeout 1 \ + --max-time 1 \ + --silent \ + --head \ + --write-out "%{http_code}\n" "${i}/${j}") == 200 ]]; then + curl "${i}/${j}" -o "/var/lib/clamav/${j}" -# + fi + done +done + +chown clamav:clamav /var/lib/clamav/*.cvd diff --git a/data/Dockerfiles/dovecot/Dockerfile b/data/Dockerfiles/dovecot/Dockerfile index abd07c1f..1d3bfbef 100644 --- a/data/Dockerfiles/dovecot/Dockerfile +++ b/data/Dockerfiles/dovecot/Dockerfile @@ -1,33 +1,30 @@ -FROM ubuntu:xenial +FROM debian:stretch-slim +#ubuntu:xenial MAINTAINER Andre Peters <andre.peters@servercow.de> ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C +ENV DOVECOT_VERSION 2.2.29.1 +ENV PIGEONHOLE_VERSION 0.4.18 -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - -RUN apt-get update -RUN apt-get -y install dovecot-common \ - dovecot-core \ - dovecot-imapd \ - dovecot-lmtpd \ - dovecot-managesieved \ - dovecot-sieve \ - dovecot-mysql \ - dovecot-pop3d \ - dovecot-dev \ +RUN apt-get update \ + && apt-get -y install libpam-dev \ + default-libmysqlclient-dev \ + lzma-dev \ + liblz-dev \ + libbz2-dev \ + liblz4-dev \ + liblzma-dev \ + build-essential \ + autotools-dev \ + automake \ syslog-ng \ syslog-ng-core \ ca-certificates \ supervisor \ wget \ curl \ - build-essential \ - autotools-dev \ - automake \ + libssl-dev \ libauthen-ntlm-perl \ libcrypt-ssleay-perl \ libdigest-hmac-perl \ @@ -52,36 +49,57 @@ RUN apt-get -y install dovecot-common \ make \ cpanminus + +RUN wget https://www.dovecot.org/releases/2.2/dovecot-$DOVECOT_VERSION.tar.gz -O - | tar xvz \ + && cd dovecot-$DOVECOT_VERSION \ + && ./configure --with-mysql --with-lzma --with-lz4 --with-ssl=openssl --with-notify=inotify --with-storages=mdbox,sdbox,maildir,mbox,imapc,pop3c --with-bzlib --with-zlib \ + && make -j3 \ + && make install \ + && make clean + +RUN wget https://pigeonhole.dovecot.org/releases/2.2/dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION.tar.gz -O - | tar xvz \ + && cd dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \ + && ./configure \ + && make -j3 \ + && make install \ + && make clean + RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf RUN cpanm Data::Uniqid Mail::IMAPClient String::Util RUN echo '* * * * * root /usr/local/bin/imapsync_cron.pl' > /etc/cron.d/imapsync RUN echo '30 3 * * * vmail /usr/bin/doveadm quota recalc -A' > /etc/cron.d/dovecot-sync -WORKDIR /tmp - -RUN wget http://hg.dovecot.org/dovecot-antispam-plugin/archive/tip.tar.gz -O - | tar xvz \ - && cd /tmp/dovecot-antispam* \ - && ./autogen.sh \ - && ./configure --prefix=/usr \ - && make \ - && make install - COPY ./imapsync /usr/local/bin/imapsync COPY ./postlogin.sh /usr/local/bin/postlogin.sh COPY ./imapsync_cron.pl /usr/local/bin/imapsync_cron.pl -COPY ./rspamd-pipe /usr/local/bin/rspamd-pipe +COPY ./report-spam.sieve /usr/local/lib/dovecot/sieve/report-spam.sieve +COPY ./report-ham.sieve /usr/local/lib/dovecot/sieve/report-ham.sieve +COPY ./rspamd-pipe-ham /usr/local/lib/dovecot/sieve/rspamd-pipe-ham +COPY ./rspamd-pipe-spam /usr/local/lib/dovecot/sieve/rspamd-pipe-spam COPY ./docker-entrypoint.sh / COPY ./supervisord.conf /etc/supervisor/supervisord.conf -RUN chmod +x /usr/local/bin/rspamd-pipe -RUN chmod +x /usr/local/bin/imapsync_cron.pl +RUN chmod +x /usr/local/lib/dovecot/sieve/rspamd-pipe-ham \ + /usr/local/lib/dovecot/sieve/rspamd-pipe-spam \ + /usr/local/bin/imapsync_cron.pl \ + /usr/local/bin/postlogin.sh \ + /usr/local/bin/imapsync -RUN groupadd -g 5000 vmail -RUN useradd -g vmail -u 5000 vmail -d /var/vmail +RUN groupadd -g 5000 vmail \ + && groupadd -g 401 dovecot \ + && groupadd -g 402 dovenull \ + && useradd -g vmail -u 5000 vmail -d /var/vmail \ + && useradd -c "Dovecot unprivileged user" -d /dev/null -u 401 -g dovecot -s /bin/false dovecot \ + && useradd -c "Dovecot login user" -d /dev/null -u 402 -g dovenull -s /bin/false dovenull EXPOSE 24 10001 ENTRYPOINT ["/docker-entrypoint.sh"] CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf -RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* +RUN apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ + /tmp/* \ + /var/tmp/* \ + /dovecot-2.2-pigeonhole-$PIGEONHOLE_VERSION \ + /dovecot-$DOVECOT_VERSION diff --git a/data/Dockerfiles/dovecot/docker-entrypoint.sh b/data/Dockerfiles/dovecot/docker-entrypoint.sh index 8ef09dba..a7191306 100755 --- a/data/Dockerfiles/dovecot/docker-entrypoint.sh +++ b/data/Dockerfiles/dovecot/docker-entrypoint.sh @@ -6,13 +6,17 @@ sed -i "/^\$DBUSER/c\\\$DBUSER='${DBUSER}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBPASS/c\\\$DBPASS='${DBPASS}';" /usr/local/bin/imapsync_cron.pl sed -i "/^\$DBNAME/c\\\$DBNAME='${DBNAME}';" /usr/local/bin/imapsync_cron.pl -[[ ! -d /etc/dovecot/sql/ ]] && mkdir -p /etc/dovecot/sql/ +# Create missing directories +[[ ! -d /usr/local/etc/dovecot/sql/ ]] && mkdir -p /usr/local/etc/dovecot/sql/ +[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve +[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo # Set Dovecot sql config parameters, escape " in db password DBPASS=$(echo ${DBPASS} | sed 's/"/\\"/g') -cat <<EOF > /etc/dovecot/sql/dovecot-dict-sql.conf -connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" +# Create quota dict for Dovecot +cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-dict-sql.conf +connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" map { pattern = priv/quota/storage table = quota2 @@ -27,28 +31,45 @@ map { } EOF -cat <<EOF > /etc/dovecot/sql/dovecot-mysql.conf +# Create user and pass dict for Dovecot +cat <<EOF > /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = mysql -connect = "host=mysql dbname=${DBNAME} user=${DBNAME} password=${DBPASS}" +connect = "host=mysql dbname=${DBNAME} user=${DBUSER} password=${DBPASS}" default_pass_scheme = SSHA256 password_query = SELECT password FROM mailbox WHERE username = '%u' AND domain IN (SELECT domain FROM domain WHERE domain='%d' AND active='1') user_query = SELECT CONCAT('maildir:/var/vmail/',maildir) AS mail, 5000 AS uid, 5000 AS gid, concat('*:bytes=', quota) AS quota_rule FROM mailbox WHERE username = '%u' AND active = '1' iterate_query = SELECT username FROM mailbox WHERE active='1'; EOF -[[ ! -d /var/vmail/sieve ]] && mkdir -p /var/vmail/sieve -[[ ! -d /etc/sogo ]] && mkdir -p /etc/sogo -cat /etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve -sievec /var/vmail/sieve/global.sieve -chown -R vmail:vmail /var/vmail/sieve +# Create global sieve_after script +cat /usr/local/etc/dovecot/sieve_after > /var/vmail/sieve/global.sieve +# Check permissions of vmail directory. # Do not do this every start-up, it may take a very long time. So we use a stat check here. if [[ $(stat -c %U /var/vmail/) != "vmail" ]] ; then chown -R vmail:vmail /var/vmail ; fi # Create random master for SOGo sieve features RAND_USER=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 16 | head -n 1) RAND_PASS=$(cat /dev/urandom | tr -dc 'a-z0-9' | fold -w 24 | head -n 1) -echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /etc/dovecot/dovecot-master.passwd +echo ${RAND_USER}:$(doveadm pw -s SHA1 -p ${RAND_PASS}) > /usr/local/etc/dovecot/dovecot-master.passwd echo ${RAND_USER}:${RAND_PASS} > /etc/sogo/sieve.creds +# 401 is user dovecot +if [[ ! -f /mail_crypt/ecprivkey.pem || ! -f /mail_crypt/ecpubkey.pem ]]; then + openssl ecparam -name prime256v1 -genkey | openssl pkey -out /mail_crypt/ecprivkey.pem + openssl pkey -in /mail_crypt/ecprivkey.pem -pubout -out /mail_crypt/ecpubkey.pem + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem +else + chown 401 /mail_crypt/ecprivkey.pem /mail_crypt/ecpubkey.pem +fi + +# Compile sieve scripts +sievec /var/vmail/sieve/global.sieve +sievec /usr/local/lib/dovecot/sieve/report-spam.sieve +sievec /usr/local/lib/dovecot/sieve/report-ham.sieve + +# Fix permissions +chown -R vmail:vmail /var/vmail/sieve + + exec "$@" diff --git a/data/Dockerfiles/dovecot/imapsync_cron.pl b/data/Dockerfiles/dovecot/imapsync_cron.pl index 5c47eb47..a3cbf8a1 100755 --- a/data/Dockerfiles/dovecot/imapsync_cron.pl +++ b/data/Dockerfiles/dovecot/imapsync_cron.pl @@ -21,7 +21,7 @@ open my $file, '<', "/etc/sogo/sieve.creds"; my $creds = <$file>; close $file; my ($master_user, $master_pass) = split /:/, $creds; -my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); +my $sth = $dbh->prepare("SELECT id, user1, user2, host1, authmech1, password1, exclude, port1, enc1, delete2duplicates, maxage, subfolder2, delete1 FROM imapsync WHERE active = 1 AND (UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(last_run) > mins_interval * 60 OR last_run IS NULL)"); $sth->execute(); my $row; @@ -39,6 +39,7 @@ while ($row = $sth->fetchrow_arrayref()) { $delete2duplicates = @$row[9]; $maxage = @$row[10]; $subfolder2 = @$row[11]; + $delete1 = @$row[12]; if ($enc1 eq "TLS") { $enc1 = "--tls1"; } elsif ($enc1 eq "SSL") { $enc1 = "--ssl1"; } else { undef $enc1; } @@ -46,11 +47,12 @@ while ($row = $sth->fetchrow_arrayref()) { "--timeout1", "10", "--tmpdir", "/tmp", "--subscribeall", - ($exclude eq "" ? () : ("--exclude", $exclude)), - ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), - ($maxage eq "0" ? () : ('--maxage', $maxage)), + ($exclude eq "" ? () : ("--exclude", $exclude)), + ($subfolder2 eq "" ? () : ('--subfolder2', $subfolder2)), + ($maxage eq "0" ? () : ('--maxage', $maxage)), ($delete2duplicates ne "1" ? () : ('--delete2duplicates')), - (!defined($enc1) ? () : ($enc1)), + ($delete1 ne "1" ? () : ('--delete')), + (!defined($enc1) ? () : ($enc1)), "--host1", $host1, "--user1", $user1, "--password1", $password1, diff --git a/data/Dockerfiles/dovecot/report-ham.sieve b/data/Dockerfiles/dovecot/report-ham.sieve new file mode 100644 index 00000000..80c7f44e --- /dev/null +++ b/data/Dockerfiles/dovecot/report-ham.sieve @@ -0,0 +1,11 @@ +require ["vnd.dovecot.pipe", "copy", "imapsieve", "environment", "variables"]; + +if environment :matches "imap.mailbox" "*" { + set "mailbox" "${1}"; +} + +if string "${mailbox}" "Trash" { + stop; +} + +pipe :copy "rspamd-pipe-ham"; diff --git a/data/Dockerfiles/dovecot/report-spam.sieve b/data/Dockerfiles/dovecot/report-spam.sieve new file mode 100644 index 00000000..d44cb9a7 --- /dev/null +++ b/data/Dockerfiles/dovecot/report-spam.sieve @@ -0,0 +1,3 @@ +require ["vnd.dovecot.pipe", "copy"]; + +pipe :copy "rspamd-pipe-spam"; diff --git a/data/Dockerfiles/dovecot/rspamd-pipe b/data/Dockerfiles/dovecot/rspamd-pipe deleted file mode 100755 index f9236e17..00000000 --- a/data/Dockerfiles/dovecot/rspamd-pipe +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/bash -if [[ ${2} == "learn_spam" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnspam < /dev/stdin -elif [[ ${2} == "learn_ham" ]]; then -/usr/bin/curl --data-binary @- http://rspamd:11334/learnham < /dev/stdin -fi -# Always return 0 to satisfy Dovecot... -exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-ham b/data/Dockerfiles/dovecot/rspamd-pipe-ham new file mode 100755 index 00000000..7c3ab03f --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-ham @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnham < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/rspamd-pipe-spam b/data/Dockerfiles/dovecot/rspamd-pipe-spam new file mode 100755 index 00000000..67cccb2c --- /dev/null +++ b/data/Dockerfiles/dovecot/rspamd-pipe-spam @@ -0,0 +1,4 @@ +#!/bin/bash +/usr/bin/curl -s --data-binary @- http://rspamd:11334/learnspam < /dev/stdin +# Always return 0 to satisfy Dovecot... +exit 0 diff --git a/data/Dockerfiles/dovecot/supervisord.conf b/data/Dockerfiles/dovecot/supervisord.conf index 45f9ddd5..e5a66f22 100644 --- a/data/Dockerfiles/dovecot/supervisord.conf +++ b/data/Dockerfiles/dovecot/supervisord.conf @@ -8,7 +8,7 @@ autostart=true stdout_syslog=true [program:dovecot] -command=/usr/sbin/dovecot -F +command=/usr/local/sbin/dovecot -F autorestart=true [program:logfiles] diff --git a/data/Dockerfiles/postfix/Dockerfile b/data/Dockerfiles/postfix/Dockerfile index 6a36f443..210de532 100644 --- a/data/Dockerfiles/postfix/Dockerfile +++ b/data/Dockerfiles/postfix/Dockerfile @@ -1,4 +1,4 @@ -FROM ubuntu:xenial +FROM debian:stretch-slim MAINTAINER Andre Peters <andre.peters@servercow.de> ENV DEBIAN_FRONTEND noninteractive @@ -19,12 +19,23 @@ RUN apt-get install -y --no-install-recommends supervisor \ postfix-pcre \ syslog-ng \ syslog-ng-core \ - ca-certificates + ca-certificates \ + gnupg \ + python-gpgme \ + sudo \ + curl \ + dirmngr +RUN addgroup --system --gid 600 zeyple +RUN adduser --system --home /var/lib/zeyple --no-create-home --uid 600 --gid 600 --disabled-login zeyple +RUN touch /var/log/zeyple.log && chown zeyple: /var/log/zeyple.log RUN sed -i -E 's/^(\s*)system\(\);/\1unix-stream("\/dev\/log");/' /etc/syslog-ng/syslog-ng.conf +COPY zeyple.py /usr/local/bin/zeyple.py +COPY zeyple.conf /etc/zeyple.conf COPY supervisord.conf /etc/supervisor/supervisord.conf COPY postfix.sh /opt/postfix.sh +COPY whitelist_forwardinghosts.sh /usr/local/bin/whitelist_forwardinghosts.sh EXPOSE 588 diff --git a/data/Dockerfiles/postfix/postfix.sh b/data/Dockerfiles/postfix/postfix.sh index c628e771..640538b0 100755 --- a/data/Dockerfiles/postfix/postfix.sh +++ b/data/Dockerfiles/postfix/postfix.sh @@ -17,7 +17,7 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', 'DUNNO') AS 'tls_enforce_in'; +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_in = '1' AND mailbox.active = '1'), 'reject_plaintext_session', NULL) AS 'tls_enforce_in'; EOF cat <<EOF > /opt/postfix/conf/sql/mysql_tls_enforce_out_policy.cf @@ -25,7 +25,7 @@ user = ${DBUSER} password = ${DBPASS} hosts = mysql dbname = ${DBNAME} -query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.address WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', 'DUNNO') AS 'tls_enforce_out'; +query = SELECT IF( EXISTS( SELECT 'TLS_ACTIVE' FROM alias LEFT OUTER JOIN mailbox ON mailbox.username = alias.goto WHERE (address='%s' OR address IN (SELECT CONCAT('%u', '@', target_domain) FROM alias_domain WHERE alias_domain='%d')) AND mailbox.tls_enforce_out = '1' AND mailbox.active = '1'), 'smtp_enforced_tls:', NULL) AS 'tls_enforce_out'; EOF cat <<EOF > /opt/postfix/conf/sql/mysql_virtual_alias_domain_catchall_maps.cf @@ -92,11 +92,24 @@ dbname = ${DBNAME} query = SELECT goto FROM spamalias WHERE address='%s' AND validity >= UNIX_TIMESTAMP() EOF +# Reset GPG key permissions +mkdir -p /var/lib/zeyple/keys +chmod 700 /var/lib/zeyple/keys +chown -R 600:600 /var/lib/zeyple/keys + +# Fix Postfix permissions +chgrp -R postdrop /var/spool/postfix/public +chgrp -R postdrop /var/spool/postfix/maildrop +postfix set-permissions + +# Check Postfix configuration postconf -c /opt/postfix/conf + if [[ $? != 0 ]]; then echo "Postfix configuration error, refusing to start." exit 1 else postfix -c /opt/postfix/conf start + supervisorctl restart postfix-maillog sleep 126144000 fi diff --git a/data/Dockerfiles/postfix/supervisord.conf b/data/Dockerfiles/postfix/supervisord.conf index 4268899d..72523a61 100644 --- a/data/Dockerfiles/postfix/supervisord.conf +++ b/data/Dockerfiles/postfix/supervisord.conf @@ -12,6 +12,17 @@ command=/opt/postfix.sh autorestart=true [program:postfix-maillog] -command=/usr/bin/tail -f /var/log/mail.log -stdout_logfile=/dev/fd/1 +command=/bin/tail -f /var/log/zeyple.log /var/log/mail.log +stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 + +[unix_http_server] +file=/var/tmp/supervisord.sock +chmod=0770 +chown=nobody:nogroup + +[supervisorctl] +serverurl=unix:///var/tmp/supervisord.sock + +[rpcinterface:supervisor] +supervisor.rpcinterface_factory = supervisor.rpcinterface:make_main_rpcinterface diff --git a/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh b/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh new file mode 100755 index 00000000..4ad5ab32 --- /dev/null +++ b/data/Dockerfiles/postfix/whitelist_forwardinghosts.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +while read QUERY; do + QUERY=($QUERY) + if [ "${QUERY[0]}" != "get" ]; then + echo "500 dunno" + continue + fi + result=$(curl -s http://nginx:8081/forwardinghosts.php?host=${QUERY[1]}) + logger -t whitelist_forwardinghosts -p mail.info "Look up ${QUERY[1]} on whitelist, result $result" + echo ${result} +done diff --git a/data/Dockerfiles/postfix/zeyple.conf b/data/Dockerfiles/postfix/zeyple.conf new file mode 100644 index 00000000..7f039582 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.conf @@ -0,0 +1,9 @@ +[zeyple] +log_file = /var/log/zeyple.log + +[gpg] +home = /var/lib/zeyple/keys + +[relay] +host = localhost +port = 10026 diff --git a/data/Dockerfiles/postfix/zeyple.py b/data/Dockerfiles/postfix/zeyple.py new file mode 100755 index 00000000..bb218831 --- /dev/null +++ b/data/Dockerfiles/postfix/zeyple.py @@ -0,0 +1,274 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +import sys +import os +import logging +import email +import email.mime.multipart +import email.mime.application +import email.encoders +import smtplib +import copy +from io import BytesIO + +try: + from configparser import SafeConfigParser # Python 3 +except ImportError: + from ConfigParser import SafeConfigParser # Python 2 + +import gpgme + +# Boiler plate to avoid dependency on six +# BBB: Python 2.7 support +PY3K = sys.version_info > (3, 0) + + +def message_from_binary(message): + if PY3K: + return email.message_from_bytes(message) + else: + return email.message_from_string(message) + + +def as_binary_string(email): + if PY3K: + return email.as_bytes() + else: + return email.as_string() + + +def encode_string(string): + if isinstance(string, bytes): + return string + else: + return string.encode('utf-8') + + +__title__ = 'Zeyple' +__version__ = '1.2.0' +__author__ = 'Cédric Félizard' +__license__ = 'AGPLv3+' +__copyright__ = 'Copyright 2012-2016 Cédric Félizard' + + +class Zeyple: + """Zeyple Encrypts Your Precious Log Emails""" + + def __init__(self, config_fname='zeyple.conf'): + self.config = self.load_configuration(config_fname) + + log_file = self.config.get('zeyple', 'log_file') + logging.basicConfig( + filename=log_file, level=logging.DEBUG, + format='%(asctime)s %(process)s %(levelname)s %(message)s' + ) + logging.info("Zeyple ready to encrypt outgoing emails") + + def load_configuration(self, filename): + """Reads and parses the config file""" + + config = SafeConfigParser() + config.read([ + os.path.join('/etc/', filename), + filename, + ]) + if not config.sections(): + raise IOError('Cannot open config file.') + return config + + @property + def gpg(self): + protocol = gpgme.PROTOCOL_OpenPGP + + if self.config.has_option('gpg', 'executable'): + executable = self.config.get('gpg', 'executable') + else: + executable = None # Default value + + home_dir = self.config.get('gpg', 'home') + + ctx = gpgme.Context() + ctx.set_engine_info(protocol, executable, home_dir) + ctx.armor = True + + return ctx + + def process_message(self, message_data, recipients): + """Encrypts the message with recipient keys""" + message_data = encode_string(message_data) + + in_message = message_from_binary(message_data) + logging.info( + "Processing outgoing message %s", in_message['Message-id']) + + if not recipients: + logging.warn("Cannot find any recipients, ignoring") + + sent_messages = [] + for recipient in recipients: + logging.info("Recipient: %s", recipient) + + key_id = self._user_key(recipient) + logging.info("Key ID: %s", key_id) + + if key_id: + out_message = self._encrypt_message(in_message, key_id) + + # Delete Content-Transfer-Encoding if present to default to + # "7bit" otherwise Thunderbird seems to hang in some cases. + del out_message["Content-Transfer-Encoding"] + else: + logging.warn("No keys found, message will be sent unencrypted") + out_message = copy.copy(in_message) + + self._add_zeyple_header(out_message) + self._send_message(out_message, recipient) + sent_messages.append(out_message) + + return sent_messages + + def _get_version_part(self): + ret = email.mime.application.MIMEApplication( + 'Version: 1\n', + 'pgp-encrypted', + email.encoders.encode_noop, + ) + ret.add_header( + 'Content-Description', + "PGP/MIME version identification", + ) + return ret + + def _get_encrypted_part(self, payload): + ret = email.mime.application.MIMEApplication( + payload, + 'octet-stream', + email.encoders.encode_noop, + name="encrypted.asc", + ) + ret.add_header('Content-Description', "OpenPGP encrypted message") + ret.add_header( + 'Content-Disposition', + 'inline', + filename='encrypted.asc', + ) + return ret + + def _encrypt_message(self, in_message, key_id): + if in_message.is_multipart(): + # get the body (after the first \n\n) + payload = in_message.as_string().split("\n\n", 1)[1].strip() + + # prepend the Content-Type including the boundary + content_type = "Content-Type: " + in_message["Content-Type"] + payload = content_type + "\n\n" + payload + + message = email.message.Message() + message.set_payload(payload) + + payload = message.get_payload() + + else: + payload = in_message.get_payload() + payload = encode_string(payload) + + quoted_printable = email.charset.Charset('ascii') + quoted_printable.body_encoding = email.charset.QP + + message = email.mime.nonmultipart.MIMENonMultipart( + 'text', 'plain', charset='utf-8' + ) + message.set_payload(payload, charset=quoted_printable) + + mixed = email.mime.multipart.MIMEMultipart( + 'mixed', + None, + [message], + ) + + # remove superfluous header + del mixed['MIME-Version'] + + payload = as_binary_string(mixed) + + encrypted_payload = self._encrypt_payload(payload, [key_id]) + + version = self._get_version_part() + encrypted = self._get_encrypted_part(encrypted_payload) + + out_message = copy.copy(in_message) + out_message.preamble = "This is an OpenPGP/MIME encrypted " \ + "message (RFC 4880 and 3156)" + + if 'Content-Type' not in out_message: + out_message['Content-Type'] = 'multipart/encrypted' + else: + out_message.replace_header( + 'Content-Type', + 'multipart/encrypted', + ) + + out_message.set_param('protocol', 'application/pgp-encrypted') + out_message.set_payload([version, encrypted]) + + return out_message + + def _encrypt_payload(self, payload, key_ids): + """Encrypts the payload with the given keys""" + payload = encode_string(payload) + + plaintext = BytesIO(payload) + ciphertext = BytesIO() + + self.gpg.armor = True + + recipient = [self.gpg.get_key(key_id) for key_id in key_ids] + + self.gpg.encrypt(recipient, gpgme.ENCRYPT_ALWAYS_TRUST, + plaintext, ciphertext) + + return ciphertext.getvalue() + + def _user_key(self, email): + """Returns the GPG key for the given email address""" + logging.info("Trying to encrypt for %s", email) + keys = [key for key in self.gpg.keylist(email)] + + if keys: + key = keys.pop() # NOTE: looks like keys[0] is the master key + key_id = key.subkeys[0].keyid + return key_id + + return None + + def _add_zeyple_header(self, message): + if self.config.has_option('zeyple', 'add_header') and \ + self.config.getboolean('zeyple', 'add_header'): + message.add_header( + 'X-Zeyple', + "processed by {0} v{1}".format(__title__, __version__) + ) + + def _send_message(self, message, recipient): + """Sends the given message through the SMTP relay""" + logging.info("Sending message %s", message['Message-id']) + + smtp = smtplib.SMTP(self.config.get('relay', 'host'), + self.config.get('relay', 'port')) + + smtp.sendmail(message['From'], recipient, message.as_string()) + smtp.quit() + + logging.info("Message %s sent", message['Message-id']) + + +if __name__ == '__main__': + recipients = sys.argv[1:] + + # BBB: Python 2.7 support + binary_stdin = sys.stdin.buffer if PY3K else sys.stdin + message = binary_stdin.read() + + zeyple = Zeyple() + zeyple.process_message(message, recipients) diff --git a/data/Dockerfiles/rmilter/Dockerfile b/data/Dockerfiles/rmilter/Dockerfile index 364c78c1..1d5db5b0 100644 --- a/data/Dockerfiles/rmilter/Dockerfile +++ b/data/Dockerfiles/rmilter/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters <andre.peters@servercow.de> ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get --no-install-recommends -y --force-yes install rmilter cron syslog-ng syslog-ng-core supervisor diff --git a/data/Dockerfiles/rspamd/Dockerfile b/data/Dockerfiles/rspamd/Dockerfile index 70a0bbab..46a97748 100644 --- a/data/Dockerfiles/rspamd/Dockerfile +++ b/data/Dockerfiles/rspamd/Dockerfile @@ -1,16 +1,11 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters <andre.peters@servercow.de> ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-key adv --fetch-keys http://rspamd.com/apt-stable/gpg.key \ - && echo "deb http://rspamd.com/apt-stable/ xenial main" > /etc/apt/sources.list.d/rspamd.list \ + && echo "deb http://rspamd.com/apt-stable/ jessie main" > /etc/apt/sources.list.d/rspamd.list \ && apt-get update \ && apt-get -y install rspamd ca-certificates python-pip diff --git a/data/Dockerfiles/sogo/Dockerfile b/data/Dockerfiles/sogo/Dockerfile index 43960438..348231de 100644 --- a/data/Dockerfiles/sogo/Dockerfile +++ b/data/Dockerfiles/sogo/Dockerfile @@ -1,17 +1,12 @@ -FROM ubuntu:xenial +FROM debian:jessie-slim MAINTAINER Andre Peters <andre.peters@servercow.de> ENV DEBIAN_FRONTEND noninteractive ENV LC_ALL C ENV GOSU_VERSION 1.9 -RUN dpkg-divert --local --rename --add /sbin/initctl \ - && ln -sf /bin/true /sbin/initctl \ - && dpkg-divert --local --rename --add /usr/bin/ischroot \ - && ln -sf /bin/true /usr/bin/ischroot - RUN apt-get update \ - && apt-get install -y --no-install-recommends apt-transport-https \ + && apt-get install -y --no-install-recommends apt-transport-https gnupg \ ca-certificates \ wget \ syslog-ng \ @@ -29,8 +24,11 @@ RUN apt-get update \ && chmod +x /usr/local/bin/gosu \ && gosu nobody true +RUN mkdir /usr/share/doc/sogo +RUN touch /usr/share/doc/sogo/empty.sh + RUN apt-key adv --keyserver keys.gnupg.net --recv-key 0x810273C4 \ - && echo "deb http://packages.inverse.ca/SOGo/nightly/3/ubuntu/ xenial xenial" > /etc/apt/sources.list.d/sogo.list \ + && echo "deb http://packages.inverse.ca/SOGo/nightly/3/debian/ jessie jessie" > /etc/apt/sources.list.d/sogo.list \ && apt-get update \ && apt-get -y --force-yes install sogo sogo-activesync @@ -42,10 +40,6 @@ RUN echo '0 0 * * * sogo /usr/sbin/sogo-tool update-autoreply -p /etc/sogo/s COPY ./reconf-domains.sh / COPY supervisord.conf /etc/supervisor/supervisord.conf -#EXPOSE 20000 -#EXPOSE 9191 -#EXPOSE 9192 - CMD exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf RUN apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* diff --git a/data/conf/dovecot/dovecot.conf b/data/conf/dovecot/dovecot.conf index 1d05571e..b4501e1a 100644 --- a/data/conf/dovecot/dovecot.conf +++ b/data/conf/dovecot/dovecot.conf @@ -10,9 +10,8 @@ disable_plaintext_auth = yes login_log_format_elements = "user=<%u> method=%m rip=%r lip=%l mpid=%e %c %k" mail_home = /var/vmail/%d/%n mail_location = maildir:~/ -mail_plugins = quota acl zlib antispam -auth_username_chars = abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890.-_@ -ssl_protocols = !SSLv3 !SSLv2 +mail_plugins = quota acl zlib #mail_crypt +ssl_protocols = !SSLv3 ssl_prefer_server_ciphers = yes ssl_cipher_list = EDH+CAMELLIA:EDH+aRSA:EECDH+aRSA+AESGCM:EECDH+aRSA+SHA256:EECDH:+CAMELLIA128:+AES128:+SSLv3:!aNULL:!eNULL:!LOW:!3DES:!MD5:!EXP:!PSK:!DSS:!RC4:!SEED:!IDEA:!ECDSA:kEDH:CAMELLIA128-SHA:AES128-SHA ssl_options = no_compression @@ -24,12 +23,12 @@ auth_master_user_separator = * mail_prefetch_count = 30 passdb { driver = passwd-file - args = /etc/dovecot/dovecot-master.passwd + args = /usr/local/etc/dovecot/dovecot-master.passwd master = yes pass = yes } passdb { - args = /etc/dovecot/sql/dovecot-mysql.conf + args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = sql } namespace inbox { @@ -202,15 +201,15 @@ listen = *,[::] ssl_cert = </etc/ssl/mail/cert.pem ssl_key = </etc/ssl/mail/key.pem userdb { - args = /etc/dovecot/sql/dovecot-mysql.conf + args = /usr/local/etc/dovecot/sql/dovecot-mysql.conf driver = sql } protocol imap { - mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib antispam + mail_plugins = quota imap_quota imap_acl acl zlib imap_zlib imap_sieve #mail_crypt } protocol lmtp { - mail_plugins = quota sieve acl zlib - auth_socket_path = /var/run/dovecot/auth-master + mail_plugins = quota sieve acl zlib #mail_crypt + auth_socket_path = /usr/local/var/run/dovecot/auth-master } protocol sieve { managesieve_logout_format = bytes=%i/%o @@ -221,22 +220,31 @@ plugin { acl = vfile quota = dict:Userquota::proxy::sqlquota quota_rule2 = Trash:storage=+100%% - antispam_backend = mailtrain - antispam_spam = Junk - antispam_trash = Trash - antispam_mail_sendmail = /usr/local/bin/rspamd-pipe - antispam_mail_spam = learn_spam - antispam_mail_notspam = learn_ham - # Do not complain about empty parameter - antispam_mail_sendmail_args = --blind sieve = /var/vmail/sieve/%u.sieve + sieve_plugins = sieve_imapsieve sieve_extprograms + # From elsewhere to Spam folder + imapsieve_mailbox1_name = Junk + imapsieve_mailbox1_causes = COPY + imapsieve_mailbox1_before = file:/usr/local/lib/dovecot/sieve/report-spam.sieve + # END + # From Spam folder to elsewhere + imapsieve_mailbox2_name = * + imapsieve_mailbox2_from = Junk + imapsieve_mailbox2_causes = COPY + imapsieve_mailbox2_before = file:/usr/local/lib/dovecot/sieve/report-ham.sieve + # END + sieve_pipe_bin_dir = /usr/local/lib/dovecot/sieve + sieve_global_extensions = +vnd.dovecot.pipe +vnd.dovecot.execute sieve_after = /var/vmail/sieve/global.sieve sieve_max_script_size = 1M sieve_quota_max_scripts = 0 sieve_quota_max_storage = 0 + #mail_crypt_global_private_key = </mail_crypt/ecprivkey.pem + #mail_crypt_global_public_key = </mail_crypt/ecpubkey.pem + #mail_crypt_save_version = 2 } dict { - sqlquota = mysql:/etc/dovecot/sql/dovecot-dict-sql.conf + sqlquota = mysql:/usr/local/etc/dovecot/sql/dovecot-dict-sql.conf } remote 127.0.0.1 { disable_plaintext_auth = no diff --git a/data/conf/nginx/site.conf b/data/conf/nginx/site.conf index d724b4f2..ef6ec107 100644 --- a/data/conf/nginx/site.conf +++ b/data/conf/nginx/site.conf @@ -1,4 +1,8 @@ proxy_cache_path /tmp levels=1:2 keys_zone=sogo:10m inactive=24h max_size=1g; +map $http_x_forwarded_proto $client_req_scheme { + default $scheme; + https https; +} server { include /etc/nginx/conf.d/listen_ssl.active; include /etc/nginx/mime.types; @@ -16,8 +20,13 @@ server { include /etc/nginx/conf.d/server_name.active; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; + absolute_redirect off; root /web; + location ~ ^/api/v1/(.*)$ { + try_files $uri $uri/ /json_api.php?query=$1; + } + location ^~ /.well-known/acme-challenge/ { allow all; default_type "text/plain"; @@ -29,7 +38,7 @@ server { real_ip_recursive on; location = /principals/ { - rewrite ^ $scheme://$host:$server_port/SOGo/dav; + rewrite ^ $client_req_scheme://$http_host/SOGo/dav; allow all; } @@ -47,34 +56,26 @@ server { fastcgi_read_timeout 1200; } - rewrite ^(/save.+)$ /rspamd$1 last; location /rspamd/ { proxy_pass http://172.22.1.253:11334/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; - add_header X-Content-Type-Options nosniff; - add_header X-Frame-Options SAMEORIGIN; - add_header X-XSS-Protection "1; mode=block"; - } - - location ^~ /inc/init.sql { - deny all; + proxy_redirect off; } location ~ /(?:a|A)utodiscover/(?:a|A)utodiscover.xml { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9000; - include /etc/nginx/fastcgi_params; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9000; + include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; try_files /autodiscover.php =404; } location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9000; - include /etc/nginx/fastcgi_params; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9000; + include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; try_files /autoconfig.php =404; } @@ -91,11 +92,11 @@ server { proxy_busy_buffers_size 64k; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; proxy_set_header x-webobjects-server-port $server_port; client_body_buffer_size 128k; client_max_body_size 100m; @@ -105,11 +106,11 @@ server { proxy_pass http://172.22.1.252:20000; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; proxy_set_header x-webobjects-server-port $server_port; client_body_buffer_size 128k; client_max_body_size 100m; @@ -118,7 +119,7 @@ server { location /SOGo.woa/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -128,7 +129,7 @@ server { location /.woa/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -138,7 +139,7 @@ server { location /SOGo/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -148,7 +149,7 @@ server { location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$ { proxy_pass http://172.22.1.252:9192/$1.SOGo/Resources/$2; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -164,8 +165,13 @@ server { include /etc/nginx/conf.d/server_name.active; error_log /var/log/nginx/error.log; access_log /var/log/nginx/access.log; + absolute_redirect off; root /web; + location ~ ^/api/v1/(.*)$ { + try_files $uri $uri/ /json_api.php?query=$1; + } + location ^~ /.well-known/acme-challenge/ { allow all; default_type "text/plain"; @@ -177,7 +183,7 @@ server { real_ip_recursive on; location = /principals/ { - rewrite ^ $scheme://$host:$server_port/SOGo/dav; + rewrite ^ $client_req_scheme://$http_host/SOGo/dav; allow all; } @@ -195,34 +201,26 @@ server { fastcgi_read_timeout 1200; } - rewrite ^(/save.+)$ /rspamd$1 last; location /rspamd/ { proxy_pass http://172.22.1.253:11334/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Real-IP $remote_addr; - add_header Strict-Transport-Security "max-age=31536000; includeSubdomains"; - add_header X-Content-Type-Options nosniff; - add_header X-Frame-Options SAMEORIGIN; - add_header X-XSS-Protection "1; mode=block"; - } - - location ^~ /inc/init.sql { - deny all; + proxy_redirect off; } location ~ /(?:a|A)utodiscover/(?:a|A)utodiscover.xml { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9000; - include /etc/nginx/fastcgi_params; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9000; + include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; try_files /autodiscover.php =404; } location ~ /(?:m|M)ail/(?:c|C)onfig-v1.1.xml { - fastcgi_split_path_info ^(.+\.php)(/.+)$; - fastcgi_pass phpfpm:9000; - include /etc/nginx/fastcgi_params; + fastcgi_split_path_info ^(.+\.php)(/.+)$; + fastcgi_pass phpfpm:9000; + include /etc/nginx/fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; try_files /autoconfig.php =404; } @@ -239,11 +237,11 @@ server { proxy_busy_buffers_size 64k; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; proxy_set_header x-webobjects-server-port $server_port; client_body_buffer_size 128k; client_max_body_size 100m; @@ -253,11 +251,11 @@ server { proxy_pass http://172.22.1.252:20000; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_set_header x-webobjects-server-protocol HTTP/1.0; proxy_set_header x-webobjects-remote-host $remote_addr; proxy_set_header x-webobjects-server-name $server_name; - proxy_set_header x-webobjects-server-url $scheme://$host:$server_port; + proxy_set_header x-webobjects-server-url $client_req_scheme://$http_host; proxy_set_header x-webobjects-server-port $server_port; client_body_buffer_size 128k; client_max_body_size 100m; @@ -266,7 +264,7 @@ server { location /SOGo.woa/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -276,7 +274,7 @@ server { location /.woa/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -286,7 +284,7 @@ server { location /SOGo/WebServerResources/ { proxy_pass http://172.22.1.252:9192/WebServerResources/; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; @@ -296,7 +294,7 @@ server { location (^/SOGo/so/ControlPanel/Products/[^/]*UI/Resources/.*\.(jpg|png|gif|css|js)$ { proxy_pass http://172.22.1.252:9192/$1.SOGo/Resources/$2; - proxy_set_header Host $host; + proxy_set_header Host $http_host; proxy_cache sogo; proxy_cache_valid 200 1d; proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; diff --git a/data/conf/postfix/main.cf b/data/conf/postfix/main.cf index cbfc4e0c..52e86681 100644 --- a/data/conf/postfix/main.cf +++ b/data/conf/postfix/main.cf @@ -24,7 +24,7 @@ milter_default_action = accept milter_protocol = 6 minimal_backoff_time = 300s plaintext_reject_code = 550 -postscreen_access_list = permit_mynetworks, cidr:/opt/postfix/conf/postscreen_access.cidr +postscreen_access_list = permit_mynetworks, cidr:/opt/postfix/conf/postscreen_access.cidr, tcp:127.0.0.1:10027 postscreen_bare_newline_enable = no postscreen_blacklist_action = drop postscreen_cache_cleanup_interval = 24h @@ -91,3 +91,4 @@ smtpd_milters = inet:rmilter:9900 non_smtpd_milters = inet:rmilter:9900 milter_mail_macros = i {mail_addr} {client_addr} {client_name} {auth_authen} mydestination = localhost.localdomain, localhost +#content_filter=zeyple diff --git a/data/conf/postfix/master.cf b/data/conf/postfix/master.cf index e48ea92e..955728d1 100644 --- a/data/conf/postfix/master.cf +++ b/data/conf/postfix/master.cf @@ -16,6 +16,7 @@ smtp_enforced_tls unix - - n - - smtp -o smtp_tls_security_level=encrypt -o syslog_name=enforced-tls-smtp -o smtp_delivery_status_filter=pcre:/opt/postfix/conf/smtp_dsn_filter + tlsproxy unix - - n - 0 tlsproxy dnsblog unix - - n - 0 dnsblog pickup fifo n - n 60 1 pickup @@ -43,3 +44,16 @@ anvil unix - - n - 1 anvil scache unix - - n - 1 scache maildrop unix - n n - - pipe flags=DRhu user=vmail argv=/usr/bin/maildrop -d ${recipient} +zeyple unix - n n - - pipe + user=zeyple argv=/usr/local/bin/zeyple.py ${recipient} +127.0.0.1:10026 inet n - n - 10 smtpd + -o content_filter= + -o receive_override_options=no_unknown_recipient_checks,no_header_body_checks,no_milters + -o smtpd_helo_restrictions= + -o smtpd_client_restrictions= + -o smtpd_sender_restrictions= + -o smtpd_recipient_restrictions=permit_mynetworks,reject + -o mynetworks=127.0.0.0/8 + -o smtpd_authorized_xforward_hosts=127.0.0.0/8 + +127.0.0.1:10027 inet n n n - 0 spawn user=nobody argv=/usr/local/bin/whitelist_forwardinghosts.sh diff --git a/data/conf/rmilter/rmilter.conf b/data/conf/rmilter/rmilter.conf index f84408cd..d957935c 100644 --- a/data/conf/rmilter/rmilter.conf +++ b/data/conf/rmilter/rmilter.conf @@ -28,7 +28,6 @@ redis { }; tempdir = /tmp; tempfiles_mode = 00600; -max_size = 20M; strict_auth = yes; use_dcc = no; limits { diff --git a/data/conf/rspamd/dynmaps/forwardinghosts.php b/data/conf/rspamd/dynmaps/forwardinghosts.php new file mode 100644 index 00000000..377c5e7e --- /dev/null +++ b/data/conf/rspamd/dynmaps/forwardinghosts.php @@ -0,0 +1,56 @@ +<?php +header('Content-Type: text/plain'); +require_once "vars.inc.php"; + +ini_set('error_reporting', 0); + +function in_net($addr, $net) +{ + $net = explode('/', $net); + if (count($net) > 1) + $mask = $net[1]; + $net = inet_pton($net[0]); + $addr = inet_pton($addr); + + $length = strlen($net); // 4 for IPv4, 16 for IPv6 + if (strlen($net) != strlen($addr)) + return FALSE; + if (!isset($mask)) + $mask = $length * 8; + + $addr_bin = ''; + $net_bin = ''; + for ($i = 0; $i < $length; ++$i) + { + $addr_bin .= str_pad(decbin(ord(substr($addr, $i, $i+1))), 8, '0', STR_PAD_LEFT); + $net_bin .= str_pad(decbin(ord(substr($net, $i, $i+1))), 8, '0', STR_PAD_LEFT); + } + + return substr($addr_bin, 0, $mask) == substr($net_bin, 0, $mask); +} + +$dsn = $database_type . ':host=' . $database_host . ';dbname=' . $database_name; +$opt = [ + PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, +]; +try { + $pdo = new PDO($dsn, $database_user, $database_pass, $opt); + $stmt = $pdo->query("SELECT host FROM `forwarding_hosts`"); + $networks = $stmt->fetchAll(PDO::FETCH_COLUMN); + foreach ($networks as $network) + { + if (in_net($_GET['host'], $network)) + { + echo '200 permit'; + exit; + } + } + echo '200 dunno'; +} +catch (PDOException $e) { + echo '200 dunno'; + exit; +} +?> diff --git a/data/conf/rspamd/dynmaps/settings.php b/data/conf/rspamd/dynmaps/settings.php index 9be1f696..098ffbd9 100644 --- a/data/conf/rspamd/dynmaps/settings.php +++ b/data/conf/rspamd/dynmaps/settings.php @@ -32,6 +32,35 @@ catch (PDOException $e) { ?> settings { <?php +try { + $stmt = $pdo->query("SELECT `host` FROM `forwarding_hosts`"); + $rows = $stmt->fetchAll(PDO::FETCH_COLUMN); +} +catch (PDOException $e) { + $rows = array(); +} + +if ($rows) +{ +?> + whitelist_forwarding_hosts { + priority = high; +<?php +foreach ($rows as $host) { + echo "\t\t" . 'ip = "' . $host . '";' . "\n"; +} +?> + apply "default" { + actions { + reject = 999.9; + } + } + symbols [ + "WHITELIST_FORWARDING_HOST" + ] + } +<?php +} $stmt = $pdo->query("SELECT DISTINCT `object` FROM `filterconf` WHERE `option` = 'highspamlevel' OR `option` = 'lowspamlevel'"); $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); @@ -207,8 +236,11 @@ while ($row = array_shift($rows)) { } ?> apply "default" { - MAILCOW_MOO = -999.0; + MAILCOW_WHITE = -999.0; } + symbols [ + "MAILCOW_WHITE" + ] } <?php } @@ -302,10 +334,13 @@ while ($row = array_shift($rows)) { } ?> apply "default" { - MAILCOW_MOO = 999.0; + MAILCOW_BLACK = 999.0; } + symbols [ + "MAILCOW_BLACK" + ] } <?php } ?> -} \ No newline at end of file +} diff --git a/data/conf/rspamd/local.d/antivirus.conf b/data/conf/rspamd/local.d/antivirus.conf new file mode 100644 index 00000000..92ba684c --- /dev/null +++ b/data/conf/rspamd/local.d/antivirus.conf @@ -0,0 +1,7 @@ +clamav { + attachments_only = false; + symbol = "CLAM_VIRUS"; + type = "clamav"; + log_clean = true; + servers = "clamd:3310"; +} diff --git a/data/conf/rspamd/local.d/dkim.conf b/data/conf/rspamd/local.d/dkim.conf deleted file mode 100644 index c199c6ae..00000000 --- a/data/conf/rspamd/local.d/dkim.conf +++ /dev/null @@ -1,34 +0,0 @@ -sign_condition =<<EOD -return function(task) - local smtp_from = task:get_from('smtp') - local mime_from = task:get_from('mime') - local rspamd_logger = require "rspamd_logger" - if smtp_from[1]['domain'] ~= nil and smtp_from[1]['domain'] ~= '' then - domain = smtp_from[1]['domain'] - rspamd_logger.infox(task, "set domain found in smtp from field to %s", domain) - if not task:get_user() then - rspamd_logger.infox(task, "found domain in smtp header field, but user is not authenticated - skipped") - return false - end - elseif mime_from[1]['domain'] ~= nil and mime_from[1]['domain'] ~= '' then - domain = mime_from[1]['domain'] - rspamd_logger.infox(task, "set domain found in mime from field to %s", domain) - else - rspamd_logger.infox(task, "cannot determine domain for dkim signing") - return false - end - local keyfile = io.open("/data/dkim/keys/" .. domain .. ".dkim") - if keyfile then - rspamd_logger.infox(task, "found dkim key file for domain %s", domain) - keyfile:close() - return { - key = "/data/dkim/keys/" .. domain .. ".dkim", - domain = domain, - selector = "dkim" - } - else - rspamd_logger.infox(task, "no key file for domain %s - skipped", domain) - end - return false -end -EOD; diff --git a/data/conf/rspamd/local.d/dkim_signing.conf b/data/conf/rspamd/local.d/dkim_signing.conf new file mode 100644 index 00000000..23eeadb6 --- /dev/null +++ b/data/conf/rspamd/local.d/dkim_signing.conf @@ -0,0 +1,28 @@ +# If false, messages with empty envelope from are not signed +allow_envfrom_empty = false; +# If true, envelope/header domain mismatch is ignored +allow_hdrfrom_mismatch = true; +# If true, multiple from headers are allowed (but only first is used) +allow_hdrfrom_multiple = true; +# If true, username does not need to contain matching domain +allow_username_mismatch = true; +# If false, messages from authenticated users are not selected for signing +auth_only = true; +# Default path to key, can include '$domain' and '$selector' variables +path = "/data/dkim/keys/$domain.dkim"; +# Default selector to use +selector = "dkim"; +# If false, messages from local networks are not selected for signing +sign_local = true; +# Symbol to add when message is signed +symbol = "DKIM_SIGNED"; +# Whether to fallback to global config +try_fallback = true; +# Domain to use for DKIM signing: can be "header" or "envelope" +use_domain = "envelope"; +# Whether to normalise domains to eSLD +use_esld = false; +# Whether to get keys from Redis +use_redis = false; +# Hash for DKIM keys in Redis +hash_key = "DKIM_KEYS"; diff --git a/data/conf/rspamd/local.d/force_actions.conf b/data/conf/rspamd/local.d/force_actions.conf new file mode 100644 index 00000000..1aa10659 --- /dev/null +++ b/data/conf/rspamd/local.d/force_actions.conf @@ -0,0 +1,12 @@ +rules { + DKIM_FAIL { + action = "add header"; + expression = "R_DKIM_REJECT & !MAILLIST & !MAILCOW_WHITE & !MAILCOW_BLACK"; + require_action = ["no action", "greylist"]; + } + VIRUS_FOUND { + action = "reject"; + expression = "CLAM_VIRUS & !MAILCOW_WHITE"; + honor_action = ["reject"]; + } +} diff --git a/data/conf/rspamd/lua/rspamd.local.lua b/data/conf/rspamd/lua/rspamd.local.lua index 7d6aaa20..09cf9d10 100644 --- a/data/conf/rspamd/lua/rspamd.local.lua +++ b/data/conf/rspamd/lua/rspamd.local.lua @@ -7,10 +7,6 @@ rspamd_config.MAILCOW_AUTH = { end } -rspamd_config.MAILCOW_MOO = function (task) - return true -end - modify_subject_map = rspamd_config:add_map({ url = 'http://172.22.1.251:8081/tags.php', type = 'map', diff --git a/data/web/add.php b/data/web/add.php index 9361d14b..5f1aa0d6 100644 --- a/data/web/add.php +++ b/data/web/add.php @@ -350,6 +350,13 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == </div> </div> </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" name="delete1"> <?=$lang['add']['delete1'];?></label> + </div> + </div> + </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <div class="checkbox"> diff --git a/data/web/admin.php b/data/web/admin.php index 5377616f..6da0f396 100644 --- a/data/web/admin.php +++ b/data/web/admin.php @@ -67,6 +67,7 @@ $tfa_data = get_tfa(); <select data-width="auto" id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>"> <option value="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option> <option value="u2f"><?=$lang['tfa']['u2f'];?></option> + <option value="totp"><?=$lang['tfa']['totp'];?></option> <option value="none"><?=$lang['tfa']['none'];?></option> </select> </div> @@ -81,14 +82,14 @@ $tfa_data = get_tfa(); <div class="panel-body"> <form method="post"> <div class="table-responsive"> - <table class="table table-striped sortable-theme-bootstrap" data-sortable id="domainadminstable"> + <table class="table table-striped" id="domainadminstable"> <thead> <tr> - <th class="sort-table" style="min-width: 100px;"><?=$lang['admin']['username'];?></th> - <th class="sort-table" style="min-width: 166px;"><?=$lang['admin']['admin_domains'];?></th> - <th class="sort-table" style="min-width: 76px;"><?=$lang['admin']['active'];?></th> - <th class="sort-table" style="min-width: 76px;"><?=$lang['tfa']['tfa'];?></th> - <th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['admin']['action'];?></th> + <th style="min-width: 100px;"><?=$lang['admin']['username'];?></th> + <th style="min-width: 166px;"><?=$lang['admin']['admin_domains'];?></th> + <th style="min-width: 76px;"><?=$lang['admin']['active'];?></th> + <th style="min-width: 76px;"><?=$lang['tfa']['tfa'];?></th> + <th style="text-align: right; min-width: 200px;"><?=$lang['admin']['action'];?></th> </tr> </thead> <tbody> @@ -183,9 +184,11 @@ $tfa_data = get_tfa(); </div> <h4><span class="glyphicon glyphicon-wrench" aria-hidden="true"></span> <?=$lang['admin']['configuration'];?></h4> + + <div class="panel-group" id="accordion_access"> + <div class="panel panel-default"> <div class="panel-heading"><?=$lang['admin']['dkim_keys'];?></div> - <div id="collapseDKIM" class="panel-collapse"> <div class="panel-body"> <p style="margin-bottom:40px"><?=$lang['admin']['dkim_key_hint'];?></p> <?php @@ -297,10 +300,81 @@ $tfa_data = get_tfa(); </form> </div> </div> + + <div class="panel panel-default"> + <div class="panel-heading"><?=$lang['admin']['forwarding_hosts'];?></div> + <div class="panel-body"> + <p style="margin-bottom:40px"><?=$lang['admin']['forwarding_hosts_hint'];?></p> + <form method="post"> + <div class="table-responsive"> + <table class="table table-striped" id="forwardinghoststable"> + <thead> + <tr> + <th style="min-width: 100px;"><?=$lang['edit']['host'];?></th> + <th style="min-width: 100px;"><?=$lang['edit']['source'];?></th> + <th style="text-align: right; min-width: 200px;"><?=$lang['admin']['action'];?></th> + </tr> + </thead> + <tbody> + <?php + $forwarding_hosts = get_forwarding_hosts(); + if ($forwarding_hosts) { + foreach ($forwarding_hosts as $host) { + $source = $host->source; + $host = $host->host; + ?> + <tr id="data"> + <td><?=htmlspecialchars(strtolower($host));?></td> + <td><?=htmlspecialchars(strtolower($source));?></td> + <td style="text-align: right;"> + <div class="btn-group"> + <a href="delete.php?forwardinghost=<?=$host;?>" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> <?=$lang['admin']['remove'];?></a> + </div> + </td> + </td> + </tr> + + <?php + } + } else { + ?> + <tr id="no-data"><td colspan="4" style="text-align: center; font-style: italic;"><?=$lang['admin']['no_record'];?></td></tr> + <?php + } + ?> + </tbody> + </table> + </div> + </form> + <legend><?=$lang['admin']['add_forwarding_host'];?></legend> + <p class="help-block"><?=$lang['admin']['forwarding_hosts_add_hint'];?></p> + <form class="form-horizontal" role="form" method="post"> + <div class="form-group"> + <label class="control-label col-sm-2" for="hostname"><?=$lang['edit']['host'];?>:</label> + <div class="col-sm-10"> + <input type="text" class="form-control" name="hostname" id="hostname" required> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <button type="submit" name="add_forwarding_host" class="btn btn-default"><?=$lang['admin']['add'];?></button> + </div> + </div> + </form> + </div> </div> + + </div> + </div> <!-- /container --> -<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.11.4/jquery-ui.min.js" integrity="sha384-YWP9O4NjmcGo4oEJFXvvYSEzuHIvey+LbXkBNJ1Kd0yfugEZN9NCQNpRYBVC1RvA" crossorigin="anonymous"></script> -<script src="js/sorttable.js"></script> +<script type='text/javascript'> +<?php +$lang_admin = json_encode($lang['admin']); +echo "var lang = ". $lang_admin . ";\n"; +echo "var pagination_size = '". $PAGINATION_SIZE . "';\n"; +?> +</script> +<script src="js/footable.min.js"></script> <script src="js/admin.js"></script> <?php require_once("inc/footer.inc.php"); diff --git a/data/web/css/admin.css b/data/web/css/admin.css new file mode 100644 index 00000000..61f23763 --- /dev/null +++ b/data/web/css/admin.css @@ -0,0 +1,13 @@ +table.footable>tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; +} +.pagination a { + text-decoration: none !important; +} +.panel panel-default { + overflow: visible !important; +} +.table-responsive { + overflow: visible !important; +} \ No newline at end of file diff --git a/data/web/css/mailbox.css b/data/web/css/mailbox.css index b5c69343..2e6c1afe 100644 --- a/data/web/css/mailbox.css +++ b/data/web/css/mailbox.css @@ -1,19 +1,41 @@ -.panel-heading div { - margin-top: -18px; - font-size: 15px; +table.footable>tbody>tr.footable-empty>td { + font-size:15px !important; + font-style:italic; } -.panel-heading div span { - margin-left:5px; +.pagination a { + text-decoration: none !important; } -.panel-body { - display: none; +.panel panel-default { + overflow: visible !important; } -.clickable { - cursor: pointer; +.table-responsive { + overflow: visible !important; } -.progress { - margin-bottom: 0px; +.footer-add-item { + display:block; + padding: 10px; + background: #F5F5F5; } -.table>thead>tr>th { - vertical-align: top !important; +.mass-each-action { + padding: 0 3px 0 3px; + user-select: none; +} +.mass-actions { + user-select: none; + padding:10px; +} +.mass-select-all { + cursor:pointer; + color:#555; +} +#alias_table { + cursor:pointer; +} +#alias_table .footable-paging { + cursor: auto; +} +@media (min-width: 992px) { + .container { + width: 80%; + } } \ No newline at end of file diff --git a/data/web/css/mailcow.css b/data/web/css/mailcow.css index 1b3a691c..20dfb69a 100644 --- a/data/web/css/mailcow.css +++ b/data/web/css/mailcow.css @@ -50,3 +50,9 @@ pre{white-space:pre-wrap;white-space:-moz-pre-wrap;white-space:-pre-wrap;white-s -ms-user-select: none; user-select: none; } +/* Fix modal moving content left */ +body.modal-open { + overflow: inherit; + padding-right: inherit !important; +} + diff --git a/data/web/css/tables.css b/data/web/css/tables.css deleted file mode 100644 index 651e1665..00000000 --- a/data/web/css/tables.css +++ /dev/null @@ -1,79 +0,0 @@ -ul[id*="sortable"] { word-wrap: break-word; list-style-type: none; float: left; padding: 0 15px 0 0; width: 48%; cursor:move} -ul[id$="sortable-active"] li {cursor:move; } -ul[id$="sortable-inactive"] li {cursor:move } -.list-heading { cursor:default !important} -.ui-state-disabled { cursor:no-drop; color:#ccc; } -.ui-state-highlight {background: #F5F5F5 !important; height: 41px !important; cursor:move } -table[data-sortable] { - border-collapse: collapse; - border-spacing: 0; -} -table[data-sortable] th { - vertical-align: bottom; - font-weight: bold; -} -table[data-sortable] th, table[data-sortable] td { - text-align: left; - padding: 10px; -} -table[data-sortable] th:not([data-sortable="false"]) { - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - -o-user-select: none; - user-select: none; - -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - -webkit-touch-callout: none; - cursor: pointer; -} -table[data-sortable] th:after { - content: ""; - visibility: hidden; - display: inline-block; - vertical-align: inherit; - height: 0; - width: 0; - border-width: 5px; - border-style: solid; - border-color: transparent; - margin-right: 1px; - margin-left: 10px; - float: right; -} -table[data-sortable] th[data-sortable="false"]:after { - display: none; -} -table[data-sortable] th[data-sorted="true"]:after { - visibility: visible; -} -table[data-sortable] th[data-sorted-direction="descending"]:after { - border-top-color: inherit; - margin-top: 8px; -} -table[data-sortable] th[data-sorted-direction="ascending"]:after { - border-bottom-color: inherit; - margin-top: 3px; -} -table[data-sortable].sortable-theme-bootstrap thead th { - border-bottom: 2px solid #e0e0e0; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"] { - color: #3a87ad; - background: #d9edf7; - border-bottom-color: #bce8f1; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="descending"]:after { - border-top-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap th[data-sorted="true"][data-sorted-direction="ascending"]:after { - border-bottom-color: #3a87ad; -} -table[data-sortable].sortable-theme-bootstrap.sortable-theme-bootstrap-striped tbody > tr:nth-child(odd) > td { - background-color: #f9f9f9; -} -#data td, #no-data td { - vertical-align: middle; -} -.sort-table:hover { - border-bottom-color: #00B7DC !important; -} \ No newline at end of file diff --git a/data/web/delete.php b/data/web/delete.php index 6ba93d31..bfeff0f3 100644 --- a/data/web/delete.php +++ b/data/web/delete.php @@ -105,6 +105,23 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm </form> <?php } + // DELETE FORWARDING HOST + elseif (isset($_GET["forwardinghost"]) && + !empty($_GET["forwardinghost"]) && + $_SESSION['mailcow_cc_role'] == "admin") { + $host = $_GET["forwardinghost"]; + ?> + <div class="alert alert-warning" role="alert"><?=sprintf($lang['delete']['remove_forwardinghost_warning'], htmlspecialchars($_GET["forwardinghost"]));?></div> + <form class="form-horizontal" role="form" method="post" action="/admin.php"> + <input type="hidden" name="forwardinghost" value="<?=htmlspecialchars($host);?>"> + <div class="form-group"> + <div class="col-sm-offset-1 col-sm-10"> + <button type="submit" name="delete_forwarding_host" class="btn btn-default btn-sm"><?=$lang['delete']['remove_button'];?></button> + </div> + </div> + </form> + <?php + } // DELETE MAILBOX elseif (isset($_GET["mailbox"]) && filter_var($_GET["mailbox"], FILTER_VALIDATE_EMAIL) && diff --git a/data/web/edit.php b/data/web/edit.php index 5879df6d..3f3311d0 100644 --- a/data/web/edit.php +++ b/data/web/edit.php @@ -620,6 +620,20 @@ elseif (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == <input type="text" class="form-control" name="exclude" id="exclude" value="<?=htmlspecialchars($result['exclude'], ENT_QUOTES, 'UTF-8');?>"> </div> </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" name="delete2duplicates" <?=($result['delete2duplicates']=="1") ? "checked" : "";?>> <?=$lang['edit']['delete2duplicates'];?></label> + </div> + </div> + </div> + <div class="form-group"> + <div class="col-sm-offset-2 col-sm-10"> + <div class="checkbox"> + <label><input type="checkbox" name="delete1" <?=($result['delete1']=="1") ? "checked" : "";?>> <?=$lang['edit']['delete1'];?></label> + </div> + </div> + </div> <div class="form-group"> <div class="col-sm-offset-2 col-sm-10"> <div class="checkbox"> diff --git a/data/web/inc/footer.inc.php b/data/web/inc/footer.inc.php index 5523ec58..97b59c0e 100644 --- a/data/web/inc/footer.inc.php +++ b/data/web/inc/footer.inc.php @@ -19,6 +19,23 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi </div> </div> </div> +<div id="ConfirmDeleteModal" class="modal fade" role="dialog"> + <div class="modal-dialog"> + <div class="modal-content"> + <div class="modal-header"> + <button type="button" class="close" data-dismiss="modal">×</button> + <h4 class="modal-title"><?=$lang['footer']['confirm_delete'];?></h4> + </div> + <div class="modal-body"> + <p><?=$lang['footer']['delete_these_items'];?></p> + <ul id="ItemsToDelete"></ul> + <hr /> + <button class="btn btn-sm btn-danger" id="IsConfirmed"><?=$lang['footer']['delete_now'];?></button> + <button class="btn btn-sm btn-default" id="isCanceled"><?=$lang['footer']['cancel'];?></button> + </div> + </div> + </div> +</div> <?php endif; ?> @@ -50,11 +67,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "json_api.php", - data: { - 'action':'get_u2f_auth_challenge', - 'object':'<?=(isset($_SESSION['pending_mailcow_cc_username'])) ? $_SESSION['pending_mailcow_cc_username'] : null;?>', - }, + url: "/api/v1/get/u2f-authentication/<?=(isset($_SESSION['pending_mailcow_cc_username'])) ? $_SESSION['pending_mailcow_cc_username'] : null;?>", success: function(data){ data; } @@ -80,6 +93,10 @@ $(document).ready(function() { $('#YubiOTPModal').modal('show'); $("option:selected").prop("selected", false); } + if ($(this).val() == "totp") { + $('#TOTPModal').modal('show'); + $("option:selected").prop("selected", false); + } if ($(this).val() == "u2f") { $('#U2FModal').modal('show'); $("option:selected").prop("selected", false); @@ -87,11 +104,7 @@ $(document).ready(function() { type: "GET", cache: false, dataType: 'script', - url: "json_api.php", - data: { - 'action':'get_u2f_reg_challenge', - 'object':'<?=(isset($_SESSION['mailcow_cc_username'])) ? $_SESSION['mailcow_cc_username'] : null;?>', - }, + url: "/api/v1/get/u2f-registration/<?=(isset($_SESSION['mailcow_cc_username'])) ? $_SESSION['mailcow_cc_username'] : null;?>", success: function(data){ data; } @@ -132,25 +145,27 @@ $(document).ready(function() { // Remember last navigation pill (function () { 'use strict'; - $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { - var id = $(this).parents('[role="tablist"]').attr('id'); - var key = 'lastTag'; - if (id) { - key += ':' + id; - } - localStorage.setItem(key, $(e.target).attr('href')); - }); - $('[role="tablist"]').each(function (idx, elem) { - var id = $(elem).attr('id'); - var key = 'lastTag'; - if (id) { - key += ':' + id; - } - var lastTab = localStorage.getItem(key); - if (lastTab) { - $('[href="' + lastTab + '"]').tab('show'); - } - }); + if ($('a[data-toggle="tab"]').length) { + $('a[data-toggle="tab"]').on('shown.bs.tab', function (e) { + var id = $(this).parents('[role="tablist"]').attr('id'); + var key = 'lastTag'; + if (id) { + key += ':' + id; + } + localStorage.setItem(key, $(e.target).attr('href')); + }); + $('[role="tablist"]').each(function (idx, elem) { + var id = $(elem).attr('id'); + var key = 'lastTag'; + if (id) { + key += ':' + id; + } + var lastTab = localStorage.getItem(key); + if (lastTab) { + $('[href="' + lastTab + '"]').tab('show'); + } + }); + } })(); // Disable submit after submitting form diff --git a/data/web/inc/functions.inc.php b/data/web/inc/functions.inc.php index c37e39ab..573a1a0b 100644 --- a/data/web/inc/functions.inc.php +++ b/data/web/inc/functions.inc.php @@ -62,64 +62,6 @@ function hasMailboxObjectAccess($username, $role, $object) { } return false; } -function init_db_schema() { - // This will be much better in future releases... - global $pdo; - try { - $stmt = $pdo->prepare("SELECT NULL FROM `admin`, `imapsync`, `tfa`"); - $stmt->execute(); - } - catch (Exception $e) { - $lines = file('/web/inc/init.sql'); - $data = ''; - foreach ($lines as $line) { - if (substr($line, 0, 2) == '--' || $line == '') { - continue; - } - $data .= $line; - if (substr(trim($line), -1, 1) == ';') { - $pdo->query($data); - $data = ''; - } - } - // Create index if not exists - $stmt = $pdo->query("SHOW INDEX FROM sogo_acl WHERE KEY_NAME = 'sogo_acl_c_folder_id_idx'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("CREATE INDEX sogo_acl_c_folder_id_idx ON sogo_acl(c_folder_id)"); - } - $stmt = $pdo->query("SHOW INDEX FROM sogo_acl WHERE KEY_NAME = 'sogo_acl_c_uid_idx'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("CREATE INDEX sogo_acl_c_uid_idx ON sogo_acl(c_uid)"); - } - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => 'Database initialization completed.' - ); - } - // Add newly added columns - $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'kind'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("ALTER TABLE `mailbox` ADD `kind` VARCHAR(100) NOT NULL DEFAULT ''"); - } - $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'multiple_bookings'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("ALTER TABLE `mailbox` ADD `multiple_bookings` tinyint(1) NOT NULL DEFAULT '0'"); - } - $stmt = $pdo->query("SHOW COLUMNS FROM `mailbox` LIKE 'wants_tagged_subject'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("ALTER TABLE `mailbox` ADD `wants_tagged_subject` tinyint(1) NOT NULL DEFAULT '0'"); - } - $stmt = $pdo->query("SHOW COLUMNS FROM `tfa` LIKE 'key_id'"); - $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); - if ($num_results == 0) { - $pdo->query("ALTER TABLE `tfa` ADD `key_id` VARCHAR(255) DEFAULT 'unidentified'"); - } -} function verify_ssha256($hash, $password) { // Remove tag if any $hash = ltrim($hash, '{SSHA256}'); @@ -282,13 +224,11 @@ function edit_admin_account($postarray) { $password_hashed = hash_password($password); try { $stmt = $pdo->prepare("UPDATE `admin` SET - `modified` = :modified, `password` = :password_hashed, `username` = :username1 WHERE `username` = :username2"); $stmt->execute(array( ':password_hashed' => $password_hashed, - ':modified' => date('Y-m-d H:i:s'), ':username1' => $username, ':username2' => $username_now )); @@ -304,12 +244,10 @@ function edit_admin_account($postarray) { else { try { $stmt = $pdo->prepare("UPDATE `admin` SET - `modified` = :modified, `username` = :username1 WHERE `username` = :username2"); $stmt->execute(array( ':username1' => $username, - ':modified' => date('Y-m-d H:i:s'), ':username2' => $username_now )); } @@ -603,10 +541,9 @@ function edit_user_account($postarray) { } $password_hashed = hash_password($password_new); try { - $stmt = $pdo->prepare("UPDATE `mailbox` SET `modified` = :modified, `password` = :password_hashed WHERE `username` = :username"); + $stmt = $pdo->prepare("UPDATE `mailbox` SET `password` = :password_hashed WHERE `username` = :username"); $stmt->execute(array( ':password_hashed' => $password_hashed, - ':modified' => date('Y-m-d H:i:s'), ':username' => $username )); } @@ -1075,6 +1012,7 @@ function add_syncjob($postarray) { } isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0'; + isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0'; $port1 = $postarray['port1']; $host1 = $postarray['host1']; $password1 = $postarray['password1']; @@ -1147,12 +1085,13 @@ function add_syncjob($postarray) { return false; } try { - $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`) - VALUES (:user2, :exclude, :maxage, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)"); + $stmt = $pdo->prepare("INSERT INTO `imapsync` (`user2`, `exclude`, `delete1`, `maxage`, `subfolder2`, `host1`, `authmech1`, `user1`, `password1`, `mins_interval`, `port1`, `enc1`, `delete2duplicates`, `active`) + VALUES (:user2, :exclude, :maxage, :delete1, :subfolder2, :host1, :authmech1, :user1, :password1, :mins_interval, :port1, :enc1, :delete2duplicates, :active)"); $stmt->execute(array( ':user2' => $username, ':exclude' => $exclude, ':maxage' => $maxage, + ':delete1' => $delete1, ':subfolder2' => $subfolder2, ':host1' => $host1, ':authmech1' => 'PLAIN', @@ -1200,6 +1139,7 @@ function edit_syncjob($postarray) { } isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['delete2duplicates']) ? $delete2duplicates = '1' : $delete2duplicates = '0'; + isset($postarray['delete1']) ? $delete1 = '1' : $delete1 = '0'; $id = $postarray['id']; $port1 = $postarray['port1']; $host1 = $postarray['host1']; @@ -1273,10 +1213,11 @@ function edit_syncjob($postarray) { return false; } try { - $stmt = $pdo->prepare("UPDATE `imapsync` set `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active + $stmt = $pdo->prepare("UPDATE `imapsync` set `delete1` = :delete1, `maxage` = :maxage, `subfolder2` = :subfolder2, `exclude` = :exclude, `host1` = :host1, `user1` = :user1, `password1` = :password1, `mins_interval` = :mins_interval, `port1` = :port1, `enc1` = :enc1, `delete2duplicates` = :delete2duplicates, `active` = :active WHERE `user2` = :user2 AND `id` = :id"); $stmt->execute(array( ':user2' => $username, + ':delete1' => $delete1, ':id' => $id, ':exclude' => $exclude, ':maxage' => $maxage, @@ -1624,13 +1565,11 @@ function add_domain_admin($postarray) { } } try { - $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`) - VALUES (:username, :password_hashed, '0', :created, :modified, :active)"); + $stmt = $pdo->prepare("INSERT INTO `admin` (`username`, `password`, `superadmin`, `active`) + VALUES (:username, :password_hashed, '0', :active)"); $stmt->execute(array( ':username' => $username, ':password_hashed' => $password_hashed, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); } @@ -1757,6 +1696,7 @@ function get_domain_admin_details($domain_admin) { try { $stmt = $pdo->prepare("SELECT `tfa`.`active` AS `tfa_active_int`, + CASE `tfa`.`active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `tfa_active`, `domain_admins`.`username`, `domain_admins`.`created`, `domain_admins`.`active` AS `active_int`, @@ -1768,11 +1708,15 @@ function get_domain_admin_details($domain_admin) { ':domain_admin' => $domain_admin )); $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (empty($row)) { + return false; + } $domainadmindata['username'] = $row['username']; + $domainadmindata['tfa_active'] = $row['tfa_active']; $domainadmindata['active'] = $row['active']; - $domainadmindata['active_int'] = $row['active_int']; $domainadmindata['tfa_active_int'] = $row['tfa_active_int']; - $domainadmindata['created'] = $row['created']; + $domainadmindata['active_int'] = $row['active_int']; + $domainadmindata['modified'] = $row['created']; // GET SELECTED $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain` IN ( @@ -1793,6 +1737,9 @@ function get_domain_admin_details($domain_admin) { while($row = array_shift($rows)) { $domainadmindata['unselected_domains'][] = $row['domain']; } + if (!isset($domainadmindata['unselected_domains'])) { + $domainadmindata['unselected_domains'] = ""; + } } catch(PDOException $e) { $_SESSION['return'] = array( @@ -1807,6 +1754,7 @@ function set_tfa($postarray) { global $pdo; global $yubi; global $u2f; + global $tfa; if ($_SESSION['mailcow_cc_role'] != "domainadmin" && $_SESSION['mailcow_cc_role'] != "admin") { @@ -1903,6 +1851,36 @@ function set_tfa($postarray) { 'msg' => "U2F: " . $e->getMessage() ); $_SESSION['regReq'] = null; + return false; + } + break; + + case "totp": + (!isset($postarray["key_id"])) ? $key_id = 'unidentified' : $key_id = $postarray["key_id"]; + if ($tfa->verifyCode($_POST['totp_secret'], $_POST['totp_confirm_token']) === true) { + try { + $stmt = $pdo->prepare("DELETE FROM `tfa` WHERE `username` = :username"); + $stmt->execute(array(':username' => $username)); + $stmt = $pdo->prepare("INSERT INTO `tfa` (`username`, `key_id`, `authmech`, `secret`, `active`) VALUES (?, ?, 'totp', ?, '1')"); + $stmt->execute(array($username, $key_id, $_POST['totp_secret'])); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['object_modified'], $username) + ); + } + else { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'TOTP verification failed' + ); } break; @@ -2023,8 +2001,16 @@ function get_tfa($username = null) { case "totp": $data['name'] = "totp"; $data['pretty'] = "Time-based OTP"; + $stmt = $pdo->prepare("SELECT `id`, `key_id`, `secret` FROM `tfa` WHERE `authmech` = 'totp' AND `username` = :username"); + $stmt->execute(array( + ':username' => $username, + )); + $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); + while($row = array_shift($rows)) { + $data['additional'][] = $row; + } return $data; - break; + break; default: $data['name'] = 'none'; $data['pretty'] = "-"; @@ -2036,6 +2022,8 @@ function verify_tfa_login($username, $token) { global $pdo; global $lang; global $yubi; + global $u2f; + global $tfa; $stmt = $pdo->prepare("SELECT `authmech` FROM `tfa` WHERE `username` = :username AND `active` = '1'"); @@ -2073,7 +2061,6 @@ function verify_tfa_login($username, $token) { break; case "u2f": try { - global $u2f; $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), get_u2f_registrations($username), json_decode($token)); $stmt = $pdo->prepare("UPDATE `tfa` SET `counter` = ? WHERE `id` = ?"); $stmt->execute(array($reg->counter, $reg->id)); @@ -2095,7 +2082,26 @@ function verify_tfa_login($username, $token) { return false; break; case "totp": + try { + $stmt = $pdo->prepare("SELECT `id`, `secret` FROM `tfa` + WHERE `username` = :username + AND `authmech` = 'totp' + AND `active`='1'"); + $stmt->execute(array(':username' => $username)); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if ($tfa->verifyCode($row['secret'], $_POST['token']) === true) { + $_SESSION['tfa_id'] = $row['id']; + return true; + } return false; + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } break; default: return false; @@ -2134,6 +2140,14 @@ function edit_domain_admin($postarray) { } } + if (empty($postarray['domain'])) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['domain_invalid']) + ); + return false; + } + if (!ctype_alnum(str_replace(array('_', '.', '-'), '', $username))) { $_SESSION['return'] = array( 'type' => 'danger', @@ -2164,7 +2178,7 @@ function edit_domain_admin($postarray) { return false; } - if(isset($postarray['domain'])) { + if (isset($postarray['domain'])) { foreach ($postarray['domain'] as $domain) { try { $stmt = $pdo->prepare("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) @@ -2203,12 +2217,11 @@ function edit_domain_admin($postarray) { } $password_hashed = hash_password($password); try { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `modified` = :modified, `active` = :active, `password` = :password_hashed WHERE `username` = :username2"); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `active` = :active, `password` = :password_hashed WHERE `username` = :username2"); $stmt->execute(array( ':password_hashed' => $password_hashed, ':username1' => $username, ':username2' => $username_now, - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); if (isset($postarray['disable_tfa'])) { @@ -2230,11 +2243,10 @@ function edit_domain_admin($postarray) { } else { try { - $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `modified` = :modified, `active` = :active WHERE `username` = :username2"); + $stmt = $pdo->prepare("UPDATE `admin` SET `username` = :username1, `active` = :active WHERE `username` = :username2"); $stmt->execute(array( ':username1' => $username, ':username2' => $username_now, - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); if (isset($postarray['disable_tfa'])) { @@ -2296,10 +2308,9 @@ function edit_domain_admin($postarray) { } $password_hashed = hash_password($password_new); try { - $stmt = $pdo->prepare("UPDATE `admin` SET `modified` = :modified, `password` = :password_hashed WHERE `username` = :username"); + $stmt = $pdo->prepare("UPDATE `admin` SET `password` = :password_hashed WHERE `username` = :username"); $stmt->execute(array( ':password_hashed' => $password_hashed, - ':modified' => date('Y-m-d H:i:s'), ':username' => $username )); } @@ -2331,7 +2342,7 @@ function get_admin_details() { return false; } try { - $stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin`WHERE `superadmin`='1' AND active='1'"); + $stmt = $pdo->prepare("SELECT `username`, `modified`, `created` FROM `admin` WHERE `superadmin`='1' AND active='1'"); $stmt->execute(); $data = $stmt->fetch(PDO::FETCH_ASSOC); } @@ -2519,6 +2530,14 @@ function mailbox_add_domain($postarray) { return false; } + if ($maxquota == "0" || empty($maxquota)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['maxquota_empty']) + ); + return false; + } + isset($postarray['active']) ? $active = '1' : $active = '0'; isset($postarray['relay_all_recipients']) ? $relay_all_recipients = '1' : $relay_all_recipients = '0'; isset($postarray['backupmx']) ? $backupmx = '1' : $backupmx = '0'; @@ -2568,8 +2587,8 @@ function mailbox_add_domain($postarray) { } try { - $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `transport`, `backupmx`, `created`, `modified`, `active`, `relay_all_recipients`) - VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, 'virtual', :backupmx, :created, :modified, :active, :relay_all_recipients)"); + $stmt = $pdo->prepare("INSERT INTO `domain` (`domain`, `description`, `aliases`, `mailboxes`, `maxquota`, `quota`, `transport`, `backupmx`, `active`, `relay_all_recipients`) + VALUES (:domain, :description, :aliases, :mailboxes, :maxquota, :quota, 'virtual', :backupmx, :active, :relay_all_recipients)"); $stmt->execute(array( ':domain' => $domain, ':description' => $description, @@ -2579,8 +2598,6 @@ function mailbox_add_domain($postarray) { ':quota' => $quota, ':backupmx' => $backupmx, ':active' => $active, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':relay_all_recipients' => $relay_all_recipients )); $_SESSION['return'] = array( @@ -2623,6 +2640,18 @@ function mailbox_add_alias($postarray) { return false; } + $stmt = $pdo->prepare("SELECT `address` FROM `alias` + WHERE `address`= :address"); + $stmt->execute(array(':address' => $address)); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['is_alias_or_mailbox'], htmlspecialchars($address)) + ); + return false; + } + foreach ($addresses as $address) { if (empty($address)) { continue; @@ -2632,6 +2661,15 @@ function mailbox_add_alias($postarray) { $local_part = strstr($address, '@', true); $address = $local_part.'@'.$domain; + $domaindata = mailbox_get_domain_details($domain); + if (is_array($domaindata) && $domaindata['aliases_left'] == "0") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['max_alias_exceeded']) + ); + return false; + } + try { $stmt = $pdo->prepare("SELECT `domain` FROM `domain` WHERE `domain`= :domain1 OR `domain` = (SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain2)"); @@ -2735,16 +2773,14 @@ function mailbox_add_alias($postarray) { $goto = implode(",", $gotos); try { - $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`) - VALUES (:address, :goto, :domain, :created, :modified, :active)"); + $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `active`) + VALUES (:address, :goto, :domain, :active)"); if (!filter_var($address, FILTER_VALIDATE_EMAIL) === true) { $stmt->execute(array( ':address' => '@'.$domain, ':goto' => $goto, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); } @@ -2753,8 +2789,6 @@ function mailbox_add_alias($postarray) { ':address' => $address, ':goto' => $goto, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); } @@ -2855,13 +2889,11 @@ function mailbox_add_alias_domain($postarray) { } try { - $stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `created`, `modified`, `active`) - VALUES (:alias_domain, :target_domain, :created, :modified, :active)"); + $stmt = $pdo->prepare("INSERT INTO `alias_domain` (`alias_domain`, `target_domain`, `active`) + VALUES (:alias_domain, :target_domain, :active)"); $stmt->execute(array( ':alias_domain' => $alias_domain, ':target_domain' => $target_domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); $_SESSION['return'] = array( @@ -3064,8 +3096,8 @@ function mailbox_add_mailbox($postarray) { } try { - $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `created`, `modified`, `active`) - VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :created, :modified, :active)"); + $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `active`) + VALUES (:username, :password_hashed, :name, :maildir, :quota_b, :local_part, :domain, :active)"); $stmt->execute(array( ':username' => $username, ':password_hashed' => $password_hashed, @@ -3074,8 +3106,6 @@ function mailbox_add_mailbox($postarray) { ':quota_b' => $quota_b, ':local_part' => $local_part, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); @@ -3083,14 +3113,12 @@ function mailbox_add_mailbox($postarray) { VALUES (:username, '0', '0')"); $stmt->execute(array(':username' => $username)); - $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `created`, `modified`, `active`) - VALUES (:username1, :username2, :domain, :created, :modified, :active)"); + $stmt = $pdo->prepare("INSERT INTO `alias` (`address`, `goto`, `domain`, `active`) + VALUES (:username1, :username2, :domain, :active)"); $stmt->execute(array( ':username1' => $username, ':username2' => $username, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); @@ -3220,15 +3248,13 @@ function mailbox_add_resource($postarray) { } try { - $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `created`, `modified`, `active`, `multiple_bookings`, `kind`) - VALUES (:name, 'RESOURCE', :description, 'RESOURCE', 0, :local_part, :domain, :created, :modified, :active, :multiple_bookings, :kind)"); + $stmt = $pdo->prepare("INSERT INTO `mailbox` (`username`, `password`, `name`, `maildir`, `quota`, `local_part`, `domain`, `active`, `multiple_bookings`, `kind`) + VALUES (:name, 'RESOURCE', :description, 'RESOURCE', 0, :local_part, :domain, :active, :multiple_bookings, :kind)"); $stmt->execute(array( ':name' => $name, ':description' => $description, ':local_part' => $local_part, ':domain' => $domain, - ':created' => date('Y-m-d H:i:s'), - ':modified' => date('Y-m-d H:i:s'), ':active' => $active, ':kind' => $kind, ':multiple_bookings' => $multiple_bookings @@ -3319,12 +3345,10 @@ function mailbox_edit_alias_domain($postarray) { try { $stmt = $pdo->prepare("UPDATE `alias_domain` SET `alias_domain` = :alias_domain, - `active` = :active, - `modified` = :modified + `active` = :active WHERE `alias_domain` = :alias_domain_now"); $stmt->execute(array( ':alias_domain' => $alias_domain, - ':modified' => date('Y-m-d H:i:s'), ':alias_domain_now' => $alias_domain_now, ':active' => $active )); @@ -3343,84 +3367,95 @@ function mailbox_edit_alias_domain($postarray) { ); } function mailbox_edit_alias($postarray) { - // Array elements - // address string - // goto string (separated by " ", "," ";" "\n") - email address or domain - // active int + // We can edit multiple addresses at once, but only set one "goto" and/or "active" attribute for all + // address string or array containing strings | email | required + // goto string | separated by " ", "," ";" "\n", email or domain | optional + // active set (active) or unset (inactive) global $lang; global $pdo; - $address = $postarray['address']; - $domain = idn_to_ascii(substr(strstr($address, '@'), 1)); - $local_part = strstr($address, '@', true); - if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - if (empty($postarray['goto'])) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['goto_empty']) - ); - return false; - } - $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto'])); - foreach ($gotos as &$goto) { - if (empty($goto)) { - continue; - } - if (!filter_var($goto, FILTER_VALIDATE_EMAIL)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' =>sprintf($lang['danger']['goto_invalid']) - ); - return false; - } - if ($goto == $address) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['alias_goto_identical']) - ); - return false; - } - } - $gotos = array_filter($gotos); - $goto = implode(",", $gotos); + if (!is_array($postarray['address'])) { + $address_array = array(); + $address_array[] = $postarray['address']; + } + else { + $address_array = $postarray['address']; + } + if (isset($postarray['goto']) || !empty($postarray['goto'])) { + $gotos = array_map('trim', preg_split( "/( |,|;|\n)/", $postarray['goto'])); + foreach ($gotos as &$goto) { + if (empty($goto)) { + continue; + } + if (!filter_var($goto, FILTER_VALIDATE_EMAIL)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' =>sprintf($lang['danger']['goto_invalid']) + ); + return false; + } + if ($goto == $address) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['alias_goto_identical']) + ); + return false; + } + } + $gotos = array_filter($gotos); + $goto = implode(",", $gotos); + } isset($postarray['active']) ? $active = '1' : $active = '0'; - if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['alias_invalid']) - ); - return false; - } - - try { - $stmt = $pdo->prepare("UPDATE `alias` SET - `goto` = :goto, - `active`= :active, - `modified` = :modified - WHERE `address` = :address"); - $stmt->execute(array( - ':goto' => $goto, - ':active' => $active, - ':address' => $address, - ':modified' => date('Y-m-d H:i:s'), - )); - $_SESSION['return'] = array( - 'type' => 'success', - 'msg' => sprintf($lang['success']['alias_modified'], htmlspecialchars($address)) - ); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; + foreach ($address_array as $address) { + $domain = idn_to_ascii(substr(strstr($address, '@'), 1)); + $local_part = strstr($address, '@', true); + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + if ((!filter_var($address, FILTER_VALIDATE_EMAIL) === true) && !empty($local_part)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['alias_invalid']) + ); + return false; + } + try { + if (isset($goto) && !empty($goto)) { + $stmt = $pdo->prepare("UPDATE `alias` SET + `goto` = :goto, + `active`= :active + WHERE `address` = :address"); + $stmt->execute(array( + ':goto' => $goto, + ':active' => $active, + ':address' => $address + )); + } + else { + $stmt = $pdo->prepare("UPDATE `alias` SET + `active`= :active + WHERE `address` = :address"); + $stmt->execute(array( + ':active' => $active, + ':address' => $address + )); + } + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['alias_modified'], htmlspecialchars(implode(', ', $address_array))) + ); } function mailbox_edit_domain($postarray) { // Array elements @@ -3432,7 +3467,7 @@ function mailbox_edit_domain($postarray) { // aliases float // mailboxes float // maxquota float - // quota float (Byte) + // quota float (Byte) // active int global $lang; @@ -3452,11 +3487,9 @@ function mailbox_edit_domain($postarray) { isset($postarray['active']) ? $active = '1' : $active = '0'; try { $stmt = $pdo->prepare("UPDATE `domain` SET - `modified`= :modified, `description` = :description WHERE `domain` = :domain"); $stmt->execute(array( - ':modified' => date('Y-m-d H:i:s'), ':description' => $description, ':domain' => $domain )); @@ -3519,6 +3552,14 @@ function mailbox_edit_domain($postarray) { return false; } + if ($maxquota == "0" || empty($maxquota)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['maxquota_empty']) + ); + return false; + } + if ($MailboxData['maxquota'] > $maxquota) { $_SESSION['return'] = array( 'type' => 'danger', @@ -3552,7 +3593,6 @@ function mailbox_edit_domain($postarray) { } try { $stmt = $pdo->prepare("UPDATE `domain` SET - `modified`= :modified, `relay_all_recipients` = :relay_all_recipients, `backupmx` = :backupmx, `active` = :active, @@ -3570,7 +3610,6 @@ function mailbox_edit_domain($postarray) { ':maxquota' => $maxquota, ':mailboxes' => $mailboxes, ':aliases' => $aliases, - ':modified' => date('Y-m-d H:i:s'), ':description' => $description, ':domain' => $domain )); @@ -3782,23 +3821,19 @@ function mailbox_edit_mailbox($postarray) { $password_hashed = hash_password($password); try { $stmt = $pdo->prepare("UPDATE `alias` SET - `modified` = :modified, `active` = :active WHERE `address` = :address"); $stmt->execute(array( ':address' => $username, - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); $stmt = $pdo->prepare("UPDATE `mailbox` SET - `modified` = :modified, `active` = :active, `password` = :password_hashed, `name`= :name, `quota` = :quota_b WHERE `username` = :username"); $stmt->execute(array( - ':modified' => date('Y-m-d H:i:s'), ':password_hashed' => $password_hashed, ':active' => $active, ':name' => $name, @@ -3821,23 +3856,19 @@ function mailbox_edit_mailbox($postarray) { } try { $stmt = $pdo->prepare("UPDATE `alias` SET - `modified` = :modified, `active` = :active WHERE `address` = :address"); $stmt->execute(array( ':address' => $username, - ':modified' => date('Y-m-d H:i:s'), ':active' => $active )); $stmt = $pdo->prepare("UPDATE `mailbox` SET - `modified` = :modified, `active` = :active, `name`= :name, `quota` = :quota_b WHERE `username` = :username"); $stmt->execute(array( ':active' => $active, - ':modified' => date('Y-m-d H:i:s'), ':name' => $name, ':quota_b' => $quota_b, ':username' => $username @@ -3900,7 +3931,6 @@ function mailbox_edit_resource($postarray) { try { $stmt = $pdo->prepare("UPDATE `mailbox` SET - `modified` = :modified, `active` = :active, `name`= :description, `kind`= :kind, @@ -3908,7 +3938,6 @@ function mailbox_edit_resource($postarray) { WHERE `username` = :name"); $stmt->execute(array( ':active' => $active, - ':modified' => date('Y-m-d H:i:s'), ':description' => $description, ':multiple_bookings' => $multiple_bookings, ':kind' => $kind, @@ -4137,6 +4166,17 @@ function mailbox_get_alias_details($address) { ':address' => $address, )); $row = $stmt->fetch(PDO::FETCH_ASSOC); + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain"); + $stmt->execute(array( + ':domain' => $row['domain'], + )); + $row_alias_domain = $stmt->fetch(PDO::FETCH_ASSOC); + if (isset($row_alias_domain['target_domain']) && !empty($row_alias_domain['target_domain'])) { + $aliasdata['in_primary_domain'] = $row_alias_domain['target_domain']; + } + else { + $aliasdata['in_primary_domain'] = ""; + } $aliasdata['domain'] = $row['domain']; $aliasdata['goto'] = $row['goto']; $aliasdata['address'] = $row['address']; @@ -4253,6 +4293,15 @@ function mailbox_get_domain_details($domain) { } try { + $stmt = $pdo->prepare("SELECT `target_domain` FROM `alias_domain` WHERE `alias_domain` = :domain"); + $stmt->execute(array( + ':domain' => $domain + )); + $row = $stmt->fetch(PDO::FETCH_ASSOC); + if (!empty($row)) { + $domain = $row['target_domain']; + } + $stmt = $pdo->prepare("SELECT `domain`, `description`, @@ -4268,10 +4317,18 @@ function mailbox_get_domain_details($domain) { CASE `active` WHEN 1 THEN '".$lang['mailbox']['yes']."' ELSE '".$lang['mailbox']['no']."' END AS `active` FROM `domain` WHERE `domain`= :domain"); $stmt->execute(array( - ':domain' => $domain, + ':domain' => $domain )); $row = $stmt->fetch(PDO::FETCH_ASSOC); - $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, COALESCE(SUM(`quota`), 0) as `in_use` FROM `mailbox` WHERE `kind` NOT REGEXP 'location|thing|group' AND `domain` = :domain"); + if (empty($row)) { + return false; + } + + $stmt = $pdo->prepare("SELECT COUNT(*) AS `count`, + COALESCE(SUM(`quota`), 0) AS `in_use` + FROM `mailbox` + WHERE `kind` NOT REGEXP 'location|thing|group' + AND `domain` = :domain"); $stmt->execute(array(':domain' => $row['domain'])); $MailboxDataDomain = $stmt->fetch(PDO::FETCH_ASSOC); @@ -4296,15 +4353,17 @@ function mailbox_get_domain_details($domain) { $domaindata['relay_all_recipients_int'] = $row['relay_all_recipients_int']; $stmt = $pdo->prepare("SELECT COUNT(*) AS `alias_count` FROM `alias` - WHERE `domain`= :domain + WHERE (`domain`= :domain OR `domain` IN (SELECT `alias_domain` FROM `alias_domain` WHERE `target_domain` = :domain2)) AND `address` NOT IN ( SELECT `username` FROM `mailbox` )"); $stmt->execute(array( ':domain' => $domain, + ':domain2' => $domain )); - $AliasData = $stmt->fetch(PDO::FETCH_ASSOC); - (isset($AliasData['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasData['alias_count'] : $domaindata['aliases_in_domain'] = "0"; + $AliasDataDomain = $stmt->fetch(PDO::FETCH_ASSOC); + (isset($AliasDataDomain['alias_count'])) ? $domaindata['aliases_in_domain'] = $AliasDataDomain['alias_count'] : $domaindata['aliases_in_domain'] = "0"; + $domaindata['aliases_left'] = $row['aliases'] - $AliasDataDomain['alias_count']; } catch (PDOException $e) { $_SESSION['return'] = array( @@ -4540,48 +4599,57 @@ function mailbox_delete_domain($postarray) { return true; } function mailbox_delete_alias($postarray) { + // $postarray['address'] can be a single element or an array global $lang; global $pdo; - $address = $postarray['address']; - $local_part = strstr($address, '@', true); - $domain = mailbox_get_alias_details($address)['domain']; - try { - $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :address"); - $stmt->execute(array(':address' => $address)); - $gotos = $stmt->fetch(PDO::FETCH_ASSOC); - } - catch(PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } - $goto_array = explode(',', $gotos['goto']); + if (!is_array($postarray['address'])) { + $address_array = array(); + $address_array[] = $postarray['address']; + } + else { + $address_array = $postarray['address']; + } + foreach ($address_array as $address) { + $local_part = strstr($address, '@', true); + $domain = mailbox_get_alias_details($address)['domain']; + try { + $stmt = $pdo->prepare("SELECT `goto` FROM `alias` WHERE `address` = :address"); + $stmt->execute(array(':address' => $address)); + $gotos = $stmt->fetch(PDO::FETCH_ASSOC); + } + catch(PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $goto_array = explode(',', $gotos['goto']); - if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => sprintf($lang['danger']['access_denied']) - ); - return false; - } - try { - $stmt = $pdo->prepare("DELETE FROM `alias` WHERE `address` = :address AND `address` NOT IN (SELECT `username` FROM `mailbox`)"); - $stmt->execute(array( - ':address' => $address - )); - } - catch (PDOException $e) { - $_SESSION['return'] = array( - 'type' => 'danger', - 'msg' => 'MySQL: '.$e - ); - return false; - } + if (!hasDomainAccess($_SESSION['mailcow_cc_username'], $_SESSION['mailcow_cc_role'], $domain)) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + try { + $stmt = $pdo->prepare("DELETE FROM `alias` WHERE `address` = :address AND `address` NOT IN (SELECT `username` FROM `mailbox`)"); + $stmt->execute(array( + ':address' => $address + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } $_SESSION['return'] = array( 'type' => 'success', - 'msg' => sprintf($lang['success']['alias_removed'], htmlspecialchars($address)) + 'msg' => sprintf($lang['success']['alias_removed'], htmlspecialchars(implode(', ', $address_array))) ); } @@ -4729,12 +4797,10 @@ function mailbox_delete_mailbox($postarray) { } $gotos_rebuild = implode(',', $goto_exploded); $stmt = $pdo->prepare("UPDATE `alias` SET - `goto` = :goto, - `modified` = :modified, + `goto` = :goto WHERE `address` = :address"); $stmt->execute(array( ':goto' => $gotos_rebuild, - ':modified' => date('Y-m-d H:i:s'), ':address' => $gotos['address'] )); } @@ -4972,4 +5038,93 @@ function get_u2f_registrations($username) { $sel->execute(array($username)); return $sel->fetchAll(PDO::FETCH_OBJ); } +function get_forwarding_hosts() { + global $pdo; + $sel = $pdo->prepare("SELECT host, source FROM `forwarding_hosts`"); + $sel->execute(); + return $sel->fetchAll(PDO::FETCH_OBJ); +} +function add_forwarding_host($postarray) { + require_once 'spf.inc.php'; + global $pdo; + global $lang; + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $source = $postarray['hostname']; + $host = $postarray['hostname']; + $hosts = array(); + if (preg_match('/^[0-9a-fA-F:\/]+$/', $host)) { // IPv6 address + $hosts = array($host); + } + elseif (preg_match('/^[0-9\.\/]+$/', $host)) { // IPv4 address + $hosts = array($host); + } + else { + $hosts = get_outgoing_hosts_best_guess($host); + } + if (!$hosts) + { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Invalid host specified: '. htmlspecialchars($host) + ); + return false; + } + foreach ($hosts as $host) { + if ($source == $host) + $source = ''; + try { + $stmt = $pdo->prepare("INSERT IGNORE INTO `forwarding_hosts` (`host`, `source`) VALUES (:host, :source)"); + $stmt->execute(array( + ':host' => $host, + ':source' => $source, + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['forwarding_host_added'], htmlspecialchars(implode(', ', $hosts))) + ); +} +function delete_forwarding_host($postarray) { + global $pdo; + global $lang; + if ($_SESSION['mailcow_cc_role'] != "admin") { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => sprintf($lang['danger']['access_denied']) + ); + return false; + } + $host = $postarray['forwardinghost']; + try { + $stmt = $pdo->prepare("DELETE FROM `forwarding_hosts` WHERE `host` = :host"); + $stmt->execute(array( + ':host' => $host, + )); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'MySQL: '.$e + ); + return false; + } + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => sprintf($lang['success']['forwarding_host_removed'], htmlspecialchars($host)) + ); +} ?> diff --git a/data/web/inc/header.inc.php b/data/web/inc/header.inc.php index a206a35d..678c2590 100644 --- a/data/web/inc/header.inc.php +++ b/data/web/inc/header.inc.php @@ -16,11 +16,10 @@ <link rel="stylesheet" href="/css/bootstrap-slider.min.css"> <link rel="stylesheet" href="/css/bootstrap-switch.min.css"> <link rel="stylesheet" href="/css/footable.bootstrap.min.css"> -<link rel="stylesheet" href="//fonts.googleapis.com/css?family=Source+Sans+Pro:400,600,700&subset=latin,latin-ext"> <link rel="stylesheet" href="/inc/languages.min.css"> <link rel="stylesheet" href="/css/mailcow.css"> -<link rel="stylesheet" href="/css/tables.css"> <?=(preg_match("/mailbox.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/mailbox.css">' : null;?> +<?=(preg_match("/admin.php/i", $_SERVER['REQUEST_URI'])) ? '<link rel="stylesheet" href="/css/admin.css">' : null;?> <link rel="shortcut icon" href="/favicon.png" type="image/png"> <link rel="icon" href="/favicon.png" type="image/png"> </head> @@ -57,7 +56,7 @@ if (isset($_SESSION['mailcow_cc_role'])) { ?> <li class="dropdown"> - <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><?=$lang['header']['mailcow_settings'];?><span class="caret"></span></a> + <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"> <?=$lang['header']['mailcow_settings'];?> <span class="caret"></span></a> <ul class="dropdown-menu" role="menu"> <?php if (isset($_SESSION['mailcow_cc_role'])) { @@ -87,7 +86,19 @@ <?php endif; ?> - <?php + <li class="dropdown"> + <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false"><span class="glyphicon glyphicon-link" aria-hidden="true"></span> Apps <span class="caret"></span></a> + <ul class="dropdown-menu" role="menu"> + <?php + foreach ($MAILCOW_APPS as $app): + ?> + <li><a href="<?=$app['link'];?>"><?=$app['name'];?></a></li> + <?php + endforeach; + ?> + </ul> + </li> + <?php } if (!isset($_SESSION["dual-login"]) && isset($_SESSION['mailcow_cc_username'])): ?> diff --git a/data/web/inc/init.sql b/data/web/inc/init.sql deleted file mode 100644 index 84e19f74..00000000 --- a/data/web/inc/init.sql +++ /dev/null @@ -1,281 +0,0 @@ -CREATE TABLE IF NOT EXISTS `admin` ( - `username` VARCHAR(255) NOT NULL, - `password` VARCHAR(255) NOT NULL, - `superadmin` TINYINT(1) NOT NULL DEFAULT '0', - `created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `active` TINYINT(1) NOT NULL DEFAULT '1', - PRIMARY KEY (`username`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `alias` ( - `address` VARCHAR(255) NOT NULL, - `goto` TEXT NOT NULL, - `domain` VARCHAR(255) NOT NULL, - `created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `active` TINYINT(1) NOT NULL DEFAULT '1', - PRIMARY KEY (`address`), - KEY `domain` (`domain`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `sender_acl` ( - `logged_in_as` VARCHAR(255) NOT NULL, - `send_as` VARCHAR(255) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `spamalias` ( - `address` VARCHAR(255) NOT NULL, - `goto` TEXT NOT NULL, - `validity` INT(11) NOT NULL, - PRIMARY KEY (`address`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `alias_domain` ( - `alias_domain` VARCHAR(255) NOT NULL, - `target_domain` VARCHAR(255) NOT NULL, - `created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `active` TINYINT(1) NOT NULL DEFAULT '1', - PRIMARY KEY (`alias_domain`), - KEY `active` (`active`), - KEY `target_domain` (`target_domain`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `domain` ( - `domain` VARCHAR(255) NOT NULL, - `description` VARCHAR(255), - `aliases` INT(10) NOT NULL DEFAULT '0', - `mailboxes` INT(10) NOT NULL DEFAULT '0', - `maxquota` BIGINT(20) NOT NULL DEFAULT '0', - `quota` BIGINT(20) NOT NULL DEFAULT '0', - `transport` VARCHAR(255) NOT NULL, - `backupmx` TINYINT(1) NOT NULL DEFAULT '0', - `relay_all_recipients` TINYINT(1) NOT NULL DEFAULT '0', - `created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `active` TINYINT(1) NOT NULL DEFAULT '1', - PRIMARY KEY (`domain`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `domain_admins` ( - `username` VARCHAR(255) NOT NULL, - `domain` VARCHAR(255) NOT NULL, - `created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `active` TINYINT(1) NOT NULL DEFAULT '1', - KEY `username` (`username`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `mailbox` ( - `username` VARCHAR(255) NOT NULL, - `password` VARCHAR(255) NOT NULL, - `name` VARCHAR(255), - `maildir` VARCHAR(255) NOT NULL, - `quota` BIGINT(20) NOT NULL DEFAULT '0', - `local_part` VARCHAR(255) NOT NULL, - `domain` VARCHAR(255) NOT NULL, - `created` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `modified` DATETIME NOT NULL DEFAULT '2016-01-01 00:00:00', - `tls_enforce_in` TINYINT(1) NOT NULL DEFAULT '0', - `tls_enforce_out` TINYINT(1) NOT NULL DEFAULT '0', - `kind` VARCHAR(100) NOT NULL DEFAULT '', - `multiple_bookings` TINYINT(1) NOT NULL DEFAULT '0', - `wants_tagged_subject` TINYINT(1) NOT NULL DEFAULT '0', - `active` TINYINT(1) NOT NULL DEFAULT '1', - PRIMARY KEY (`username`), - KEY `domain` (`domain`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `quota2` ( - `username` VARCHAR(100) NOT NULL, - `bytes` BIGINT(20) NOT NULL DEFAULT '0', - `messages` INT(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`username`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `filterconf` ( - `object` VARCHAR(100) NOT NULL DEFAULT '', - `option` VARCHAR(50) NOT NULL DEFAULT '', - `value` VARCHAR(100) NOT NULL DEFAULT '', - `prefid` INT(11) NOT NULL AUTO_INCREMENT, - PRIMARY KEY (`prefid`), - KEY `object` (`object`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `imapsync` ( - `id` INT NOT NULL AUTO_INCREMENT, - `user2` VARCHAR(255) NOT NULL, - `host1` VARCHAR(255) NOT NULL, - `authmech1` ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN', - `regextrans2` VARCHAR(255) DEFAULT '', - `authmd51` TINYINT(1) NOT NULL DEFAULT 0, - `domain2` VARCHAR(255) NOT NULL DEFAULT '', - `subfolder2` VARCHAR(255) NOT NULL DEFAULT '', - `user1` VARCHAR(255) NOT NULL, - `password1` VARCHAR(255) NOT NULL, - `exclude` VARCHAR(500) NOT NULL DEFAULT '', - `maxage` SMALLINT NOT NULL DEFAULT '0', - `mins_interval` VARCHAR(50) NOT NULL, - `port1` SMALLINT NOT NULL, - `enc1` ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS', - `delete2duplicates` TINYINT(1) NOT NULL DEFAULT '1', - `returned_text` TEXT, - `last_run` TIMESTAMP NULL DEFAULT NULL, - `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - `modified` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - `active` TINYINT(1) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS `tfa` ( - `id` INT NOT NULL AUTO_INCREMENT, - `username` VARCHAR(255) NOT NULL, - `authmech` ENUM('yubi_otp', 'u2f', 'hotp', 'totp'), - `secret` VARCHAR(255) DEFAULT NULL, - `keyHandle` VARCHAR(255) DEFAULT NULL, - `publicKey` VARCHAR(255) DEFAULT NULL, - `counter` INT NOT NULL DEFAULT '0', - `certificate` TEXT, - `active` TINYINT(1) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -DROP VIEW IF EXISTS grouped_mail_aliases; -DROP VIEW IF EXISTS grouped_sender_acl; -DROP VIEW IF EXISTS grouped_domain_alias_address; - -CREATE VIEW grouped_mail_aliases (username, aliases) AS -SELECT goto, IFNULL(GROUP_CONCAT(address SEPARATOR ' '), '') AS address FROM alias -WHERE address!=goto -AND active = '1' -AND address NOT LIKE '@%' -GROUP BY goto; - -CREATE VIEW grouped_sender_acl (username, send_as) AS -SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as FROM sender_acl -WHERE send_as NOT LIKE '@%' -GROUP BY logged_in_as; - -CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS -SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox -LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username; - -CREATE TABLE IF NOT EXISTS sogo_acl ( - c_folder_id INTEGER NOT NULL, - c_object character varying(255) NOT NULL, - c_uid character varying(255) NOT NULL, - c_role character varying(80) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS sogo_alarms_folder ( - c_path VARCHAR(255) NOT NULL, - c_name VARCHAR(255) NOT NULL, - c_uid VARCHAR(255) NOT NULL, - c_recurrence_id INT(11) DEFAULT NULL, - c_alarm_number INT(11) NOT NULL, - c_alarm_date INT(11) NOT NULL -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS sogo_cache_folder ( - c_uid VARCHAR(255) NOT NULL, - c_path VARCHAR(255) NOT NULL, - c_parent_path VARCHAR(255) DEFAULT NULL, - c_type TINYINT(3) unsigned NOT NULL, - c_creationdate INT(11) NOT NULL, - c_lastmodified INT(11) NOT NULL, - c_version INT(11) NOT NULL DEFAULT '0', - c_deleted TINYINT(4) NOT NULL DEFAULT '0', - c_content longTEXT, - PRIMARY KEY (c_uid,c_path) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS sogo_folder_info ( - c_folder_id BIGINT(20) unsigned NOT NULL AUTO_INCREMENT, - c_path VARCHAR(255) NOT NULL, - c_path1 VARCHAR(255) NOT NULL, - c_path2 VARCHAR(255) DEFAULT NULL, - c_path3 VARCHAR(255) DEFAULT NULL, - c_path4 VARCHAR(255) DEFAULT NULL, - c_foldername VARCHAR(255) NOT NULL, - c_location INTeger NULL, - c_quick_location VARCHAR(2048) DEFAULT NULL, - c_acl_location VARCHAR(2048) DEFAULT NULL, - c_folder_type VARCHAR(255) NOT NULL, - PRIMARY KEY (c_path), - UNIQUE KEY c_folder_id (c_folder_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS sogo_quick_appointment ( - c_folder_id INTeger NOT NULL, - c_name character varying(255) NOT NULL, - c_uid character varying(255) NOT NULL, - c_startdate INTeger, - c_enddate INTeger, - c_cycleenddate INTeger, - c_title character varying(1000) NOT NULL, - c_participants TEXT, - c_isallday INTeger, - c_iscycle INTeger, - c_cycleinfo TEXT, - c_classification INTeger NOT NULL, - c_isopaque INTeger NOT NULL, - c_status INTeger NOT NULL, - c_priority INTeger, - c_location character varying(255), - c_orgmail character varying(255), - c_partmails TEXT, - c_partstates TEXT, - c_category character varying(255), - c_sequence INTeger, - c_component character varying(10) NOT NULL, - c_nextalarm INTeger, - c_description TEXT, - CONSTRAINT sogo_quick_appointment_pkey PRIMARY KEY (c_folder_id, c_name) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS sogo_quick_contact ( - c_folder_id INTeger NOT NULL, - c_name character varying(255) NOT NULL, - c_givenname character varying(255), - c_cn character varying(255), - c_sn character varying(255), - c_screenname character varying(255), - c_l character varying(255), - c_mail character varying(255), - c_o character varying(255), - c_ou character varying(255), - c_telephonenumber character varying(255), - c_categories character varying(255), - c_component character varying(10) NOT NULL, - CONSTRAINT sogo_quick_contact_pkey PRIMARY KEY (c_folder_id, c_name) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS sogo_sessions_folder ( - c_id VARCHAR(255) NOT NULL, - c_value VARCHAR(255) NOT NULL, - c_creationdate INT(11) NOT NULL, - c_lastseen INT(11) NOT NULL, - PRIMARY KEY (c_id) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS sogo_store ( - c_folder_id INTeger NOT NULL, - c_name character varying(255) NOT NULL, - c_content mediumTEXT NOT NULL, - c_creationdate INTeger NOT NULL, - c_lastmodified INTeger NOT NULL, - c_version INTeger NOT NULL, - c_deleted INTeger, - CONSTRAINT sogo_store_pkey PRIMARY KEY (c_folder_id, c_name) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -CREATE TABLE IF NOT EXISTS sogo_user_profile ( - c_uid VARCHAR(255) NOT NULL, - c_defaults TEXT, - c_settings TEXT, - PRIMARY KEY (c_uid) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC; - -INSERT INTO `admin` (username, password, superadmin, created, modified, active) SELECT 'admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, NOW(), NOW(), 1 WHERE NOT EXISTS (SELECT * FROM `admin`); -DELETE FROM `domain_admins`; -INSERT INTO `domain_admins` (username, domain, created, active) SELECT `username`, 'ALL', NOW(), 1 FROM `admin` WHERE superadmin='1' AND `username` NOT IN (SELECT `username` FROM `domain_admins`); diff --git a/data/web/inc/init_db.inc.php b/data/web/inc/init_db.inc.php new file mode 100644 index 00000000..81c9be2a --- /dev/null +++ b/data/web/inc/init_db.inc.php @@ -0,0 +1,598 @@ +<?php +function init_db_schema() { + try { + global $pdo; + + $db_version = "01052017_1702"; + + $stmt = $pdo->query("SHOW TABLES LIKE 'versions'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $stmt = $pdo->query("SELECT `version` FROM `versions`"); + if ($stmt->fetch(PDO::FETCH_ASSOC)['version'] == $db_version) { + return true; + } + } + + $views = array( + "grouped_mail_aliases" => "CREATE VIEW grouped_mail_aliases (username, aliases) AS + SELECT goto, IFNULL(GROUP_CONCAT(address SEPARATOR ' '), '') AS address FROM alias + WHERE address!=goto + AND active = '1' + AND address NOT LIKE '@%' + GROUP BY goto;", + "grouped_sender_acl" => "CREATE VIEW grouped_sender_acl (username, send_as) AS + SELECT logged_in_as, IFNULL(GROUP_CONCAT(send_as SEPARATOR ' '), '') AS send_as FROM sender_acl + WHERE send_as NOT LIKE '@%' + GROUP BY logged_in_as;", + "grouped_domain_alias_address" => "CREATE VIEW grouped_domain_alias_address (username, ad_alias) AS + SELECT username, IFNULL(GROUP_CONCAT(local_part, '@', alias_domain SEPARATOR ' '), '') AS ad_alias FROM mailbox + LEFT OUTER JOIN alias_domain on target_domain=domain GROUP BY username;" + ); + + $tables = array( + "versions" => array( + "cols" => array( + "application" => "VARCHAR(255) NOT NULL", + "version" => "VARCHAR(100) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + ), + "keys" => array( + "primary" => array( + "" => array("application") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "admin" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "superadmin" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE NOW(0)", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "alias" => array( + "cols" => array( + "address" => "VARCHAR(255) NOT NULL", + "goto" => "TEXT NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("address") + ), + "key" => array( + "domain" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sender_acl" => array( + "cols" => array( + "logged_in_as" => "VARCHAR(255) NOT NULL", + "send_as" => "VARCHAR(255) NOT NULL" + ), + "keys" => array(), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "domain" => array( + "cols" => array( + "domain" => "VARCHAR(255) NOT NULL", + "description" => "VARCHAR(255)", + "aliases" => "INT(10) NOT NULL DEFAULT '0'", + "mailboxes" => "INT(10) NOT NULL DEFAULT '0'", + "maxquota" => "BIGINT(20) NOT NULL DEFAULT '0'", + "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", + "transport" => "VARCHAR(255) NOT NULL", + "backupmx" => "TINYINT(1) NOT NULL DEFAULT '0'", + "relay_all_recipients" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "alias_domain" => array( + "cols" => array( + "alias_domain" => "VARCHAR(255) NOT NULL", + "target_domain" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("alias_domain") + ), + "key" => array( + "active" => array("active"), + "target_domain" => array("target_domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "spamalias" => array( + "cols" => array( + "address" => "VARCHAR(255) NOT NULL", + "goto" => "TEXT NOT NULL", + "validity" => "INT(11) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("address") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "filterconf" => array( + "cols" => array( + "object" => "VARCHAR(255) NOT NULL DEFAULT ''", + "option" => "VARCHAR(50) NOT NULL DEFAULT ''", + "value" => "VARCHAR(100) NOT NULL DEFAULT ''", + "prefid" => "INT(11) NOT NULL AUTO_INCREMENT" + ), + "keys" => array( + "primary" => array( + "" => array("prefid") + ), + "key" => array( + "object" => array("object") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "quota2" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "bytes" => "BIGINT(20) NOT NULL DEFAULT '0'", + "messages" => "BIGINT(20) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "mailbox" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "password" => "VARCHAR(255) NOT NULL", + "name" => "VARCHAR(255)", + "maildir" => "VARCHAR(255) NOT NULL", + "quota" => "BIGINT(20) NOT NULL DEFAULT '102400'", + "local_part" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "tls_enforce_in" => "TINYINT(1) NOT NULL DEFAULT '0'", + "tls_enforce_out" => "TINYINT(1) NOT NULL DEFAULT '0'", + "kind" => "VARCHAR(100) NOT NULL DEFAULT ''", + "multiple_bookings" => "TINYINT(1) NOT NULL DEFAULT '0'", + "wants_tagged_subject" => "TINYINT(1) NOT NULL DEFAULT '0'", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "primary" => array( + "" => array("username") + ), + "key" => array( + "domain" => array("domain") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "domain_admins" => array( + "cols" => array( + "username" => "VARCHAR(255) NOT NULL", + "domain" => "VARCHAR(255) NOT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "active" => "TINYINT(1) NOT NULL DEFAULT '1'" + ), + "keys" => array( + "key" => array( + "username" => array("username") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "imapsync" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "user2" => "VARCHAR(255) NOT NULL", + "host1" => "VARCHAR(255) NOT NULL", + "authmech1" => "ENUM('PLAIN','LOGIN','CRAM-MD5') DEFAULT 'PLAIN'", + "regextrans2" => "VARCHAR(255) DEFAULT ''", + "authmd51" => "TINYINT(1) NOT NULL DEFAULT 0", + "domain2" => "VARCHAR(255) NOT NULL DEFAULT ''", + "subfolder2" => "VARCHAR(255) NOT NULL DEFAULT ''", + "user1" => "VARCHAR(255) NOT NULL", + "password1" => "VARCHAR(255) NOT NULL", + "exclude" => "VARCHAR(500) NOT NULL DEFAULT ''", + "maxage" => "SMALLINT NOT NULL DEFAULT '0'", + "mins_interval" => "VARCHAR(50) NOT NULL", + "port1" => "SMALLINT NOT NULL", + "enc1" => "ENUM('TLS','SSL','PLAIN') DEFAULT 'TLS'", + "delete2duplicates" => "TINYINT(1) NOT NULL DEFAULT '1'", + "delete1" => "TINYINT(1) NOT NULL DEFAULT '0'", + "returned_text" => "TEXT", + "last_run" => "TIMESTAMP NULL DEFAULT NULL", + "created" => "DATETIME(0) NOT NULL DEFAULT NOW(0)", + "modified" => "DATETIME ON UPDATE CURRENT_TIMESTAMP", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "tfa" => array( + "cols" => array( + "id" => "INT NOT NULL AUTO_INCREMENT", + "key_id" => "VARCHAR(255) NOT NULL", + "username" => "VARCHAR(255) NOT NULL", + "authmech" => "ENUM('yubi_otp', 'u2f', 'hotp', 'totp')", + "secret" => "VARCHAR(255) DEFAULT NULL", + "keyHandle" => "VARCHAR(255) DEFAULT NULL", + "publicKey" => "VARCHAR(255) DEFAULT NULL", + "counter" => "INT NOT NULL DEFAULT '0'", + "certificate" => "TEXT", + "active" => "TINYINT(1) NOT NULL DEFAULT '0'" + ), + "keys" => array( + "primary" => array( + "" => array("id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "forwarding_hosts" => array( + "cols" => array( + "host" => "VARCHAR(255) NOT NULL", + "source" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("host") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_acl" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_object" => "VARCHAR(255) NOT NULL", + "c_uid" => "VARCHAR(255) NOT NULL", + "c_role" => "VARCHAR(80) NOT NULL" + ), + "keys" => array( + "key" => array( + "sogo_acl_c_folder_id_idx" => array("c_folder_id"), + "sogo_acl_c_uid_idx" => array("c_uid") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_alarms_folder" => array( + "cols" => array( + "c_path" => "VARCHAR(255) NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_uid" => "VARCHAR(255) NOT NULL", + "c_recurrence_id" => "INT(11) DEFAULT NULL", + "c_alarm_number" => "INT(11) NOT NULL", + "c_alarm_date" => "INT(11) NOT NULL" + ), + "keys" => array(), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_cache_folder" => array( + "cols" => array( + "c_uid" => "VARCHAR(255) NOT NULL", + "c_path" => "VARCHAR(255) NOT NULL", + "c_parent_path" => "VARCHAR(255) DEFAULT NULL", + "c_type" => "TINYINT(3) unsigned NOT NULL", + "c_creationdate" => "INT(11) NOT NULL", + "c_lastmodified" => "INT(11) NOT NULL", + "c_version" => "INT(11) NOT NULL DEFAULT '0'", + "c_deleted" => "TINYINT(4) NOT NULL DEFAULT '0'", + "c_content" => "LONGTEXT" + ), + "keys" => array( + "primary" => array( + "" => array("c_uid", "c_path") + ), + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_folder_info" => array( + "cols" => array( + "c_folder_id" => "BIGINT(20) unsigned NOT NULL AUTO_INCREMENT", + "c_path" => "VARCHAR(255) NOT NULL", + "c_path1" => "VARCHAR(255) NOT NULL", + "c_path2" => "VARCHAR(255) DEFAULT NULL", + "c_path3" => "VARCHAR(255) DEFAULT NULL", + "c_path4" => "VARCHAR(255) DEFAULT NULL", + "c_foldername" => "VARCHAR(255) NOT NULL", + "c_location" => "INT NULL", + "c_quick_location" => "VARCHAR(2048) DEFAULT NULL", + "c_acl_location" => "VARCHAR(2048) DEFAULT NULL", + "c_folder_type" => "VARCHAR(255) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("c_path") + ), + "unique" => array( + "c_folder_id" => array("c_folder_id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_quick_appointment" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_uid" => "VARCHAR(255) NOT NULL", + "c_startdate" => "INT", + "c_enddate" => "INT", + "c_cycleenddate" => "INT", + "c_title" => "VARCHAR(1000) NOT NULL", + "c_participants" => "TEXT", + "c_isallday" => "INT", + "c_iscycle" => "INT", + "c_cycleinfo" => "TEXT", + "c_classification" => "INT NOT NULL", + "c_isopaque" => "INT NOT NULL", + "c_status" => "INT NOT NULL", + "c_priority" => "INT", + "c_location" => "VARCHAR(255)", + "c_orgmail" => "VARCHAR(255)", + "c_partmails" => "TEXT", + "c_partstates" => "TEXT", + "c_category" => "VARCHAR(255)", + "c_sequence" => "INT", + "c_component" => "VARCHAR(10) NOT NULL", + "c_nextalarm" => "INT", + "c_description" => "TEXT" + ), + "keys" => array( + "primary" => array( + "" => array("c_folder_id", "c_name") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_quick_contact" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_givenname" => "VARCHAR(255)", + "c_cn" => "VARCHAR(255)", + "c_sn" => "VARCHAR(255)", + "c_screenname" => "VARCHAR(255)", + "c_l" => "VARCHAR(255)", + "c_mail" => "VARCHAR(255)", + "c_o" => "VARCHAR(255)", + "c_ou" => "VARCHAR(255)", + "c_telephonenumber" => "VARCHAR(255)", + "c_categories" => "VARCHAR(255)", + "c_component" => "VARCHAR(10) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("c_folder_id", "c_name") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_sessions_folder" => array( + "cols" => array( + "c_id" => "VARCHAR(255) NOT NULL", + "c_value" => "VARCHAR(255) NOT NULL", + "c_creationdate" => "INT(11) NOT NULL", + "c_lastseen" => "INT(11) NOT NULL" + ), + "keys" => array( + "primary" => array( + "" => array("c_id") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_store" => array( + "cols" => array( + "c_folder_id" => "INT NOT NULL", + "c_name" => "VARCHAR(255) NOT NULL", + "c_content" => "MEDIUMTEXT NOT NULL", + "c_creationdate" => "INT NOT NULL", + "c_lastmodified" => "INT NOT NULL", + "c_version" => "INT NOT NULL", + "c_deleted" => "INT" + ), + "keys" => array( + "primary" => array( + "" => array("c_folder_id", "c_name") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ), + "sogo_user_profile" => array( + "cols" => array( + "c_uid" => "VARCHAR(255) NOT NULL", + "c_defaults" => "TEXT", + "c_settings" => "TEXT" + ), + "keys" => array( + "primary" => array( + "" => array("c_uid") + ) + ), + "attr" => "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC" + ) + ); + + foreach ($tables as $table => $properties) { + $stmt = $pdo->query("SHOW TABLES LIKE '" . $table . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + foreach($properties['cols'] as $column => $type) { + $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "` LIKE '" . $column . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results == 0) { + $pdo->query("ALTER TABLE `" . $table . "` ADD `" . $column . "` " . $type); + } + else { + $pdo->query("ALTER TABLE `" . $table . "` MODIFY COLUMN `" . $column . "` " . $type); + } + } + foreach($properties['keys'] as $key_type => $key_content) { + if (strtolower($key_type) == 'primary') { + foreach ($key_content as $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + $is_drop = ($num_results != 0) ? "DROP PRIMARY KEY, " : ""; + $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD PRIMARY KEY (" . $fields . ")"); + } + } + if (strtolower($key_type) == 'key') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; + $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD KEY `" . $key_name . "` (" . $fields . ")"); + } + } + if (strtolower($key_type) == 'unique') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = '" . $key_name . "'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + $is_drop = ($num_results != 0) ? "DROP INDEX `" . $key_name . "`, " : ""; + $pdo->query("ALTER TABLE `" . $table . "` " . $is_drop . "ADD UNIQUE KEY `" . $key_name . "` (" . $fields . ")"); + } + } + } + // Drop all vanished columns + $stmt = $pdo->query("SHOW COLUMNS FROM `" . $table . "`"); + $cols_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); + while ($row = array_shift($cols_in_table)) { + if (!array_key_exists($row['Field'], $properties['cols'])) { + $pdo->query("ALTER TABLE `" . $table . "` DROP COLUMN `" . $row['Field'] . "`;"); + } + } + + // Step 1: Get all non-primary keys, that currently exist and those that should exist + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE `Key_name` != 'PRIMARY'"); + $keys_in_table = $stmt->fetchAll(PDO::FETCH_ASSOC); + $keys_to_exist = array(); + if (isset($properties['keys']['unique']) && is_array($properties['keys']['unique'])) { + foreach ($properties['keys']['unique'] as $key_name => $key_values) { + $keys_to_exist[] = $key_name; + } + } + if (isset($properties['keys']['key']) && is_array($properties['keys']['key'])) { + foreach ($properties['keys']['key'] as $key_name => $key_values) { + $keys_to_exist[] = $key_name; + } + } + // Step 2: Drop all vanished indexes + while ($row = array_shift($keys_in_table)) { + if (!in_array($row['Key_name'], $keys_to_exist)) { + $pdo->query("ALTER TABLE `" . $table . "` DROP INDEX `" . $row['Key_name'] . "`"); + } + } + // Step 3: Drop all vanished primary keys + if (!isset($properties['keys']['primary'])) { + $stmt = $pdo->query("SHOW KEYS FROM `" . $table . "` WHERE Key_name = 'PRIMARY'"); + $num_results = count($stmt->fetchAll(PDO::FETCH_ASSOC)); + if ($num_results != 0) { + $pdo->query("ALTER TABLE `" . $table . "` DROP PRIMARY KEY"); + } + } + } + else { + // Create table if it is missing + $sql = "CREATE TABLE IF NOT EXISTS `" . $table . "` ("; + foreach($properties['cols'] as $column => $type) { + $sql .= $column . " " . $type . ","; + } + foreach($properties['keys'] as $key_type => $key_content) { + if (strtolower($key_type) == 'primary') { + foreach ($key_content as $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $sql .= "PRIMARY KEY (" . $fields . ")" . ","; + } + } + elseif (strtolower($key_type) == 'key') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $sql .= "KEY `" . $key_name . "` (" . $fields . ")" . ","; + } + } + elseif (strtolower($key_type) == 'unique') { + foreach ($key_content as $key_name => $key_values) { + $fields = "`" . implode("`, `", $key_values) . "`"; + $sql .= "UNIQUE KEY `" . $key_name . "` (" . $fields . ")" . ","; + } + } + } + $sql = rtrim($sql, ","); + $sql .= ") " . $properties['attr']; + $pdo->query($sql); + } + // Reset table attributes + $pdo->query("ALTER TABLE `" . $table . "` " . $properties['attr'] . ";"); + } + + // Recreate SQL views + foreach ($views as $view => $create) { + $pdo->query("DROP VIEW IF EXISTS `" . $view . "`;"); + $pdo->query($create); + } + + // Inject admin if not exists + $stmt = $pdo->query("INSERT INTO `admin` (`username`, `password`, `superadmin`, `created`, `modified`, `active`) + SELECT 'admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, NOW(), NOW(), 1 + WHERE NOT EXISTS (SELECT * FROM `admin`);"); + $stmt = $pdo->query("DELETE FROM `domain_admins`;"); + $stmt = $pdo->query("INSERT INTO `domain_admins` (`username`, `domain`, `created`, `active`) + SELECT `username`, 'ALL', NOW(), 1 FROM `admin` + WHERE superadmin='1' AND `username` NOT IN (SELECT `username` FROM `domain_admins`);"); + + // Insert new DB schema version + $stmt = $pdo->query("REPLACE INTO `versions` (`application`, `version`) VALUES ('db_schema', '" . $db_version . "');"); + + $_SESSION['return'] = array( + 'type' => 'success', + 'msg' => 'Database initialisation completed' + ); + } + catch (PDOException $e) { + $_SESSION['return'] = array( + 'type' => 'danger', + 'msg' => 'Database initialisation failed: ' . $e->getMessage() + ); + } +} +?> \ No newline at end of file diff --git a/data/web/inc/lib/composer.json b/data/web/inc/lib/composer.json new file mode 100644 index 00000000..d811958b --- /dev/null +++ b/data/web/inc/lib/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "robthree/twofactorauth": "^1.6", + "yubico/u2flib-server": "^1.0" + } +} diff --git a/data/web/inc/lib/composer.lock b/data/web/inc/lib/composer.lock new file mode 100644 index 00000000..692521d2 --- /dev/null +++ b/data/web/inc/lib/composer.lock @@ -0,0 +1,100 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "5652a086b6d277d72d7ae0341e517b1e", + "packages": [ + { + "name": "robthree/twofactorauth", + "version": "1.6", + "source": { + "type": "git", + "url": "https://github.com/RobThree/TwoFactorAuth.git", + "reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/5093ab230cd8f1296d792afb6a49545f37e7fd5a", + "reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "type": "library", + "autoload": { + "psr-4": { + "RobThree\\Auth\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Janssen", + "homepage": "http://robiii.me", + "role": "Developer" + } + ], + "description": "Two Factor Authentication", + "homepage": "https://github.com/RobThree/TwoFactorAuth", + "keywords": [ + "Authentication", + "MFA", + "Multi Factor Authentication", + "Two Factor Authentication", + "authenticator", + "authy", + "php", + "tfa" + ], + "time": "2017-02-17T15:24:54+00:00" + }, + { + "name": "yubico/u2flib-server", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/Yubico/php-u2flib-server.git", + "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yubico/php-u2flib-server/zipball/407eb21da24150aad30bcd8cc0ee72963eac5e9d", + "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d", + "shasum": "" + }, + "require": { + "ext-openssl": "*" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Library for U2F implementation", + "homepage": "https://developers.yubico.com/php-u2flib-server", + "time": "2016-02-19T09:47:51+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/data/web/inc/lib/vendor/autoload.php b/data/web/inc/lib/vendor/autoload.php new file mode 100644 index 00000000..011c5277 --- /dev/null +++ b/data/web/inc/lib/vendor/autoload.php @@ -0,0 +1,7 @@ +<?php + +// autoload.php @generated by Composer + +require_once __DIR__ . '/composer/autoload_real.php'; + +return ComposerAutoloaderInit873464e4bd965a3168f133248b1b218b::getLoader(); diff --git a/data/web/inc/lib/vendor/composer/ClassLoader.php b/data/web/inc/lib/vendor/composer/ClassLoader.php new file mode 100644 index 00000000..2c72175e --- /dev/null +++ b/data/web/inc/lib/vendor/composer/ClassLoader.php @@ -0,0 +1,445 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Jordi Boggiano <j.boggiano@seld.be> + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', $this->prefixesPsr0); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && ini_get('apc.enabled') ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath.'\\'; + if (isset($this->prefixDirsPsr4[$search])) { + foreach ($this->prefixDirsPsr4[$search] as $dir) { + $length = $this->prefixLengthsPsr4[$first][$search]; + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/data/web/inc/lib/vendor/composer/LICENSE b/data/web/inc/lib/vendor/composer/LICENSE new file mode 100644 index 00000000..f27399a0 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/data/web/inc/lib/vendor/composer/autoload_classmap.php b/data/web/inc/lib/vendor/composer/autoload_classmap.php new file mode 100644 index 00000000..44393069 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/autoload_classmap.php @@ -0,0 +1,14 @@ +<?php + +// autoload_classmap.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'u2flib_server\\Error' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\RegisterRequest' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\Registration' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\SignRequest' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\U2F' => $vendorDir . '/yubico/u2flib-server/src/u2flib_server/U2F.php', +); diff --git a/data/web/inc/lib/vendor/composer/autoload_namespaces.php b/data/web/inc/lib/vendor/composer/autoload_namespaces.php new file mode 100644 index 00000000..b7fc0125 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ +<?php + +// autoload_namespaces.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( +); diff --git a/data/web/inc/lib/vendor/composer/autoload_psr4.php b/data/web/inc/lib/vendor/composer/autoload_psr4.php new file mode 100644 index 00000000..55ee8027 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/autoload_psr4.php @@ -0,0 +1,10 @@ +<?php + +// autoload_psr4.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'RobThree\\Auth\\' => array($vendorDir . '/robthree/twofactorauth/lib'), +); diff --git a/data/web/inc/lib/vendor/composer/autoload_real.php b/data/web/inc/lib/vendor/composer/autoload_real.php new file mode 100644 index 00000000..caf455f3 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/autoload_real.php @@ -0,0 +1,52 @@ +<?php + +// autoload_real.php @generated by Composer + +class ComposerAutoloaderInit873464e4bd965a3168f133248b1b218b +{ + private static $loader; + + public static function loadClassLoader($class) + { + if ('Composer\Autoload\ClassLoader' === $class) { + require __DIR__ . '/ClassLoader.php'; + } + } + + public static function getLoader() + { + if (null !== self::$loader) { + return self::$loader; + } + + spl_autoload_register(array('ComposerAutoloaderInit873464e4bd965a3168f133248b1b218b', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(); + spl_autoload_unregister(array('ComposerAutoloaderInit873464e4bd965a3168f133248b1b218b', 'loadClassLoader')); + + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInit873464e4bd965a3168f133248b1b218b::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/data/web/inc/lib/vendor/composer/autoload_static.php b/data/web/inc/lib/vendor/composer/autoload_static.php new file mode 100644 index 00000000..5e2dabab --- /dev/null +++ b/data/web/inc/lib/vendor/composer/autoload_static.php @@ -0,0 +1,40 @@ +<?php + +// autoload_static.php @generated by Composer + +namespace Composer\Autoload; + +class ComposerStaticInit873464e4bd965a3168f133248b1b218b +{ + public static $prefixLengthsPsr4 = array ( + 'R' => + array ( + 'RobThree\\Auth\\' => 14, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'RobThree\\Auth\\' => + array ( + 0 => __DIR__ . '/..' . '/robthree/twofactorauth/lib', + ), + ); + + public static $classMap = array ( + 'u2flib_server\\Error' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\RegisterRequest' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\Registration' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\SignRequest' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + 'u2flib_server\\U2F' => __DIR__ . '/..' . '/yubico/u2flib-server/src/u2flib_server/U2F.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInit873464e4bd965a3168f133248b1b218b::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInit873464e4bd965a3168f133248b1b218b::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInit873464e4bd965a3168f133248b1b218b::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/data/web/inc/lib/vendor/composer/installed.json b/data/web/inc/lib/vendor/composer/installed.json new file mode 100644 index 00000000..bbe76d82 --- /dev/null +++ b/data/web/inc/lib/vendor/composer/installed.json @@ -0,0 +1,88 @@ +[ + { + "name": "robthree/twofactorauth", + "version": "1.6", + "version_normalized": "1.6.0.0", + "source": { + "type": "git", + "url": "https://github.com/RobThree/TwoFactorAuth.git", + "reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/RobThree/TwoFactorAuth/zipball/5093ab230cd8f1296d792afb6a49545f37e7fd5a", + "reference": "5093ab230cd8f1296d792afb6a49545f37e7fd5a", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "time": "2017-02-17T15:24:54+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "RobThree\\Auth\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Janssen", + "homepage": "http://robiii.me", + "role": "Developer" + } + ], + "description": "Two Factor Authentication", + "homepage": "https://github.com/RobThree/TwoFactorAuth", + "keywords": [ + "Authentication", + "MFA", + "Multi Factor Authentication", + "Two Factor Authentication", + "authenticator", + "authy", + "php", + "tfa" + ] + }, + { + "name": "yubico/u2flib-server", + "version": "1.0.0", + "version_normalized": "1.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/Yubico/php-u2flib-server.git", + "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Yubico/php-u2flib-server/zipball/407eb21da24150aad30bcd8cc0ee72963eac5e9d", + "reference": "407eb21da24150aad30bcd8cc0ee72963eac5e9d", + "shasum": "" + }, + "require": { + "ext-openssl": "*" + }, + "time": "2016-02-19T09:47:51+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-2-Clause" + ], + "description": "Library for U2F implementation", + "homepage": "https://developers.yubico.com/php-u2flib-server" + } +] diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore b/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore new file mode 100644 index 00000000..1a31666a --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/.gitignore @@ -0,0 +1,186 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.sln.docstates + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +build/ +bld/ +[Bb]in/ +[Oo]bj/ + +# Roslyn cache directories +*.ide/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +#NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding addin-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# If using the old MSBuild-Integrated Package Restore, uncomment this: +#!**/packages/repositories.config + +# Windows Azure Build Output +csx/ +*.build.csdef + +# Windows Store app package directory +AppPackages/ + +# Others +sql/ +*.Cache +ClientBin/ +[Ss]tyle[Cc]op.* +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# Composer +/vendor \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml b/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml new file mode 100644 index 00000000..034653bb --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/.travis.yml @@ -0,0 +1,11 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7 + - hhvm + +script: phpunit --coverage-text tests diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config b/data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config new file mode 100644 index 00000000..4b9bf477 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/.vs/config/applicationhost.config @@ -0,0 +1,1031 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + + IIS configuration sections. + + For schema documentation, see + %IIS_BIN%\config\schema\IIS_schema.xml. + + Please make a backup of this file before making any changes to it. + + NOTE: The following environment variables are available to be used + within this file and are understood by the IIS Express. + + %IIS_USER_HOME% - The IIS Express home directory for the user + %IIS_SITES_HOME% - The default home directory for sites + %IIS_BIN% - The location of the IIS Express binaries + %SYSTEMDRIVE% - The drive letter of %IIS_BIN% + +--> +<configuration> + + <!-- + + The <configSections> section controls the registration of sections. + Section is the basic unit of deployment, locking, searching and + containment for configuration settings. + + Every section belongs to one section group. + A section group is a container of logically-related sections. + + Sections cannot be nested. + Section groups may be nested. + + <section + name="" [Required, Collection Key] [XML name of the section] + allowDefinition="Everywhere" [MachineOnly|MachineToApplication|AppHostOnly|Everywhere] [Level where it can be set] + overrideModeDefault="Allow" [Allow|Deny] [Default delegation mode] + allowLocation="true" [true|false] [Allowed in location tags] + /> + + The recommended way to unlock sections is by using a location tag: + <location path="Default Web Site" overrideMode="Allow"> + <system.webServer> + <asp /> + </system.webServer> + </location> + + --> + <configSections> + <sectionGroup name="system.applicationHost"> + <section name="applicationPools" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="configHistory" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="customMetadata" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="listenerAdapters" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="log" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="serviceAutoStartProviders" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="sites" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="webLimits" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + </sectionGroup> + + <sectionGroup name="system.webServer"> + <section name="asp" overrideModeDefault="Deny" /> + <section name="caching" overrideModeDefault="Allow" /> + <section name="cgi" overrideModeDefault="Deny" /> + <section name="defaultDocument" overrideModeDefault="Allow" /> + <section name="directoryBrowse" overrideModeDefault="Allow" /> + <section name="fastCgi" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="globalModules" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="handlers" overrideModeDefault="Deny" /> + <section name="httpCompression" overrideModeDefault="Allow" /> + <section name="httpErrors" overrideModeDefault="Allow" /> + <section name="httpLogging" overrideModeDefault="Deny" /> + <section name="httpProtocol" overrideModeDefault="Allow" /> + <section name="httpRedirect" overrideModeDefault="Allow" /> + <section name="httpTracing" overrideModeDefault="Deny" /> + <section name="isapiFilters" allowDefinition="MachineToApplication" overrideModeDefault="Deny" /> + <section name="modules" allowDefinition="MachineToApplication" overrideModeDefault="Deny" /> + <section name="applicationInitialization" allowDefinition="MachineToApplication" overrideModeDefault="Allow" /> + <section name="odbcLogging" overrideModeDefault="Deny" /> + <sectionGroup name="security"> + <section name="access" overrideModeDefault="Deny" /> + <section name="applicationDependencies" overrideModeDefault="Deny" /> + <sectionGroup name="authentication"> + <section name="anonymousAuthentication" overrideModeDefault="Deny" /> + <section name="basicAuthentication" overrideModeDefault="Deny" /> + <section name="clientCertificateMappingAuthentication" overrideModeDefault="Deny" /> + <section name="digestAuthentication" overrideModeDefault="Deny" /> + <section name="iisClientCertificateMappingAuthentication" overrideModeDefault="Deny" /> + <section name="windowsAuthentication" overrideModeDefault="Deny" /> + </sectionGroup> + <section name="authorization" overrideModeDefault="Allow" /> + <section name="ipSecurity" overrideModeDefault="Deny" /> + <section name="dynamicIpSecurity" overrideModeDefault="Deny" /> + <section name="isapiCgiRestriction" allowDefinition="AppHostOnly" overrideModeDefault="Deny" /> + <section name="requestFiltering" overrideModeDefault="Allow" /> + </sectionGroup> + <section name="serverRuntime" overrideModeDefault="Deny" /> + <section name="serverSideInclude" overrideModeDefault="Deny" /> + <section name="staticContent" overrideModeDefault="Allow" /> + <sectionGroup name="tracing"> + <section name="traceFailedRequests" overrideModeDefault="Allow" /> + <section name="traceProviderDefinitions" overrideModeDefault="Deny" /> + </sectionGroup> + <section name="urlCompression" overrideModeDefault="Allow" /> + <section name="validation" overrideModeDefault="Allow" /> + <sectionGroup name="webdav"> + <section name="globalSettings" overrideModeDefault="Deny" /> + <section name="authoring" overrideModeDefault="Deny" /> + <section name="authoringRules" overrideModeDefault="Deny" /> + </sectionGroup> + <sectionGroup name="rewrite"> + <section name="allowedServerVariables" overrideModeDefault="Deny" /> + <section name="rules" overrideModeDefault="Allow" /> + <section name="outboundRules" overrideModeDefault="Allow" /> + <section name="globalRules" overrideModeDefault="Deny" allowDefinition="AppHostOnly" /> + <section name="providers" overrideModeDefault="Allow" /> + <section name="rewriteMaps" overrideModeDefault="Allow" /> + </sectionGroup> + <section name="webSocket" overrideModeDefault="Deny" /> + <section name="aspNetCore" overrideModeDefault="Allow" /></sectionGroup> + </configSections> + + <configProtectedData> + <providers> + <add name="IISWASOnlyRsaProvider" type="" description="Uses RsaCryptoServiceProvider to encrypt and decrypt" keyContainerName="iisWasKey" cspProviderName="" useMachineContainer="true" useOAEP="false" /> + <add name="AesProvider" type="Microsoft.ApplicationHost.AesProtectedConfigurationProvider" description="Uses an AES session key to encrypt and decrypt" keyContainerName="iisConfigurationKey" cspProviderName="" useOAEP="false" useMachineContainer="true" sessionKey="AQIAAA5mAAAApAAAKmFQvWHDEETRz8l2bjZlRxIkwcqTFaCUnCLljn3Q1OkesrhEO9YyLyx4bUhsj1/DyShAv7OAFFhXlrlomaornnk5PLeyO4lIXxaiT33yOFUUgxDx4GSaygkqghVV0tO5yQ/XguUBp2juMfZyztnsNa4pLcz7ZNZQ6p4yn9hxwNs=" /> + <add name="IISWASOnlyAesProvider" type="Microsoft.ApplicationHost.AesProtectedConfigurationProvider" description="Uses an AES session key to encrypt and decrypt" keyContainerName="iisWasKey" cspProviderName="" useOAEP="false" useMachineContainer="true" sessionKey="AQIAAA5mAAAApAAA4WoiRJ8KHwzAG8AgejPxEOO4/2Vhkolbwo/8gZeNdUDSD36m55hWv4uC9tr/MlKdnwRLL0NhT50Gccyftqz5xTZ0dg5FtvQhTw/he1NwexTKbV+I4Zrd+sZUqHZTsr7JiEr6OHGXL70qoISW5G2m9U8wKT3caPiDPNj2aAaYPLo=" /> + </providers> + </configProtectedData> + + <system.applicationHost> + + <applicationPools> + <add name="Clr4IntegratedAppPool" managedRuntimeVersion="v4.0" managedPipelineMode="Integrated" CLRConfigFile="%IIS_USER_HOME%\config\aspnet.config" autoStart="true" /> + <add name="Clr4ClassicAppPool" managedRuntimeVersion="v4.0" managedPipelineMode="Classic" CLRConfigFile="%IIS_USER_HOME%\config\aspnet.config" autoStart="true" /> + <add name="Clr2IntegratedAppPool" managedRuntimeVersion="v2.0" managedPipelineMode="Integrated" CLRConfigFile="%IIS_USER_HOME%\config\aspnet.config" autoStart="true" /> + <add name="Clr2ClassicAppPool" managedRuntimeVersion="v2.0" managedPipelineMode="Classic" CLRConfigFile="%IIS_USER_HOME%\config\aspnet.config" autoStart="true" /> + <add name="UnmanagedClassicAppPool" managedRuntimeVersion="" managedPipelineMode="Classic" autoStart="true" /> + <applicationPoolDefaults managedRuntimeLoader="v4.0"> + <processModel /> + </applicationPoolDefaults> + </applicationPools> + + <!-- + + The <listenerAdapters> section defines the protocols with which the + Windows Process Activation Service (WAS) binds. + + --> + <listenerAdapters> + <add name="http" /> + </listenerAdapters> + + <sites> + <site name="WebSite1" id="1" serverAutoStart="true"> + <application path="/"> + <virtualDirectory path="/" physicalPath="%IIS_SITES_HOME%\WebSite1" /> + </application> + <bindings> + <binding protocol="http" bindingInformation=":8080:localhost" /> + </bindings> + </site> + <siteDefaults> + <logFile logFormat="W3C" directory="%IIS_USER_HOME%\Logs" /> + <traceFailedRequestsLogging directory="%IIS_USER_HOME%\TraceLogFiles" enabled="true" maxLogFileSizeKB="1024" /> + </siteDefaults> + <applicationDefaults applicationPool="Clr4IntegratedAppPool" /> + <virtualDirectoryDefaults allowSubDirConfig="true" /> + </sites> + + <webLimits /> + + </system.applicationHost> + + <system.webServer> + + <serverRuntime /> + + <asp scriptErrorSentToBrowser="true"> + <cache diskTemplateCacheDirectory="%TEMP%\iisexpress\ASP Compiled Templates" /> + <limits /> + </asp> + + <caching enabled="true" enableKernelCache="true"> + </caching> + + <cgi /> + + <defaultDocument enabled="true"> + <files> + <add value="Default.htm" /> + <add value="Default.asp" /> + <add value="index.htm" /> + <add value="index.html" /> + <add value="iisstart.htm" /> + <add value="default.aspx" /> + </files> + </defaultDocument> + + <directoryBrowse enabled="false" /> + + <fastCgi /> + + <!-- + + The <globalModules> section defines all native-code modules. + To enable a module, specify it in the <modules> section. + + --> + <globalModules> + <add name="HttpLoggingModule" image="%IIS_BIN%\loghttp.dll" /> + <add name="UriCacheModule" image="%IIS_BIN%\cachuri.dll" /> +<!-- <add name="FileCacheModule" image="%IIS_BIN%\cachfile.dll" /> --> + <add name="TokenCacheModule" image="%IIS_BIN%\cachtokn.dll" /> +<!-- <add name="HttpCacheModule" image="%IIS_BIN%\cachhttp.dll" /> --> + <add name="DynamicCompressionModule" image="%IIS_BIN%\compdyn.dll" /> + <add name="StaticCompressionModule" image="%IIS_BIN%\compstat.dll" /> + <add name="DefaultDocumentModule" image="%IIS_BIN%\defdoc.dll" /> + <add name="DirectoryListingModule" image="%IIS_BIN%\dirlist.dll" /> + <add name="ProtocolSupportModule" image="%IIS_BIN%\protsup.dll" /> + <add name="HttpRedirectionModule" image="%IIS_BIN%\redirect.dll" /> + <add name="ServerSideIncludeModule" image="%IIS_BIN%\iis_ssi.dll" /> + <add name="StaticFileModule" image="%IIS_BIN%\static.dll" /> + <add name="AnonymousAuthenticationModule" image="%IIS_BIN%\authanon.dll" /> + <add name="CertificateMappingAuthenticationModule" image="%IIS_BIN%\authcert.dll" /> + <add name="UrlAuthorizationModule" image="%IIS_BIN%\urlauthz.dll" /> + <add name="BasicAuthenticationModule" image="%IIS_BIN%\authbas.dll" /> + <add name="WindowsAuthenticationModule" image="%IIS_BIN%\authsspi.dll" /> +<!-- <add name="DigestAuthenticationModule" image="%IIS_BIN%\authmd5.dll" /> --> + <add name="IISCertificateMappingAuthenticationModule" image="%IIS_BIN%\authmap.dll" /> + <add name="IpRestrictionModule" image="%IIS_BIN%\iprestr.dll" /> + <add name="DynamicIpRestrictionModule" image="%IIS_BIN%\diprestr.dll" /> + <add name="RequestFilteringModule" image="%IIS_BIN%\modrqflt.dll" /> + <add name="CustomLoggingModule" image="%IIS_BIN%\logcust.dll" /> + <add name="CustomErrorModule" image="%IIS_BIN%\custerr.dll" /> +<!-- <add name="TracingModule" image="%IIS_BIN%\iisetw.dll" /> --> + <add name="FailedRequestsTracingModule" image="%IIS_BIN%\iisfreb.dll" /> + <add name="RequestMonitorModule" image="%IIS_BIN%\iisreqs.dll" /> + <add name="IsapiModule" image="%IIS_BIN%\isapi.dll" /> + <add name="IsapiFilterModule" image="%IIS_BIN%\filter.dll" /> + <add name="CgiModule" image="%IIS_BIN%\cgi.dll" /> + <add name="FastCgiModule" image="%IIS_BIN%\iisfcgi.dll" /> +<!-- <add name="WebDAVModule" image="%IIS_BIN%\webdav.dll" /> --> + <add name="RewriteModule" image="%IIS_BIN%\rewrite.dll" /> + <add name="ConfigurationValidationModule" image="%IIS_BIN%\validcfg.dll" /> + <add name="WebSocketModule" image="%IIS_BIN%\iiswsock.dll" /> + <add name="WebMatrixSupportModule" image="%IIS_BIN%\webmatrixsup.dll" /> + <add name="ManagedEngine" image="%windir%\Microsoft.NET\Framework\v2.0.50727\webengine.dll" preCondition="integratedMode,runtimeVersionv2.0,bitness32" /> + <add name="ManagedEngine64" image="%windir%\Microsoft.NET\Framework64\v2.0.50727\webengine.dll" preCondition="integratedMode,runtimeVersionv2.0,bitness64" /> + <add name="ManagedEngineV4.0_32bit" image="%windir%\Microsoft.NET\Framework\v4.0.30319\webengine4.dll" preCondition="integratedMode,runtimeVersionv4.0,bitness32" /> + <add name="ManagedEngineV4.0_64bit" image="%windir%\Microsoft.NET\Framework64\v4.0.30319\webengine4.dll" preCondition="integratedMode,runtimeVersionv4.0,bitness64" /> + <add name="ApplicationInitializationModule" image="%IIS_BIN%\warmup.dll" /> + <add name="AspNetCoreModule" image="%IIS_BIN%\aspnetcore.dll" /> + </globalModules> + + <httpCompression directory="%TEMP%\iisexpress\IIS Temporary Compressed Files"> + <scheme name="gzip" dll="%IIS_BIN%\gzip.dll" /> + <dynamicTypes> + <add mimeType="text/*" enabled="true" /> + <add mimeType="message/*" enabled="true" /> + <add mimeType="application/javascript" enabled="true" /> + <add mimeType="application/atom+xml" enabled="true" /> + <add mimeType="application/xaml+xml" enabled="true" /> + <add mimeType="*/*" enabled="false" /> + </dynamicTypes> + <staticTypes> + <add mimeType="text/*" enabled="true" /> + <add mimeType="message/*" enabled="true" /> + <add mimeType="image/svg+xml" enabled="true" /> + <add mimeType="application/javascript" enabled="true" /> + <add mimeType="application/atom+xml" enabled="true" /> + <add mimeType="application/xaml+xml" enabled="true" /> + <add mimeType="*/*" enabled="false" /> + </staticTypes> + </httpCompression> + + <httpErrors lockAttributes="allowAbsolutePathsWhenDelegated,defaultPath"> + <error statusCode="401" prefixLanguageFilePath="%IIS_BIN%\custerr" path="401.htm" /> + <error statusCode="403" prefixLanguageFilePath="%IIS_BIN%\custerr" path="403.htm" /> + <error statusCode="404" prefixLanguageFilePath="%IIS_BIN%\custerr" path="404.htm" /> + <error statusCode="405" prefixLanguageFilePath="%IIS_BIN%\custerr" path="405.htm" /> + <error statusCode="406" prefixLanguageFilePath="%IIS_BIN%\custerr" path="406.htm" /> + <error statusCode="412" prefixLanguageFilePath="%IIS_BIN%\custerr" path="412.htm" /> + <error statusCode="500" prefixLanguageFilePath="%IIS_BIN%\custerr" path="500.htm" /> + <error statusCode="501" prefixLanguageFilePath="%IIS_BIN%\custerr" path="501.htm" /> + <error statusCode="502" prefixLanguageFilePath="%IIS_BIN%\custerr" path="502.htm" /> + </httpErrors> + + <httpLogging dontLog="false" /> + + <httpProtocol> + <customHeaders> + <clear /> + <add name="X-Powered-By" value="ASP.NET" /> + </customHeaders> + <redirectHeaders> + <clear /> + </redirectHeaders> + </httpProtocol> + + <httpRedirect enabled="false" /> + + <httpTracing> + </httpTracing> + + <isapiFilters> + <filter name="ASP.Net_2.0.50727-64" path="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_filter.dll" enableCache="true" preCondition="bitness64,runtimeVersionv2.0" /> + <filter name="ASP.Net_2.0.50727.0" path="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_filter.dll" enableCache="true" preCondition="bitness32,runtimeVersionv2.0" /> + <filter name="ASP.Net_2.0_for_v1.1" path="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_filter.dll" enableCache="true" preCondition="runtimeVersionv1.1" /> + <filter name="ASP.Net_4.0_32bit" path="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_filter.dll" enableCache="true" preCondition="bitness32,runtimeVersionv4.0" /> + <filter name="ASP.Net_4.0_64bit" path="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_filter.dll" enableCache="true" preCondition="bitness64,runtimeVersionv4.0" /> + </isapiFilters> + + <odbcLogging /> + + <security> + + <access sslFlags="None" /> + + <applicationDependencies> + <application name="Active Server Pages" groupId="ASP" /> + </applicationDependencies> + + <authentication> + + <anonymousAuthentication enabled="true" userName="" /> + + <basicAuthentication enabled="false" /> + + <clientCertificateMappingAuthentication enabled="false" /> + + <digestAuthentication enabled="false" /> + + <iisClientCertificateMappingAuthentication enabled="false"> + </iisClientCertificateMappingAuthentication> + + <windowsAuthentication enabled="false"> + <providers> + <add value="Negotiate" /> + <add value="NTLM" /> + </providers> + </windowsAuthentication> + + </authentication> + + <authorization> + <add accessType="Allow" users="*" /> + </authorization> + + <ipSecurity allowUnlisted="true" /> + + <isapiCgiRestriction notListedIsapisAllowed="true" notListedCgisAllowed="true"> + <add path="%windir%\Microsoft.NET\Framework64\v4.0.30319\webengine4.dll" allowed="true" groupId="ASP.NET_v4.0" description="ASP.NET_v4.0" /> + <add path="%windir%\Microsoft.NET\Framework\v4.0.30319\webengine4.dll" allowed="true" groupId="ASP.NET_v4.0" description="ASP.NET_v4.0" /> + <add path="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" allowed="true" groupId="ASP.NET v2.0.50727" description="ASP.NET v2.0.50727" /> + <add path="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" allowed="true" groupId="ASP.NET v2.0.50727" description="ASP.NET v2.0.50727" /> + </isapiCgiRestriction> + + <requestFiltering> + <fileExtensions allowUnlisted="true" applyToWebDAV="true"> + <add fileExtension=".asa" allowed="false" /> + <add fileExtension=".asax" allowed="false" /> + <add fileExtension=".ascx" allowed="false" /> + <add fileExtension=".master" allowed="false" /> + <add fileExtension=".skin" allowed="false" /> + <add fileExtension=".browser" allowed="false" /> + <add fileExtension=".sitemap" allowed="false" /> + <add fileExtension=".config" allowed="false" /> + <add fileExtension=".cs" allowed="false" /> + <add fileExtension=".csproj" allowed="false" /> + <add fileExtension=".vb" allowed="false" /> + <add fileExtension=".vbproj" allowed="false" /> + <add fileExtension=".webinfo" allowed="false" /> + <add fileExtension=".licx" allowed="false" /> + <add fileExtension=".resx" allowed="false" /> + <add fileExtension=".resources" allowed="false" /> + <add fileExtension=".mdb" allowed="false" /> + <add fileExtension=".vjsproj" allowed="false" /> + <add fileExtension=".java" allowed="false" /> + <add fileExtension=".jsl" allowed="false" /> + <add fileExtension=".ldb" allowed="false" /> + <add fileExtension=".dsdgm" allowed="false" /> + <add fileExtension=".ssdgm" allowed="false" /> + <add fileExtension=".lsad" allowed="false" /> + <add fileExtension=".ssmap" allowed="false" /> + <add fileExtension=".cd" allowed="false" /> + <add fileExtension=".dsprototype" allowed="false" /> + <add fileExtension=".lsaprototype" allowed="false" /> + <add fileExtension=".sdm" allowed="false" /> + <add fileExtension=".sdmDocument" allowed="false" /> + <add fileExtension=".mdf" allowed="false" /> + <add fileExtension=".ldf" allowed="false" /> + <add fileExtension=".ad" allowed="false" /> + <add fileExtension=".dd" allowed="false" /> + <add fileExtension=".ldd" allowed="false" /> + <add fileExtension=".sd" allowed="false" /> + <add fileExtension=".adprototype" allowed="false" /> + <add fileExtension=".lddprototype" allowed="false" /> + <add fileExtension=".exclude" allowed="false" /> + <add fileExtension=".refresh" allowed="false" /> + <add fileExtension=".compiled" allowed="false" /> + <add fileExtension=".msgx" allowed="false" /> + <add fileExtension=".vsdisco" allowed="false" /> + <add fileExtension=".rules" allowed="false" /> + </fileExtensions> + <verbs allowUnlisted="true" applyToWebDAV="true" /> + <hiddenSegments applyToWebDAV="true"> + <add segment="web.config" /> + <add segment="bin" /> + <add segment="App_code" /> + <add segment="App_GlobalResources" /> + <add segment="App_LocalResources" /> + <add segment="App_WebReferences" /> + <add segment="App_Data" /> + <add segment="App_Browsers" /> + </hiddenSegments> + </requestFiltering> + + </security> + + <serverSideInclude ssiExecDisable="false" /> + + <staticContent lockAttributes="isDocFooterFileName"> + <mimeMap fileExtension=".323" mimeType="text/h323" /> + <mimeMap fileExtension=".3g2" mimeType="video/3gpp2" /> + <mimeMap fileExtension=".3gp2" mimeType="video/3gpp2" /> + <mimeMap fileExtension=".3gp" mimeType="video/3gpp" /> + <mimeMap fileExtension=".3gpp" mimeType="video/3gpp" /> + <mimeMap fileExtension=".aac" mimeType="audio/aac" /> + <mimeMap fileExtension=".aaf" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".aca" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".accdb" mimeType="application/msaccess" /> + <mimeMap fileExtension=".accde" mimeType="application/msaccess" /> + <mimeMap fileExtension=".accdt" mimeType="application/msaccess" /> + <mimeMap fileExtension=".acx" mimeType="application/internet-property-stream" /> + <mimeMap fileExtension=".adt" mimeType="audio/vnd.dlna.adts" /> + <mimeMap fileExtension=".adts" mimeType="audio/vnd.dlna.adts" /> + <mimeMap fileExtension=".afm" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".ai" mimeType="application/postscript" /> + <mimeMap fileExtension=".aif" mimeType="audio/x-aiff" /> + <mimeMap fileExtension=".aifc" mimeType="audio/aiff" /> + <mimeMap fileExtension=".aiff" mimeType="audio/aiff" /> + <mimeMap fileExtension=".appcache" mimeType="text/cache-manifest" /> + <mimeMap fileExtension=".application" mimeType="application/x-ms-application" /> + <mimeMap fileExtension=".art" mimeType="image/x-jg" /> + <mimeMap fileExtension=".asd" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".asf" mimeType="video/x-ms-asf" /> + <mimeMap fileExtension=".asi" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".asm" mimeType="text/plain" /> + <mimeMap fileExtension=".asr" mimeType="video/x-ms-asf" /> + <mimeMap fileExtension=".asx" mimeType="video/x-ms-asf" /> + <mimeMap fileExtension=".atom" mimeType="application/atom+xml" /> + <mimeMap fileExtension=".au" mimeType="audio/basic" /> + <mimeMap fileExtension=".avi" mimeType="video/msvideo" /> + <mimeMap fileExtension=".axs" mimeType="application/olescript" /> + <mimeMap fileExtension=".bas" mimeType="text/plain" /> + <mimeMap fileExtension=".bcpio" mimeType="application/x-bcpio" /> + <mimeMap fileExtension=".bin" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".bmp" mimeType="image/bmp" /> + <mimeMap fileExtension=".c" mimeType="text/plain" /> + <mimeMap fileExtension=".cab" mimeType="application/vnd.ms-cab-compressed" /> + <mimeMap fileExtension=".calx" mimeType="application/vnd.ms-office.calx" /> + <mimeMap fileExtension=".cat" mimeType="application/vnd.ms-pki.seccat" /> + <mimeMap fileExtension=".cdf" mimeType="application/x-cdf" /> + <mimeMap fileExtension=".chm" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".class" mimeType="application/x-java-applet" /> + <mimeMap fileExtension=".clp" mimeType="application/x-msclip" /> + <mimeMap fileExtension=".cmx" mimeType="image/x-cmx" /> + <mimeMap fileExtension=".cnf" mimeType="text/plain" /> + <mimeMap fileExtension=".cod" mimeType="image/cis-cod" /> + <mimeMap fileExtension=".cpio" mimeType="application/x-cpio" /> + <mimeMap fileExtension=".cpp" mimeType="text/plain" /> + <mimeMap fileExtension=".crd" mimeType="application/x-mscardfile" /> + <mimeMap fileExtension=".crl" mimeType="application/pkix-crl" /> + <mimeMap fileExtension=".crt" mimeType="application/x-x509-ca-cert" /> + <mimeMap fileExtension=".csh" mimeType="application/x-csh" /> + <mimeMap fileExtension=".css" mimeType="text/css" /> + <mimeMap fileExtension=".csv" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".cur" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".dcr" mimeType="application/x-director" /> + <mimeMap fileExtension=".deploy" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".der" mimeType="application/x-x509-ca-cert" /> + <mimeMap fileExtension=".dib" mimeType="image/bmp" /> + <mimeMap fileExtension=".dir" mimeType="application/x-director" /> + <mimeMap fileExtension=".disco" mimeType="text/xml" /> + <mimeMap fileExtension=".dll" mimeType="application/x-msdownload" /> + <mimeMap fileExtension=".dll.config" mimeType="text/xml" /> + <mimeMap fileExtension=".dlm" mimeType="text/dlm" /> + <mimeMap fileExtension=".doc" mimeType="application/msword" /> + <mimeMap fileExtension=".docm" mimeType="application/vnd.ms-word.document.macroEnabled.12" /> + <mimeMap fileExtension=".docx" mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.document" /> + <mimeMap fileExtension=".dot" mimeType="application/msword" /> + <mimeMap fileExtension=".dotm" mimeType="application/vnd.ms-word.template.macroEnabled.12" /> + <mimeMap fileExtension=".dotx" mimeType="application/vnd.openxmlformats-officedocument.wordprocessingml.template" /> + <mimeMap fileExtension=".dsp" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".dtd" mimeType="text/xml" /> + <mimeMap fileExtension=".dvi" mimeType="application/x-dvi" /> + <mimeMap fileExtension=".dvr-ms" mimeType="video/x-ms-dvr" /> + <mimeMap fileExtension=".dwf" mimeType="drawing/x-dwf" /> + <mimeMap fileExtension=".dwp" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".dxr" mimeType="application/x-director" /> + <mimeMap fileExtension=".eml" mimeType="message/rfc822" /> + <mimeMap fileExtension=".emz" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".eot" mimeType="application/vnd.ms-fontobject" /> + <mimeMap fileExtension=".eps" mimeType="application/postscript" /> + <mimeMap fileExtension=".etx" mimeType="text/x-setext" /> + <mimeMap fileExtension=".evy" mimeType="application/envoy" /> + <mimeMap fileExtension=".exe" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".exe.config" mimeType="text/xml" /> + <mimeMap fileExtension=".fdf" mimeType="application/vnd.fdf" /> + <mimeMap fileExtension=".fif" mimeType="application/fractals" /> + <mimeMap fileExtension=".fla" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".flr" mimeType="x-world/x-vrml" /> + <mimeMap fileExtension=".flv" mimeType="video/x-flv" /> + <mimeMap fileExtension=".gif" mimeType="image/gif" /> + <mimeMap fileExtension=".gtar" mimeType="application/x-gtar" /> + <mimeMap fileExtension=".gz" mimeType="application/x-gzip" /> + <mimeMap fileExtension=".h" mimeType="text/plain" /> + <mimeMap fileExtension=".hdf" mimeType="application/x-hdf" /> + <mimeMap fileExtension=".hdml" mimeType="text/x-hdml" /> + <mimeMap fileExtension=".hhc" mimeType="application/x-oleobject" /> + <mimeMap fileExtension=".hhk" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".hhp" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".hlp" mimeType="application/winhlp" /> + <mimeMap fileExtension=".hqx" mimeType="application/mac-binhex40" /> + <mimeMap fileExtension=".hta" mimeType="application/hta" /> + <mimeMap fileExtension=".htc" mimeType="text/x-component" /> + <mimeMap fileExtension=".htm" mimeType="text/html" /> + <mimeMap fileExtension=".html" mimeType="text/html" /> + <mimeMap fileExtension=".htt" mimeType="text/webviewhtml" /> + <mimeMap fileExtension=".hxt" mimeType="text/html" /> + <mimeMap fileExtension=".ico" mimeType="image/x-icon" /> + <mimeMap fileExtension=".ics" mimeType="text/calendar" /> + <mimeMap fileExtension=".ief" mimeType="image/ief" /> + <mimeMap fileExtension=".iii" mimeType="application/x-iphone" /> + <mimeMap fileExtension=".inf" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".ins" mimeType="application/x-internet-signup" /> + <mimeMap fileExtension=".isp" mimeType="application/x-internet-signup" /> + <mimeMap fileExtension=".IVF" mimeType="video/x-ivf" /> + <mimeMap fileExtension=".jar" mimeType="application/java-archive" /> + <mimeMap fileExtension=".java" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".jck" mimeType="application/liquidmotion" /> + <mimeMap fileExtension=".jcz" mimeType="application/liquidmotion" /> + <mimeMap fileExtension=".jfif" mimeType="image/pjpeg" /> + <mimeMap fileExtension=".jpb" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".jpe" mimeType="image/jpeg" /> + <mimeMap fileExtension=".jpeg" mimeType="image/jpeg" /> + <mimeMap fileExtension=".jpg" mimeType="image/jpeg" /> + <mimeMap fileExtension=".js" mimeType="application/javascript" /> + <mimeMap fileExtension=".json" mimeType="application/json" /> + <mimeMap fileExtension=".jsonld" mimeType="application/ld+json" /> + <mimeMap fileExtension=".jsx" mimeType="text/jscript" /> + <mimeMap fileExtension=".latex" mimeType="application/x-latex" /> + <mimeMap fileExtension=".less" mimeType="text/css" /> + <mimeMap fileExtension=".lit" mimeType="application/x-ms-reader" /> + <mimeMap fileExtension=".lpk" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".lsf" mimeType="video/x-la-asf" /> + <mimeMap fileExtension=".lsx" mimeType="video/x-la-asf" /> + <mimeMap fileExtension=".lzh" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".m13" mimeType="application/x-msmediaview" /> + <mimeMap fileExtension=".m14" mimeType="application/x-msmediaview" /> + <mimeMap fileExtension=".m1v" mimeType="video/mpeg" /> + <mimeMap fileExtension=".m2ts" mimeType="video/vnd.dlna.mpeg-tts" /> + <mimeMap fileExtension=".m3u" mimeType="audio/x-mpegurl" /> + <mimeMap fileExtension=".m4a" mimeType="audio/mp4" /> + <mimeMap fileExtension=".m4v" mimeType="video/mp4" /> + <mimeMap fileExtension=".man" mimeType="application/x-troff-man" /> + <mimeMap fileExtension=".manifest" mimeType="application/x-ms-manifest" /> + <mimeMap fileExtension=".map" mimeType="text/plain" /> + <mimeMap fileExtension=".mdb" mimeType="application/x-msaccess" /> + <mimeMap fileExtension=".mdp" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".me" mimeType="application/x-troff-me" /> + <mimeMap fileExtension=".mht" mimeType="message/rfc822" /> + <mimeMap fileExtension=".mhtml" mimeType="message/rfc822" /> + <mimeMap fileExtension=".mid" mimeType="audio/mid" /> + <mimeMap fileExtension=".midi" mimeType="audio/mid" /> + <mimeMap fileExtension=".mix" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".mmf" mimeType="application/x-smaf" /> + <mimeMap fileExtension=".mno" mimeType="text/xml" /> + <mimeMap fileExtension=".mny" mimeType="application/x-msmoney" /> + <mimeMap fileExtension=".mov" mimeType="video/quicktime" /> + <mimeMap fileExtension=".movie" mimeType="video/x-sgi-movie" /> + <mimeMap fileExtension=".mp2" mimeType="video/mpeg" /> + <mimeMap fileExtension=".mp3" mimeType="audio/mpeg" /> + <mimeMap fileExtension=".mp4" mimeType="video/mp4" /> + <mimeMap fileExtension=".mp4v" mimeType="video/mp4" /> + <mimeMap fileExtension=".mpa" mimeType="video/mpeg" /> + <mimeMap fileExtension=".mpe" mimeType="video/mpeg" /> + <mimeMap fileExtension=".mpeg" mimeType="video/mpeg" /> + <mimeMap fileExtension=".mpg" mimeType="video/mpeg" /> + <mimeMap fileExtension=".mpp" mimeType="application/vnd.ms-project" /> + <mimeMap fileExtension=".mpv2" mimeType="video/mpeg" /> + <mimeMap fileExtension=".ms" mimeType="application/x-troff-ms" /> + <mimeMap fileExtension=".msi" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".mso" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".mvb" mimeType="application/x-msmediaview" /> + <mimeMap fileExtension=".mvc" mimeType="application/x-miva-compiled" /> + <mimeMap fileExtension=".nc" mimeType="application/x-netcdf" /> + <mimeMap fileExtension=".nsc" mimeType="video/x-ms-asf" /> + <mimeMap fileExtension=".nws" mimeType="message/rfc822" /> + <mimeMap fileExtension=".ocx" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".oda" mimeType="application/oda" /> + <mimeMap fileExtension=".odc" mimeType="text/x-ms-odc" /> + <mimeMap fileExtension=".ods" mimeType="application/oleobject" /> + <mimeMap fileExtension=".oga" mimeType="audio/ogg" /> + <mimeMap fileExtension=".ogg" mimeType="video/ogg" /> + <mimeMap fileExtension=".ogv" mimeType="video/ogg" /> + <mimeMap fileExtension=".one" mimeType="application/onenote" /> + <mimeMap fileExtension=".onea" mimeType="application/onenote" /> + <mimeMap fileExtension=".onetoc" mimeType="application/onenote" /> + <mimeMap fileExtension=".onetoc2" mimeType="application/onenote" /> + <mimeMap fileExtension=".onetmp" mimeType="application/onenote" /> + <mimeMap fileExtension=".onepkg" mimeType="application/onenote" /> + <mimeMap fileExtension=".osdx" mimeType="application/opensearchdescription+xml" /> + <mimeMap fileExtension=".otf" mimeType="font/otf" /> + <mimeMap fileExtension=".p10" mimeType="application/pkcs10" /> + <mimeMap fileExtension=".p12" mimeType="application/x-pkcs12" /> + <mimeMap fileExtension=".p7b" mimeType="application/x-pkcs7-certificates" /> + <mimeMap fileExtension=".p7c" mimeType="application/pkcs7-mime" /> + <mimeMap fileExtension=".p7m" mimeType="application/pkcs7-mime" /> + <mimeMap fileExtension=".p7r" mimeType="application/x-pkcs7-certreqresp" /> + <mimeMap fileExtension=".p7s" mimeType="application/pkcs7-signature" /> + <mimeMap fileExtension=".pbm" mimeType="image/x-portable-bitmap" /> + <mimeMap fileExtension=".pcx" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".pcz" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".pdf" mimeType="application/pdf" /> + <mimeMap fileExtension=".pfb" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".pfm" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".pfx" mimeType="application/x-pkcs12" /> + <mimeMap fileExtension=".pgm" mimeType="image/x-portable-graymap" /> + <mimeMap fileExtension=".pko" mimeType="application/vnd.ms-pki.pko" /> + <mimeMap fileExtension=".pma" mimeType="application/x-perfmon" /> + <mimeMap fileExtension=".pmc" mimeType="application/x-perfmon" /> + <mimeMap fileExtension=".pml" mimeType="application/x-perfmon" /> + <mimeMap fileExtension=".pmr" mimeType="application/x-perfmon" /> + <mimeMap fileExtension=".pmw" mimeType="application/x-perfmon" /> + <mimeMap fileExtension=".png" mimeType="image/png" /> + <mimeMap fileExtension=".pnm" mimeType="image/x-portable-anymap" /> + <mimeMap fileExtension=".pnz" mimeType="image/png" /> + <mimeMap fileExtension=".pot" mimeType="application/vnd.ms-powerpoint" /> + <mimeMap fileExtension=".potm" mimeType="application/vnd.ms-powerpoint.template.macroEnabled.12" /> + <mimeMap fileExtension=".potx" mimeType="application/vnd.openxmlformats-officedocument.presentationml.template" /> + <mimeMap fileExtension=".ppam" mimeType="application/vnd.ms-powerpoint.addin.macroEnabled.12" /> + <mimeMap fileExtension=".ppm" mimeType="image/x-portable-pixmap" /> + <mimeMap fileExtension=".pps" mimeType="application/vnd.ms-powerpoint" /> + <mimeMap fileExtension=".ppsm" mimeType="application/vnd.ms-powerpoint.slideshow.macroEnabled.12" /> + <mimeMap fileExtension=".ppsx" mimeType="application/vnd.openxmlformats-officedocument.presentationml.slideshow" /> + <mimeMap fileExtension=".ppt" mimeType="application/vnd.ms-powerpoint" /> + <mimeMap fileExtension=".pptm" mimeType="application/vnd.ms-powerpoint.presentation.macroEnabled.12" /> + <mimeMap fileExtension=".pptx" mimeType="application/vnd.openxmlformats-officedocument.presentationml.presentation" /> + <mimeMap fileExtension=".prf" mimeType="application/pics-rules" /> + <mimeMap fileExtension=".prm" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".prx" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".ps" mimeType="application/postscript" /> + <mimeMap fileExtension=".psd" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".psm" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".psp" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".pub" mimeType="application/x-mspublisher" /> + <mimeMap fileExtension=".qt" mimeType="video/quicktime" /> + <mimeMap fileExtension=".qtl" mimeType="application/x-quicktimeplayer" /> + <mimeMap fileExtension=".qxd" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".ra" mimeType="audio/x-pn-realaudio" /> + <mimeMap fileExtension=".ram" mimeType="audio/x-pn-realaudio" /> + <mimeMap fileExtension=".rar" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".ras" mimeType="image/x-cmu-raster" /> + <mimeMap fileExtension=".rf" mimeType="image/vnd.rn-realflash" /> + <mimeMap fileExtension=".rgb" mimeType="image/x-rgb" /> + <mimeMap fileExtension=".rm" mimeType="application/vnd.rn-realmedia" /> + <mimeMap fileExtension=".rmi" mimeType="audio/mid" /> + <mimeMap fileExtension=".roff" mimeType="application/x-troff" /> + <mimeMap fileExtension=".rpm" mimeType="audio/x-pn-realaudio-plugin" /> + <mimeMap fileExtension=".rtf" mimeType="application/rtf" /> + <mimeMap fileExtension=".rtx" mimeType="text/richtext" /> + <mimeMap fileExtension=".scd" mimeType="application/x-msschedule" /> + <mimeMap fileExtension=".sct" mimeType="text/scriptlet" /> + <mimeMap fileExtension=".sea" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".setpay" mimeType="application/set-payment-initiation" /> + <mimeMap fileExtension=".setreg" mimeType="application/set-registration-initiation" /> + <mimeMap fileExtension=".sgml" mimeType="text/sgml" /> + <mimeMap fileExtension=".sh" mimeType="application/x-sh" /> + <mimeMap fileExtension=".shar" mimeType="application/x-shar" /> + <mimeMap fileExtension=".sit" mimeType="application/x-stuffit" /> + <mimeMap fileExtension=".sldm" mimeType="application/vnd.ms-powerpoint.slide.macroEnabled.12" /> + <mimeMap fileExtension=".sldx" mimeType="application/vnd.openxmlformats-officedocument.presentationml.slide" /> + <mimeMap fileExtension=".smd" mimeType="audio/x-smd" /> + <mimeMap fileExtension=".smi" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".smx" mimeType="audio/x-smd" /> + <mimeMap fileExtension=".smz" mimeType="audio/x-smd" /> + <mimeMap fileExtension=".snd" mimeType="audio/basic" /> + <mimeMap fileExtension=".snp" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".spc" mimeType="application/x-pkcs7-certificates" /> + <mimeMap fileExtension=".spl" mimeType="application/futuresplash" /> + <mimeMap fileExtension=".spx" mimeType="audio/ogg" /> + <mimeMap fileExtension=".src" mimeType="application/x-wais-source" /> + <mimeMap fileExtension=".ssm" mimeType="application/streamingmedia" /> + <mimeMap fileExtension=".sst" mimeType="application/vnd.ms-pki.certstore" /> + <mimeMap fileExtension=".stl" mimeType="application/vnd.ms-pki.stl" /> + <mimeMap fileExtension=".sv4cpio" mimeType="application/x-sv4cpio" /> + <mimeMap fileExtension=".sv4crc" mimeType="application/x-sv4crc" /> + <mimeMap fileExtension=".svg" mimeType="image/svg+xml" /> + <mimeMap fileExtension=".svgz" mimeType="image/svg+xml" /> + <mimeMap fileExtension=".swf" mimeType="application/x-shockwave-flash" /> + <mimeMap fileExtension=".t" mimeType="application/x-troff" /> + <mimeMap fileExtension=".tar" mimeType="application/x-tar" /> + <mimeMap fileExtension=".tcl" mimeType="application/x-tcl" /> + <mimeMap fileExtension=".tex" mimeType="application/x-tex" /> + <mimeMap fileExtension=".texi" mimeType="application/x-texinfo" /> + <mimeMap fileExtension=".texinfo" mimeType="application/x-texinfo" /> + <mimeMap fileExtension=".tgz" mimeType="application/x-compressed" /> + <mimeMap fileExtension=".thmx" mimeType="application/vnd.ms-officetheme" /> + <mimeMap fileExtension=".thn" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".tif" mimeType="image/tiff" /> + <mimeMap fileExtension=".tiff" mimeType="image/tiff" /> + <mimeMap fileExtension=".toc" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".tr" mimeType="application/x-troff" /> + <mimeMap fileExtension=".trm" mimeType="application/x-msterminal" /> + <mimeMap fileExtension=".ts" mimeType="video/vnd.dlna.mpeg-tts" /> + <mimeMap fileExtension=".tsv" mimeType="text/tab-separated-values" /> + <mimeMap fileExtension=".ttf" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".tts" mimeType="video/vnd.dlna.mpeg-tts" /> + <mimeMap fileExtension=".txt" mimeType="text/plain" /> + <mimeMap fileExtension=".u32" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".uls" mimeType="text/iuls" /> + <mimeMap fileExtension=".ustar" mimeType="application/x-ustar" /> + <mimeMap fileExtension=".vbs" mimeType="text/vbscript" /> + <mimeMap fileExtension=".vcf" mimeType="text/x-vcard" /> + <mimeMap fileExtension=".vcs" mimeType="text/plain" /> + <mimeMap fileExtension=".vdx" mimeType="application/vnd.ms-visio.viewer" /> + <mimeMap fileExtension=".vml" mimeType="text/xml" /> + <mimeMap fileExtension=".vsd" mimeType="application/vnd.visio" /> + <mimeMap fileExtension=".vss" mimeType="application/vnd.visio" /> + <mimeMap fileExtension=".vst" mimeType="application/vnd.visio" /> + <mimeMap fileExtension=".vsto" mimeType="application/x-ms-vsto" /> + <mimeMap fileExtension=".vsw" mimeType="application/vnd.visio" /> + <mimeMap fileExtension=".vsx" mimeType="application/vnd.visio" /> + <mimeMap fileExtension=".vtx" mimeType="application/vnd.visio" /> + <mimeMap fileExtension=".wav" mimeType="audio/wav" /> + <mimeMap fileExtension=".wax" mimeType="audio/x-ms-wax" /> + <mimeMap fileExtension=".wbmp" mimeType="image/vnd.wap.wbmp" /> + <mimeMap fileExtension=".wcm" mimeType="application/vnd.ms-works" /> + <mimeMap fileExtension=".wdb" mimeType="application/vnd.ms-works" /> + <mimeMap fileExtension=".webm" mimeType="video/webm" /> + <mimeMap fileExtension=".wks" mimeType="application/vnd.ms-works" /> + <mimeMap fileExtension=".wm" mimeType="video/x-ms-wm" /> + <mimeMap fileExtension=".wma" mimeType="audio/x-ms-wma" /> + <mimeMap fileExtension=".wmd" mimeType="application/x-ms-wmd" /> + <mimeMap fileExtension=".wmf" mimeType="application/x-msmetafile" /> + <mimeMap fileExtension=".wml" mimeType="text/vnd.wap.wml" /> + <mimeMap fileExtension=".wmlc" mimeType="application/vnd.wap.wmlc" /> + <mimeMap fileExtension=".wmls" mimeType="text/vnd.wap.wmlscript" /> + <mimeMap fileExtension=".wmlsc" mimeType="application/vnd.wap.wmlscriptc" /> + <mimeMap fileExtension=".wmp" mimeType="video/x-ms-wmp" /> + <mimeMap fileExtension=".wmv" mimeType="video/x-ms-wmv" /> + <mimeMap fileExtension=".wmx" mimeType="video/x-ms-wmx" /> + <mimeMap fileExtension=".wmz" mimeType="application/x-ms-wmz" /> + <mimeMap fileExtension=".woff" mimeType="font/x-woff" /> + <mimeMap fileExtension=".woff2" mimeType="application/font-woff2" /> + <mimeMap fileExtension=".wps" mimeType="application/vnd.ms-works" /> + <mimeMap fileExtension=".wri" mimeType="application/x-mswrite" /> + <mimeMap fileExtension=".wrl" mimeType="x-world/x-vrml" /> + <mimeMap fileExtension=".wrz" mimeType="x-world/x-vrml" /> + <mimeMap fileExtension=".wsdl" mimeType="text/xml" /> + <mimeMap fileExtension=".wtv" mimeType="video/x-ms-wtv" /> + <mimeMap fileExtension=".wvx" mimeType="video/x-ms-wvx" /> + <mimeMap fileExtension=".x" mimeType="application/directx" /> + <mimeMap fileExtension=".xaf" mimeType="x-world/x-vrml" /> + <mimeMap fileExtension=".xaml" mimeType="application/xaml+xml" /> + <mimeMap fileExtension=".xap" mimeType="application/x-silverlight-app" /> + <mimeMap fileExtension=".xbap" mimeType="application/x-ms-xbap" /> + <mimeMap fileExtension=".xbm" mimeType="image/x-xbitmap" /> + <mimeMap fileExtension=".xdr" mimeType="text/plain" /> + <mimeMap fileExtension=".xht" mimeType="application/xhtml+xml" /> + <mimeMap fileExtension=".xhtml" mimeType="application/xhtml+xml" /> + <mimeMap fileExtension=".xla" mimeType="application/vnd.ms-excel" /> + <mimeMap fileExtension=".xlam" mimeType="application/vnd.ms-excel.addin.macroEnabled.12" /> + <mimeMap fileExtension=".xlc" mimeType="application/vnd.ms-excel" /> + <mimeMap fileExtension=".xlm" mimeType="application/vnd.ms-excel" /> + <mimeMap fileExtension=".xls" mimeType="application/vnd.ms-excel" /> + <mimeMap fileExtension=".xlsb" mimeType="application/vnd.ms-excel.sheet.binary.macroEnabled.12" /> + <mimeMap fileExtension=".xlsm" mimeType="application/vnd.ms-excel.sheet.macroEnabled.12" /> + <mimeMap fileExtension=".xlsx" mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" /> + <mimeMap fileExtension=".xlt" mimeType="application/vnd.ms-excel" /> + <mimeMap fileExtension=".xltm" mimeType="application/vnd.ms-excel.template.macroEnabled.12" /> + <mimeMap fileExtension=".xltx" mimeType="application/vnd.openxmlformats-officedocument.spreadsheetml.template" /> + <mimeMap fileExtension=".xlw" mimeType="application/vnd.ms-excel" /> + <mimeMap fileExtension=".xml" mimeType="text/xml" /> + <mimeMap fileExtension=".xof" mimeType="x-world/x-vrml" /> + <mimeMap fileExtension=".xpm" mimeType="image/x-xpixmap" /> + <mimeMap fileExtension=".xps" mimeType="application/vnd.ms-xpsdocument" /> + <mimeMap fileExtension=".xsd" mimeType="text/xml" /> + <mimeMap fileExtension=".xsf" mimeType="text/xml" /> + <mimeMap fileExtension=".xsl" mimeType="text/xml" /> + <mimeMap fileExtension=".xslt" mimeType="text/xml" /> + <mimeMap fileExtension=".xsn" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".xtp" mimeType="application/octet-stream" /> + <mimeMap fileExtension=".xwd" mimeType="image/x-xwindowdump" /> + <mimeMap fileExtension=".z" mimeType="application/x-compress" /> + <mimeMap fileExtension=".zip" mimeType="application/x-zip-compressed" /> + </staticContent> + + <tracing> + + <traceProviderDefinitions> + <add name="WWW Server" guid="{3a2a4e84-4c21-4981-ae10-3fda0d9b0f83}"> + <areas> + <clear /> + <add name="Authentication" value="2" /> + <add name="Security" value="4" /> + <add name="Filter" value="8" /> + <add name="StaticFile" value="16" /> + <add name="CGI" value="32" /> + <add name="Compression" value="64" /> + <add name="Cache" value="128" /> + <add name="RequestNotifications" value="256" /> + <add name="Module" value="512" /> + <add name="Rewrite" value="1024" /> + <add name="FastCGI" value="4096" /> + <add name="WebSocket" value="16384" /> + </areas> + </add> + <add name="ASP" guid="{06b94d9a-b15e-456e-a4ef-37c984a2cb4b}"> + <areas> + <clear /> + </areas> + </add> + <add name="ISAPI Extension" guid="{a1c2040e-8840-4c31-ba11-9871031a19ea}"> + <areas> + <clear /> + </areas> + </add> + <add name="ASPNET" guid="{AFF081FE-0247-4275-9C4E-021F3DC1DA35}"> + <areas> + <add name="Infrastructure" value="1" /> + <add name="Module" value="2" /> + <add name="Page" value="4" /> + <add name="AppServices" value="8" /> + </areas> + </add> + </traceProviderDefinitions> + + <traceFailedRequests> + <add path="*"> + <traceAreas> + <add provider="ASP" verbosity="Verbose" /> + <add provider="ASPNET" areas="Infrastructure,Module,Page,AppServices" verbosity="Verbose" /> + <add provider="ISAPI Extension" verbosity="Verbose" /> + <add provider="WWW Server" areas="Authentication,Security,Filter,StaticFile,CGI,Compression,Cache,RequestNotifications,Module,Rewrite,WebSocket" verbosity="Verbose" /> + </traceAreas> + <failureDefinitions statusCodes="200-999" /> + </add> + </traceFailedRequests> + + </tracing> + + <urlCompression /> + + <validation /> + <webdav> + <globalSettings> + <propertyStores> + <add name="webdav_simple_prop" image="%IIS_BIN%\webdav_simple_prop.dll" image32="%IIS_BIN%\webdav_simple_prop.dll" /> + </propertyStores> + <lockStores> + <add name="webdav_simple_lock" image="%IIS_BIN%\webdav_simple_lock.dll" image32="%IIS_BIN%\webdav_simple_lock.dll" /> + </lockStores> + + </globalSettings> + <authoring> + <locks enabled="true" lockStore="webdav_simple_lock" /> + </authoring> + <authoringRules /> + </webdav> + <webSocket /> + <applicationInitialization /> + + </system.webServer> + <location path="" overrideMode="Allow"> + <system.webServer> + <modules> + <add name="IsapiFilterModule" lockItem="true" /> + <add name="BasicAuthenticationModule" lockItem="true" /> + <add name="IsapiModule" lockItem="true" /> + <add name="HttpLoggingModule" lockItem="true" /> + <!-- + <add name="HttpCacheModule" lockItem="true" /> +--> + <add name="DynamicCompressionModule" lockItem="true" /> + <add name="StaticCompressionModule" lockItem="true" /> + <add name="DefaultDocumentModule" lockItem="true" /> + <add name="DirectoryListingModule" lockItem="true" /> + + <add name="ProtocolSupportModule" lockItem="true" /> + <add name="HttpRedirectionModule" lockItem="true" /> + <add name="ServerSideIncludeModule" lockItem="true" /> + <add name="StaticFileModule" lockItem="true" /> + <add name="AnonymousAuthenticationModule" lockItem="true" /> + <add name="CertificateMappingAuthenticationModule" lockItem="true" /> + <add name="UrlAuthorizationModule" lockItem="true" /> + <add name="WindowsAuthenticationModule" lockItem="true" /> + <!-- + <add name="DigestAuthenticationModule" lockItem="true" /> +--> + <add name="IISCertificateMappingAuthenticationModule" lockItem="true" /> + <add name="WebMatrixSupportModule" lockItem="true" /> + <add name="IpRestrictionModule" lockItem="true" /> + <add name="DynamicIpRestrictionModule" lockItem="true" /> + <add name="RequestFilteringModule" lockItem="true" /> + <add name="CustomLoggingModule" lockItem="true" /> + <add name="CustomErrorModule" lockItem="true" /> + <add name="FailedRequestsTracingModule" lockItem="true" /> + <add name="CgiModule" lockItem="true" /> + <add name="FastCgiModule" lockItem="true" /> + <!-- <add name="WebDAVModule" /> --> + <add name="RewriteModule" /> + <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" preCondition="managedHandler" /> + <add name="Session" type="System.Web.SessionState.SessionStateModule" preCondition="managedHandler" /> + <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" preCondition="managedHandler" /> + <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" preCondition="managedHandler" /> + <add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" preCondition="managedHandler" /> + <add name="RoleManager" type="System.Web.Security.RoleManagerModule" preCondition="managedHandler" /> + <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" preCondition="managedHandler" /> + <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" preCondition="managedHandler" /> + <add name="AnonymousIdentification" type="System.Web.Security.AnonymousIdentificationModule" preCondition="managedHandler" /> + <add name="Profile" type="System.Web.Profile.ProfileModule" preCondition="managedHandler" /> + <add name="UrlMappingsModule" type="System.Web.UrlMappingsModule" preCondition="managedHandler" /> + <add name="ConfigurationValidationModule" lockItem="true" /> + <add name="WebSocketModule" lockItem="true" /> + <add name="ServiceModel-4.0" type="System.ServiceModel.Activation.ServiceHttpModule,System.ServiceModel.Activation,Version=4.0.0.0,Culture=neutral,PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler,runtimeVersionv4.0" /> + <add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule" preCondition="managedHandler,runtimeVersionv4.0" /> + <add name="ScriptModule-4.0" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler,runtimeVersionv4.0" /> + <add name="ServiceModel" type="System.ServiceModel.Activation.HttpModule, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="managedHandler,runtimeVersionv2.0" /> + <add name="ApplicationInitializationModule" lockItem="true" /> + <add name="AspNetCoreModule" lockItem="true" /> + </modules> + <handlers accessPolicy="Read, Script"> + <!-- <add name="WebDAV" path="*" verb="PROPFIND,PROPPATCH,MKCOL,PUT,COPY,DELETE,MOVE,LOCK,UNLOCK" modules="WebDAVModule" resourceType="Unspecified" requireAccess="None" /> --> + <add name="AXD-ISAPI-4.0_64bit" path="*.axd" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="PageHandlerFactory-ISAPI-4.0_64bit" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="SimpleHandlerFactory-ISAPI-4.0_64bit" path="*.ashx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="WebServiceHandlerFactory-ISAPI-4.0_64bit" path="*.asmx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="HttpRemotingHandlerFactory-rem-ISAPI-4.0_64bit" path="*.rem" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="HttpRemotingHandlerFactory-soap-ISAPI-4.0_64bit" path="*.soap" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="svc-ISAPI-4.0_64bit" path="*.svc" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" /> + <add name="rules-ISAPI-4.0_64bit" path="*.rules" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" /> + <add name="xoml-ISAPI-4.0_64bit" path="*.xoml" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" /> + <add name="xamlx-ISAPI-4.0_64bit" path="*.xamlx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" /> + <add name="aspq-ISAPI-4.0_64bit" path="*.aspq" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="cshtm-ISAPI-4.0_64bit" path="*.cshtm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="cshtml-ISAPI-4.0_64bit" path="*.cshtml" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="vbhtm-ISAPI-4.0_64bit" path="*.vbhtm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="vbhtml-ISAPI-4.0_64bit" path="*.vbhtml" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="svc-Integrated" path="*.svc" verb="*" type="System.ServiceModel.Activation.HttpHandler, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="svc-ISAPI-2.0" path="*.svc" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" /> + <add name="xoml-Integrated" path="*.xoml" verb="*" type="System.ServiceModel.Activation.HttpHandler, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="xoml-ISAPI-2.0" path="*.xoml" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" /> + <add name="rules-Integrated" path="*.rules" verb="*" type="System.ServiceModel.Activation.HttpHandler, System.ServiceModel, Version=3.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="rules-ISAPI-2.0" path="*.rules" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" /> + <add name="AXD-ISAPI-4.0_32bit" path="*.axd" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="PageHandlerFactory-ISAPI-4.0_32bit" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="SimpleHandlerFactory-ISAPI-4.0_32bit" path="*.ashx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="WebServiceHandlerFactory-ISAPI-4.0_32bit" path="*.asmx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="HttpRemotingHandlerFactory-rem-ISAPI-4.0_32bit" path="*.rem" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="HttpRemotingHandlerFactory-soap-ISAPI-4.0_32bit" path="*.soap" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="svc-ISAPI-4.0_32bit" path="*.svc" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" /> + <add name="rules-ISAPI-4.0_32bit" path="*.rules" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" /> + <add name="xoml-ISAPI-4.0_32bit" path="*.xoml" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" /> + <add name="xamlx-ISAPI-4.0_32bit" path="*.xamlx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" /> + <add name="aspq-ISAPI-4.0_32bit" path="*.aspq" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="cshtm-ISAPI-4.0_32bit" path="*.cshtm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="cshtml-ISAPI-4.0_32bit" path="*.cshtml" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="vbhtm-ISAPI-4.0_32bit" path="*.vbhtm" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="vbhtml-ISAPI-4.0_32bit" path="*.vbhtml" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="TraceHandler-Integrated-4.0" path="trace.axd" verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TraceHandler" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="WebAdminHandler-Integrated-4.0" path="WebAdmin.axd" verb="GET,DEBUG" type="System.Web.Handlers.WebAdminHandler" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="AssemblyResourceLoader-Integrated-4.0" path="WebResource.axd" verb="GET,DEBUG" type="System.Web.Handlers.AssemblyResourceLoader" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="PageHandlerFactory-Integrated-4.0" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="SimpleHandlerFactory-Integrated-4.0" path="*.ashx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.SimpleHandlerFactory" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="WebServiceHandlerFactory-Integrated-4.0" path="*.asmx" verb="GET,HEAD,POST,DEBUG" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="HttpRemotingHandlerFactory-rem-Integrated-4.0" path="*.rem" verb="GET,HEAD,POST,DEBUG" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="HttpRemotingHandlerFactory-soap-Integrated-4.0" path="*.soap" verb="GET,HEAD,POST,DEBUG" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory, System.Runtime.Remoting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="svc-Integrated-4.0" path="*.svc" verb="*" type="System.ServiceModel.Activation.ServiceHttpHandlerFactory, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="rules-Integrated-4.0" path="*.rules" verb="*" type="System.ServiceModel.Activation.ServiceHttpHandlerFactory, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="xoml-Integrated-4.0" path="*.xoml" verb="*" type="System.ServiceModel.Activation.ServiceHttpHandlerFactory, System.ServiceModel.Activation, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="xamlx-Integrated-4.0" path="*.xamlx" verb="GET,HEAD,POST,DEBUG" type="System.Xaml.Hosting.XamlHttpHandlerFactory, System.Xaml.Hosting, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="aspq-Integrated-4.0" path="*.aspq" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="cshtm-Integrated-4.0" path="*.cshtm" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="cshtml-Integrated-4.0" path="*.cshtml" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="vbhtm-Integrated-4.0" path="*.vbhtm" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="vbhtml-Integrated-4.0" path="*.vbhtml" verb="GET,HEAD,POST,DEBUG" type="System.Web.HttpForbiddenHandler" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="ScriptHandlerFactoryAppServices-Integrated-4.0" path="*_AppService.axd" verb="*" type="System.Web.Script.Services.ScriptHandlerFactory, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="ScriptResourceIntegrated-4.0" path="*ScriptResource.axd" verb="GET,HEAD" type="System.Web.Handlers.ScriptResourceHandler, System.Web.Extensions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" preCondition="integratedMode,runtimeVersionv4.0" /> + <add name="ASPClassic" path="*.asp" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="%IIS_BIN%\asp.dll" resourceType="File" /> + <add name="SecurityCertificate" path="*.cer" verb="GET,HEAD,POST" modules="IsapiModule" scriptProcessor="%IIS_BIN%\asp.dll" resourceType="File" /> + <add name="ISAPI-dll" path="*.dll" verb="*" modules="IsapiModule" resourceType="File" requireAccess="Execute" allowPathInfo="true" /> + <add name="TraceHandler-Integrated" path="trace.axd" verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TraceHandler" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="WebAdminHandler-Integrated" path="WebAdmin.axd" verb="GET,DEBUG" type="System.Web.Handlers.WebAdminHandler" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="AssemblyResourceLoader-Integrated" path="WebResource.axd" verb="GET,DEBUG" type="System.Web.Handlers.AssemblyResourceLoader" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="PageHandlerFactory-Integrated" path="*.aspx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.PageHandlerFactory" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="SimpleHandlerFactory-Integrated" path="*.ashx" verb="GET,HEAD,POST,DEBUG" type="System.Web.UI.SimpleHandlerFactory" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="WebServiceHandlerFactory-Integrated" path="*.asmx" verb="GET,HEAD,POST,DEBUG" type="System.Web.Services.Protocols.WebServiceHandlerFactory,System.Web.Services,Version=2.0.0.0,Culture=neutral,PublicKeyToken=b03f5f7f11d50a3a" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="HttpRemotingHandlerFactory-rem-Integrated" path="*.rem" verb="GET,HEAD,POST,DEBUG" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory,System.Runtime.Remoting,Version=2.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="HttpRemotingHandlerFactory-soap-Integrated" path="*.soap" verb="GET,HEAD,POST,DEBUG" type="System.Runtime.Remoting.Channels.Http.HttpRemotingHandlerFactory,System.Runtime.Remoting,Version=2.0.0.0,Culture=neutral,PublicKeyToken=b77a5c561934e089" preCondition="integratedMode,runtimeVersionv2.0" /> + <add name="AXD-ISAPI-2.0" path="*.axd" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" /> + <add name="PageHandlerFactory-ISAPI-2.0" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" /> + <add name="SimpleHandlerFactory-ISAPI-2.0" path="*.ashx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" /> + <add name="WebServiceHandlerFactory-ISAPI-2.0" path="*.asmx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" /> + <add name="HttpRemotingHandlerFactory-rem-ISAPI-2.0" path="*.rem" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" /> + <add name="HttpRemotingHandlerFactory-soap-ISAPI-2.0" path="*.soap" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness32" responseBufferLimit="0" /> + <add name="svc-ISAPI-2.0-64" path="*.svc" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" /> + <add name="AXD-ISAPI-2.0-64" path="*.axd" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" /> + <add name="PageHandlerFactory-ISAPI-2.0-64" path="*.aspx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" /> + <add name="SimpleHandlerFactory-ISAPI-2.0-64" path="*.ashx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" /> + <add name="WebServiceHandlerFactory-ISAPI-2.0-64" path="*.asmx" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" /> + <add name="HttpRemotingHandlerFactory-rem-ISAPI-2.0-64" path="*.rem" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" /> + <add name="HttpRemotingHandlerFactory-soap-ISAPI-2.0-64" path="*.soap" verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" responseBufferLimit="0" /> + <add name="rules-64-ISAPI-2.0" path="*.rules" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" /> + <add name="xoml-64-ISAPI-2.0" path="*.xoml" verb="*" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v2.0.50727\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv2.0,bitness64" /> + <add name="CGI-exe" path="*.exe" verb="*" modules="CgiModule" resourceType="File" requireAccess="Execute" allowPathInfo="true" /> + <add name="SSINC-stm" path="*.stm" verb="GET,HEAD,POST" modules="ServerSideIncludeModule" resourceType="File" /> + <add name="SSINC-shtm" path="*.shtm" verb="GET,HEAD,POST" modules="ServerSideIncludeModule" resourceType="File" /> + <add name="SSINC-shtml" path="*.shtml" verb="GET,HEAD,POST" modules="ServerSideIncludeModule" resourceType="File" /> + <add name="TRACEVerbHandler" path="*" verb="TRACE" modules="ProtocolSupportModule" requireAccess="None" /> + <add name="OPTIONSVerbHandler" path="*" verb="OPTIONS" modules="ProtocolSupportModule" requireAccess="None" /> + <add name="ExtensionlessUrl-ISAPI-4.0_32bit" path="*." verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness32" responseBufferLimit="0" /> + <add name="ExtensionlessUrlHandler-ISAPI-4.0_64bit" path="*." verb="GET,HEAD,POST,DEBUG" modules="IsapiModule" scriptProcessor="%windir%\Microsoft.NET\Framework64\v4.0.30319\aspnet_isapi.dll" preCondition="classicMode,runtimeVersionv4.0,bitness64" responseBufferLimit="0" /> + <add name="ExtensionlessUrl-Integrated-4.0" path="*." verb="GET,HEAD,POST,DEBUG" type="System.Web.Handlers.TransferRequestHandler" preCondition="integratedMode,runtimeVersionv4.0" responseBufferLimit="0" /> + <add name="StaticFile" path="*" verb="*" modules="StaticFileModule,DefaultDocumentModule,DirectoryListingModule" resourceType="Either" requireAccess="Read" /> + </handlers> + </system.webServer> + </location> +</configuration> diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/LICENSE b/data/web/inc/lib/vendor/robthree/twofactorauth/LICENSE new file mode 100644 index 00000000..75a96312 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2014-2015 Rob Janssen + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/README.md b/data/web/inc/lib/vendor/robthree/twofactorauth/README.md new file mode 100644 index 00000000..d65eb199 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/README.md @@ -0,0 +1,197 @@ +#  PHP library for Two Factor Authentication + +[](https://travis-ci.org/RobThree/TwoFactorAuth/) [](https://packagist.org/packages/robthree/twofactorauth) [](LICENSE) [](https://packagist.org/packages/robthree/twofactorauth) [](http://hhvm.h4cc.de/package/robthree/twofactorauth) [](https://codeclimate.com/github/RobThree/TwoFactorAuth) [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6MB5M2SQLP636 "Keep me off the streets") + +PHP library for [two-factor (or multi-factor) authentication](http://en.wikipedia.org/wiki/Multi-factor_authentication) using [TOTP](http://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) and [QR-codes](http://en.wikipedia.org/wiki/QR_code). Inspired by, based on but most importantly an *improvement* on '[PHPGangsta/GoogleAuthenticator](https://github.com/PHPGangsta/GoogleAuthenticator)'. There's a [.Net implementation](https://github.com/RobThree/TwoFactorAuth.Net) of this library as well. + +<p align="center"> +<img src="https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/multifactorauthforeveryone.png"> +</p> + +## Requirements + +* Tested on PHP 5.3, 5.4, 5.5 and 5.6, 7 and HHVM +* [cURL](http://php.net/manual/en/book.curl.php) when using the provided `GoogleQRCodeProvider` (default), `QRServerProvider` or `QRicketProvider` but you can also provide your own QR-code provider. +* [random_bytes()](http://php.net/manual/en/function.random-bytes.php), [MCrypt](http://php.net/manual/en/book.mcrypt.php), [OpenSSL](http://php.net/manual/en/book.openssl.php) or [Hash](http://php.net/manual/en/book.hash.php) depending on which built-in RNG you use (TwoFactorAuth will try to 'autodetect' and use the best available); however: feel free to provide your own (CS)RNG. + +## Installation + +Run the following command: + +`php composer.phar require robthree/twofactorauth` + +## Quick start + +If you want to hit the ground running then have a look at the [demo](demo/demo.php). It's very simple and easy! + +## Usage + +Here are some code snippets that should help you get started... + +````php +// Create a TwoFactorAuth instance +$tfa = new RobThree\Auth\TwoFactorAuth('My Company'); +```` + +The TwoFactorAuth class constructor accepts 7 parameters (all optional): + +Parameter | Default value | Use +------------------|---------------|-------------------------------------------------- +`$issuer` | `null` | Will be displayed in the app as issuer name +`$digits` | `6` | The number of digits the resulting codes will be +`$period` | `30` | The number of seconds a code will be valid +`$algorithm` | `sha1` | The algorithm used +`$qrcodeprovider` | `null` | QR-code provider (more on this later) +`$rngprovider` | `null` | Random Number Generator provider (more on this later) +`$timeprovider` | `null` | Time provider (more on this later) + +These parameters are all '`write once`'; the class will, for it's lifetime, use these values when generating / calculating codes. The number of digits, the period and algorithm are all set to values Google's Authticator app uses (and supports). You may specify `8` digits, a period of `45` seconds and the `sha256` algorithm but the authenticator app (be it Google's implementation, Authy or any other app) may or may not support these values. Your mileage may vary; keep it on the safe side if you don't control which app your audience uses. + +### Step 1: Set up secret shared key + +When a user wants to setup two-factor auth (or, more correctly, multi-factor auth) you need to create a secret. This will be your **shared secret**. This secret will need to be entered by the user in their app. This can be done manually, in which case you simply display the secret and have the user type it in the app: + +````php +$secret = $tfa->createSecret(); +```` + +The `createSecret()` method accepts two arguments: `$bits` (default: `80`) and `$requirecryptosecure` (default: `true`). The former is the number of bits generated for the shared secret. Make sure this argument is a multiple of 8 and, again, keep in mind that not all combinations may be supported by all apps. Google authenticator seems happy with 80 and 160, the default is set to 80 because that's what most sites (that I know of) currently use; however a value of 160 or higher is recommended (see [RFC 4226 - Algorithm Requirements](https://tools.ietf.org/html/rfc4226#section-4)). The latter is used to ensure that the secret is cryptographically secure; if you don't care very much for cryptographically secure secrets you can specify `false` and use a **non**-cryptographically secure RNG provider. + +````php +// Display shared secret +<p>Please enter the following code in your app: '<?php echo $secret; ?>'</p> +```` + +Another, more user-friendly, way to get the shared secret into the app is to generate a [QR-code](http://en.wikipedia.org/wiki/QR_code) which can be scanned by the app. To generate these QR codes you can use any one of the built-in `QRProvider` classes: + +1. `GoogleQRCodeProvider` (default) +2. `QRServerProvider` +3. `QRicketProvider` + +...or implement your own provider. To implement your own provider all you need to do is implement the `IQRCodeProvider` interface. You can use the built-in providers mentioned before to serve as an example or read the next chapter in this file. The built-in classes all use a 3rd (e.g. external) party (Google, QRServer and QRicket) for the hard work of generating QR-codes (note: each of these services might at some point not be available or impose limitations to the number of codes generated per day, hour etc.). You could, however, easily use a project like [PHP QR Code](http://phpqrcode.sourceforge.net/) (or one of the [many others](https://packagist.org/search/?q=qr)) to generate your QR-codes without depending on external sources. Later on we'll [demonstrate](#qr-code-providers) how to do this. + +The built-in providers all have some provider-specific 'tweaks' you can 'apply'. Some provide support for different colors, others may let you specify the desired image-format etc. What they all have in common is that they return a QR-code as binary blob which, in turn, will be turned into a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) by the `TwoFactorAuth` class. This makes it easy for you to display the image without requiring extra 'roundtrips' from browser to server and vice versa. + +````php +// Display QR code to user +<p>Scan the following image with your app:</p> +<p><img src="<?php echo $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret); ?>"></p> +```` + +When outputting a QR-code you can choose a `$label` for the user (which, when entering a shared secret manually, will have to be chosen by the user). This label may be an empty string or `null`. Also a `$size` may be specified (in pixels, width == height) for which we use a default value of `200`. + +### Step 2: Verify secret shared key + +When the shared secret is added to the app, the app will be ready to start generating codes which 'expire' each '`$period`' number of seconds. To make sure the secret was entered, or scanned, correctly you need to verify this by having the user enter a generated code. To check if the generated code is valid you call the `verifyCode()` method: + +````php +// Verify code +$result = $tfa->verifyCode($_SESSION['secret'], $_POST['verification']); +```` + +`verifyCode()` will return either `true` (the code was valid) or `false` (the code was invalid; no points for you!). You may need to store `$secret` in a `$_SESSION` or other persistent storage between requests. The `verifyCode()` accepts, aside from `$secret` and `$code`, two more parameters. The first being `$discrepancy`. Since TOTP codes are based on time("slices") it is very important that the server (but also client) have a correct date/time. But because the two *may* differ a bit we usually allow a certain amount of leeway. Because generated codes are valid for a specific period (remember the `$period` parameter in the `TwoFactorAuth`'s constructor?) we usually check the period directly before and the period directly after the current time when validating codes. So when the current time is `14:34:21`, which results in a 'current timeslice' of `14:34:00` to `14:34:30` we also calculate/verify the codes for `14:33:30` to `14:34:00` and for `14:34:30` to `14:35:00`. This gives us a 'window' of `14:33:30` to `14:35:00`. The `$discrepancy` parameter specifies how many periods (or: timeslices) we check in either direction of the current time. The default `$discrepancy` of `1` results in (max.) 3 period checks: -1, current and +1 period. A `$discrepancy` of `4` would result in a larger window (or: bigger time difference between client and server) of -4, -3, -2, -1, current, +1, +2, +3 and +4 periods. + +The second parameter `$time` allows you to check a code for a specific point in time. This parameter has no real practical use but can be handy for unittesting etc. The default value, `null`, means: use the current time. + +### Step 3: Store `$secret` with user and we're done! + +Ok, so now the code has been verified and found to be correct. Now we can store the `$secret` with our user in our database (or elsewhere) and whenever the user begins a new session we ask for a code generated by the authentication app of their choice. All we need to do is call `verifyCode()` again with the shared secret and the entered code and we know if the user is legit or not. + +Simple as 1-2-3. + +All we need is 3 methods and a constructor: + +````php +public function __construct( + $issuer = null, + $digits = 6, + $period = 30, + $algorithm = 'sha1', + RobThree\Auth\Providers\Qr\IQRCodeProvider $qrcodeprovider = null, + RobThree\Auth\Providers\Rng\IRNGProvider $rngprovider = null +); +public function createSecret($bits = 80, $requirecryptosecure = true): string; +public function getQRCodeImageAsDataUri($label, $secret, $size = 200): string; +public function verifyCode($secret, $code, $discrepancy = 1, $time = null): bool; +```` + +### QR-code providers + +As mentioned before, this library comes with three 'built-in' QR-code providers. This chapter will touch the subject a bit but most of it should be self-explanatory. The `TwoFactorAuth`-class accepts a `$qrcodeprovider` parameter which lets you specify a built-in or custom QR-code provider. All three built-in providers do a simple HTTP request to retrieve an image using cURL and implement the [`IQRCodeProvider`](lib/Providers/Qr/IQRCodeProvider.php) interface which is all you need to implement to write your own QR-code provider. + +The default provider is the [`GoogleQRCodeProvider`](lib/Providers/Qr/GoogleQRCodeProvider.php) which uses the [Google Chart Tools](https://developers.google.com/chart/infographics/docs/qr_codes) to render QR-codes. Then we have the [`QRServerProvider`](lib/Providers/Qr/QRServerProvider.php) which uses the [goqr.me API](http://goqr.me/api/doc/create-qr-code/) and finally we have the [`QRicketProvider`](lib/Providers/Qr/QRicketProvider.php) which uses the [QRickit API](http://qrickit.com/qrickit_apps/qrickit_api.php). All three inherit from a common (abstract) baseclass named [`BaseHTTPQRCodeProvider`](lib/Providers/Qr/BaseHTTPQRCodeProvider.php) because all three share the same functionality: retrieve an image from a 3rd party over HTTP. All three classes have constructors that allow you to tweak some settings and most, if not all, arguments should speak for themselves. If you're not sure which values are supported, click the links in this paragraph for documentation on the API's that are utilized by these classes. + +If you don't like any of the built-in classes because you don't want to rely on external resources for example or because you're paranoid about sending the TOTP secret to these 3rd parties (which is useless to them since they miss *at least one* other factor in the [MFA process](http://en.wikipedia.org/wiki/Multi-factor_authentication)), feel tree to implement your own. The `IQRCodeProvider` interface couldn't be any simpler. All you need to do is implement 2 methods: + +````php +getMimeType(); +getQRCodeImage($qrtext, $size); +```` + +The `getMimeType()` method should return the [MIME type](http://en.wikipedia.org/wiki/Internet_media_type) of the image that is returned by our implementation of `getQRCodeImage()`. In this example it's simply `image/png`. The `getQRCodeImage()` method is passed two arguments: `$qrtext` and `$size`. The latter, `$size`, is simply the width/height in pixels of the image desired by the caller. The first, `$qrtext` is the text that should be encoded in the QR-code. An example of such a text would be: + +`otpauth://totp/LABEL:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=ISSUER` + +All you need to do is return the QR-code as binary image data and you're done. All parts of the `$qrtext` have been escaped for you (but note: you *may* need to escape the entire `$qrtext` just once more when passing the data to another server as GET-parameter). + +Let's see if we can use [PHP QR Code](http://phpqrcode.sourceforge.net/) to implement our own, custom, no-3rd-parties-allowed-here, provider. We start with downloading the [required (single) file](https://github.com/t0k4rt/phpqrcode/blob/master/phpqrcode.php) and putting it in the directory where `TwoFactorAuth.php` is located as well. Now let's implement the provider: create another file named `myprovider.php` in the `Providers\Qr` directory and paste in this content: + +````php +<?php +require_once '../../phpqrcode.php'; // Yeah, we're gonna need that + +namespace RobThree\Auth\Providers\Qr; + +class MyProvider implements IQRCodeProvider { + public function getMimeType() { + return 'image/png'; // This provider only returns PNG's + } + + public function getQRCodeImage($qrtext, $size) { + ob_start(); // 'Catch' QRCode's output + QRCode::png($qrtext, null, QR_ECLEVEL_L, 3, 4); // We ignore $size and set it to 3 + // since phpqrcode doesn't support + // a size in pixels... + $result = ob_get_contents(); // 'Catch' QRCode's output + ob_end_clean(); // Cleanup + return $result; // Return image + } +} +```` + +That's it. We're done! We've implemented our own provider (with help of PHP QR Code). No more external dependencies, no more unnecessary latencies. Now let's *use* our provider: + +````php +<?php +$mp = new RobThree\Auth\Providers\Qr\MyProvider(); +$tfa = new RobThree\Auth\TwoFactorAuth('My Company', 6, 30, 'sha1', $mp); +$secret = $tfa->createSecret(); +?> +<p><img src="<?php echo $tfa->getQRCodeImageAsDataUri('Bob Ross', $secret); ?>"></p> +```` + +Voilà. Couldn't make it any simpler. + +### RNG providers + +This library also comes with three 'built-in' RNG providers ([Random Number Generator](https://en.wikipedia.org/wiki/Random_number_generation)). The RNG provider generates a number of random bytes and returns these bytes as a string. These values are then used to create the secret. By default (no RNG provider specified) TwoFactorAuth will try to determine the best available RNG provider to use. It will, by default, try to use the [`CSRNGProvider`](lib/Providers/Rng/CSRNGProvider.php) for PHP7+ or the [`MCryptRNGProvider`](lib/Providers/Rng/MCryptRNGProvider.php); if this is not available/supported for any reason it will try to use the [`OpenSSLRNGProvider`](lib/Providers/Rng/OpenSSLRNGProvider.php) and if that is also not available/supported it will try to use the final RNG provider: [`HashRNGProvider`](lib/Providers/Rng/HashRNGProvider.php). Each of these providers use their own method of generating a random sequence of bytes. The first three (`CSRNGProvider`, `OpenSSLRNGProvider` and `MCryptRNGProvider`) return a [cryptographically secure](https://en.wikipedia.org/wiki/Cryptographically_secure_pseudorandom_number_generator) sequence of random bytes whereas the `HashRNGProvider` returns a **non-cryptographically secure** sequence. + +You can easily implement your own `RNGProvider` by simply implementing the `IRNGProvider` interface. Each of the 'built-in' RNG providers have some constructor parameters that allow you to 'tweak' some of the settings to use when creating the random bytes such as which source to use (`MCryptRNGProvider`) or which hashing algorithm (`HashRNGProvider`). I encourage you to have a look at some of the ['built-in' RNG providers](lib/Providers/Rng) for details and the [`IRNGProvider` interface](lib/Providers/Rng/IRNGProvider.php). + +### Time providers + +Another set of providers in this library are the Time Providers; this library provides three 'built-in' ones. The default Time Provider used is the [`LocalMachineTimeProvider`](lib/Providers/Time/LocalMachineTimeProvider.php); this provider simply returns the output of `Time()` and is *highly recommended* as default provider. The [`HttpTimeProvider`](lib/Providers/Time/HttpTimeProvider.php) executes a `HEAD` request against a given webserver (default: google.com) and tries to extract the `Date:`-HTTP header and returns it's date. Other url's/domains can be used by specifying the url in the constructor. The final Time Provider is the [`ConvertUnixTimeDotComTimeProvider`](lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php) which does a HTTP request to `convert-unix-time.com/api` and decodes the `JSON` result to retrieve the time. + +You can easily implement your own `TimeProvider` by simply implementing the `ITimeProvider` interface. + +As to *why* these Time Providers are implemented: it allows the TwoFactorAuth library to ensure the hosts time is correct (or rather: within a margin). You can use the `ensureCorrectTime()` method to ensure the hosts time is correct. By default this method will compare the hosts time (returned by calling `time()` on the `LocalMachineTimeProvider`) to Google's and convert-unix-time.com's current time. You can pass an array of `ITimeProvider`s and specify the `leniency` (second argument) allowed (default: 5 seconds). The method will throw when the TwoFactorAuth's timeprovider (which can be any `ITimeProvider`, see constructor) differs more than the given amount of seconds from any of the given `ITimeProviders`. We advise to call this method sparingly when relying on 3rd parties (which both the `HttpTimeProvider` and `ConvertUnixTimeDotComTimeProvider` do) or, if you need to ensure time is correct on a (very) regular basis to implement an `ITimeProvider` that is more efficient than the 'built-in' ones (like use a GPS signal). The `ensureCorrectTime()` method is mostly to be used to make sure the server is configured correctly. + +## Integrations + +- [CakePHP 3](https://github.com/andrej-griniuk/cakephp-two-factor-auth) + +## License + +Licensed under MIT license. See [LICENSE](https://raw.githubusercontent.com/RobThree/TwoFactorAuth/master/LICENSE) for details. + +[Logo / icon](http://www.iconmay.com/Simple/Travel_and_Tourism_Part_2/luggage_lock_safety_baggage_keys_cylinder_lock_hotel_travel_tourism_luggage_lock_icon_465) under CC0 1.0 Universal (CC0 1.0) Public Domain Dedication ([Archived page](http://riii.nl/tm7ap)) diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.phpproj b/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.phpproj new file mode 100644 index 00000000..7fa2a58e --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.phpproj @@ -0,0 +1,69 @@ +<?xml version="1.0" encoding="utf-8"?> +<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> + <PropertyGroup> + <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> + <Name>TwoFactorAuth</Name> + <ProjectGuid>{e569f53a-a604-4579-91ce-4e35b27da47b}</ProjectGuid> + <RootNamespace>TwoFactorAuth</RootNamespace> + <OutputType>Library</OutputType> + <ProjectTypeGuids>{A0786B88-2ADB-4C21-ABE8-AA2D79766269}</ProjectTypeGuids> + <SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile> + <Server>PHPDev</Server> + <PublishEvent>None</PublishEvent> + <PHPDevAutoPort>True</PHPDevAutoPort> + <PHPDevPort>41315</PHPDevPort> + <PHPDevHostName>localhost</PHPDevHostName> + <IISProjectUrl>http://localhost:41315/</IISProjectUrl> + <Runtime>PHP</Runtime> + <RuntimeVersion>7.0</RuntimeVersion> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)' == 'Debug' "> + <IncludeDebugInformation>true</IncludeDebugInformation> + </PropertyGroup> + <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> + <IncludeDebugInformation>false</IncludeDebugInformation> + </PropertyGroup> + <ItemGroup> + <Compile Include="demo\demo.php" /> + <Compile Include="demo\loader.php" /> + <Compile Include="lib\Providers\Qr\BaseHTTPQRCodeProvider.php" /> + <Compile Include="lib\Providers\Qr\GoogleQRCodeProvider.php" /> + <Compile Include="lib\Providers\Qr\IQRCodeProvider.php" /> + <Compile Include="lib\Providers\Qr\QRException.php" /> + <Compile Include="lib\Providers\Qr\QRicketProvider.php" /> + <Compile Include="lib\Providers\Qr\QRServerProvider.php" /> + <Compile Include="lib\Providers\Rng\CSRNGProvider.php" /> + <Compile Include="lib\Providers\Rng\IRNGProvider.php" /> + <Compile Include="lib\Providers\Rng\MCryptRNGProvider.php" /> + <Compile Include="lib\Providers\Rng\OpenSSLRNGProvider.php" /> + <Compile Include="lib\Providers\Rng\HashRNGProvider.php" /> + <Compile Include="lib\Providers\Rng\RNGException.php" /> + <Compile Include="lib\Providers\Time\ConvertUnixTimeDotComTimeProvider.php" /> + <Compile Include="lib\Providers\Time\HttpTimeProvider.php" /> + <Compile Include="lib\Providers\Time\ITimeProvider.php" /> + <Compile Include="lib\Providers\Time\LocalMachineTimeProvider.php" /> + <Compile Include="lib\Providers\Time\TimeException.php" /> + <Compile Include="lib\TwoFactorAuth.php" /> + <Compile Include=".gitignore" /> + <Compile Include="README.md" /> + <Compile Include="lib\TwoFactorAuthException.php" /> + <Compile Include="tests\TwoFactorAuthTest.php" /> + </ItemGroup> + <ItemGroup> + <Folder Include="lib\" /> + <Folder Include="lib\Providers\" /> + <Folder Include="lib\Providers\Time\" /> + <Folder Include="lib\Providers\Qr\" /> + <Folder Include="lib\Providers\Rng\" /> + <Folder Include="demo\" /> + <Folder Include="tests\" /> + </ItemGroup> + <ItemGroup> + <Content Include=".travis.yml" /> + <Content Include="composer.json" /> + <Content Include="composer.lock" /> + <Content Include="logo.png" /> + <Content Include="multifactorauthforeveryone.png" /> + <Content Include="LICENSE" /> + </ItemGroup> +</Project> \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.sln b/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.sln new file mode 100644 index 00000000..df901f6a --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/TwoFactorAuth.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 2013 +VisualStudioVersion = 12.0.30723.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{A0786B88-2ADB-4C21-ABE8-AA2D79766269}") = "TwoFactorAuth", "TwoFactorAuth.phpproj", "{E569F53A-A604-4579-91CE-4E35B27DA47B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E569F53A-A604-4579-91CE-4E35B27DA47B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E569F53A-A604-4579-91CE-4E35B27DA47B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json new file mode 100644 index 00000000..a4c13758 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.json @@ -0,0 +1,36 @@ +{ + "name": "robthree/twofactorauth", + "description": "Two Factor Authentication", + "version": "1.6", + "type": "library", + "keywords": [ "Authentication", "Two Factor Authentication", "Multi Factor Authentication", "TFA", "MFA", "PHP", "Authenticator", "Authy" ], + "homepage": "https://github.com/RobThree/TwoFactorAuth", + "license": "MIT", + "authors": [ + { + "name": "Rob Janssen", + "homepage": "http://robiii.me", + "role": "Developer" + } + ], + "support": { + "issues": "https://github.com/RobThree/TwoFactorAuth/issues", + "source": "https://github.com/RobThree/TwoFactorAuth" + }, + "require": { + "php": ">=5.3.0" + }, + "require-dev": { + "phpunit/phpunit": "@stable" + }, + "autoload": { + "psr-4": { + "RobThree\\Auth\\": "lib" + } + }, + "autoload-dev": { + "psr-4": { + "RobThree\\Auth\\Test\\": "tests" + } + } +} diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/composer.lock b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.lock new file mode 100644 index 00000000..63df9377 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/composer.lock @@ -0,0 +1,980 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "9647de85f54ba6db237f5ff42ff85a1f", + "packages": [], + "packages-dev": [ + { + "name": "doctrine/instantiator", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/instantiator.git", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", + "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", + "shasum": "" + }, + "require": { + "php": ">=5.3,<8.0-DEV" + }, + "require-dev": { + "athletic/athletic": "~0.1.8", + "ext-pdo": "*", + "ext-phar": "*", + "phpunit/phpunit": "~4.0", + "squizlabs/php_codesniffer": "~2.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Marco Pivetta", + "email": "ocramius@gmail.com", + "homepage": "http://ocramius.github.com/" + } + ], + "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", + "homepage": "https://github.com/doctrine/instantiator", + "keywords": [ + "constructor", + "instantiate" + ], + "time": "2015-06-14T21:17:01+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/d68dbdc53dc358a816f00b300704702b2eaff7b8", + "reference": "d68dbdc53dc358a816f00b300704702b2eaff7b8", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.0" + }, + "suggest": { + "dflydev/markdown": "~1.0", + "erusev/parsedown": "~1.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2015-02-03T12:10:50+00:00" + }, + { + "name": "phpspec/prophecy", + "version": "v1.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6c52c2722f8460122f96f86346600e1077ce22cb", + "reference": "6c52c2722f8460122f96f86346600e1077ce22cb", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": "^5.3|^7.0", + "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", + "sebastian/comparator": "^1.1", + "sebastian/recursion-context": "^1.0|^2.0" + }, + "require-dev": { + "phpspec/phpspec": "^2.0", + "phpunit/phpunit": "^4.8 || ^5.6.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "https://github.com/phpspec/prophecy", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2016-11-21T14:58:47+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "2.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "phpunit/php-file-iterator": "~1.3", + "phpunit/php-text-template": "~1.2", + "phpunit/php-token-stream": "~1.3", + "sebastian/environment": "^1.3.2", + "sebastian/version": "~1.0" + }, + "require-dev": { + "ext-xdebug": ">=2.1.4", + "phpunit/phpunit": "~4" + }, + "suggest": { + "ext-dom": "*", + "ext-xdebug": ">=2.2.1", + "ext-xmlwriter": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "time": "2015-10-06T15:47:00+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "1.4.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "time": "2016-10-03T07:40:28+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "1.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "time": "2015-06-21T13:50:34+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "1.0.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/38e9124049cf1a164f1e4537caf19c99bf1eb260", + "reference": "38e9124049cf1a164f1e4537caf19c99bf1eb260", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4|~5" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "time": "2016-05-12T18:03:57+00:00" + }, + { + "name": "phpunit/php-token-stream", + "version": "1.4.9", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-token-stream.git", + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/3b402f65a4cc90abf6e1104e388b896ce209631b", + "reference": "3b402f65a4cc90abf6e1104e388b896ce209631b", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Wrapper around PHP's tokenizer extension.", + "homepage": "https://github.com/sebastianbergmann/php-token-stream/", + "keywords": [ + "tokenizer" + ], + "time": "2016-11-15T14:06:22+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "4.8.35", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "791b1a67c25af50e230f841ee7a9c6eba507dc87" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/791b1a67c25af50e230f841ee7a9c6eba507dc87", + "reference": "791b1a67c25af50e230f841ee7a9c6eba507dc87", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-spl": "*", + "php": ">=5.3.3", + "phpspec/prophecy": "^1.3.1", + "phpunit/php-code-coverage": "~2.1", + "phpunit/php-file-iterator": "~1.4", + "phpunit/php-text-template": "~1.2", + "phpunit/php-timer": "^1.0.6", + "phpunit/phpunit-mock-objects": "~2.3", + "sebastian/comparator": "~1.2.2", + "sebastian/diff": "~1.2", + "sebastian/environment": "~1.3", + "sebastian/exporter": "~1.2", + "sebastian/global-state": "~1.0", + "sebastian/version": "~1.0", + "symfony/yaml": "~2.1|~3.0" + }, + "suggest": { + "phpunit/php-invoker": "~1.1" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.8.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "time": "2017-02-06T05:18:07+00:00" + }, + { + "name": "phpunit/phpunit-mock-objects", + "version": "2.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", + "shasum": "" + }, + "require": { + "doctrine/instantiator": "^1.0.2", + "php": ">=5.3.3", + "phpunit/php-text-template": "~1.2", + "sebastian/exporter": "~1.2" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "suggest": { + "ext-soap": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sb@sebastian-bergmann.de", + "role": "lead" + } + ], + "description": "Mock Object library for PHPUnit", + "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", + "keywords": [ + "mock", + "xunit" + ], + "time": "2015-10-02T06:51:40+00:00" + }, + { + "name": "sebastian/comparator", + "version": "1.2.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/diff": "~1.2", + "sebastian/exporter": "~1.2 || ~2.0" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "http://www.github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "time": "2017-01-29T09:50:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "1.4.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", + "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.8" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.4-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff" + ], + "time": "2015-12-08T07:14:41+00:00" + }, + { + "name": "sebastian/environment", + "version": "1.3.8", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", + "shasum": "" + }, + "require": { + "php": "^5.3.3 || ^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "http://www.github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "time": "2016-08-18T05:49:44+00:00" + }, + { + "name": "sebastian/exporter", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", + "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3", + "sebastian/recursion-context": "~1.0" + }, + "require-dev": { + "ext-mbstring": "*", + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.3.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "http://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "time": "2016-06-17T09:04:28+00:00" + }, + { + "name": "sebastian/global-state", + "version": "1.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", + "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.2" + }, + "suggest": { + "ext-uopz": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "http://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "time": "2015-10-12T03:26:01+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/913401df809e99e4f47b27cdd781f4a258d58791", + "reference": "913401df809e99e4f47b27cdd781f4a258d58791", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "~4.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "http://www.github.com/sebastianbergmann/recursion-context", + "time": "2015-11-11T19:50:13+00:00" + }, + { + "name": "sebastian/version", + "version": "1.0.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", + "shasum": "" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "time": "2015-06-21T13:59:46+00:00" + }, + { + "name": "symfony/yaml", + "version": "v2.8.17", + "source": { + "type": "git", + "url": "https://github.com/symfony/yaml.git", + "reference": "322a8c2dfbca15ad6b1b27e182899f98ec0e0153" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/yaml/zipball/322a8c2dfbca15ad6b1b27e182899f98ec0e0153", + "reference": "322a8c2dfbca15ad6b1b27e182899f98ec0e0153", + "shasum": "" + }, + "require": { + "php": ">=5.3.9" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.8-dev" + } + }, + "autoload": { + "psr-4": { + "Symfony\\Component\\Yaml\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Symfony Yaml Component", + "homepage": "https://symfony.com", + "time": "2017-01-21T16:40:50+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "phpunit/phpunit": 0 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": ">=5.3.0" + }, + "platform-dev": [] +} diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/demo/demo.php b/data/web/inc/lib/vendor/robthree/twofactorauth/demo/demo.php new file mode 100644 index 00000000..996dd928 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/demo/demo.php @@ -0,0 +1,35 @@ +<!doctype html> +<html> +<head> + <title>Demo</title> +</head> +<body> + <ol> + <?php + require_once 'loader.php'; + Loader::register('../lib','RobThree\\Auth'); + + use \RobThree\Auth\TwoFactorAuth; + + $tfa = new TwoFactorAuth('MyApp'); + + echo '<li>First create a secret and associate it with a user'; + $secret = $tfa->createSecret(160); // Though the default is an 80 bits secret (for backwards compatibility reasons) we recommend creating 160+ bits secrets (see RFC 4226 - Algorithm Requirements) + echo '<li>Next create a QR code and let the user scan it:<br><img src="' . $tfa->getQRCodeImageAsDataUri('My label', $secret) . '"><br>...or display the secret to the user for manual entry: ' . chunk_split($secret, 4, ' '); + $code = $tfa->getCode($secret); + echo '<li>Next, have the user verify the code; at this time the code displayed by a 2FA-app would be: <span style="color:#00c">' . $code . '</span> (but that changes periodically)'; + echo '<li>When the code checks out, 2FA can be / is enabled; store (encrypted?) secret with user and have the user verify a code each time a new session is started.'; + echo '<li>When aforementioned code (' . $code . ') was entered, the result would be: ' . (($tfa->verifyCode($secret, $code) === true) ? '<span style="color:#0c0">OK</span>' : '<span style="color:#c00">FAIL</span>'); + ?> + </ol> + <p>Note: Make sure your server-time is <a href="http://en.wikipedia.org/wiki/Network_Time_Protocol">NTP-synced</a>! Depending on the $discrepancy allowed your time cannot drift too much from the users' time!</p> + <?php + try { + $tfa->ensureCorrectTime(); + echo 'Your hosts time seems to be correct / within margin'; + } catch (RobThree\Auth\TwoFactorAuthException $ex) { + echo '<b>Warning:</b> Your hosts time seems to be off: ' . $ex->getMessage(); + } + ?> +</body> +</html> diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/demo/loader.php b/data/web/inc/lib/vendor/robthree/twofactorauth/demo/loader.php new file mode 100644 index 00000000..208f24d4 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/demo/loader.php @@ -0,0 +1,50 @@ +<?php + +//http://www.leaseweblabs.com/2014/04/psr-0-psr-4-autoloading-classes-php/ +class Loader +{ + protected static $parentPath = null; + protected static $paths = null; + protected static $files = null; + protected static $nsChar = '\\'; + protected static $initialized = false; + + protected static function initialize() + { + if (static::$initialized) return; + static::$initialized = true; + static::$parentPath = __FILE__; + for ($i=substr_count(get_class(), static::$nsChar);$i>=0;$i--) { + static::$parentPath = dirname(static::$parentPath); + } + static::$paths = array(); + static::$files = array(__FILE__); + } + + public static function register($path,$namespace) { + if (!static::$initialized) static::initialize(); + static::$paths[$namespace] = trim($path,DIRECTORY_SEPARATOR); + } + + public static function load($class) { + if (class_exists($class,false)) return; + if (!static::$initialized) static::initialize(); + + foreach (static::$paths as $namespace => $path) { + if (!$namespace || $namespace.static::$nsChar === substr($class, 0, strlen($namespace.static::$nsChar))) { + + $fileName = substr($class,strlen($namespace.static::$nsChar)-1); + $fileName = str_replace(static::$nsChar, DIRECTORY_SEPARATOR, ltrim($fileName,static::$nsChar)); + $fileName = static::$parentPath.DIRECTORY_SEPARATOR.$path.DIRECTORY_SEPARATOR.$fileName.'.php'; + + if (file_exists($fileName)) { + include $fileName; + return true; + } + } + } + return false; + } +} + +spl_autoload_register(array('Loader', 'load')); \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/BaseHTTPQRCodeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/BaseHTTPQRCodeProvider.php new file mode 100644 index 00000000..5cb3adda --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/BaseHTTPQRCodeProvider.php @@ -0,0 +1,27 @@ +<?php + +namespace RobThree\Auth\Providers\Qr; + +abstract class BaseHTTPQRCodeProvider implements IQRCodeProvider +{ + protected $verifyssl; + + protected function getContent($url) + { + $curlhandle = curl_init(); + + curl_setopt_array($curlhandle, array( + CURLOPT_URL => $url, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_DNS_CACHE_TIMEOUT => 10, + CURLOPT_TIMEOUT => 10, + CURLOPT_SSL_VERIFYPEER => $this->verifyssl, + CURLOPT_USERAGENT => 'TwoFactorAuth' + )); + $data = curl_exec($curlhandle); + + curl_close($curlhandle); + return $data; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/GoogleQRCodeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/GoogleQRCodeProvider.php new file mode 100644 index 00000000..19e086b7 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/GoogleQRCodeProvider.php @@ -0,0 +1,39 @@ +<?php + +namespace RobThree\Auth\Providers\Qr; + +// https://developers.google.com/chart/infographics/docs/qr_codes +class GoogleQRCodeProvider extends BaseHTTPQRCodeProvider +{ + public $errorcorrectionlevel; + public $margin; + + function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 1) + { + if (!is_bool($verifyssl)) + throw new \QRException('VerifySSL must be bool'); + + $this->verifyssl = $verifyssl; + + $this->errorcorrectionlevel = $errorcorrectionlevel; + $this->margin = $margin; + } + + public function getMimeType() + { + return 'image/png'; + } + + public function getQRCodeImage($qrtext, $size) + { + return $this->getContent($this->getUrl($qrtext, $size)); + } + + public function getUrl($qrtext, $size) + { + return 'https://chart.googleapis.com/chart?cht=qr' + . '&chs=' . $size . 'x' . $size + . '&chld=' . $this->errorcorrectionlevel . '|' . $this->margin + . '&chl=' . rawurlencode($qrtext); + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/IQRCodeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/IQRCodeProvider.php new file mode 100644 index 00000000..83ed67ba --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/IQRCodeProvider.php @@ -0,0 +1,9 @@ +<?php + +namespace RobThree\Auth\Providers\Qr; + +interface IQRCodeProvider +{ + public function getQRCodeImage($qrtext, $size); + public function getMimeType(); +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRException.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRException.php new file mode 100644 index 00000000..c28e8290 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRException.php @@ -0,0 +1,5 @@ +<?php + +use RobThree\Auth\TwoFactorAuthException; + +class QRException extends TwoFactorAuthException {} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRServerProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRServerProvider.php new file mode 100644 index 00000000..928b87b1 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRServerProvider.php @@ -0,0 +1,71 @@ +<?php + +namespace RobThree\Auth\Providers\Qr; + +// http://goqr.me/api/doc/create-qr-code/ +class QRServerProvider extends BaseHTTPQRCodeProvider +{ + public $errorcorrectionlevel; + public $margin; + public $qzone; + public $bgcolor; + public $color; + public $format; + + function __construct($verifyssl = false, $errorcorrectionlevel = 'L', $margin = 4, $qzone = 1, $bgcolor = 'ffffff', $color = '000000', $format = 'png') + { + if (!is_bool($verifyssl)) + throw new QRException('VerifySSL must be bool'); + + $this->verifyssl = $verifyssl; + + $this->errorcorrectionlevel = $errorcorrectionlevel; + $this->margin = $margin; + $this->qzone = $qzone; + $this->bgcolor = $bgcolor; + $this->color = $color; + $this->format = $format; + } + + public function getMimeType() + { + switch (strtolower($this->format)) + { + case 'png': + return 'image/png'; + case 'gif': + return 'image/gif'; + case 'jpg': + case 'jpeg': + return 'image/jpeg'; + case 'svg': + return 'image/svg+xml'; + case 'eps': + return 'application/postscript'; + } + throw new \QRException(sprintf('Unknown MIME-type: %s', $this->format)); + } + + public function getQRCodeImage($qrtext, $size) + { + return $this->getContent($this->getUrl($qrtext, $size)); + } + + private function decodeColor($value) + { + return vsprintf('%d-%d-%d', sscanf($value, "%02x%02x%02x")); + } + + public function getUrl($qrtext, $size) + { + return 'https://api.qrserver.com/v1/create-qr-code/' + . '?size=' . $size . 'x' . $size + . '&ecc=' . strtoupper($this->errorcorrectionlevel) + . '&margin=' . $this->margin + . '&qzone=' . $this->qzone + . '&bgcolor=' . $this->decodeColor($this->bgcolor) + . '&color=' . $this->decodeColor($this->color) + . '&format=' . strtolower($this->format) + . '&data=' . rawurlencode($qrtext); + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRicketProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRicketProvider.php new file mode 100644 index 00000000..59e27ccd --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Qr/QRicketProvider.php @@ -0,0 +1,54 @@ +<?php + +namespace RobThree\Auth\Providers\Qr; + +// http://qrickit.com/qrickit_apps/qrickit_api.php +class QRicketProvider extends BaseHTTPQRCodeProvider +{ + public $errorcorrectionlevel; + public $margin; + public $qzone; + public $bgcolor; + public $color; + public $format; + + function __construct($errorcorrectionlevel = 'L', $bgcolor = 'ffffff', $color = '000000', $format = 'p') + { + $this->verifyssl = false; + + $this->errorcorrectionlevel = $errorcorrectionlevel; + $this->bgcolor = $bgcolor; + $this->color = $color; + $this->format = $format; + } + + public function getMimeType() + { + switch (strtolower($this->format)) + { + case 'p': + return 'image/png'; + case 'g': + return 'image/gif'; + case 'j': + return 'image/jpeg'; + } + throw new \QRException(sprintf('Unknown MIME-type: %s', $this->format)); + } + + public function getQRCodeImage($qrtext, $size) + { + return $this->getContent($this->getUrl($qrtext, $size)); + } + + public function getUrl($qrtext, $size) + { + return 'http://qrickit.com/api/qr' + . '?qrsize=' . $size + . '&e=' . strtolower($this->errorcorrectionlevel) + . '&bgdcolor=' . $this->bgcolor + . '&fgdcolor=' . $this->color + . '&t=' . strtolower($this->format) + . '&d=' . rawurlencode($qrtext); + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/CSRNGProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/CSRNGProvider.php new file mode 100644 index 00000000..8dba7fc9 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/CSRNGProvider.php @@ -0,0 +1,14 @@ +<?php + +namespace RobThree\Auth\Providers\Rng; + +class CSRNGProvider implements IRNGProvider +{ + public function getRandomBytes($bytecount) { + return random_bytes($bytecount); // PHP7+ + } + + public function isCryptographicallySecure() { + return true; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/HashRNGProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/HashRNGProvider.php new file mode 100644 index 00000000..ca6e8593 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/HashRNGProvider.php @@ -0,0 +1,28 @@ +<?php +namespace RobThree\Auth\Providers\Rng; + +class HashRNGProvider implements IRNGProvider +{ + private $algorithm; + + function __construct($algorithm = 'sha256' ) { + $algos = array_values(hash_algos()); + if (!in_array($algorithm, $algos, true)) + throw new \RNGException('Unsupported algorithm specified'); + $this->algorithm = $algorithm; + } + + public function getRandomBytes($bytecount) { + $result = ''; + $hash = mt_rand(); + for ($i = 0; $i < $bytecount; $i++) { + $hash = hash($this->algorithm, $hash.mt_rand(), true); + $result .= $hash[mt_rand(0, sizeof($hash))]; + } + return $result; + } + + public function isCryptographicallySecure() { + return false; + } +} diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/IRNGProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/IRNGProvider.php new file mode 100644 index 00000000..6be28006 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/IRNGProvider.php @@ -0,0 +1,9 @@ +<?php + +namespace RobThree\Auth\Providers\Rng; + +interface IRNGProvider +{ + public function getRandomBytes($bytecount); + public function isCryptographicallySecure(); +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/MCryptRNGProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/MCryptRNGProvider.php new file mode 100644 index 00000000..1f55fa36 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/MCryptRNGProvider.php @@ -0,0 +1,23 @@ +<?php + +namespace RobThree\Auth\Providers\Rng; + +class MCryptRNGProvider implements IRNGProvider +{ + private $source; + + function __construct($source = MCRYPT_DEV_URANDOM) { + $this->source = $source; + } + + public function getRandomBytes($bytecount) { + $result = mcrypt_create_iv($bytecount, $this->source); + if ($result === false) + throw new \RNGException('mcrypt_create_iv returned an invalid value'); + return $result; + } + + public function isCryptographicallySecure() { + return true; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/OpenSSLRNGProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/OpenSSLRNGProvider.php new file mode 100644 index 00000000..dc66c64a --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/OpenSSLRNGProvider.php @@ -0,0 +1,25 @@ +<?php + +namespace RobThree\Auth\Providers\Rng; + +class OpenSSLRNGProvider implements IRNGProvider +{ + private $requirestrong; + + function __construct($requirestrong = true) { + $this->requirestrong = $requirestrong; + } + + public function getRandomBytes($bytecount) { + $result = openssl_random_pseudo_bytes($bytecount, $crypto_strong); + if ($this->requirestrong && ($crypto_strong === false)) + throw new \RNGException('openssl_random_pseudo_bytes returned non-cryptographically strong value'); + if ($result === false) + throw new \RNGException('openssl_random_pseudo_bytes returned an invalid value'); + return $result; + } + + public function isCryptographicallySecure() { + return $this->requirestrong; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/RNGException.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/RNGException.php new file mode 100644 index 00000000..eb5e913d --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Rng/RNGException.php @@ -0,0 +1,5 @@ +<?php + +use RobThree\Auth\TwoFactorAuthException; + +class RNGException extends TwoFactorAuthException {} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php new file mode 100644 index 00000000..4939f0d4 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php @@ -0,0 +1,15 @@ +<?php + +namespace RobThree\Auth\Providers\Time; + +class ConvertUnixTimeDotComTimeProvider implements ITimeProvider +{ + public function getTime() { + $json = @json_decode( + @file_get_contents('http://www.convert-unix-time.com/api?timestamp=now') + ); + if ($json === null || !is_int($json->timestamp)) + throw new \TimeException('Unable to retrieve time from convert-unix-time.com'); + return $json->timestamp; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php new file mode 100644 index 00000000..c761bd97 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/HttpTimeProvider.php @@ -0,0 +1,53 @@ +<?php + +namespace RobThree\Auth\Providers\Time; + +/** + * Takes the time from any webserver by doing a HEAD request on the specified URL and extracting the 'Date:' header + */ +class HttpTimeProvider implements ITimeProvider +{ + public $url; + public $options; + public $expectedtimeformat; + + function __construct($url = 'https://google.com', $expectedtimeformat = 'D, d M Y H:i:s O+', array $options = null) + { + $this->url = $url; + $this->expectedtimeformat = $expectedtimeformat; + $this->options = $options; + if ($this->options === null) { + $this->options = array( + 'http' => array( + 'method' => 'HEAD', + 'follow_location' => false, + 'ignore_errors' => true, + 'max_redirects' => 0, + 'request_fulluri' => true, + 'header' => array( + 'Connection: close', + 'User-agent: TwoFactorAuth HttpTimeProvider (https://github.com/RobThree/TwoFactorAuth)' + ) + ) + ); + } + } + + public function getTime() { + try { + $context = stream_context_create($this->options); + $fd = fopen($this->url, 'rb', false, $context); + $headers = stream_get_meta_data($fd); + fclose($fd); + + foreach ($headers['wrapper_data'] as $h) { + if (strcasecmp(substr($h, 0, 5), 'Date:') === 0) + return \DateTime::createFromFormat($this->expectedtimeformat, trim(substr($h,5)))->getTimestamp(); + } + throw new \TimeException(sprintf('Unable to retrieve time from %s (Invalid or no "Date:" header found)', $this->url)); + } + catch (Exception $ex) { + throw new \TimeException(sprintf('Unable to retrieve time from %s (%s)', $this->url, $ex->getMessage())); + } + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ITimeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ITimeProvider.php new file mode 100644 index 00000000..a3b87a20 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/ITimeProvider.php @@ -0,0 +1,8 @@ +<?php + +namespace RobThree\Auth\Providers\Time; + +interface ITimeProvider +{ + public function getTime(); +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/LocalMachineTimeProvider.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/LocalMachineTimeProvider.php new file mode 100644 index 00000000..572cedc7 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/LocalMachineTimeProvider.php @@ -0,0 +1,9 @@ +<?php + +namespace RobThree\Auth\Providers\Time; + +class LocalMachineTimeProvider implements ITimeProvider { + public function getTime() { + return time(); + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/TimeException.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/TimeException.php new file mode 100644 index 00000000..8de544d0 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/Providers/Time/TimeException.php @@ -0,0 +1,5 @@ +<?php + +use RobThree\Auth\TwoFactorAuthException; + +class TimeException extends TwoFactorAuthException {} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuth.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuth.php new file mode 100644 index 00000000..e6a1fa92 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuth.php @@ -0,0 +1,249 @@ +<?php +namespace RobThree\Auth; + +use RobThree\Auth\Providers\Qr\IQRCodeProvider; +use RobThree\Auth\Providers\Rng\IRNGProvider; +use RobThree\Auth\Providers\Time\ITimeProvider; + +// Based on / inspired by: https://github.com/PHPGangsta/GoogleAuthenticator +// Algorithms, digits, period etc. explained: https://github.com/google/google-authenticator/wiki/Key-Uri-Format +class TwoFactorAuth +{ + private $algorithm; + private $period; + private $digits; + private $issuer; + private $qrcodeprovider = null; + private $rngprovider = null; + private $timeprovider = null; + private static $_base32dict = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567='; + private static $_base32; + private static $_base32lookup = array(); + private static $_supportedalgos = array('sha1', 'sha256', 'sha512', 'md5'); + + function __construct($issuer = null, $digits = 6, $period = 30, $algorithm = 'sha1', IQRCodeProvider $qrcodeprovider = null, IRNGProvider $rngprovider = null, ITimeProvider $timeprovider = null) + { + $this->issuer = $issuer; + if (!is_int($digits) || $digits <= 0) + throw new TwoFactorAuthException('Digits must be int > 0'); + $this->digits = $digits; + + if (!is_int($period) || $period <= 0) + throw new TwoFactorAuthException('Period must be int > 0'); + $this->period = $period; + + $algorithm = strtolower(trim($algorithm)); + if (!in_array($algorithm, self::$_supportedalgos)) + throw new TwoFactorAuthException('Unsupported algorithm: ' . $algorithm); + $this->algorithm = $algorithm; + $this->qrcodeprovider = $qrcodeprovider; + $this->rngprovider = $rngprovider; + $this->timeprovider = $timeprovider; + + self::$_base32 = str_split(self::$_base32dict); + self::$_base32lookup = array_flip(self::$_base32); + } + + /** + * Create a new secret + */ + public function createSecret($bits = 80, $requirecryptosecure = true) + { + $secret = ''; + $bytes = ceil($bits / 5); //We use 5 bits of each byte (since we have a 32-character 'alphabet' / BASE32) + $rngprovider = $this->getRngprovider(); + if ($requirecryptosecure && !$rngprovider->isCryptographicallySecure()) + throw new TwoFactorAuthException('RNG provider is not cryptographically secure'); + $rnd = $rngprovider->getRandomBytes($bytes); + for ($i = 0; $i < $bytes; $i++) + $secret .= self::$_base32[ord($rnd[$i]) & 31]; //Mask out left 3 bits for 0-31 values + return $secret; + } + + /** + * Calculate the code with given secret and point in time + */ + public function getCode($secret, $time = null) + { + $secretkey = $this->base32Decode($secret); + + $timestamp = "\0\0\0\0" . pack('N*', $this->getTimeSlice($this->getTime($time))); // Pack time into binary string + $hashhmac = hash_hmac($this->algorithm, $timestamp, $secretkey, true); // Hash it with users secret key + $hashpart = substr($hashhmac, ord(substr($hashhmac, -1)) & 0x0F, 4); // Use last nibble of result as index/offset and grab 4 bytes of the result + $value = unpack('N', $hashpart); // Unpack binary value + $value = $value[1] & 0x7FFFFFFF; // Drop MSB, keep only 31 bits + + return str_pad($value % pow(10, $this->digits), $this->digits, '0', STR_PAD_LEFT); + } + + /** + * Check if the code is correct. This will accept codes starting from ($discrepancy * $period) sec ago to ($discrepancy * period) sec from now + */ + public function verifyCode($secret, $code, $discrepancy = 1, $time = null) + { + $result = false; + $timetamp = $this->getTime($time); + + // To keep safe from timing-attachs we iterate *all* possible codes even though we already may have verified a code is correct + for ($i = -$discrepancy; $i <= $discrepancy; $i++) + $result |= $this->codeEquals($this->getCode($secret, $timetamp + ($i * $this->period)), $code); + + return (bool)$result; + } + + /** + * Timing-attack safe comparison of 2 codes (see http://blog.ircmaxell.com/2014/11/its-all-about-time.html) + */ + private function codeEquals($safe, $user) { + if (function_exists('hash_equals')) { + return hash_equals($safe, $user); + } + // In general, it's not possible to prevent length leaks. So it's OK to leak the length. The important part is that + // we don't leak information about the difference of the two strings. + if (strlen($safe)===strlen($user)) { + $result = 0; + for ($i = 0; $i < strlen($safe); $i++) + $result |= (ord($safe[$i]) ^ ord($user[$i])); + return $result === 0; + } + return false; + } + + /** + * Get data-uri of QRCode + */ + public function getQRCodeImageAsDataUri($label, $secret, $size = 200) + { + if (!is_int($size) || $size <= 0) + throw new TwoFactorAuthException('Size must be int > 0'); + + $qrcodeprovider = $this->getQrCodeProvider(); + return 'data:' + . $qrcodeprovider->getMimeType() + . ';base64,' + . base64_encode($qrcodeprovider->getQRCodeImage($this->getQRText($label, $secret), $size)); + } + + /** + * Compare default timeprovider with specified timeproviders and ensure the time is within the specified number of seconds (leniency) + */ + public function ensureCorrectTime(array $timeproviders = null, $leniency = 5) + { + if ($timeproviders != null && !is_array($timeproviders)) + throw new TwoFactorAuthException('No timeproviders specified'); + + if ($timeproviders == null) + $timeproviders = array( + new Providers\Time\ConvertUnixTimeDotComTimeProvider(), + new Providers\Time\HttpTimeProvider() + ); + + // Get default time provider + $timeprovider = $this->getTimeProvider(); + + // Iterate specified time providers + foreach ($timeproviders as $t) { + if (!($t instanceof ITimeProvider)) + throw new TwoFactorAuthException('Object does not implement ITimeProvider'); + + // Get time from default time provider and compare to specific time provider and throw if time difference is more than specified number of seconds leniency + if (abs($timeprovider->getTime() - $t->getTime()) > $leniency) + throw new TwoFactorAuthException(sprintf('Time for timeprovider is off by more than %d seconds when compared to %s', $leniency, get_class($t))); + } + } + + private function getTime($time) + { + return ($time === null) ? $this->getTimeProvider()->getTime() : $time; + } + + private function getTimeSlice($time = null, $offset = 0) + { + return (int)floor($time / $this->period) + ($offset * $this->period); + } + + /** + * Builds a string to be encoded in a QR code + */ + public function getQRText($label, $secret) + { + return 'otpauth://totp/' . rawurlencode($label) + . '?secret=' . rawurlencode($secret) + . '&issuer=' . rawurlencode($this->issuer) + . '&period=' . intval($this->period) + . '&algorithm=' . rawurlencode(strtoupper($this->algorithm)) + . '&digits=' . intval($this->digits); + } + + private function base32Decode($value) + { + if (strlen($value)==0) return ''; + + if (preg_match('/[^'.preg_quote(self::$_base32dict).']/', $value) !== 0) + throw new TwoFactorAuthException('Invalid base32 string'); + + $buffer = ''; + foreach (str_split($value) as $char) + { + if ($char !== '=') + $buffer .= str_pad(decbin(self::$_base32lookup[$char]), 5, 0, STR_PAD_LEFT); + } + $length = strlen($buffer); + $blocks = trim(chunk_split(substr($buffer, 0, $length - ($length % 8)), 8, ' ')); + + $output = ''; + foreach (explode(' ', $blocks) as $block) + $output .= chr(bindec(str_pad($block, 8, 0, STR_PAD_RIGHT))); + return $output; + } + + /** + * @return IQRCodeProvider + * @throws TwoFactorAuthException + */ + public function getQrCodeProvider() + { + // Set default QR Code provider if none was specified + if (null === $this->qrcodeprovider) { + return $this->qrcodeprovider = new Providers\Qr\GoogleQRCodeProvider(); + } + return $this->qrcodeprovider; + } + + /** + * @return IRNGProvider + * @throws TwoFactorAuthException + */ + public function getRngprovider() + { + if (null !== $this->rngprovider) { + return $this->rngprovider; + } + if (function_exists('random_bytes')) { + return $this->rngprovider = new Providers\Rng\CSRNGProvider(); + } + if (function_exists('mcrypt_create_iv')) { + return $this->rngprovider = new Providers\Rng\MCryptRNGProvider(); + } + if (function_exists('openssl_random_pseudo_bytes')) { + return $this->rngprovider = new Providers\Rng\OpenSSLRNGProvider(); + } + if (function_exists('hash')) { + return $this->rngprovider = new Providers\Rng\HashRNGProvider(); + } + throw new TwoFactorAuthException('Unable to find a suited RNGProvider'); + } + + /** + * @return ITimeProvider + * @throws TwoFactorAuthException + */ + public function getTimeProvider() + { + // Set default time provider if none was specified + if (null === $this->timeprovider) { + return $this->timeprovider = new Providers\Time\LocalMachineTimeProvider(); + } + return $this->timeprovider; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuthException.php b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuthException.php new file mode 100644 index 00000000..af51b748 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/lib/TwoFactorAuthException.php @@ -0,0 +1,7 @@ +<?php + +namespace RobThree\Auth; + +use Exception; + +class TwoFactorAuthException extends \Exception {} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/logo.png b/data/web/inc/lib/vendor/robthree/twofactorauth/logo.png new file mode 100644 index 00000000..65af9b2e Binary files /dev/null and b/data/web/inc/lib/vendor/robthree/twofactorauth/logo.png differ diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/multifactorauthforeveryone.png b/data/web/inc/lib/vendor/robthree/twofactorauth/multifactorauthforeveryone.png new file mode 100644 index 00000000..086bd928 Binary files /dev/null and b/data/web/inc/lib/vendor/robthree/twofactorauth/multifactorauthforeveryone.png differ diff --git a/data/web/inc/lib/vendor/robthree/twofactorauth/tests/TwoFactorAuthTest.php b/data/web/inc/lib/vendor/robthree/twofactorauth/tests/TwoFactorAuthTest.php new file mode 100644 index 00000000..587b19b8 --- /dev/null +++ b/data/web/inc/lib/vendor/robthree/twofactorauth/tests/TwoFactorAuthTest.php @@ -0,0 +1,381 @@ +<?php +require_once 'lib/TwoFactorAuth.php'; +require_once 'lib/TwoFactorAuthException.php'; + +require_once 'lib/Providers/Qr/IQRCodeProvider.php'; +require_once 'lib/Providers/Qr/BaseHTTPQRCodeProvider.php'; +require_once 'lib/Providers/Qr/GoogleQRCodeProvider.php'; +require_once 'lib/Providers/Qr/QRException.php'; + +require_once 'lib/Providers/Rng/IRNGProvider.php'; +require_once 'lib/Providers/Rng/RNGException.php'; +require_once 'lib/Providers/Rng/CSRNGProvider.php'; +require_once 'lib/Providers/Rng/MCryptRNGProvider.php'; +require_once 'lib/Providers/Rng/OpenSSLRNGProvider.php'; +require_once 'lib/Providers/Rng/HashRNGProvider.php'; +require_once 'lib/Providers/Rng/RNGException.php'; + +require_once 'lib/Providers/Time/ITimeProvider.php'; +require_once 'lib/Providers/Time/LocalMachineTimeProvider.php'; +require_once 'lib/Providers/Time/HttpTimeProvider.php'; +require_once 'lib/Providers/Time/ConvertUnixTimeDotComTimeProvider.php'; +require_once 'lib/Providers/Time/TimeException.php'; + +use RobThree\Auth\TwoFactorAuth; +use RobThree\Auth\Providers\Qr\IQRCodeProvider; +use RobThree\Auth\Providers\Rng\IRNGProvider; +use RobThree\Auth\Providers\Time\ITimeProvider; + + +class TwoFactorAuthTest extends PHPUnit_Framework_TestCase +{ + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testConstructorThrowsOnInvalidDigits() { + + new TwoFactorAuth('Test', 0); + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testConstructorThrowsOnInvalidPeriod() { + + new TwoFactorAuth('Test', 6, 0); + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testConstructorThrowsOnInvalidAlgorithm() { + + new TwoFactorAuth('Test', 6, 30, 'xxx'); + } + + public function testGetCodeReturnsCorrectResults() { + + $tfa = new TwoFactorAuth('Test'); + $this->assertEquals('543160', $tfa->getCode('VMR466AB62ZBOKHE', 1426847216)); + $this->assertEquals('538532', $tfa->getCode('VMR466AB62ZBOKHE', 0)); + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testCreateSecretThrowsOnInsecureRNGProvider() { + $rng = new TestRNGProvider(); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); + $tfa->createSecret(); + } + + public function testCreateSecretOverrideSecureDoesNotThrowOnInsecureRNG() { + $rng = new TestRNGProvider(); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); + $this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret(80, false)); + } + + public function testCreateSecretDoesNotThrowOnSecureRNGProvider() { + $rng = new TestRNGProvider(true); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); + $this->assertEquals('ABCDEFGHIJKLMNOP', $tfa->createSecret()); + } + + public function testCreateSecretGeneratesDesiredAmountOfEntropy() { + $rng = new TestRNGProvider(true); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, $rng); + $this->assertEquals('A', $tfa->createSecret(5)); + $this->assertEquals('AB', $tfa->createSecret(6)); + $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ', $tfa->createSecret(128)); + $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(160)); + $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567', $tfa->createSecret(320)); + $this->assertEquals('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUVWXYZ234567A', $tfa->createSecret(321)); + } + + public function testEnsureCorrectTimeDoesNotThrowForCorrectTime() { + $tpr1 = new TestTimeProvider(123); + $tpr2 = new TestTimeProvider(128); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1); + $tfa->ensureCorrectTime(array($tpr2)); // 128 - 123 = 5 => within default leniency + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testEnsureCorrectTimeThrowsOnIncorrectTime() { + $tpr1 = new TestTimeProvider(123); + $tpr2 = new TestTimeProvider(124); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', null, null, $tpr1); + $tfa->ensureCorrectTime(array($tpr2), 0); // We force a leniency of 0, 124-123 = 1 so this should throw + } + + + public function testEnsureDefaultTimeProviderReturnsCorrectTime() { + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1'); + $tfa->ensureCorrectTime(array(new TestTimeProvider(time())), 1); // Use a leniency of 1, should the time change between both time() calls + } + + public function testEnsureAllTimeProvidersReturnCorrectTime() { + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1'); + $tfa->ensureCorrectTime(array( + new RobThree\Auth\Providers\Time\ConvertUnixTimeDotComTimeProvider(), + new RobThree\Auth\Providers\Time\HttpTimeProvider(), // Uses google.com by default + new RobThree\Auth\Providers\Time\HttpTimeProvider('https://github.com'), + new RobThree\Auth\Providers\Time\HttpTimeProvider('https://yahoo.com'), + )); + } + + public function testVerifyCodeWorksCorrectly() { + + $tfa = new TwoFactorAuth('Test', 6, 30); + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847190)); + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 29)); //Test discrepancy + $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 + 30)); //Test discrepancy + $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 0, 1426847190 - 1)); //Test discrepancy + + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 0)); //Test discrepancy + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 35)); //Test discrepancy + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 35)); //Test discrepancy + + $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 + 65)); //Test discrepancy + $this->assertEquals(false, $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 1, 1426847205 - 65)); //Test discrepancy + + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 + 65)); //Test discrepancy + $this->assertEquals(true , $tfa->verifyCode('VMR466AB62ZBOKHE', '543160', 2, 1426847205 - 65)); //Test discrepancy + } + + public function testTotpUriIsCorrect() { + $qr = new TestQrProvider(); + + $tfa = new TwoFactorAuth('Test&Issuer', 6, 30, 'sha1', $qr); + $data = $this->DecodeDataUri($tfa->getQRCodeImageAsDataUri('Test&Label', 'VMR466AB62ZBOKHE')); + $this->assertEquals('test/test', $data['mimetype']); + $this->assertEquals('base64', $data['encoding']); + $this->assertEquals('otpauth://totp/Test%26Label?secret=VMR466AB62ZBOKHE&issuer=Test%26Issuer&period=30&algorithm=SHA1&digits=6@200', $data['data']); + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testGetQRCodeImageAsDataUriThrowsOnInvalidSize() { + $qr = new TestQrProvider(); + + $tfa = new TwoFactorAuth('Test', 6, 30, 'sha1', $qr); + $tfa->getQRCodeImageAsDataUri('Test', 'VMR466AB62ZBOKHE', 0); + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testGetCodeThrowsOnInvalidBase32String1() { + $tfa = new TwoFactorAuth('Test'); + $tfa->getCode('FOO1BAR8BAZ9'); //1, 8 & 9 are invalid chars + } + + /** + * @expectedException \RobThree\Auth\TwoFactorAuthException + */ + public function testGetCodeThrowsOnInvalidBase32String2() { + $tfa = new TwoFactorAuth('Test'); + $tfa->getCode('mzxw6==='); //Lowercase + } + + public function testKnownBase32DecodeTestVectors() { + // We usually don't test internals (e.g. privates) but since we rely heavily on base32 decoding and don't want + // to expose this method nor do we want to give people the possibility of implementing / providing their own base32 + // decoding/decoder (as we do with Rng/QR providers for example) we simply test the private base32Decode() method + // with some known testvectors **only** to ensure base32 decoding works correctly following RFC's so there won't + // be any bugs hiding in there. We **could** 'fool' ourselves by calling the public getCode() method (which uses + // base32decode internally) and then make sure getCode's output (in digits) equals expected output since that would + // mean the base32Decode() works as expected but that **could** hide some subtle bug(s) in decoding the base32 string. + + // "In general, you don't want to break any encapsulation for the sake of testing (or as Mom used to say, "don't + // expose your privates!"). Most of the time, you should be able to test a class by exercising its public methods." + // Dave Thomas and Andy Hunt -- "Pragmatic Unit Testing + $tfa = new TwoFactorAuth('Test'); + + $method = new ReflectionMethod('RobThree\Auth\TwoFactorAuth', 'base32Decode'); + $method->setAccessible(true); + + // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12 + $this->assertEquals('', $method->invoke($tfa, '')); + $this->assertEquals('f', $method->invoke($tfa, 'MY======')); + $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ====')); + $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6===')); + $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ=')); + $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB')); + $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI======')); + } + + public function testKnownBase32DecodeUnpaddedTestVectors() { + // See testKnownBase32DecodeTestVectors() for the rationale behind testing the private base32Decode() method. + // This test ensures that strings without the padding-char ('=') are also decoded correctly. + // https://tools.ietf.org/html/rfc4648#page-4: + // "In some circumstances, the use of padding ("=") in base-encoded data is not required or used." + $tfa = new TwoFactorAuth('Test'); + + $method = new ReflectionMethod('RobThree\Auth\TwoFactorAuth', 'base32Decode'); + $method->setAccessible(true); + + // Test vectors from: https://tools.ietf.org/html/rfc4648#page-12 + $this->assertEquals('', $method->invoke($tfa, '')); + $this->assertEquals('f', $method->invoke($tfa, 'MY')); + $this->assertEquals('fo', $method->invoke($tfa, 'MZXQ')); + $this->assertEquals('foo', $method->invoke($tfa, 'MZXW6')); + $this->assertEquals('foob', $method->invoke($tfa, 'MZXW6YQ')); + $this->assertEquals('fooba', $method->invoke($tfa, 'MZXW6YTB')); + $this->assertEquals('foobar', $method->invoke($tfa, 'MZXW6YTBOI')); + } + + + public function testKnownTestVectors_sha1() { + //Known test vectors for SHA1: https://tools.ietf.org/html/rfc6238#page-15 + $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ'; //== base32encode('12345678901234567890') + $tfa = new TwoFactorAuth('Test', 8, 30, 'sha1'); + $this->assertEquals('94287082', $tfa->getCode($secret, 59)); + $this->assertEquals('07081804', $tfa->getCode($secret, 1111111109)); + $this->assertEquals('14050471', $tfa->getCode($secret, 1111111111)); + $this->assertEquals('89005924', $tfa->getCode($secret, 1234567890)); + $this->assertEquals('69279037', $tfa->getCode($secret, 2000000000)); + $this->assertEquals('65353130', $tfa->getCode($secret, 20000000000)); + } + + public function testKnownTestVectors_sha256() { + //Known test vectors for SHA256: https://tools.ietf.org/html/rfc6238#page-15 + $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZA'; //== base32encode('12345678901234567890123456789012') + $tfa = new TwoFactorAuth('Test', 8, 30, 'sha256'); + $this->assertEquals('46119246', $tfa->getCode($secret, 59)); + $this->assertEquals('68084774', $tfa->getCode($secret, 1111111109)); + $this->assertEquals('67062674', $tfa->getCode($secret, 1111111111)); + $this->assertEquals('91819424', $tfa->getCode($secret, 1234567890)); + $this->assertEquals('90698825', $tfa->getCode($secret, 2000000000)); + $this->assertEquals('77737706', $tfa->getCode($secret, 20000000000)); + } + + public function testKnownTestVectors_sha512() { + //Known test vectors for SHA512: https://tools.ietf.org/html/rfc6238#page-15 + $secret = 'GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQGEZDGNA'; //== base32encode('1234567890123456789012345678901234567890123456789012345678901234') + $tfa = new TwoFactorAuth('Test', 8, 30, 'sha512'); + $this->assertEquals('90693936', $tfa->getCode($secret, 59)); + $this->assertEquals('25091201', $tfa->getCode($secret, 1111111109)); + $this->assertEquals('99943326', $tfa->getCode($secret, 1111111111)); + $this->assertEquals('93441116', $tfa->getCode($secret, 1234567890)); + $this->assertEquals('38618901', $tfa->getCode($secret, 2000000000)); + $this->assertEquals('47863826', $tfa->getCode($secret, 20000000000)); + } + + /** + * @requires function random_bytes + */ + public function testCSRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\CSRNGProvider(); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(true, $rng->isCryptographicallySecure()); + } + + /** + * @requires function hash_algos + * @requires function hash + */ + public function testHashRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\HashRNGProvider(); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(false, $rng->isCryptographicallySecure()); + } + + /** + * @requires function mcrypt_create_iv + */ + public function testMCryptRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\MCryptRNGProvider(); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(true, $rng->isCryptographicallySecure()); + } + + /** + * @requires function openssl_random_pseudo_bytes + */ + public function testStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(true); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(true, $rng->isCryptographicallySecure()); + } + + /** + * @requires function openssl_random_pseudo_bytes + */ + public function testNonStrongOpenSSLRNGProvidersReturnExpectedNumberOfBytes() { + $rng = new \RobThree\Auth\Providers\Rng\OpenSSLRNGProvider(false); + foreach ($this->getRngTestLengths() as $l) + $this->assertEquals($l, strlen($rng->getRandomBytes($l))); + $this->assertEquals(false, $rng->isCryptographicallySecure()); + } + + + private function getRngTestLengths() { + return array(1, 16, 32, 256); + } + + private function DecodeDataUri($datauri) { + if (preg_match('/data:(?P<mimetype>[\w\.\-\/]+);(?P<encoding>\w+),(?P<data>.*)/', $datauri, $m) === 1) { + return array( + 'mimetype' => $m['mimetype'], + 'encoding' => $m['encoding'], + 'data' => base64_decode($m['data']) + ); + } + return null; + } +} + +class TestRNGProvider implements IRNGProvider { + private $isSecure; + + function __construct($isSecure = false) { + $this->isSecure = $isSecure; + } + + public function getRandomBytes($bytecount) { + $result = ''; + for ($i=0; $i<$bytecount; $i++) + $result.=chr($i); + return $result; + + } + + public function isCryptographicallySecure() { + return $this->isSecure; + } +} + +class TestQrProvider implements IQRCodeProvider { + public function getQRCodeImage($qrtext, $size) { + return $qrtext . '@' . $size; + } + + public function getMimeType() { + return 'test/test'; + } +} + +class TestTimeProvider implements ITimeProvider { + private $time; + + function __construct($time) { + $this->time = $time; + } + + public function getTime() { + return $this->time; + } +} \ No newline at end of file diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/.gitignore b/data/web/inc/lib/vendor/yubico/u2flib-server/.gitignore new file mode 100644 index 00000000..0d968f1a --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/.gitignore @@ -0,0 +1,7 @@ +composer.lock +vendor/ +.*.swp +php-u2flib-server-*.tar.gz +php-u2flib-server-*.tar.gz.sig +apidocs/ +build/ diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/.travis.yml b/data/web/inc/lib/vendor/yubico/u2flib-server/.travis.yml new file mode 100644 index 00000000..781f2b84 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/.travis.yml @@ -0,0 +1,19 @@ +language: php +sudo: false +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + - hhvm-nightly +after_success: + - test -z $COVERALLS || (composer require satooshi/php-coveralls && vendor/bin/coveralls -v) +matrix: + include: + - php: 5.6 + env: COVERALLS=true + allow_failures: + - php: hhvm + - php: hhvm-nightly diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/BLURB b/data/web/inc/lib/vendor/yubico/u2flib-server/BLURB new file mode 100644 index 00000000..c5797421 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/BLURB @@ -0,0 +1,9 @@ +Author: Yubico +Basename: php-u2flib-server +Homepage: https://developers.yubico.com/php-u2flib-server +License: BSD-2-Clause +Name: Native U2F library in PHP +Project: php-u2flib-server +Summary: Native U2F library in PHP +Yubico-Category: U2F projects +Travis: https://travis-ci.org/Yubico/php-u2flib-server diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/COPYING b/data/web/inc/lib/vendor/yubico/u2flib-server/COPYING new file mode 100644 index 00000000..427c9175 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/COPYING @@ -0,0 +1,26 @@ +Copyright (c) 2014 Yubico AB +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/NEWS b/data/web/inc/lib/vendor/yubico/u2flib-server/NEWS new file mode 100644 index 00000000..0fffd587 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/NEWS @@ -0,0 +1,24 @@ +php-u2flib-server NEWS -- History of user-visible changes. + +* Version 1.0.0 (released 2016-02-19) + ** Give an early error on openssl < 1.0 + ** Support devices with initial counter 0 + ** Fixes to examples + ** Handle errorCode: 0 correctly + +* Version 0.1.0 (released 2015-03-03) + ** Use openssl for all crypto instead of third party extensions. + ** Properly check the request challenge on authenticate. + ** Switch from returning error codes to throwing exceptions. + ** Stop recommending composer for installation. + +* Version 0.0.2 (released 2014-10-24) + ** Refactor the API to return objects instead of encoded objects. + ** Add a second example that uses PDO to store registrations. + ** Add documentation to the API. + ** Check that randomness returned is good. + ** Drop the unneeded mcrypt extension. + ** More tests. + +* Version 0.0.1 (released 2014-10-16) + ** Initial release. diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/README b/data/web/inc/lib/vendor/yubico/u2flib-server/README new file mode 100644 index 00000000..0116a270 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/README @@ -0,0 +1,34 @@ +php-u2flib-server +----------------- + +image:https://travis-ci.org/Yubico/php-u2flib-server.svg?branch=master["Build Status", link="https://travis-ci.org/Yubico/php-u2flib-server"] +image:https://coveralls.io/repos/Yubico/php-u2flib-server/badge.svg?branch=master&service=github["Coverage", link="https://coveralls.io/github/Yubico/php-u2flib-server?branch=master"] +image:https://scrutinizer-ci.com/g/Yubico/php-u2flib-server/badges/quality-score.png?b=master["Scrutinizer Code Quality", link="https://scrutinizer-ci.com/g/Yubico/php-u2flib-server/?branch=master"] + +=== Introduction === + +Serverside U2F library for PHP. Provides functionality for registering +tokens and authentication with said tokens. + +To read more about U2F and how to use a U2F library, visit +link:http://developers.yubico.com/U2F[developers.yubico.com/U2F]. + +=== License === + +The project is licensed under a BSD license. See the file COPYING for +exact wording. For any copyright year range specified as YYYY-ZZZZ in +this package note that the range specifies every single year in that +closed interval. + +=== Dependencies === + +The only dependency is the openssl extension to PHP that has to be enabled. + +A composer.json is included in the distribution to make things simpler for +other project using composer. + +=== Tests === + +To run the test suite link:https://phpunit.de[PHPUnit] is required. To run it, type: + + $ phpunit diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/README.adoc b/data/web/inc/lib/vendor/yubico/u2flib-server/README.adoc new file mode 120000 index 00000000..100b9382 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/README.adoc @@ -0,0 +1 @@ +README \ No newline at end of file diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/apigen.neon b/data/web/inc/lib/vendor/yubico/u2flib-server/apigen.neon new file mode 100644 index 00000000..80d9e744 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/apigen.neon @@ -0,0 +1,12 @@ +destination: apidocs + +source: + - src/u2flib_server + +exclude: "*/tests/*" + +groups: none + +tree: false + +title: php-u2flib-server API diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/composer.json b/data/web/inc/lib/vendor/yubico/u2flib-server/composer.json new file mode 100644 index 00000000..f14a88f6 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/composer.json @@ -0,0 +1,13 @@ +{ + "name":"yubico/u2flib-server", + "description":"Library for U2F implementation", + "homepage":"https://developers.yubico.com/php-u2flib-server", + "license":"BSD-2-Clause", + "require": { + "ext-openssl":"*" + }, + "autoload": { + "classmap": ["src/"] + } +} + diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/do-source-release.sh b/data/web/inc/lib/vendor/yubico/u2flib-server/do-source-release.sh new file mode 100755 index 00000000..3c592ea3 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/do-source-release.sh @@ -0,0 +1,40 @@ +#!/bin/sh + +set -e + +VERSION=$1 +PGP_KEYID=$2 + +if [ "x$PGP_KEYID" = "x" ]; then + echo "try with $0 VERSION PGP_KEYID" + echo "example: $0 0.0.1 B2168C0A" + exit +fi + +if ! head -3 NEWS | grep -q "Version $VERSION .released `date -I`"; then + echo "You need to update date/version in NEWS" + exit +fi + +if [ "x$YUBICO_GITHUB_REPO" = "x" ]; then + echo "you need to define YUBICO_GITHUB_REPO" + exit +fi + +releasename=php-u2flib-server-${VERSION} + +git push +git tag -u ${PGP_KEYID} -m $VERSION $VERSION +git push --tags +tmpdir=`mktemp -d /tmp/release.XXXXXX` +releasedir=${tmpdir}/${releasename} +mkdir -p $releasedir +git archive $VERSION --format=tar | tar -xC $releasedir +git2cl > $releasedir/ChangeLog +cd $releasedir +apigen +cd - +tar -cz --directory=$tmpdir --file=${releasename}.tar.gz $releasename +gpg --detach-sign --default-key $PGP_KEYID ${releasename}.tar.gz +$YUBICO_GITHUB_REPO/publish php-u2flib-server $VERSION ${releasename}.tar.gz* +rm -rf $tmpdir diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/examples/assets/u2f-api.js b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/assets/u2f-api.js new file mode 100644 index 00000000..0f06f50d --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/assets/u2f-api.js @@ -0,0 +1,651 @@ +// Copyright 2014-2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +/** + * @fileoverview The U2F api. + */ + +'use strict'; + +/** Namespace for the U2F api. + * @type {Object} + */ +var u2f = u2f || {}; + +/** + * The U2F extension id + * @type {string} + * @const + */ +u2f.EXTENSION_ID = 'kmendfapggjehodndflmmgagdbamhnfd'; + +/** + * Message types for messsages to/from the extension + * @const + * @enum {string} + */ +u2f.MessageTypes = { + 'U2F_REGISTER_REQUEST': 'u2f_register_request', + 'U2F_SIGN_REQUEST': 'u2f_sign_request', + 'U2F_REGISTER_RESPONSE': 'u2f_register_response', + 'U2F_SIGN_RESPONSE': 'u2f_sign_response' +}; + +/** + * Response status codes + * @const + * @enum {number} + */ +u2f.ErrorCodes = { + 'OK': 0, + 'OTHER_ERROR': 1, + 'BAD_REQUEST': 2, + 'CONFIGURATION_UNSUPPORTED': 3, + 'DEVICE_INELIGIBLE': 4, + 'TIMEOUT': 5 +}; + +/** + * A message type for registration requests + * @typedef {{ + * type: u2f.MessageTypes, + * signRequests: Array<u2f.SignRequest>, + * registerRequests: ?Array<u2f.RegisterRequest>, + * timeoutSeconds: ?number, + * requestId: ?number + * }} + */ +u2f.Request; + +/** + * A message for registration responses + * @typedef {{ + * type: u2f.MessageTypes, + * responseData: (u2f.Error | u2f.RegisterResponse | u2f.SignResponse), + * requestId: ?number + * }} + */ +u2f.Response; + +/** + * An error object for responses + * @typedef {{ + * errorCode: u2f.ErrorCodes, + * errorMessage: ?string + * }} + */ +u2f.Error; + +/** + * Data object for a single sign request. + * @typedef {{ + * version: string, + * challenge: string, + * keyHandle: string, + * appId: string + * }} + */ +u2f.SignRequest; + +/** + * Data object for a sign response. + * @typedef {{ + * keyHandle: string, + * signatureData: string, + * clientData: string + * }} + */ +u2f.SignResponse; + +/** + * Data object for a registration request. + * @typedef {{ + * version: string, + * challenge: string, + * appId: string + * }} + */ +u2f.RegisterRequest; + +/** + * Data object for a registration response. + * @typedef {{ + * registrationData: string, + * clientData: string + * }} + */ +u2f.RegisterResponse; + + +// Low level MessagePort API support + +/** + * Sets up a MessagePort to the U2F extension using the + * available mechanisms. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + */ +u2f.getMessagePort = function(callback) { + if (typeof chrome != 'undefined' && chrome.runtime) { + // The actual message here does not matter, but we need to get a reply + // for the callback to run. Thus, send an empty signature request + // in order to get a failure response. + var msg = { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: [] + }; + chrome.runtime.sendMessage(u2f.EXTENSION_ID, msg, function() { + if (!chrome.runtime.lastError) { + // We are on a whitelisted origin and can talk directly + // with the extension. + u2f.getChromeRuntimePort_(callback); + } else { + // chrome.runtime was available, but we couldn't message + // the extension directly, use iframe + u2f.getIframePort_(callback); + } + }); + } else if (u2f.isAndroidChrome_()) { + u2f.getAuthenticatorPort_(callback); + } else { + // chrome.runtime was not available at all, which is normal + // when this origin doesn't have access to any extensions. + u2f.getIframePort_(callback); + } +}; + +/** + * Detect chrome running on android based on the browser's useragent. + * @private + */ +u2f.isAndroidChrome_ = function() { + var userAgent = navigator.userAgent; + return userAgent.indexOf('Chrome') != -1 && + userAgent.indexOf('Android') != -1; +}; + +/** + * Connects directly to the extension via chrome.runtime.connect + * @param {function(u2f.WrappedChromeRuntimePort_)} callback + * @private + */ +u2f.getChromeRuntimePort_ = function(callback) { + var port = chrome.runtime.connect(u2f.EXTENSION_ID, + {'includeTlsChannelId': true}); + setTimeout(function() { + callback(new u2f.WrappedChromeRuntimePort_(port)); + }, 0); +}; + +/** + * Return a 'port' abstraction to the Authenticator app. + * @param {function(u2f.WrappedAuthenticatorPort_)} callback + * @private + */ +u2f.getAuthenticatorPort_ = function(callback) { + setTimeout(function() { + callback(new u2f.WrappedAuthenticatorPort_()); + }, 0); +}; + +/** + * A wrapper for chrome.runtime.Port that is compatible with MessagePort. + * @param {Port} port + * @constructor + * @private + */ +u2f.WrappedChromeRuntimePort_ = function(port) { + this.port_ = port; +}; + +/** + * Format a return a sign request. + * @param {Array<u2f.SignRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.WrappedChromeRuntimePort_.prototype.formatSignRequest_ = + function(signRequests, timeoutSeconds, reqId) { + return { + type: u2f.MessageTypes.U2F_SIGN_REQUEST, + signRequests: signRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Format a return a register request. + * @param {Array<u2f.SignRequest>} signRequests + * @param {Array<u2f.RegisterRequest>} signRequests + * @param {number} timeoutSeconds + * @param {number} reqId + * @return {Object} + */ +u2f.WrappedChromeRuntimePort_.prototype.formatRegisterRequest_ = + function(signRequests, registerRequests, timeoutSeconds, reqId) { + return { + type: u2f.MessageTypes.U2F_REGISTER_REQUEST, + signRequests: signRequests, + registerRequests: registerRequests, + timeoutSeconds: timeoutSeconds, + requestId: reqId + }; + }; + +/** + * Posts a message on the underlying channel. + * @param {Object} message + */ +u2f.WrappedChromeRuntimePort_.prototype.postMessage = function(message) { + this.port_.postMessage(message); +}; + +/** + * Emulates the HTML 5 addEventListener interface. Works only for the + * onmessage event, which is hooked up to the chrome.runtime.Port.onMessage. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedChromeRuntimePort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message' || name == 'onmessage') { + this.port_.onMessage.addListener(function(message) { + // Emulate a minimal MessageEvent object + handler({'data': message}); + }); + } else { + console.error('WrappedChromeRuntimePort only supports onMessage'); + } + }; + +/** + * Wrap the Authenticator app with a MessagePort interface. + * @constructor + * @private + */ +u2f.WrappedAuthenticatorPort_ = function() { + this.requestId_ = -1; + this.requestObject_ = null; +} + +/** + * Launch the Authenticator intent. + * @param {Object} message + */ +u2f.WrappedAuthenticatorPort_.prototype.postMessage = function(message) { + var intentLocation = /** @type {string} */ (message); + document.location = intentLocation; +}; + +/** + * Emulates the HTML 5 addEventListener interface. + * @param {string} eventName + * @param {function({data: Object})} handler + */ +u2f.WrappedAuthenticatorPort_.prototype.addEventListener = + function(eventName, handler) { + var name = eventName.toLowerCase(); + if (name == 'message') { + var self = this; + /* Register a callback to that executes when + * chrome injects the response. */ + window.addEventListener( + 'message', self.onRequestUpdate_.bind(self, handler), false); + } else { + console.error('WrappedAuthenticatorPort only supports message'); + } + }; + +/** + * Callback invoked when a response is received from the Authenticator. + * @param function({data: Object}) callback + * @param {Object} message message Object + */ +u2f.WrappedAuthenticatorPort_.prototype.onRequestUpdate_ = + function(callback, message) { + var messageObject = JSON.parse(message.data); + var intentUrl = messageObject['intentURL']; + + var errorCode = messageObject['errorCode']; + var responseObject = null; + if (messageObject.hasOwnProperty('data')) { + responseObject = /** @type {Object} */ ( + JSON.parse(messageObject['data'])); + responseObject['requestId'] = this.requestId_; + } + + /* Sign responses from the authenticator do not conform to U2F, + * convert to U2F here. */ + responseObject = this.doResponseFixups_(responseObject); + callback({'data': responseObject}); + }; + +/** + * Fixup the response provided by the Authenticator to conform with + * the U2F spec. + * @param {Object} responseData + * @return {Object} the U2F compliant response object + */ +u2f.WrappedAuthenticatorPort_.prototype.doResponseFixups_ = + function(responseObject) { + if (responseObject.hasOwnProperty('responseData')) { + return responseObject; + } else if (this.requestObject_['type'] != u2f.MessageTypes.U2F_SIGN_REQUEST) { + // Only sign responses require fixups. If this is not a response + // to a sign request, then an internal error has occurred. + return { + 'type': u2f.MessageTypes.U2F_REGISTER_RESPONSE, + 'responseData': { + 'errorCode': u2f.ErrorCodes.OTHER_ERROR, + 'errorMessage': 'Internal error: invalid response from Authenticator' + } + }; + } + + /* Non-conformant sign response, do fixups. */ + var encodedChallengeObject = responseObject['challenge']; + if (typeof encodedChallengeObject !== 'undefined') { + var challengeObject = JSON.parse(atob(encodedChallengeObject)); + var serverChallenge = challengeObject['challenge']; + var challengesList = this.requestObject_['signData']; + var requestChallengeObject = null; + for (var i = 0; i < challengesList.length; i++) { + var challengeObject = challengesList[i]; + if (challengeObject['keyHandle'] == responseObject['keyHandle']) { + requestChallengeObject = challengeObject; + break; + } + } + } + var responseData = { + 'errorCode': responseObject['resultCode'], + 'keyHandle': responseObject['keyHandle'], + 'signatureData': responseObject['signature'], + 'clientData': encodedChallengeObject + }; + return { + 'type': u2f.MessageTypes.U2F_SIGN_RESPONSE, + 'responseData': responseData, + 'requestId': responseObject['requestId'] + } + }; + +/** + * Base URL for intents to Authenticator. + * @const + * @private + */ +u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ = + 'intent:#Intent;action=com.google.android.apps.authenticator.AUTHENTICATE'; + +/** + * Format a return a sign request. + * @param {Array<u2f.SignRequest>} signRequests + * @param {number} timeoutSeconds (ignored for now) + * @param {number} reqId + * @return {string} + */ +u2f.WrappedAuthenticatorPort_.prototype.formatSignRequest_ = + function(signRequests, timeoutSeconds, reqId) { + if (!signRequests || signRequests.length == 0) { + return null; + } + /* TODO(fixme): stash away requestId, as the authenticator app does + * not return it for sign responses. */ + this.requestId_ = reqId; + /* TODO(fixme): stash away the signRequests, to deal with the legacy + * response format returned by the Authenticator app. */ + this.requestObject_ = { + 'type': u2f.MessageTypes.U2F_SIGN_REQUEST, + 'signData': signRequests, + 'requestId': reqId, + 'timeout': timeoutSeconds + }; + + var appId = signRequests[0]['appId']; + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.appId=' + encodeURIComponent(appId) + + ';S.eventId=' + reqId + + ';S.challenges=' + + encodeURIComponent( + JSON.stringify(this.getBrowserDataList_(signRequests))) + ';end'; + return intentUrl; + }; + +/** + * Get the browser data objects from the challenge list + * @param {Array} challenges list of challenges + * @return {Array} list of browser data objects + * @private + */ +u2f.WrappedAuthenticatorPort_ + .prototype.getBrowserDataList_ = function(challenges) { + return challenges + .map(function(challenge) { + var browserData = { + 'typ': 'navigator.id.getAssertion', + 'challenge': challenge['challenge'] + }; + var challengeObject = { + 'challenge' : browserData, + 'keyHandle' : challenge['keyHandle'] + }; + return challengeObject; + }); +}; + +/** + * Format a return a register request. + * @param {Array<u2f.SignRequest>} signRequests + * @param {Array<u2f.RegisterRequest>} enrollChallenges + * @param {number} timeoutSeconds (ignored for now) + * @param {number} reqId + * @return {Object} + */ +u2f.WrappedAuthenticatorPort_.prototype.formatRegisterRequest_ = + function(signRequests, enrollChallenges, timeoutSeconds, reqId) { + if (!enrollChallenges || enrollChallenges.length == 0) { + return null; + } + // Assume the appId is the same for all enroll challenges. + var appId = enrollChallenges[0]['appId']; + var registerRequests = []; + for (var i = 0; i < enrollChallenges.length; i++) { + var registerRequest = { + 'challenge': enrollChallenges[i]['challenge'], + 'version': enrollChallenges[i]['version'] + }; + if (enrollChallenges[i]['appId'] != appId) { + // Only include the appId when it differs from the first appId. + registerRequest['appId'] = enrollChallenges[i]['appId']; + } + registerRequests.push(registerRequest); + } + var registeredKeys = []; + if (signRequests) { + for (i = 0; i < signRequests.length; i++) { + var key = { + 'keyHandle': signRequests[i]['keyHandle'], + 'version': signRequests[i]['version'] + }; + // Only include the appId when it differs from the appId that's + // being registered now. + if (signRequests[i]['appId'] != appId) { + key['appId'] = signRequests[i]['appId']; + } + registeredKeys.push(key); + } + } + var request = { + 'type': u2f.MessageTypes.U2F_REGISTER_REQUEST, + 'appId': appId, + 'registerRequests': registerRequests, + 'registeredKeys': registeredKeys, + 'requestId': reqId, + 'timeoutSeconds': timeoutSeconds + }; + var intentUrl = + u2f.WrappedAuthenticatorPort_.INTENT_URL_BASE_ + + ';S.request=' + encodeURIComponent(JSON.stringify(request)) + + ';end'; + /* TODO(fixme): stash away requestId, this is is not necessary for + * register requests, but here to keep parity with sign. + */ + this.requestId_ = reqId; + return intentUrl; + }; + + +/** + * Sets up an embedded trampoline iframe, sourced from the extension. + * @param {function(MessagePort)} callback + * @private + */ +u2f.getIframePort_ = function(callback) { + // Create the iframe + var iframeOrigin = 'chrome-extension://' + u2f.EXTENSION_ID; + var iframe = document.createElement('iframe'); + iframe.src = iframeOrigin + '/u2f-comms.html'; + iframe.setAttribute('style', 'display:none'); + document.body.appendChild(iframe); + + var channel = new MessageChannel(); + var ready = function(message) { + if (message.data == 'ready') { + channel.port1.removeEventListener('message', ready); + callback(channel.port1); + } else { + console.error('First event on iframe port was not "ready"'); + } + }; + channel.port1.addEventListener('message', ready); + channel.port1.start(); + + iframe.addEventListener('load', function() { + // Deliver the port to the iframe and initialize + iframe.contentWindow.postMessage('init', iframeOrigin, [channel.port2]); + }); +}; + + +// High-level JS API + +/** + * Default extension response timeout in seconds. + * @const + */ +u2f.EXTENSION_TIMEOUT_SEC = 30; + +/** + * A singleton instance for a MessagePort to the extension. + * @type {MessagePort|u2f.WrappedChromeRuntimePort_} + * @private + */ +u2f.port_ = null; + +/** + * Callbacks waiting for a port + * @type {Array<function((MessagePort|u2f.WrappedChromeRuntimePort_))>} + * @private + */ +u2f.waitingForPort_ = []; + +/** + * A counter for requestIds. + * @type {number} + * @private + */ +u2f.reqCounter_ = 0; + +/** + * A map from requestIds to client callbacks + * @type {Object.<number,(function((u2f.Error|u2f.RegisterResponse)) + * |function((u2f.Error|u2f.SignResponse)))>} + * @private + */ +u2f.callbackMap_ = {}; + +/** + * Creates or retrieves the MessagePort singleton to use. + * @param {function((MessagePort|u2f.WrappedChromeRuntimePort_))} callback + * @private + */ +u2f.getPortSingleton_ = function(callback) { + if (u2f.port_) { + callback(u2f.port_); + } else { + if (u2f.waitingForPort_.length == 0) { + u2f.getMessagePort(function(port) { + u2f.port_ = port; + u2f.port_.addEventListener('message', + /** @type {function(Event)} */ (u2f.responseHandler_)); + + // Careful, here be async callbacks. Maybe. + while (u2f.waitingForPort_.length) + u2f.waitingForPort_.shift()(u2f.port_); + }); + } + u2f.waitingForPort_.push(callback); + } +}; + +/** + * Handles response messages from the extension. + * @param {MessageEvent.<u2f.Response>} message + * @private + */ +u2f.responseHandler_ = function(message) { + var response = message.data; + var reqId = response['requestId']; + if (!reqId || !u2f.callbackMap_[reqId]) { + console.error('Unknown or missing requestId in response.'); + return; + } + var cb = u2f.callbackMap_[reqId]; + delete u2f.callbackMap_[reqId]; + cb(response['responseData']); +}; + +/** + * Dispatches an array of sign requests to available U2F tokens. + * @param {Array<u2f.SignRequest>} signRequests + * @param {function((u2f.Error|u2f.SignResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.sign = function(signRequests, callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = port.formatSignRequest_(signRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; + +/** + * Dispatches register requests to available U2F tokens. An array of sign + * requests identifies already registered tokens. + * @param {Array<u2f.RegisterRequest>} registerRequests + * @param {Array<u2f.SignRequest>} signRequests + * @param {function((u2f.Error|u2f.RegisterResponse))} callback + * @param {number=} opt_timeoutSeconds + */ +u2f.register = function(registerRequests, signRequests, + callback, opt_timeoutSeconds) { + u2f.getPortSingleton_(function(port) { + var reqId = ++u2f.reqCounter_; + u2f.callbackMap_[reqId] = callback; + var timeoutSeconds = (typeof opt_timeoutSeconds !== 'undefined' ? + opt_timeoutSeconds : u2f.EXTENSION_TIMEOUT_SEC); + var req = port.formatRegisterRequest_( + signRequests, registerRequests, timeoutSeconds, reqId); + port.postMessage(req); + }); +}; \ No newline at end of file diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/examples/cli/u2f-server.php b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/cli/u2f-server.php new file mode 100755 index 00000000..8acb66a4 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/cli/u2f-server.php @@ -0,0 +1,83 @@ +#!/usr/bin/php +<?php + + /* Copyright (c) 2015 Yubico AB + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This is a basic example of a u2f-server command line that can be used + * with the u2f-host binary to perform regitrations and authentications. + */ + +require_once('../../src/u2flib_server/U2F.php'); + +$options = getopt("rao:R:"); +$mode; +$challenge; +$response; +$result; +$regs; + +if(array_key_exists('r', $options)) { + $mode = "register"; +} elseif(array_key_exists('a', $options)) { + if(!array_key_exists('R', $options)) { + print "a registration must be supplied with -R"; + exit(1); + } + $regs = json_decode('[' . $options['R'] . ']'); + $mode = "authenticate"; +} else { + print "-r or -a must be used\n"; + exit(1); +} +if(!array_key_exists('o', $options)) { + print "origin must be supplied with -o\n"; + exit(1); +} + +$u2f = new u2flib_server\U2F($options['o']); + +if($mode === "register") { + $challenge = $u2f->getRegisterData(); +} elseif($mode === "authenticate") { + $challenge = $u2f->getAuthenticateData($regs); +} + +print json_encode($challenge[0]) . "\n"; +$response = fgets(STDIN); + +if($mode === "register") { + $result = $u2f->doRegister($challenge[0], json_decode($response)); +} elseif($mode === "authenticate") { + $result = $u2f->doAuthenticate($challenge, $regs, json_decode($response)); +} + +print json_encode($result) . "\n"; + +?> diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/examples/localstorage/index.php b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/localstorage/index.php new file mode 100644 index 00000000..d840dd36 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/localstorage/index.php @@ -0,0 +1,186 @@ +<?php +/** + * Copyright (c) 2014 Yubico AB + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This is a minimal example of U2F registration and authentication. + * The data that has to be stored between registration and authentication + * is stored in browser localStorage, so there's nothing real-world + * about this. + */ +require_once('../../src/u2flib_server/U2F.php'); +$scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://"; +$u2f = new u2flib_server\U2F($scheme . $_SERVER['HTTP_HOST']); +?> +<html> +<head> + <title>PHP U2F Demo</title> + + <script src="../assets/u2f-api.js"></script> + + <script> + function addRegistration(reg) { + var existing = localStorage.getItem('u2fregistration'); + var regobj = JSON.parse(reg); + var data = null; + if(existing) { + data = JSON.parse(existing); + if(Array.isArray(data)) { + for (var i = 0; i < data.length; i++) { + if(data[i].keyHandle === regobj.keyHandle) { + data.splice(i,1); + break; + } + } + data.push(regobj); + } else { + data = null; + } + } + if(data == null) { + data = [regobj]; + } + localStorage.setItem('u2fregistration', JSON.stringify(data)); + } + <?php + function fixupArray($data) { + $ret = array(); + $decoded = json_decode($data); + foreach ($decoded as $d) { + $ret[] = json_encode($d); + } + return $ret; + } + if($_SERVER['REQUEST_METHOD'] === 'POST') { + if(isset($_POST['startRegister'])) { + $regs = json_decode($_POST['registrations']) ? : array(); + list($data, $reqs) = $u2f->getRegisterData($regs); + echo "var request = " . json_encode($data) . ";\n"; + echo "var signs = " . json_encode($reqs) . ";\n"; + ?> + setTimeout(function() { + console.log("Register: ", request); + u2f.register([request], signs, function(data) { + var form = document.getElementById('form'); + var reg = document.getElementById('doRegister'); + var req = document.getElementById('request'); + console.log("Register callback", data); + if(data.errorCode && data.errorCode != 0) { + alert("registration failed with errror: " + data.errorCode); + return; + } + reg.value=JSON.stringify(data); + req.value=JSON.stringify(request); + form.submit(); + }); + }, 1000); + <?php + } else if($_POST['doRegister']) { + try { + $data = $u2f->doRegister(json_decode($_POST['request']), json_decode($_POST['doRegister'])); + echo "var registration = '" . json_encode($data) . "';\n"; + ?> + addRegistration(registration); + alert("registration successful!"); + <?php + } catch(u2flib_server\Error $e) { + echo "alert('error:" . $e->getMessage() . "');\n"; + } + } else if(isset($_POST['startAuthenticate'])) { + $regs = json_decode($_POST['registrations']); + $data = $u2f->getAuthenticateData($regs); + echo "var registrations = " . $_POST['registrations'] . ";\n"; + echo "var request = " . json_encode($data) . ";\n"; + ?> + setTimeout(function() { + console.log("sign: ", request); + u2f.sign(request, function(data) { + var form = document.getElementById('form'); + var reg = document.getElementById('doAuthenticate'); + var req = document.getElementById('request'); + var regs = document.getElementById('registrations'); + console.log("Authenticate callback", data); + reg.value=JSON.stringify(data); + req.value=JSON.stringify(request); + regs.value=JSON.stringify(registrations); + form.submit(); + }); + }, 1000); + <?php + } else if($_POST['doAuthenticate']) { + $reqs = json_decode($_POST['request']); + $regs = json_decode($_POST['registrations']); + try { + $data = $u2f->doAuthenticate($reqs, $regs, json_decode($_POST['doAuthenticate'])); + echo "var registration = '" . json_encode($data) . "';\n"; + echo "addRegistration(registration);\n"; + echo "alert('Authentication successful, counter:" . $data->counter . "');\n"; + } catch(u2flib_server\Error $e) { + echo "alert('error:" . $e->getMessage() . "');\n"; + } + } + } + ?> + </script> + +</head> +<body> +<form method="POST" id="form"> + <button name="startRegister" type="submit">Register</button> + <input type="hidden" name="doRegister" id="doRegister"/> + <button name="startAuthenticate" type="submit" id="startAuthenticate">Authenticate</button> + <input type="hidden" name="doAuthenticate" id="doAuthenticate"/> + <input type="hidden" name="request" id="request"/> + <input type="hidden" name="registrations" id="registrations"/> +</form> + +<p> + <span id="registered">0</span> Authenticators currently registered. +</p> + +<script> + var reg = localStorage.getItem('u2fregistration'); + var auth = document.getElementById('startAuthenticate'); + if(reg == null) { + auth.disabled = true; + } else { + var regs = document.getElementById('registrations'); + decoded = JSON.parse(reg); + if(!Array.isArray(decoded)) { + auth.disabled = true; + } else { + regs.value = reg; + console.log("set the registrations to : ", reg); + var regged = document.getElementById('registered'); + regged.innerHTML = decoded.length; + } + } +</script> +</body> +</html> diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/examples/pdo/index.php b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/pdo/index.php new file mode 100644 index 00000000..c04d63e2 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/examples/pdo/index.php @@ -0,0 +1,204 @@ +<?php +/** + * Copyright (c) 2014 Yubico AB + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/** + * This is a simple example using PDO and a sqlite database for storing + * registrations. It supports multiple registrations associated with each user. + */ + +require_once('../../src/u2flib_server/U2F.php'); + +$dbfile = '/var/tmp/u2f-pdo.sqlite'; + +$pdo = new PDO("sqlite:$dbfile"); +$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); +$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); + +$pdo->exec("create table if not exists users (id integer primary key, name varchar(255))"); +$pdo->exec("create table if not exists registrations (id integer primary key, user_id integer, keyHandle varchar(255), publicKey varchar(255), certificate text, counter integer)"); + +$scheme = isset($_SERVER['HTTPS']) ? "https://" : "http://"; +$u2f = new u2flib_server\U2F($scheme . $_SERVER['HTTP_HOST']); + +session_start(); + +function createAndGetUser($name) { + global $pdo; + $sel = $pdo->prepare("select * from users where name = ?"); + $sel->execute(array($name)); + $user = $sel->fetch(); + if(!$user) { + $ins = $pdo->prepare("insert into users (name) values(?)"); + $ins->execute(array($name)); + $sel->execute(array($name)); + $user = $sel->fetch(); + } + return $user; +} + +function getRegs($user_id) { + global $pdo; + $sel = $pdo->prepare("select * from registrations where user_id = ?"); + $sel->execute(array($user_id)); + return $sel->fetchAll(); +} + +function addReg($user_id, $reg) { + global $pdo; + $ins = $pdo->prepare("insert into registrations (user_id, keyHandle, publicKey, certificate, counter) values (?, ?, ?, ?, ?)"); + $ins->execute(array($user_id, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter)); +} + +function updateReg($reg) { + global $pdo; + $upd = $pdo->prepare("update registrations set counter = ? where id = ?"); + $upd->execute(array($reg->counter, $reg->id)); +} + +?> + +<html> +<head> + <title>PHP U2F example</title> + + <script src="../assets/u2f-api.js"></script> + + <script> + <?php + + if($_SERVER['REQUEST_METHOD'] === 'POST') { + if(!$_POST['username']) { + echo "alert('no username provided!');"; + } else if(!isset($_POST['action']) && !isset($_POST['register2']) && !isset($_POST['authenticate2'])) { + echo "alert('no action provided!');"; + } else { + $user = createAndGetUser($_POST['username']); + + if(isset($_POST['action'])) { + switch($_POST['action']): + case 'register': + try { + $data = $u2f->getRegisterData(getRegs($user->id)); + + list($req,$sigs) = $data; + $_SESSION['regReq'] = json_encode($req); + echo "var req = " . json_encode($req) . ";"; + echo "var sigs = " . json_encode($sigs) . ";"; + echo "var username = '" . $user->name . "';"; + ?> + setTimeout(function() { + console.log("Register: ", req); + u2f.register([req], sigs, function(data) { + var form = document.getElementById('form'); + var reg = document.getElementById('register2'); + var user = document.getElementById('username'); + console.log("Register callback", data); + if(data.errorCode && errorCode != 0) { + alert("registration failed with errror: " + data.errorCode); + return; + } + reg.value = JSON.stringify(data); + user.value = username; + form.submit(); + }); + }, 1000); + <?php + } catch( Exception $e ) { + echo "alert('error: " . $e->getMessage() . "');"; + } + + break; + + case 'authenticate': + try { + $reqs = json_encode($u2f->getAuthenticateData(getRegs($user->id))); + + $_SESSION['authReq'] = $reqs; + echo "var req = $reqs;"; + echo "var username = '" . $user->name . "';"; + ?> + setTimeout(function() { + console.log("sign: ", req); + u2f.sign(req, function(data) { + var form = document.getElementById('form'); + var auth = document.getElementById('authenticate2'); + var user = document.getElementById('username'); + console.log("Authenticate callback", data); + auth.value=JSON.stringify(data); + user.value = username; + form.submit(); + }); + }, 1000); + <?php + } catch( Exception $e ) { + echo "alert('error: " . $e->getMessage() . "');"; + } + + break; + + endswitch; + } else if($_POST['register2']) { + try { + $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_POST['register2'])); + addReg($user->id, $reg); + } catch( Exception $e ) { + echo "alert('error: " . $e->getMessage() . "');"; + } finally { + $_SESSION['regReq'] = null; + } + } else if($_POST['authenticate2']) { + try { + $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), getRegs($user->id), json_decode($_POST['authenticate2'])); + updateReg($reg); + echo "alert('success: " . $reg->counter . "');"; + } catch( Exception $e ) { + echo "alert('error: " . $e->getMessage() . "');"; + } finally { + $_SESSION['authReq'] = null; + } + } + } + } + ?> + </script> +</head> +<body> + +<form method="POST" id="form"> + username: <input name="username" id="username"/><br/> + register: <input value="register" name="action" type="radio"/><br/> + authenticate: <input value="authenticate" name="action" type="radio"/><br/> + <input type="hidden" name="register2" id="register2"/> + <input type="hidden" name="authenticate2" id="authenticate2"/> + <button type="submit">Submit!</button> +</form> + +</body> +</html> diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/phpunit.xml b/data/web/inc/lib/vendor/yubico/u2flib-server/phpunit.xml new file mode 100644 index 00000000..603e6935 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/phpunit.xml @@ -0,0 +1,9 @@ +<phpunit + colors="true"> + <testsuite name="tests"> + <directory suffix="test.php">.</directory> + </testsuite> + <logging> + <log type="coverage-clover" target="build/logs/clover.xml"/> + </logging> +</phpunit> diff --git a/data/web/inc/lib/U2F.php b/data/web/inc/lib/vendor/yubico/u2flib-server/src/u2flib_server/U2F.php similarity index 100% rename from data/web/inc/lib/U2F.php rename to data/web/inc/lib/vendor/yubico/u2flib-server/src/u2flib_server/U2F.php diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/tests/certs/yubico-u2f-ca-1.pem b/data/web/inc/lib/vendor/yubico/u2flib-server/tests/certs/yubico-u2f-ca-1.pem new file mode 100644 index 00000000..15a1dc28 --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/tests/certs/yubico-u2f-ca-1.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDHjCCAgagAwIBAgIEG0BT9zANBgkqhkiG9w0BAQsFADAuMSwwKgYDVQQDEyNZ +dWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAw +MDBaGA8yMDUwMDkwNDAwMDAwMFowLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290 +IENBIFNlcmlhbCA0NTcyMDA2MzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQC/jwYuhBVlqaiYWEMsrWFisgJ+PtM91eSrpI4TK7U53mwCIawSDHy8vUmk +5N2KAj9abvT9NP5SMS1hQi3usxoYGonXQgfO6ZXyUA9a+KAkqdFnBnlyugSeCOep +8EdZFfsaRFtMjkwz5Gcz2Py4vIYvCdMHPtwaz0bVuzneueIEz6TnQjE63Rdt2zbw +nebwTG5ZybeWSwbzy+BJ34ZHcUhPAY89yJQXuE0IzMZFcEBbPNRbWECRKgjq//qT +9nmDOFVlSRCt2wiqPSzluwn+v+suQEBsUjTGMEd25tKXXTkNW21wIWbxeSyUoTXw +LvGS6xlwQSgNpk2qXYwf8iXg7VWZAgMBAAGjQjBAMB0GA1UdDgQWBBQgIvz0bNGJ +hjgpToksyKpP9xv9oDAPBgNVHRMECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBBjAN +BgkqhkiG9w0BAQsFAAOCAQEAjvjuOMDSa+JXFCLyBKsycXtBVZsJ4Ue3LbaEsPY4 +MYN/hIQ5ZM5p7EjfcnMG4CtYkNsfNHc0AhBLdq45rnT87q/6O3vUEtNMafbhU6kt +hX7Y+9XFN9NpmYxr+ekVY5xOxi8h9JDIgoMP4VB1uS0aunL1IGqrNooL9mmFnL2k +LVVee6/VR6C5+KSTCMCWppMuJIZII2v9o4dkoZ8Y7QRjQlLfYzd3qGtKbw7xaF1U +sG/5xUb/Btwb2X2g4InpiB/yt/3CpQXpiWX/K4mBvUKiGn05ZsqeY1gx4g0xLBqc +U9psmyPzK+Vsgw2jeRQ5JlKDyqE0hebfC1tvFu0CCrJFcw== +-----END CERTIFICATE----- diff --git a/data/web/inc/lib/vendor/yubico/u2flib-server/tests/u2flib_test.php b/data/web/inc/lib/vendor/yubico/u2flib-server/tests/u2flib_test.php new file mode 100644 index 00000000..56af62bf --- /dev/null +++ b/data/web/inc/lib/vendor/yubico/u2flib-server/tests/u2flib_test.php @@ -0,0 +1,296 @@ +<?php +/** + * Copyright (c) 2014 Yubico AB + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +require_once(__DIR__ . '/../src/u2flib_server/U2F.php'); + +class U2FTest extends \PHPUnit_Framework_TestCase { + /** @var u2flib_server\U2F */ + private $u2f; + + public function setUp() { + $this->u2f = new u2flib_server\U2F("http://demo.example.com"); + } + + public function testGetRegisterData() { + list($reg, $signData) = $this->u2f->getRegisterData(); + $this->assertJsonStringEqualsJsonString(json_encode(array()), json_encode($signData)); + $this->assertEquals('U2F_V2', $reg->version); + $this->assertObjectHasAttribute('challenge', $reg); + $this->assertEquals('http://demo.example.com', $reg->appId); + } + + public function testDoRegister() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9", "errorCode": 0 }'); + $reg = $this->u2f->doRegister($req, $resp); + $this->assertEquals('CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w', $reg->keyHandle); + $this->assertEquals('BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y/yaFORPUe3c=', $reg->publicKey); + $this->assertEquals('MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp/VRZHOwd2NZNzpnB9ePNKvUaWCGK/gN+cynnYFdwJ75iSgMVYb/RnFcdPwnsBzBU68hbhTnu/FvJxWo7rZJ2q7qXpA10eLVXJr4/4oSXEk9I/0IIHqOP98Ck/fAoI5gYI7ygndyqoPJ/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh/h7oKEKamCWk19dJp5jHQmumkHlvQhH/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg/0J+xOb4zl6a1z65nae4OTj7628/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==', $reg->certificate); + $this->assertLessThan(0, $reg->counter); + } + + public function testDoRegisterNoCert() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $reg = $this->u2f->doRegister($req, $resp, false); + $this->assertEquals('CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w', $reg->keyHandle); + $this->assertEquals('BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y/yaFORPUe3c=', $reg->publicKey); + $this->assertEquals('', $reg->certificate); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_ATTESTATION_VERIFICATION + */ + public function testDoRegisterAttestFail() { + $this->u2f = new u2flib_server\U2F("http://demo.example.com", __DIR__ . "/../tests/certs"); + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp, true); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_ATTESTATION_SIGNATURE + */ + public function testDoRegisterFail2() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8NwW=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp, false); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_UNMATCHED_CHALLENGE + */ + public function testDoRegisterFail() { + $req = json_decode('{"version":"U2F_V2","challenge":"YKA0X075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp, false); + } + + public function testDoRegisterAttest() { + $this->u2f = new u2flib_server\U2F("http://demo.example.com", __DIR__ . "/../tests/certs"); + $req = json_decode('{"version":"U2F_V2","challenge":"5CBRhGBb2CXSum71GNREBGft7yz9g1jZO7JTkHGFsVY","appId":"http:\/\/demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQRX1gfcG-ofTlk9rjB9spsIMrmT9ba0DLto5fzk8FDB05ModNU2sWAqoQRemYiUrILQdbNGpN_aHA0_oq8kcd_XQCK-Ut0PWaOtz43t0aAV04U788e-dvpeqLtHxtINjgmutKM8_GJQ7F-3W0dogUjSANuRYRdkkSEHPcVdLSkpyfowggIbMIIBBaADAgECAgRAxBIlMAsGCSqGSIb3DQEBCzAuMSwwKgYDVQQDEyNZdWJpY28gVTJGIFJvb3QgQ0EgU2VyaWFsIDQ1NzIwMDYzMTAgFw0xNDA4MDEwMDAwMDBaGA8yMDUwMDkwNDAwMDAwMFowKjEoMCYGA1UEAwwfWXViaWNvIFUyRiBFRSBTZXJpYWwgMTA4NjU5MTUyNTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABK2iSVV7KGNEdPE-oHGvobNnHVw6ZZ6vB3jNIYB1C4t32OucHzMweHqM5CAMSMDHtfp1vuJYaiQSk7jb6M48WtejEjAQMA4GCisGAQQBgsQKAQEEADALBgkqhkiG9w0BAQsDggEBAVg0BoEHEEp4LJLYPYFACRGS8WZiXkCA8crYLgGnzvfKXwPwyKJlUzYxxv5xoRrl5zjkIUXhZ4mnHZVsnj9EY_VGDuRRzKX7YtxTZpFZn7ej3abjLhckTkkQ_AhUkmP7VuK2AWLgYsS8ejGUqughBsKvh_84uxTAEr5BS-OGg2yi7UIjd8W0nOCc6EN8d_8wCiPOjt2Y_-TKpLLTXKszk4UnWNzRdxBThmBBprJBZbF1VyVRvJm5yRLBpth3G8KMvrt4Nu3Ecoj_Q154IJpWe1Dp1upDFLOG9nWCRQk25Y264k9BDISfqs-wHvUjIo2iDnKl5UVoauTWaT7M6KuEwl4wRAIgYUVjS_yTwJAtF35glSbf9Et-5tJzlHOeAqmbACd6pwsCIE0MkTR5XNQoO4XqDaUZCXmadWu8yU1gfE7AJI9JUUcc", "clientData": "eyAiY2hhbGxlbmdlIjogIjVDQlJoR0JiMkNYU3VtNzFHTlJFQkdmdDd5ejlnMWpaTzdKVGtIR0ZzVlkiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $reg = $this->u2f->doRegister($req, $resp, true); + $this->assertEquals('Ir5S3Q9Zo63Pje3RoBXThTvzx752-l6ou0fG0g2OCa60ozz8YlDsX7dbR2iBSNIA25FhF2SRIQc9xV0tKSnJ-g', $reg->keyHandle); + $this->assertEquals('BFfWB9wb6h9OWT2uMH2ymwgyuZP1trQMu2jl/OTwUMHTkyh01TaxYCqhBF6ZiJSsgtB1s0ak39ocDT+iryRx39c=', $reg->publicKey); + $this->assertEquals('MIICGzCCAQWgAwIBAgIEQMQSJTALBgkqhkiG9w0BAQswLjEsMCoGA1UEAxMjWXViaWNvIFUyRiBSb290IENBIFNlcmlhbCA0NTcyMDA2MzEwIBcNMTQwODAxMDAwMDAwWhgPMjA1MDA5MDQwMDAwMDBaMCoxKDAmBgNVBAMMH1l1YmljbyBVMkYgRUUgU2VyaWFsIDEwODY1OTE1MjUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAAStoklVeyhjRHTxPqBxr6GzZx1cOmWerwd4zSGAdQuLd9jrnB8zMHh6jOQgDEjAx7X6db7iWGokEpO42+jOPFrXoxIwEDAOBgorBgEEAYLECgEBBAAwCwYJKoZIhvcNAQELA4IBAQBYNAaBBxBKeCyS2D2BQAkRkvFmYl5AgPHK2C4Bp873yl8D8MiiZVM2Mcb+caEa5ec45CFF4WeJpx2VbJ4/RGP1Rg7kUcyl+2LcU2aRWZ+3o92m4y4XJE5JEPwIVJJj+1bitgFi4GLEvHoxlKroIQbCr4f/OLsUwBK+QUvjhoNsou1CI3fFtJzgnOhDfHf/MAojzo7dmP/kyqSy01yrM5OFJ1jc0XcQU4ZgQaayQWWxdVclUbyZuckSwabYdxvCjL67eDbtxHKI/0NeeCCaVntQ6dbqQxSzhvZ1gkUJNuWNuuJPQQyEn6rPsB71IyKNog5ypeVFaGrk1mk+zOirhMJe', $reg->certificate); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_PUBKEY_DECODE + */ + public function testDoRegisterBadKeyInCert() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABdsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_PUBKEY_DECODE + */ + public function testDoRegisterBadKey() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQMtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $request of doRegister() method only accepts object. + */ + public function testDoRegisterInvalidRequest() { + $req = 'request'; + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $response of doRegister() method only accepts object. + */ + public function testDoRegisterInvalidResponse() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = 'response'; + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_BAD_UA_RETURNING + */ + public function testDoRegisterUAError() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{"errorCode": "4"}'); + $this->u2f->doRegister($req, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $include_cert of doRegister() method only accepts boolean. + */ + public function testDoRegisterInvalidInclude_cert() { + $req = json_decode('{"version":"U2F_V2","challenge":"yKA0x075tjJ-GE7fKTfnzTOSaNUOWQxRd9TWz5aFOg8","appId":"http://demo.example.com"}'); + $resp = json_decode('{ "registrationData": "BQQtEmhWVgvbh-8GpjsHbj_d5FB9iNoRL8mNEq34-ANufKWUpVdIj6BSB_m3eMoZ3GqnaDy3RA5eWP8mhTkT1Ht3QAk1GsmaPIQgXgvrBkCQoQtMFvmwYPfW5jpRgoMPFxquHS7MTt8lofZkWAK2caHD-YQQdaRBgd22yWIjPuWnHOcwggLiMIHLAgEBMA0GCSqGSIb3DQEBCwUAMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBDQTAeFw0xNDA1MTUxMjU4NTRaFw0xNDA2MTQxMjU4NTRaMB0xGzAZBgNVBAMTEll1YmljbyBVMkYgVGVzdCBFRTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABNsK2_Uhx1zOY9ym4eglBg2U5idUGU-dJK8mGr6tmUQflaNxkQo6IOc-kV4T6L44BXrVeqN-dpCPr-KKlLYw650wDQYJKoZIhvcNAQELBQADggIBAJVAa1Bhfa2Eo7TriA_jMA8togoA2SUE7nL6Z99YUQ8LRwKcPkEpSpOsKYWJLaR6gTIoV3EB76hCiBaWN5HV3-CPyTyNsM2JcILsedPGeHMpMuWrbL1Wn9VFkc7B3Y1k3OmcH1480q9RpYIYr-A35zKedgV3AnvmJKAxVhv9GcVx0_CewHMFTryFuFOe78W8nFajutknarupekDXR4tVcmvj_ihJcST0j_Qggeo4_3wKT98CgjmBgjvKCd3Kqg8n9aSDVWyaOZsVOhZj3Fv5rFu895--D4qiPDETozJIyliH-HugoQpqYJaTX10mnmMdCa6aQeW9CEf-5QmbIP0S4uZAf7pKYTNmDQ5z27DVopqaFw00MIVqQkae_zSPX4dsNeeoTTXrwUGqitLaGap5ol81LKD9JdP3nSUYLfq0vLsHNDyNgb306TfbOenRRVsgQS8tJyLcknSKktWD_Qn7E5vjOXprXPrmdp7g5OPvrbz9QkWa1JTRfo2n2AXV02LPFc-UfR9bWCBEIJBxvmbpmqt0MnBTHWnth2b0CU_KJTDCY3kAPLGbOT8A4KiI73pRW-e9SWTaQXskw3Ei_dHRILM_l9OXsqoYHJ4Dd3tbfvmjoNYggSw4j50l3unI9d1qR5xlBFpW5sLr8gKX4bnY4SR2nyNiOQNLyPc0B0nW502aMEUCIQDTGOX-i_QrffJDY8XvKbPwMuBVrOSO-ayvTnWs_WSuDQIgZ7fMAvD_Ezyy5jg6fQeuOkoJi8V2naCtzV-HTly8Nww=", "clientData": "eyAiY2hhbGxlbmdlIjogInlLQTB4MDc1dGpKLUdFN2ZLVGZuelRPU2FOVU9XUXhSZDlUV3o1YUZPZzgiLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5maW5pc2hFbnJvbGxtZW50IiB9" }'); + $this->u2f->doRegister($req, $resp, 'bar'); + } + + public function testGetAuthenticateData() { + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); + $data = $this->u2f->getAuthenticateData($regs); + $inst = $data[0]; + $this->assertEquals("U2F_V2", $inst->version); + $this->assertObjectHasAttribute("challenge", $inst); + $this->assertEquals('CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w', $inst->keyHandle); + $this->assertEquals('http://demo.example.com', $inst->appId); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $registrations of getAuthenticateData() method only accepts array of object. + */ + public function testGetAuthenticateDataInvalidRegistrations2() { + $regs = array('YubiKey NEO', 'YubiKey Standard'); + $data = $this->u2f->getAuthenticateData($regs); + } + + public function testDoAuthenticate() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w", "errorCode": 0 }'); + $data = $this->u2f->doAuthenticate($reqs, $regs, $resp); + $this->assertEquals(4, $data->counter); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_COUNTER_TOO_LOW + */ + public function testDoAuthenticateCtrFail() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":5}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_AUTHENTICATION_FAILURE + */ + public function testDoAuthenticateFail() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAnG==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_NO_MATCHING_REQUEST + */ + public function testDoAuthenticateWrongReq() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"cTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_NO_MATCHING_REGISTRATION + */ + public function testDoAuthenticateWrongReg() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"cTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg=="}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_PUBKEY_DECODE + */ + public function testDoAuthenticateBadKey() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"bC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $requests of doAuthenticate() method only accepts array of object. + */ + public function testDoAuthenticateInvalidRequests2() { + $reqs = array('YubiKey NEO', 'YubiKey Standard'); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $registrations of doAuthenticate() method only accepts array of object. + */ + public function testDoAuthenticateInvalidRegistrations2() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array('YubiKey NEO', 'YubiKey Standard'); + $resp = json_decode('{ "signatureData": "AQAAAAQwRQIhAI6FSrMD3KUUtkpiP0jpIEakql-HNhwWFngyw553pS1CAiAKLjACPOhxzZXuZsVO8im-HStEcYGC50PKhsGp_SUAng==", "clientData": "eyAiY2hhbGxlbmdlIjogImZFbmM5b1Y3OUVhQmdLNUJvTkVSVTVnUEtNMlhHWVdyejRmVWpnYzBRN2ciLCAib3JpZ2luIjogImh0dHA6XC9cL2RlbW8uZXhhbXBsZS5jb20iLCAidHlwIjogIm5hdmlnYXRvci5pZC5nZXRBc3NlcnRpb24iIH0=", "keyHandle": "CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w" }'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage $response of doAuthenticate() method only accepts object. + */ + public function testDoAuthenticateInvalidResponse() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = 'Response'; + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } + + /** + * @expectedException u2flib_server\Error + * @expectedExceptionCode u2flib_server\ERR_BAD_UA_RETURNING + */ + public function testDoAuthenticateUAError() { + $reqs = array(json_decode('{"version":"U2F_V2","challenge":"fEnc9oV79EaBgK5BoNERU5gPKM2XGYWrz4fUjgc0Q7g","keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","appId":"http://demo.example.com"}')); + $regs = array(json_decode('{"keyHandle":"CTUayZo8hCBeC-sGQJChC0wW-bBg99bmOlGCgw8XGq4dLsxO3yWh9mRYArZxocP5hBB1pEGB3bbJYiM-5acc5w","publicKey":"BC0SaFZWC9uH7wamOwduP93kUH2I2hEvyY0Srfj4A258pZSlV0iPoFIH+bd4yhncaqdoPLdEDl5Y\/yaFORPUe3c=","certificate":"MIIC4jCBywIBATANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgQ0EwHhcNMTQwNTE1MTI1ODU0WhcNMTQwNjE0MTI1ODU0WjAdMRswGQYDVQQDExJZdWJpY28gVTJGIFRlc3QgRUUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATbCtv1IcdczmPcpuHoJQYNlOYnVBlPnSSvJhq+rZlEH5WjcZEKOiDnPpFeE+i+OAV61XqjfnaQj6\/iipS2MOudMA0GCSqGSIb3DQEBCwUAA4ICAQCVQGtQYX2thKO064gP4zAPLaIKANklBO5y+mffWFEPC0cCnD5BKUqTrCmFiS2keoEyKFdxAe+oQogWljeR1d\/gj8k8jbDNiXCC7HnTxnhzKTLlq2y9Vp\/VRZHOwd2NZNzpnB9ePNKvUaWCGK\/gN+cynnYFdwJ75iSgMVYb\/RnFcdPwnsBzBU68hbhTnu\/FvJxWo7rZJ2q7qXpA10eLVXJr4\/4oSXEk9I\/0IIHqOP98Ck\/fAoI5gYI7ygndyqoPJ\/Wkg1VsmjmbFToWY9xb+axbvPefvg+KojwxE6MySMpYh\/h7oKEKamCWk19dJp5jHQmumkHlvQhH\/uUJmyD9EuLmQH+6SmEzZg0Oc9uw1aKamhcNNDCFakJGnv80j1+HbDXnqE0168FBqorS2hmqeaJfNSyg\/SXT950lGC36tLy7BzQ8jYG99Ok32znp0UVbIEEvLSci3JJ0ipLVg\/0J+xOb4zl6a1z65nae4OTj7628\/UJFmtSU0X6Np9gF1dNizxXPlH0fW1ggRCCQcb5m6ZqrdDJwUx1p7Ydm9AlPyiUwwmN5ADyxmzk\/AOCoiO96UVvnvUlk2kF7JMNxIv3R0SCzP5fTl7KqGByeA3d7W375o6DWIIEsOI+dJd7pyPXdakecZQRaVubC6\/ICl+G52OEkdp8jYjkDS8j3NAdJ1udNmg==", "counter":3}')); + $resp = json_decode('{"errorCode": "5"}'); + $this->u2f->doAuthenticate($reqs, $regs, $resp); + } +} + +?> diff --git a/data/web/inc/prerequisites.inc.php b/data/web/inc/prerequisites.inc.php index 1959ae18..b39f755a 100644 --- a/data/web/inc/prerequisites.inc.php +++ b/data/web/inc/prerequisites.inc.php @@ -24,23 +24,34 @@ if (file_exists('./inc/vars.local.inc.php')) { // Yubi OTP API require_once 'inc/lib/Yubico.php'; -// U2F API -require_once 'inc/lib/U2F.php'; +// U2F API + T/HOTP API +require_once 'inc/lib/vendor/autoload.php'; $u2f = new u2flib_server\U2F('https://' . $_SERVER['SERVER_NAME']); +$tfa = new RobThree\Auth\TwoFactorAuth('mailcow UI'); // PDO -$dsn = "$database_type:host=$database_host;dbname=$database_name"; +// Calculate offset +$now = new DateTime(); +$mins = $now->getOffset() / 60; +$sgn = ($mins < 0 ? -1 : 1); +$mins = abs($mins); +$hrs = floor($mins / 60); +$mins -= $hrs * 60; +$offset = sprintf('%+d:%02d', $hrs*$sgn, $mins); + +$dsn = $database_type . ":host=" . $database_host . ";dbname=" . $database_name; $opt = [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_EMULATE_PREPARES => false, + PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '" . $offset . "'", ]; try { $pdo = new PDO($dsn, $database_user, $database_pass, $opt); } catch (PDOException $e) { ?> -<center style='font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;'>🐮 Connection failed, database may be in warm-up state, please try again later.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center> +<center style='font-family: "Lucida Sans Unicode", "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;'>?? Connection failed, database may be in warm-up state, please try again later.<br /><br />The following error was reported:<br/> <?=$e->getMessage();?></center> <?php exit; } @@ -106,5 +117,6 @@ if (isset($_GET['lang'])) { require_once 'lang/lang.en.php'; include 'lang/lang.'.$_SESSION['mailcow_locale'].'.php'; require_once 'inc/functions.inc.php'; +require_once 'inc/init_db.inc.php'; require_once 'inc/triggers.inc.php'; -(!isset($_SESSION['mailcow_cc_username'])) ? init_db_schema() : null; +init_db_schema(); diff --git a/data/web/inc/spf.inc.php b/data/web/inc/spf.inc.php new file mode 100644 index 00000000..0e584b40 --- /dev/null +++ b/data/web/inc/spf.inc.php @@ -0,0 +1,127 @@ +<?php +function get_spf_allowed_hosts($domain) +{ + $hosts = array(); + + $records = dns_get_record($domain, DNS_TXT); + foreach ($records as $record) + { + $txt = explode(' ', $record['entries'][0]); + if (array_shift($txt) != 'v=spf1') // only handle SPF records + continue; + + foreach ($txt as $mech) + { + $qual = substr($mech, 0, 1); + if ($qual == '-' || $qual == '~') // only handle pass or neutral records + continue(2); + + if ($qual == '+' || $qual == '?') + $mech = substr($mech, 1); // remove the qualifier + + if (strpos($mech, '=') !== FALSE) // handle a modifier + { + $mod = explode('=', $mech); + if ($mod[0] == 'redirect') // handle a redirect + { + $hosts = get_spf_allowed_hosts($mod[1]); + return $hosts; + } + } + else + { + unset($cidr); + if (strpos($mech, ':') !== FALSE) // handle a domain specification + { + $split = explode(':', $mech); + $mech = array_shift($split); + $domain = implode(':', $split); + if (strpos($domain, '/') !== FALSE) // remove CIDR specification + { + $split = explode('/', $domain); + $domain = $split[0]; + $cidr = $split[1]; + } + } + + $new_hosts = array(); + if ($mech == 'include') // handle an inclusion + { + $new_hosts = get_spf_allowed_hosts($domain); + } + elseif ($mech == 'a') // handle a mechanism + { + $new_hosts = get_a_hosts($domain); + } + elseif ($mech == 'mx') // handle mx mechanism + { + $new_hosts = get_mx_hosts($domain); + } + elseif ($mech == 'ip4' || $mech == 'ip6') // handle ip mechanism + { + $new_hosts = array($domain); + } + + if (isset($cidr)) // add CIDR specification if present + { + foreach ($new_hosts as &$host) + { + $host .= '/' . $cidr; + } + unset($host); + } + + $hosts = array_unique(array_merge($hosts,$new_hosts), SORT_REGULAR); + } + } + } + + return $hosts; +} + +function get_mx_hosts($domain) +{ + $hosts = array(); + + $mx_records = dns_get_record($domain, DNS_MX); + foreach ($mx_records as $mx_record) + { + $new_hosts = get_a_hosts($mx_record['target']); + $hosts = array_unique(array_merge($hosts,$new_hosts), SORT_REGULAR); + } + + return $hosts; +} + +function get_a_hosts($domain) +{ + $hosts = array(); + + $a_records = dns_get_record($domain, DNS_A); + foreach ($a_records as $a_record) + { + $hosts[] = $a_record['ip']; + } + $a_records = dns_get_record($domain, DNS_AAAA); + foreach ($a_records as $a_record) + { + $hosts[] = $a_record['ipv6']; + } + + return $hosts; +} + +function get_outgoing_hosts_best_guess($domain) +{ + // try the SPF record to get hosts that are allowed to send outgoing mails for this domain + $hosts = get_spf_allowed_hosts($domain); + if ($hosts) return $hosts; + + // try the MX record to get mail servers for this domain + $hosts = get_mx_hosts($domain); + if ($hosts) return $hosts; + + // fall back to the A record to get the host name for this domain + return get_a_hosts($domain); +} +?> \ No newline at end of file diff --git a/data/web/inc/tfa_modals.php b/data/web/inc/tfa_modals.php index d6956724..f77ee702 100644 --- a/data/web/inc/tfa_modals.php +++ b/data/web/inc/tfa_modals.php @@ -1,3 +1,6 @@ +<?php +if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "admin" || $_SESSION['mailcow_cc_role'] == "domainadmin")): +?> <div class="modal fade" id="YubiOTPModal" tabindex="-1" role="dialog" aria-labelledby="YubiOTPModalLabel"> <div class="modal-dialog" role="document"> <div class="modal-content"> @@ -57,6 +60,44 @@ </div> </div> +<div class="modal fade" id="TOTPModal" tabindex="-1" role="dialog" aria-labelledby="TOTPModalLabel"> + <div class="modal-dialog" role="document"> + <div class="modal-content"> + <div class="modal-header"><b><?=$lang['tfa']['totp'];?></b></div> + <div class="modal-body"> + <form role="form" method="post"> + <div class="form-group"> + <input type="text" class="form-control" name="key_id" id="key_id" placeholder="<?=$lang['tfa']['key_id_totp'];?>" autocomplete="off" required> + </div> + <div class="form-group"> + <input type="password" class="form-control" name="confirm_password" id="confirm_password" placeholder="<?=$lang['user']['password_now'];?>" autocomplete="off" required> + </div> + <hr> + <?php + $totp_secret = $tfa->createSecret(); + ?> + <input type="hidden" value="<?=$totp_secret;?>" name="totp_secret" id="totp_secret"/> + <input type="hidden" name="tfa_method" value="totp"> + <ol> + <li> + <p><?=$lang['tfa']['scan_qr_code'];?></p> + <img src="<?=$tfa->getQRCodeImageAsDataUri($_SESSION['mailcow_cc_username'], $totp_secret);?>"> + <p class="help-block"><?=$lang['tfa']['enter_qr_code'];?>:<br /> + <code><?=$totp_secret;?></code> + </p> + </li> + <li> + <p><?=$lang['tfa']['confirm_totp_token'];?>:</p> + <p><input type="number" style="width:33%" class="form-control" name="totp_confirm_token" id="totp_confirm_token" autocomplete="off" required></p> + <p><button class="btn btn-default" type="submit" name="set_tfa"><?=$lang['tfa']['confirm'];?></button></p> + </li> + </ol> + </form> + </div> + </div> + </div> +</div> + <div class="modal fade" id="DisableTFAModal" tabindex="-1" role="dialog" aria-labelledby="DisableTFAModalLabel"> <div class="modal-dialog" role="document"> <div class="modal-content"> @@ -77,6 +118,7 @@ </div> <?php +endif; if (isset($_SESSION['pending_tfa_method'])): $tfa_method = $_SESSION['pending_tfa_method']; ?> @@ -114,8 +156,17 @@ if (isset($_SESSION['pending_tfa_method'])): break; case "totp": ?> - <div class="empty"></div> - <?php + <form role="form" method="post"> + <div class="form-group"> + <div class="input-group"> + <span class="input-group-addon" id="tfa-addon"><span class="glyphicon glyphicon-lock" aria-hidden="true"></span></span> + <input type="number" min="000000" max="999999" name="token" id="token" class="form-control" placeholder="123456" aria-describedby="tfa-addon"> + <input type="hidden" name="tfa_method" value="totp"> + </div> + </div> + <button class="btn btn-sm btn-default" type="submit" name="verify_tfa_login"><?=$lang['login']['login'];?></button> + </form> + <?php break; case "hotp": ?> diff --git a/data/web/inc/triggers.inc.php b/data/web/inc/triggers.inc.php index 2895420d..5d8e7b7d 100644 --- a/data/web/inc/triggers.inc.php +++ b/data/web/inc/triggers.inc.php @@ -72,6 +72,12 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "admi if (isset($_POST["delete_domain_admin"])) { delete_domain_admin($_POST); } + if (isset($_POST["add_forwarding_host"])) { + add_forwarding_host($_POST); + } + if (isset($_POST["delete_forwarding_host"])) { + delete_forwarding_host($_POST); + } } if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == "user") { if (isset($_POST["edit_user_account"])) { diff --git a/data/web/index.php b/data/web/index.php index 2e50e4b4..0c286844 100644 --- a/data/web/index.php +++ b/data/web/index.php @@ -65,9 +65,11 @@ $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; ?> <legend>mailcow Apps</legend> <?php - foreach ($MAILCOW_APPS as $app) { - echo '<a href="' . $app['link'] . '" role="button" class="btn btn-lg btn-default">' . $app['name'] . '</a> '; - } + foreach ($MAILCOW_APPS as $app): + ?> + <a href="<?=$app['link'];?>" role="button" class="btn btn-lg btn-default"><?=$app['name'];?></a> + <?php + endforeach; ?> </div> </div> diff --git a/data/web/js/add.js b/data/web/js/add.js index 1ebcdbb2..05171d10 100644 --- a/data/web/js/add.js +++ b/data/web/js/add.js @@ -2,8 +2,8 @@ $(document).ready(function() { // add.php // Get max. possible quota for a domain when domain field changes $('#addSelectDomain').on('change', function() { - $.get("json_api.php", { action:"get_domain_details", object:this.value }, function(data){ - var result = jQuery.parseJSON( data ); + $.get("/api/v1/get/domain/" + this.value, function(data){ + var result = $.parseJSON(JSON.stringify(data)); max_new_mailbox_quota = ( result.max_new_mailbox_quota / 1048576); if (max_new_mailbox_quota != '0') { $("#quotaBadge").html('max. ' + max_new_mailbox_quota + ' MiB'); diff --git a/data/web/js/admin.js b/data/web/js/admin.js index a235a422..647dba9a 100644 --- a/data/web/js/admin.js +++ b/data/web/js/admin.js @@ -1,31 +1,42 @@ $(document).ready(function() { - // Postfix restrictions, drag and drop functions - $( "[id*=srr-sortable]" ).sortable({ - items: "li:not(.list-heading)", - cancel: ".ui-state-disabled", - connectWith: "[id*=srr-sortable]", - dropOnEmpty: true, - placeholder: "ui-state-highlight" - }); - $( "[id*=ssr-sortable]" ).sortable({ - items: "li:not(.list-heading)", - cancel: ".ui-state-disabled", - connectWith: "[id*=ssr-sortable]", - dropOnEmpty: true, - placeholder: "ui-state-highlight" - }); - $('#srr_form').submit(function(){ - var srr_joined_vals = $("[id^=srr-sortable-active] li").map(function() { - return $(this).data("value"); - }).get().join(', '); - var input = $("<input>").attr("type", "hidden").attr("name", "srr_value").val(srr_joined_vals); - $('#srr_form').append($(input)); - }); - $('#ssr_form').submit(function(){ - var ssr_joined_vals = $("[id^=ssr-sortable-active] li").map(function() { - return $(this).data("value"); - }).get().join(', '); - var input = $("<input>").attr("type", "hidden").attr("name", "ssr_value").val(ssr_joined_vals); - $('#ssr_form').append($(input)); - }); + $.ajax({ + dataType: 'json', + url: '/api/v1/get/domain-admin/all', + jsonp: false, + error: function () { + alert('Cannot draw domain administrator table'); + }, + success: function (data) { + $.each(data, function (i, item) { + item.action = '<div class="btn-group">' + + '<a href="/edit.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="/delete.php?domainadmin=' + encodeURI(item.username) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + + '</div>'; + }); + $('#domainadminstable').footable({ + "columns": [ + {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}}, + {"name":"selected_domains","title":lang.admin_domains,"breakpoints":"xs sm"}, + {"name":"tfa_active","title":"TFA", "filterable": false,"style":{"maxWidth":"80px","width":"80px"}}, + {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","maxWidth":"180px","width":"180px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + ], + "rows": data, + "empty": lang.empty, + "paging": { + "enabled": true, + "limit": 5, + "size": pagination_size + }, + "filtering": { + "enabled": true, + "position": "left", + "placeholder": lang.filter_table + }, + "sorting": { + "enabled": true + } + }); + } + }); }); \ No newline at end of file diff --git a/data/web/js/bootstrap-select.min.js b/data/web/js/bootstrap-select.min.js index d2a33149..9e20a22e 100644 --- a/data/web/js/bootstrap-select.min.js +++ b/data/web/js/bootstrap-select.min.js @@ -6,4 +6,3 @@ */ !function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof module&&module.exports?module.exports=b(require("jquery")):b(a.jQuery)}(this,function(a){!function(a){"use strict";function b(b){var c=[{re:/[\xC0-\xC6]/g,ch:"A"},{re:/[\xE0-\xE6]/g,ch:"a"},{re:/[\xC8-\xCB]/g,ch:"E"},{re:/[\xE8-\xEB]/g,ch:"e"},{re:/[\xCC-\xCF]/g,ch:"I"},{re:/[\xEC-\xEF]/g,ch:"i"},{re:/[\xD2-\xD6]/g,ch:"O"},{re:/[\xF2-\xF6]/g,ch:"o"},{re:/[\xD9-\xDC]/g,ch:"U"},{re:/[\xF9-\xFC]/g,ch:"u"},{re:/[\xC7-\xE7]/g,ch:"c"},{re:/[\xD1]/g,ch:"N"},{re:/[\xF1]/g,ch:"n"}];return a.each(c,function(){b=b?b.replace(this.re,this.ch):""}),b}function c(b){var c=arguments,d=b;[].shift.apply(c);var e,f=this.each(function(){var b=a(this);if(b.is("select")){var f=b.data("selectpicker"),g="object"==typeof d&&d;if(f){if(g)for(var h in g)g.hasOwnProperty(h)&&(f.options[h]=g[h])}else{var i=a.extend({},k.DEFAULTS,a.fn.selectpicker.defaults||{},b.data(),g);i.template=a.extend({},k.DEFAULTS.template,a.fn.selectpicker.defaults?a.fn.selectpicker.defaults.template:{},b.data().template,g.template),b.data("selectpicker",f=new k(this,i))}"string"==typeof d&&(e=f[d]instanceof Function?f[d].apply(f,c):f.options[d])}});return"undefined"!=typeof e?e:f}String.prototype.includes||!function(){var a={}.toString,b=function(){try{var a={},b=Object.defineProperty,c=b(a,a,a)&&b}catch(a){}return c}(),c="".indexOf,d=function(b){if(null==this)throw new TypeError;var d=String(this);if(b&&"[object RegExp]"==a.call(b))throw new TypeError;var e=d.length,f=String(b),g=f.length,h=arguments.length>1?arguments[1]:void 0,i=h?Number(h):0;i!=i&&(i=0);var j=Math.min(Math.max(i,0),e);return!(g+j>e)&&c.call(d,f,i)!=-1};b?b(String.prototype,"includes",{value:d,configurable:!0,writable:!0}):String.prototype.includes=d}(),String.prototype.startsWith||!function(){var a=function(){try{var a={},b=Object.defineProperty,c=b(a,a,a)&&b}catch(a){}return c}(),b={}.toString,c=function(a){if(null==this)throw new TypeError;var c=String(this);if(a&&"[object RegExp]"==b.call(a))throw new TypeError;var d=c.length,e=String(a),f=e.length,g=arguments.length>1?arguments[1]:void 0,h=g?Number(g):0;h!=h&&(h=0);var i=Math.min(Math.max(h,0),d);if(f+i>d)return!1;for(var j=-1;++j<f;)if(c.charCodeAt(i+j)!=e.charCodeAt(j))return!1;return!0};a?a(String.prototype,"startsWith",{value:c,configurable:!0,writable:!0}):String.prototype.startsWith=c}(),Object.keys||(Object.keys=function(a,b,c){c=[];for(b in a)c.hasOwnProperty.call(a,b)&&c.push(b);return c});var d={useDefault:!1,_set:a.valHooks.select.set};a.valHooks.select.set=function(b,c){return c&&!d.useDefault&&a(b).data("selected",!0),d._set.apply(this,arguments)};var e=null;a.fn.triggerNative=function(a){var b,c=this[0];c.dispatchEvent?("function"==typeof Event?b=new Event(a,{bubbles:!0}):(b=document.createEvent("Event"),b.initEvent(a,!0,!1)),c.dispatchEvent(b)):c.fireEvent?(b=document.createEventObject(),b.eventType=a,c.fireEvent("on"+a,b)):this.trigger(a)},a.expr.pseudos.icontains=function(b,c,d){var e=a(b),f=(e.data("tokens")||e.text()).toString().toUpperCase();return f.includes(d[3].toUpperCase())},a.expr.pseudos.ibegins=function(b,c,d){var e=a(b),f=(e.data("tokens")||e.text()).toString().toUpperCase();return f.startsWith(d[3].toUpperCase())},a.expr.pseudos.aicontains=function(b,c,d){var e=a(b),f=(e.data("tokens")||e.data("normalizedText")||e.text()).toString().toUpperCase();return f.includes(d[3].toUpperCase())},a.expr.pseudos.aibegins=function(b,c,d){var e=a(b),f=(e.data("tokens")||e.data("normalizedText")||e.text()).toString().toUpperCase();return f.startsWith(d[3].toUpperCase())};var f={"&":"&","<":"<",">":">",'"':""","'":"'","`":"`"},g={"&":"&","<":"<",">":">",""":'"',"'":"'","`":"`"},h=function(a){var b=function(b){return a[b]},c="(?:"+Object.keys(a).join("|")+")",d=RegExp(c),e=RegExp(c,"g");return function(a){return a=null==a?"":""+a,d.test(a)?a.replace(e,b):a}},i=h(f),j=h(g),k=function(b,c){d.useDefault||(a.valHooks.select.set=d._set,d.useDefault=!0),this.$element=a(b),this.$newElement=null,this.$button=null,this.$menu=null,this.$lis=null,this.options=c,null===this.options.title&&(this.options.title=this.$element.attr("title"));var e=this.options.windowPadding;"number"==typeof e&&(this.options.windowPadding=[e,e,e,e]),this.val=k.prototype.val,this.render=k.prototype.render,this.refresh=k.prototype.refresh,this.setStyle=k.prototype.setStyle,this.selectAll=k.prototype.selectAll,this.deselectAll=k.prototype.deselectAll,this.destroy=k.prototype.destroy,this.remove=k.prototype.remove,this.show=k.prototype.show,this.hide=k.prototype.hide,this.init()};k.VERSION="1.12.2",k.DEFAULTS={noneSelectedText:"Nothing selected",noneResultsText:"No results matched {0}",countSelectedText:function(a,b){return 1==a?"{0} item selected":"{0} items selected"},maxOptionsText:function(a,b){return[1==a?"Limit reached ({n} item max)":"Limit reached ({n} items max)",1==b?"Group limit reached ({n} item max)":"Group limit reached ({n} items max)"]},selectAllText:"Select All",deselectAllText:"Deselect All",doneButton:!1,doneButtonText:"Close",multipleSeparator:", ",styleBase:"btn",style:"btn-default",size:"auto",title:null,selectedTextFormat:"values",width:!1,container:!1,hideDisabled:!1,showSubtext:!1,showIcon:!0,showContent:!0,dropupAuto:!0,header:!1,liveSearch:!1,liveSearchPlaceholder:null,liveSearchNormalize:!1,liveSearchStyle:"contains",actionsBox:!1,iconBase:"glyphicon",tickIcon:"glyphicon-ok",showTick:!1,template:{caret:'<span class="caret"></span>'},maxOptions:!1,mobile:!1,selectOnTab:!1,dropdownAlignRight:!1,windowPadding:0},k.prototype={constructor:k,init:function(){var b=this,c=this.$element.attr("id");this.$element.addClass("bs-select-hidden"),this.liObj={},this.multiple=this.$element.prop("multiple"),this.autofocus=this.$element.prop("autofocus"),this.$newElement=this.createView(),this.$element.after(this.$newElement).appendTo(this.$newElement),this.$button=this.$newElement.children("button"),this.$menu=this.$newElement.children(".dropdown-menu"),this.$menuInner=this.$menu.children(".inner"),this.$searchbox=this.$menu.find("input"),this.$element.removeClass("bs-select-hidden"),this.options.dropdownAlignRight===!0&&this.$menu.addClass("dropdown-menu-right"),"undefined"!=typeof c&&(this.$button.attr("data-id",c),a('label[for="'+c+'"]').click(function(a){a.preventDefault(),b.$button.focus()})),this.checkDisabled(),this.clickListener(),this.options.liveSearch&&this.liveSearchListener(),this.render(),this.setStyle(),this.setWidth(),this.options.container&&this.selectPosition(),this.$menu.data("this",this),this.$newElement.data("this",this),this.options.mobile&&this.mobile(),this.$newElement.on({"hide.bs.dropdown":function(a){b.$menuInner.attr("aria-expanded",!1),b.$element.trigger("hide.bs.select",a)},"hidden.bs.dropdown":function(a){b.$element.trigger("hidden.bs.select",a)},"show.bs.dropdown":function(a){b.$menuInner.attr("aria-expanded",!0),b.$element.trigger("show.bs.select",a)},"shown.bs.dropdown":function(a){b.$element.trigger("shown.bs.select",a)}}),b.$element[0].hasAttribute("required")&&this.$element.on("invalid",function(){b.$button.addClass("bs-invalid").focus(),b.$element.on({"focus.bs.select":function(){b.$button.focus(),b.$element.off("focus.bs.select")},"shown.bs.select":function(){b.$element.val(b.$element.val()).off("shown.bs.select")},"rendered.bs.select":function(){this.validity.valid&&b.$button.removeClass("bs-invalid"),b.$element.off("rendered.bs.select")}})}),setTimeout(function(){b.$element.trigger("loaded.bs.select")})},createDropdown:function(){var b=this.multiple||this.options.showTick?" show-tick":"",c=this.$element.parent().hasClass("input-group")?" input-group-btn":"",d=this.autofocus?" autofocus":"",e=this.options.header?'<div class="popover-title"><button type="button" class="close" aria-hidden="true">×</button>'+this.options.header+"</div>":"",f=this.options.liveSearch?'<div class="bs-searchbox"><input type="text" class="form-control" autocomplete="off"'+(null===this.options.liveSearchPlaceholder?"":' placeholder="'+i(this.options.liveSearchPlaceholder)+'"')+' role="textbox" aria-label="Search"></div>':"",g=this.multiple&&this.options.actionsBox?'<div class="bs-actionsbox"><div class="btn-group btn-group-sm btn-block"><button type="button" class="actions-btn bs-select-all btn btn-default">'+this.options.selectAllText+'</button><button type="button" class="actions-btn bs-deselect-all btn btn-default">'+this.options.deselectAllText+"</button></div></div>":"",h=this.multiple&&this.options.doneButton?'<div class="bs-donebutton"><div class="btn-group btn-block"><button type="button" class="btn btn-sm btn-default">'+this.options.doneButtonText+"</button></div></div>":"",j='<div class="btn-group bootstrap-select'+b+c+'"><button type="button" class="'+this.options.styleBase+' dropdown-toggle" data-toggle="dropdown"'+d+' role="button"><span class="filter-option pull-left"></span> <span class="bs-caret">'+this.options.template.caret+'</span></button><div class="dropdown-menu open" role="combobox">'+e+f+g+'<ul class="dropdown-menu inner" role="listbox" aria-expanded="false"></ul>'+h+"</div></div>";return a(j)},createView:function(){var a=this.createDropdown(),b=this.createLi();return a.find("ul")[0].innerHTML=b,a},reloadLi:function(){var a=this.createLi();this.$menuInner[0].innerHTML=a},createLi:function(){var c=this,d=[],e=0,f=document.createElement("option"),g=-1,h=function(a,b,c,d){return"<li"+("undefined"!=typeof c&""!==c?' class="'+c+'"':"")+("undefined"!=typeof b&null!==b?' data-original-index="'+b+'"':"")+("undefined"!=typeof d&null!==d?'data-optgroup="'+d+'"':"")+">"+a+"</li>"},j=function(d,e,f,g){return'<a tabindex="0"'+("undefined"!=typeof e?' class="'+e+'"':"")+(f?' style="'+f+'"':"")+(c.options.liveSearchNormalize?' data-normalized-text="'+b(i(a(d).html()))+'"':"")+("undefined"!=typeof g||null!==g?' data-tokens="'+g+'"':"")+' role="option">'+d+'<span class="'+c.options.iconBase+" "+c.options.tickIcon+' check-mark"></span></a>'};if(this.options.title&&!this.multiple&&(g--,!this.$element.find(".bs-title-option").length)){var k=this.$element[0];f.className="bs-title-option",f.innerHTML=this.options.title,f.value="",k.insertBefore(f,k.firstChild);var l=a(k.options[k.selectedIndex]);void 0===l.attr("selected")&&void 0===this.$element.data("selected")&&(f.selected=!0)}return this.$element.find("option").each(function(b){var f=a(this);if(g++,!f.hasClass("bs-title-option")){var k=this.className||"",l=this.style.cssText,m=f.data("content")?f.data("content"):f.html(),n=f.data("tokens")?f.data("tokens"):null,o="undefined"!=typeof f.data("subtext")?'<small class="text-muted">'+f.data("subtext")+"</small>":"",p="undefined"!=typeof f.data("icon")?'<span class="'+c.options.iconBase+" "+f.data("icon")+'"></span> ':"",q=f.parent(),r="OPTGROUP"===q[0].tagName,s=r&&q[0].disabled,t=this.disabled||s;if(""!==p&&t&&(p="<span>"+p+"</span>"),c.options.hideDisabled&&(t&&!r||s))return void g--;if(f.data("content")||(m=p+'<span class="text">'+m+o+"</span>"),r&&f.data("divider")!==!0){if(c.options.hideDisabled&&t){if(void 0===q.data("allOptionsDisabled")){var u=q.children();q.data("allOptionsDisabled",u.filter(":disabled").length===u.length)}if(q.data("allOptionsDisabled"))return void g--}var v=" "+q[0].className||"";if(0===f.index()){e+=1;var w=q[0].label,x="undefined"!=typeof q.data("subtext")?'<small class="text-muted">'+q.data("subtext")+"</small>":"",y=q.data("icon")?'<span class="'+c.options.iconBase+" "+q.data("icon")+'"></span> ':"";w=y+'<span class="text">'+i(w)+x+"</span>",0!==b&&d.length>0&&(g++,d.push(h("",null,"divider",e+"div"))),g++,d.push(h(w,null,"dropdown-header"+v,e))}if(c.options.hideDisabled&&t)return void g--;d.push(h(j(m,"opt "+k+v,l,n),b,"",e))}else if(f.data("divider")===!0)d.push(h("",b,"divider"));else if(f.data("hidden")===!0)d.push(h(j(m,k,l,n),b,"hidden is-hidden"));else{var z=this.previousElementSibling&&"OPTGROUP"===this.previousElementSibling.tagName;if(!z&&c.options.hideDisabled)for(var A=a(this).prevAll(),B=0;B<A.length;B++)if("OPTGROUP"===A[B].tagName){for(var C=0,D=0;D<B;D++){var E=A[D];(E.disabled||a(E).data("hidden")===!0)&&C++}C===B&&(z=!0);break}z&&(g++,d.push(h("",null,"divider",e+"div"))),d.push(h(j(m,k,l,n),b))}c.liObj[b]=g}}),this.multiple||0!==this.$element.find("option:selected").length||this.options.title||this.$element.find("option").eq(0).prop("selected",!0).attr("selected","selected"),d.join("")},findLis:function(){return null==this.$lis&&(this.$lis=this.$menu.find("li")),this.$lis},render:function(b){var c,d=this;b!==!1&&this.$element.find("option").each(function(a){var b=d.findLis().eq(d.liObj[a]);d.setDisabled(a,this.disabled||"OPTGROUP"===this.parentNode.tagName&&this.parentNode.disabled,b),d.setSelected(a,this.selected,b)}),this.togglePlaceholder(),this.tabIndex();var e=this.$element.find("option").map(function(){if(this.selected){if(d.options.hideDisabled&&(this.disabled||"OPTGROUP"===this.parentNode.tagName&&this.parentNode.disabled))return;var b,c=a(this),e=c.data("icon")&&d.options.showIcon?'<i class="'+d.options.iconBase+" "+c.data("icon")+'"></i> ':"";return b=d.options.showSubtext&&c.data("subtext")&&!d.multiple?' <small class="text-muted">'+c.data("subtext")+"</small>":"","undefined"!=typeof c.attr("title")?c.attr("title"):c.data("content")&&d.options.showContent?c.data("content").toString():e+c.html()+b}}).toArray(),f=this.multiple?e.join(this.options.multipleSeparator):e[0];if(this.multiple&&this.options.selectedTextFormat.indexOf("count")>-1){var g=this.options.selectedTextFormat.split(">");if(g.length>1&&e.length>g[1]||1==g.length&&e.length>=2){c=this.options.hideDisabled?", [disabled]":"";var h=this.$element.find("option").not('[data-divider="true"], [data-hidden="true"]'+c).length,i="function"==typeof this.options.countSelectedText?this.options.countSelectedText(e.length,h):this.options.countSelectedText;f=i.replace("{0}",e.length.toString()).replace("{1}",h.toString())}}void 0==this.options.title&&(this.options.title=this.$element.attr("title")),"static"==this.options.selectedTextFormat&&(f=this.options.title),f||(f="undefined"!=typeof this.options.title?this.options.title:this.options.noneSelectedText),this.$button.attr("title",j(a.trim(f.replace(/<[^>]*>?/g,"")))),this.$button.children(".filter-option").html(f),this.$element.trigger("rendered.bs.select")},setStyle:function(a,b){this.$element.attr("class")&&this.$newElement.addClass(this.$element.attr("class").replace(/selectpicker|mobile-device|bs-select-hidden|validate\[.*\]/gi,""));var c=a?a:this.options.style;"add"==b?this.$button.addClass(c):"remove"==b?this.$button.removeClass(c):(this.$button.removeClass(this.options.style),this.$button.addClass(c))},liHeight:function(b){if(b||this.options.size!==!1&&!this.sizeInfo){var c=document.createElement("div"),d=document.createElement("div"),e=document.createElement("ul"),f=document.createElement("li"),g=document.createElement("li"),h=document.createElement("a"),i=document.createElement("span"),j=this.options.header&&this.$menu.find(".popover-title").length>0?this.$menu.find(".popover-title")[0].cloneNode(!0):null,k=this.options.liveSearch?document.createElement("div"):null,l=this.options.actionsBox&&this.multiple&&this.$menu.find(".bs-actionsbox").length>0?this.$menu.find(".bs-actionsbox")[0].cloneNode(!0):null,m=this.options.doneButton&&this.multiple&&this.$menu.find(".bs-donebutton").length>0?this.$menu.find(".bs-donebutton")[0].cloneNode(!0):null;if(i.className="text",c.className=this.$menu[0].parentNode.className+" open",d.className="dropdown-menu open",e.className="dropdown-menu inner",f.className="divider",i.appendChild(document.createTextNode("Inner text")),h.appendChild(i),g.appendChild(h),e.appendChild(g),e.appendChild(f),j&&d.appendChild(j),k){var n=document.createElement("input");k.className="bs-searchbox",n.className="form-control",k.appendChild(n),d.appendChild(k)}l&&d.appendChild(l),d.appendChild(e),m&&d.appendChild(m),c.appendChild(d),document.body.appendChild(c);var o=h.offsetHeight,p=j?j.offsetHeight:0,q=k?k.offsetHeight:0,r=l?l.offsetHeight:0,s=m?m.offsetHeight:0,t=a(f).outerHeight(!0),u="function"==typeof getComputedStyle&&getComputedStyle(d),v=u?null:a(d),w={vert:parseInt(u?u.paddingTop:v.css("paddingTop"))+parseInt(u?u.paddingBottom:v.css("paddingBottom"))+parseInt(u?u.borderTopWidth:v.css("borderTopWidth"))+parseInt(u?u.borderBottomWidth:v.css("borderBottomWidth")),horiz:parseInt(u?u.paddingLeft:v.css("paddingLeft"))+parseInt(u?u.paddingRight:v.css("paddingRight"))+parseInt(u?u.borderLeftWidth:v.css("borderLeftWidth"))+parseInt(u?u.borderRightWidth:v.css("borderRightWidth"))},x={vert:w.vert+parseInt(u?u.marginTop:v.css("marginTop"))+parseInt(u?u.marginBottom:v.css("marginBottom"))+2,horiz:w.horiz+parseInt(u?u.marginLeft:v.css("marginLeft"))+parseInt(u?u.marginRight:v.css("marginRight"))+2};document.body.removeChild(c),this.sizeInfo={liHeight:o,headerHeight:p,searchHeight:q,actionsHeight:r,doneButtonHeight:s,dividerHeight:t,menuPadding:w,menuExtras:x}}},setSize:function(){if(this.findLis(),this.liHeight(),this.options.header&&this.$menu.css("padding-top",0),this.options.size!==!1){var b,c,d,e,f,g,h,i,j=this,k=this.$menu,l=this.$menuInner,m=a(window),n=this.$newElement[0].offsetHeight,o=this.$newElement[0].offsetWidth,p=this.sizeInfo.liHeight,q=this.sizeInfo.headerHeight,r=this.sizeInfo.searchHeight,s=this.sizeInfo.actionsHeight,t=this.sizeInfo.doneButtonHeight,u=this.sizeInfo.dividerHeight,v=this.sizeInfo.menuPadding,w=this.sizeInfo.menuExtras,x=this.options.hideDisabled?".disabled":"",y=function(){var b,c=j.$newElement.offset(),d=a(j.options.container);j.options.container&&!d.is("body")?(b=d.offset(),b.top+=parseInt(d.css("borderTopWidth")),b.left+=parseInt(d.css("borderLeftWidth"))):b={top:0,left:0};var e=j.options.windowPadding;f=c.top-b.top-m.scrollTop(),g=m.height()-f-n-b.top-e[2],h=c.left-b.left-m.scrollLeft(),i=m.width()-h-o-b.left-e[1],f-=e[0],h-=e[3]};if(y(),"auto"===this.options.size){var z=function(){var m,n=function(b,c){return function(d){return c?d.classList?d.classList.contains(b):a(d).hasClass(b):!(d.classList?d.classList.contains(b):a(d).hasClass(b))}},u=j.$menuInner[0].getElementsByTagName("li"),x=Array.prototype.filter?Array.prototype.filter.call(u,n("hidden",!1)):j.$lis.not(".hidden"),z=Array.prototype.filter?Array.prototype.filter.call(x,n("dropdown-header",!0)):x.filter(".dropdown-header");y(),b=g-w.vert,c=i-w.horiz,j.options.container?(k.data("height")||k.data("height",k.height()),d=k.data("height"),k.data("width")||k.data("width",k.width()),e=k.data("width")):(d=k.height(),e=k.width()),j.options.dropupAuto&&j.$newElement.toggleClass("dropup",f>g&&b-w.vert<d),j.$newElement.hasClass("dropup")&&(b=f-w.vert),"auto"===j.options.dropdownAlignRight&&k.toggleClass("dropdown-menu-right",h>i&&c-w.horiz<e-o),m=x.length+z.length>3?3*p+w.vert-2:0,k.css({"max-height":b+"px",overflow:"hidden","min-height":m+q+r+s+t+"px"}),l.css({"max-height":b-q-r-s-t-v.vert+"px","overflow-y":"auto","min-height":Math.max(m-v.vert,0)+"px"})};z(),this.$searchbox.off("input.getSize propertychange.getSize").on("input.getSize propertychange.getSize",z),m.off("resize.getSize scroll.getSize").on("resize.getSize scroll.getSize",z)}else if(this.options.size&&"auto"!=this.options.size&&this.$lis.not(x).length>this.options.size){var A=this.$lis.not(".divider").not(x).children().slice(0,this.options.size).last().parent().index(),B=this.$lis.slice(0,A+1).filter(".divider").length;b=p*this.options.size+B*u+v.vert,j.options.container?(k.data("height")||k.data("height",k.height()),d=k.data("height")):d=k.height(),j.options.dropupAuto&&this.$newElement.toggleClass("dropup",f>g&&b-w.vert<d),k.css({"max-height":b+q+r+s+t+"px",overflow:"hidden","min-height":""}),l.css({"max-height":b-v.vert+"px","overflow-y":"auto","min-height":""})}}},setWidth:function(){if("auto"===this.options.width){this.$menu.css("min-width","0");var a=this.$menu.parent().clone().appendTo("body"),b=this.options.container?this.$newElement.clone().appendTo("body"):a,c=a.children(".dropdown-menu").outerWidth(),d=b.css("width","auto").children("button").outerWidth();a.remove(),b.remove(),this.$newElement.css("width",Math.max(c,d)+"px")}else"fit"===this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width","").addClass("fit-width")):this.options.width?(this.$menu.css("min-width",""),this.$newElement.css("width",this.options.width)):(this.$menu.css("min-width",""),this.$newElement.css("width",""));this.$newElement.hasClass("fit-width")&&"fit"!==this.options.width&&this.$newElement.removeClass("fit-width")},selectPosition:function(){this.$bsContainer=a('<div class="bs-container" />');var b,c,d,e=this,f=a(this.options.container),g=function(a){e.$bsContainer.addClass(a.attr("class").replace(/form-control|fit-width/gi,"")).toggleClass("dropup",a.hasClass("dropup")),b=a.offset(),f.is("body")?c={top:0,left:0}:(c=f.offset(),c.top+=parseInt(f.css("borderTopWidth"))-f.scrollTop(),c.left+=parseInt(f.css("borderLeftWidth"))-f.scrollLeft()),d=a.hasClass("dropup")?0:a[0].offsetHeight,e.$bsContainer.css({top:b.top-c.top+d,left:b.left-c.left,width:a[0].offsetWidth})};this.$button.on("click",function(){var b=a(this);e.isDisabled()||(g(e.$newElement),e.$bsContainer.appendTo(e.options.container).toggleClass("open",!b.hasClass("open")).append(e.$menu))}),a(window).on("resize scroll",function(){g(e.$newElement)}),this.$element.on("hide.bs.select",function(){e.$menu.data("height",e.$menu.height()),e.$bsContainer.detach()})},setSelected:function(a,b,c){c||(this.togglePlaceholder(),c=this.findLis().eq(this.liObj[a])),c.toggleClass("selected",b).find("a").attr("aria-selected",b)},setDisabled:function(a,b,c){c||(c=this.findLis().eq(this.liObj[a])),b?c.addClass("disabled").children("a").attr("href","#").attr("tabindex",-1).attr("aria-disabled",!0):c.removeClass("disabled").children("a").removeAttr("href").attr("tabindex",0).attr("aria-disabled",!1)},isDisabled:function(){return this.$element[0].disabled},checkDisabled:function(){var a=this;this.isDisabled()?(this.$newElement.addClass("disabled"),this.$button.addClass("disabled").attr("tabindex",-1).attr("aria-disabled",!0)):(this.$button.hasClass("disabled")&&(this.$newElement.removeClass("disabled"),this.$button.removeClass("disabled").attr("aria-disabled",!1)),this.$button.attr("tabindex")!=-1||this.$element.data("tabindex")||this.$button.removeAttr("tabindex")),this.$button.click(function(){return!a.isDisabled()})},togglePlaceholder:function(){var a=this.$element.val();this.$button.toggleClass("bs-placeholder",null===a||""===a||a.constructor===Array&&0===a.length)},tabIndex:function(){this.$element.data("tabindex")!==this.$element.attr("tabindex")&&this.$element.attr("tabindex")!==-98&&"-98"!==this.$element.attr("tabindex")&&(this.$element.data("tabindex",this.$element.attr("tabindex")),this.$button.attr("tabindex",this.$element.data("tabindex"))),this.$element.attr("tabindex",-98)},clickListener:function(){var b=this,c=a(document);c.data("spaceSelect",!1),this.$button.on("keyup",function(a){/(32)/.test(a.keyCode.toString(10))&&c.data("spaceSelect")&&(a.preventDefault(),c.data("spaceSelect",!1))}),this.$button.on("click",function(){b.setSize()}),this.$element.on("shown.bs.select",function(){if(b.options.liveSearch||b.multiple){if(!b.multiple){var a=b.liObj[b.$element[0].selectedIndex];if("number"!=typeof a||b.options.size===!1)return;var c=b.$lis.eq(a)[0].offsetTop-b.$menuInner[0].offsetTop;c=c-b.$menuInner[0].offsetHeight/2+b.sizeInfo.liHeight/2,b.$menuInner[0].scrollTop=c}}else b.$menuInner.find(".selected a").focus()}),this.$menuInner.on("click","li a",function(c){var d=a(this),f=d.parent().data("originalIndex"),g=b.$element.val(),h=b.$element.prop("selectedIndex"),i=!0;if(b.multiple&&1!==b.options.maxOptions&&c.stopPropagation(),c.preventDefault(),!b.isDisabled()&&!d.parent().hasClass("disabled")){var j=b.$element.find("option"),k=j.eq(f),l=k.prop("selected"),m=k.parent("optgroup"),n=b.options.maxOptions,o=m.data("maxOptions")||!1;if(b.multiple){if(k.prop("selected",!l),b.setSelected(f,!l),d.blur(),n!==!1||o!==!1){var p=n<j.filter(":selected").length,q=o<m.find("option:selected").length;if(n&&p||o&&q)if(n&&1==n)j.prop("selected",!1),k.prop("selected",!0),b.$menuInner.find(".selected").removeClass("selected"),b.setSelected(f,!0);else if(o&&1==o){m.find("option:selected").prop("selected",!1),k.prop("selected",!0);var r=d.parent().data("optgroup");b.$menuInner.find('[data-optgroup="'+r+'"]').removeClass("selected"),b.setSelected(f,!0)}else{var s="string"==typeof b.options.maxOptionsText?[b.options.maxOptionsText,b.options.maxOptionsText]:b.options.maxOptionsText,t="function"==typeof s?s(n,o):s,u=t[0].replace("{n}",n),v=t[1].replace("{n}",o),w=a('<div class="notify"></div>');t[2]&&(u=u.replace("{var}",t[2][n>1?0:1]),v=v.replace("{var}",t[2][o>1?0:1])),k.prop("selected",!1),b.$menu.append(w),n&&p&&(w.append(a("<div>"+u+"</div>")),i=!1,b.$element.trigger("maxReached.bs.select")),o&&q&&(w.append(a("<div>"+v+"</div>")),i=!1,b.$element.trigger("maxReachedGrp.bs.select")),setTimeout(function(){b.setSelected(f,!1)},10),w.delay(750).fadeOut(300,function(){a(this).remove()})}}}else j.prop("selected",!1),k.prop("selected",!0),b.$menuInner.find(".selected").removeClass("selected").find("a").attr("aria-selected",!1),b.setSelected(f,!0);!b.multiple||b.multiple&&1===b.options.maxOptions?b.$button.focus():b.options.liveSearch&&b.$searchbox.focus(),i&&(g!=b.$element.val()&&b.multiple||h!=b.$element.prop("selectedIndex")&&!b.multiple)&&(e=[f,k.prop("selected"),l],b.$element.triggerNative("change"))}}),this.$menu.on("click","li.disabled a, .popover-title, .popover-title :not(.close)",function(c){c.currentTarget==this&&(c.preventDefault(),c.stopPropagation(),b.options.liveSearch&&!a(c.target).hasClass("close")?b.$searchbox.focus():b.$button.focus())}),this.$menuInner.on("click",".divider, .dropdown-header",function(a){a.preventDefault(),a.stopPropagation(),b.options.liveSearch?b.$searchbox.focus():b.$button.focus()}),this.$menu.on("click",".popover-title .close",function(){b.$button.click()}),this.$searchbox.on("click",function(a){a.stopPropagation()}),this.$menu.on("click",".actions-btn",function(c){b.options.liveSearch?b.$searchbox.focus():b.$button.focus(),c.preventDefault(),c.stopPropagation(),a(this).hasClass("bs-select-all")?b.selectAll():b.deselectAll()}),this.$element.change(function(){b.render(!1),b.$element.trigger("changed.bs.select",e),e=null})},liveSearchListener:function(){var c=this,d=a('<li class="no-results"></li>');this.$button.on("click.dropdown.data-api",function(){c.$menuInner.find(".active").removeClass("active"),c.$searchbox.val()&&(c.$searchbox.val(""),c.$lis.not(".is-hidden").removeClass("hidden"),d.parent().length&&d.remove()),c.multiple||c.$menuInner.find(".selected").addClass("active"),setTimeout(function(){c.$searchbox.focus()},10)}),this.$searchbox.on("click.dropdown.data-api focus.dropdown.data-api touchend.dropdown.data-api",function(a){a.stopPropagation()}),this.$searchbox.on("input propertychange",function(){if(c.$lis.not(".is-hidden").removeClass("hidden"),c.$lis.filter(".active").removeClass("active"),d.remove(),c.$searchbox.val()){var e,f=c.$lis.not(".is-hidden, .divider, .dropdown-header");if(e=c.options.liveSearchNormalize?f.find("a").not(":a"+c._searchStyle()+'("'+b(c.$searchbox.val())+'")'):f.find("a").not(":"+c._searchStyle()+'("'+c.$searchbox.val()+'")'),e.length===f.length)d.html(c.options.noneResultsText.replace("{0}",'"'+i(c.$searchbox.val())+'"')),c.$menuInner.append(d),c.$lis.addClass("hidden");else{e.parent().addClass("hidden");var g,h=c.$lis.not(".hidden");h.each(function(b){var c=a(this);c.hasClass("divider")?void 0===g?c.addClass("hidden"):(g&&g.addClass("hidden"),g=c):c.hasClass("dropdown-header")&&h.eq(b+1).data("optgroup")!==c.data("optgroup")?c.addClass("hidden"):g=null}),g&&g.addClass("hidden"),f.not(".hidden").first().addClass("active")}}})},_searchStyle:function(){var a={begins:"ibegins",startsWith:"ibegins"};return a[this.options.liveSearchStyle]||"icontains"},val:function(a){return"undefined"!=typeof a?(this.$element.val(a),this.render(),this.$element):this.$element.val()},changeAll:function(b){if(this.multiple){"undefined"==typeof b&&(b=!0),this.findLis();var c=this.$element.find("option"),d=this.$lis.not(".divider, .dropdown-header, .disabled, .hidden"),e=d.length,f=[];if(b){if(d.filter(".selected").length===d.length)return}else if(0===d.filter(".selected").length)return;d.toggleClass("selected",b);for(var g=0;g<e;g++){var h=d[g].getAttribute("data-original-index");f[f.length]=c.eq(h)[0]}a(f).prop("selected",b),this.render(!1),this.togglePlaceholder(),this.$element.triggerNative("change")}},selectAll:function(){return this.changeAll(!0)},deselectAll:function(){return this.changeAll(!1)},toggle:function(a){a=a||window.event,a&&a.stopPropagation(),this.$button.trigger("click")},keydown:function(c){var d,e,f,g,h,i,j,k,l,m=a(this),n=m.is("input")?m.parent().parent():m.parent(),o=n.data("this"),p=":not(.disabled, .hidden, .dropdown-header, .divider)",q={32:" ",48:"0",49:"1",50:"2",51:"3",52:"4",53:"5",54:"6",55:"7",56:"8",57:"9",59:";",65:"a",66:"b",67:"c",68:"d",69:"e",70:"f",71:"g",72:"h",73:"i",74:"j",75:"k",76:"l",77:"m",78:"n",79:"o",80:"p",81:"q",82:"r",83:"s",84:"t",85:"u",86:"v",87:"w",88:"x",89:"y",90:"z",96:"0",97:"1",98:"2",99:"3",100:"4",101:"5",102:"6",103:"7",104:"8",105:"9"};if(o.options.liveSearch&&(n=m.parent().parent()),o.options.container&&(n=o.$menu),d=a('[role="listbox"] li',n),l=o.$newElement.hasClass("open"),!l&&(c.keyCode>=48&&c.keyCode<=57||c.keyCode>=96&&c.keyCode<=105||c.keyCode>=65&&c.keyCode<=90))return o.options.container?o.$button.trigger("click"):(o.setSize(),o.$menu.parent().addClass("open"),l=!0),void o.$searchbox.focus();if(o.options.liveSearch&&(/(^9$|27)/.test(c.keyCode.toString(10))&&l&&(c.preventDefault(),c.stopPropagation(),o.$menuInner.click(),o.$button.focus()),d=a('[role="listbox"] li'+p,n),m.val()||/(38|40)/.test(c.keyCode.toString(10))||0===d.filter(".active").length&&(d=o.$menuInner.find("li"),d=o.options.liveSearchNormalize?d.filter(":a"+o._searchStyle()+"("+b(q[c.keyCode])+")"):d.filter(":"+o._searchStyle()+"("+q[c.keyCode]+")"))),d.length){if(/(38|40)/.test(c.keyCode.toString(10)))e=d.index(d.find("a").filter(":focus").parent()),g=d.filter(p).first().index(),h=d.filter(p).last().index(),f=d.eq(e).nextAll(p).eq(0).index(),i=d.eq(e).prevAll(p).eq(0).index(),j=d.eq(f).prevAll(p).eq(0).index(),o.options.liveSearch&&(d.each(function(b){a(this).hasClass("disabled")||a(this).data("index",b)}),e=d.index(d.filter(".active")),g=d.first().data("index"),h=d.last().data("index"),f=d.eq(e).nextAll().eq(0).data("index"),i=d.eq(e).prevAll().eq(0).data("index"),j=d.eq(f).prevAll().eq(0).data("index")),k=m.data("prevIndex"),38==c.keyCode?(o.options.liveSearch&&e--,e!=j&&e>i&&(e=i),e<g&&(e=g),e==k&&(e=h)):40==c.keyCode&&(o.options.liveSearch&&e++,e==-1&&(e=0),e!=j&&e<f&&(e=f),e>h&&(e=h),e==k&&(e=g)),m.data("prevIndex",e),o.options.liveSearch?(c.preventDefault(),m.hasClass("dropdown-toggle")||(d.removeClass("active").eq(e).addClass("active").children("a").focus(),m.focus())):d.eq(e).children("a").focus();else if(!m.is("input")){var r,s,t=[];d.each(function(){a(this).hasClass("disabled")||a.trim(a(this).children("a").text().toLowerCase()).substring(0,1)==q[c.keyCode]&&t.push(a(this).index())}),r=a(document).data("keycount"),r++,a(document).data("keycount",r),s=a.trim(a(":focus").text().toLowerCase()).substring(0,1),s!=q[c.keyCode]?(r=1,a(document).data("keycount",r)):r>=t.length&&(a(document).data("keycount",0),r>t.length&&(r=1)),d.eq(t[r-1]).children("a").focus()}if((/(13|32)/.test(c.keyCode.toString(10))||/(^9$)/.test(c.keyCode.toString(10))&&o.options.selectOnTab)&&l){if(/(32)/.test(c.keyCode.toString(10))||c.preventDefault(),o.options.liveSearch)/(32)/.test(c.keyCode.toString(10))||(o.$menuInner.find(".active a").click(), m.focus());else{var u=a(":focus");u.click(),u.focus(),c.preventDefault(),a(document).data("spaceSelect",!0)}a(document).data("keycount",0)}(/(^9$|27)/.test(c.keyCode.toString(10))&&l&&(o.multiple||o.options.liveSearch)||/(27)/.test(c.keyCode.toString(10))&&!l)&&(o.$menu.parent().removeClass("open"),o.options.container&&o.$newElement.removeClass("open"),o.$button.focus())}},mobile:function(){this.$element.addClass("mobile-device")},refresh:function(){this.$lis=null,this.liObj={},this.reloadLi(),this.render(),this.checkDisabled(),this.liHeight(!0),this.setStyle(),this.setWidth(),this.$lis&&this.$searchbox.trigger("propertychange"),this.$element.trigger("refreshed.bs.select")},hide:function(){this.$newElement.hide()},show:function(){this.$newElement.show()},remove:function(){this.$newElement.remove(),this.$element.remove()},destroy:function(){this.$newElement.before(this.$element).remove(),this.$bsContainer?this.$bsContainer.remove():this.$menu.remove(),this.$element.off(".bs.select").removeData("selectpicker").removeClass("bs-select-hidden selectpicker")}};var l=a.fn.selectpicker;a.fn.selectpicker=c,a.fn.selectpicker.Constructor=k,a.fn.selectpicker.noConflict=function(){return a.fn.selectpicker=l,this},a(document).data("keycount",0).on("keydown.bs.select",'.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input',k.prototype.keydown).on("focusin.modal",'.bootstrap-select [data-toggle=dropdown], .bootstrap-select [role="listbox"], .bs-searchbox input',function(a){a.stopPropagation()}),a(window).on("load.bs.select.data-api",function(){a(".selectpicker").each(function(){var b=a(this);c.call(b,b.data())})})}(a)}); -//# sourceMappingURL=bootstrap-select.js.map \ No newline at end of file diff --git a/data/web/js/mailbox.js b/data/web/js/mailbox.js index 1ddf0c35..6acb22eb 100644 --- a/data/web/js/mailbox.js +++ b/data/web/js/mailbox.js @@ -12,10 +12,10 @@ $(document).ready(function() { } while(Math.abs(bytes) >= 1024 && u < units.length - 1); return bytes.toFixed(1)+' '+units[u]; } - + $.ajax({ dataType: 'json', - url: '/json_api.php?action=domain_table_data', + url: '/api/v1/get/domain/all', jsonp: false, error: function () { alert('Cannot draw domain table'); @@ -24,7 +24,7 @@ $(document).ready(function() { $.each(data, function (i, item) { item.aliases = item.aliases_in_domain + " / " + item.max_num_aliases_for_domain; item.mailboxes = item.mboxes_in_domain + " / " + item.max_num_mboxes_for_domain; - item.quota = humanFileSize(item.quota_used_in_domain) + " / " + humanFileSize(item.max_quota_for_domain); + item.quota = item.quota_used_in_domain + "/" + item.max_quota_for_domain; item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox); if (role == "admin") { item.action = '<div class="btn-group">' + @@ -43,7 +43,15 @@ $(document).ready(function() { {"sorted": true,"name":"domain_name","title":lang.domain,"style":{"width":"250px"}}, {"name":"aliases","title":lang.aliases,"breakpoints":"xs sm"}, {"name":"mailboxes","title":lang.mailboxes}, - {"name":"quota","title":lang.domain_quota}, + {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota,"formatter": function(value){ + res = value.split("/"); + return humanFileSize(res[0]) + " / " + humanFileSize(res[1]); + }, + "sortValue": function(value){ + res = value.split("/"); + return res[0]; + }, + }, {"name":"max_quota_for_mbox","title":lang.mailbox_quota,"breakpoints":"xs sm"}, {"name":"backupmx","filterable": false,"style":{"maxWidth":"120px","width":"120px"},"title":lang.backup_mx,"breakpoints":"xs sm"}, {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, @@ -70,14 +78,14 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=mailbox_table_data', + url: '/api/v1/get/mailbox/all', jsonp: false, error: function () { alert('Cannot draw mailbox table'); }, success: function (data) { $.each(data, function (i, item) { - item.quota = humanFileSize(item.quota_used) + " / " + humanFileSize(item.quota); + item.quota = item.quota_used + "/" + item.quota; item.max_quota_for_mbox = humanFileSize(item.max_quota_for_mbox); if (role == "admin") { item.action = '<div class="btn-group">' + @@ -99,15 +107,23 @@ $(document).ready(function() { }); $('#mailbox_table').footable({ "columns": [ - {"sorted": true,"name":"username","title":lang.username,"style":{"width":"250px"}}, - {"name":"name","title":lang.fname,"breakpoints":"xs sm"}, + {"sorted": true,"name":"username","style":{"word-break":"break-all","min-width":"120px"},"title":lang.username}, + {"name":"name","title":lang.fname,"style":{"word-break":"break-all","min-width":"120px"},"breakpoints":"xs sm"}, {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, - {"name":"quota","title":lang.domain_quota}, - {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm"}, + {"name":"quota","style":{"whiteSpace":"nowrap"},"title":lang.domain_quota,"formatter": function(value){ + res = value.split("/"); + return humanFileSize(res[0]) + " / " + humanFileSize(res[1]); + }, + "sortValue": function(value){ + res = value.split("/"); + return res[0]; + }, + }, + {"name":"spam_aliases","filterable": false,"title":lang.spam_aliases,"breakpoints":"xs sm md"}, {"name":"in_use","filterable": false,"type":"html","title":lang.in_use}, - {"name":"messages","filterable": false,"style":{"width":"90px"},"title":lang.msg_num,"breakpoints":"xs sm"}, - {"name":"active","filterable": false,"style":{"maxWidth":"80px","width":"80px"},"title":lang.active}, - {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","width":"290px"},"type":"html","title":lang.action,"breakpoints":"xs sm"} + {"name":"messages","filterable": false,"title":lang.msg_num,"breakpoints":"xs sm md"}, + {"name":"active","filterable": false,"title":lang.active}, + {"name":"action","filterable": false,"sortable": false,"style":{"text-align":"right","min-width":"250px"},"type":"html","title":lang.action,"breakpoints":"xs sm md"} ], "empty": lang.empty, "rows": data, @@ -130,7 +146,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=resource_table_data', + url: '/api/v1/get/resource/all', jsonp: false, error: function () { alert('Cannot draw resource table'); @@ -172,7 +188,7 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=domain_alias_table_data', + url: '/api/v1/get/alias-domain/all', jsonp: false, error: function () { alert('Cannot draw alias domain table'); @@ -212,23 +228,28 @@ $(document).ready(function() { $.ajax({ dataType: 'json', - url: '/json_api.php?action=alias_table_data', + url: '/api/v1/get/alias/all', jsonp: false, error: function () { alert('Cannot draw alias table'); }, success: function (data) { $.each(data, function (i, item) { + item.action = '<div class="btn-group">' + + '<a href="/edit.php?alias=' + encodeURI(item.address) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + + '<a href="/delete.php?alias=' + encodeURI(item.address) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-pencil"></span> ' + lang.remove + '</a>' + + '</div>'; + item.chkbox = '<input type="checkbox" class="alias_item" name="sel_aliases" value="' + item.address + '" />'; if (item.is_catch_all == 1) { item.address = '<div class="label label-default">Catch-All</div> ' + item.address; } - item.action = '<div class="btn-group">' + - '<a href="/edit.php?alias=' + encodeURI(item.address) + '" class="btn btn-xs btn-default"><span class="glyphicon glyphicon-pencil"></span> ' + lang.edit + '</a>' + - '<a href="/delete.php?alias=' + encodeURI(item.address) + '" class="btn btn-xs btn-danger"><span class="glyphicon glyphicon-trash"></span> ' + lang.remove + '</a>' + - '</div>'; + if (item.in_primary_domain !== "") { + item.domain = "↳ " + item.domain + " (" + item.in_primary_domain + ")"; + } }); - $('#alias_table').footable({ + ft_aliases = FooTable.init("#alias_table", { "columns": [ + {"name":"chkbox","title":"","style":{"maxWidth":"40px","width":"40px"},"filterable": false,"sortable": false,"type":"html"}, {"sorted": true,"name":"address","title":lang.alias,"style":{"width":"250px"}}, {"name":"goto","title":lang.target_address}, {"name":"domain","title":lang.domain,"breakpoints":"xs sm"}, @@ -251,6 +272,98 @@ $(document).ready(function() { "enabled": true } }); + + var selected_aliases = []; + + $(document).on('click', 'tr', function(e) { + if (e.target.type == "checkbox") { + e.stopPropagation(); + } else { + var checkbox = $(this).find(':checkbox'); + checkbox.trigger('click'); + } + }); + + $(document).on('change', 'input[name=sel_aliases]:checkbox', function() { + if ($(this).is(':checked')) { + selected_aliases.push($(this).val()); + } + else { + selected_aliases.splice($.inArray($(this).val(), selected_aliases),1); + } + }); + + $(document).on('click', '#select_all_aliases', function(e) { + e.preventDefault(); + var alias_chkbxs = $("input[name=sel_aliases]"); + alias_chkbxs.prop("checked", !alias_chkbxs.prop("checked")).change(); + }); + + $(document).on('click', '#activate_selected_alias', function(e) { + e.preventDefault(); + if (selected_aliases.length !== 0) { + $.ajax({ + type: "POST", + dataType: "json", + data: { "address": JSON.stringify(selected_aliases), "active": "1" }, + url: '/api/v1/edit/alias', + jsonp: false, + complete: function (data) { + location.reload(); + } + }); + } + }); + + $(document).on('click', '#deactivate_selected_alias', function(e) { + e.preventDefault(); + if (Object.keys(selected_aliases).length !== 0) { + $.ajax({ + type: "POST", + dataType: "json", + data: { "address": JSON.stringify(selected_aliases), "active": "0" }, + url: '/api/v1/edit/alias', + jsonp: false, + complete: function (data) { + location.reload(); + } + }); + } + }); + + $(document).on('click', '#delete_selected_alias', function(e) { + e.preventDefault(); + if (Object.keys(selected_aliases).length !== 0) { + $(document).on('show.bs.modal','#ConfirmDeleteModal', function () { + $("#ItemsToDelete").empty(); + for (var i in selected_aliases) { + $("#ItemsToDelete").append("<li>" + selected_aliases[i] + "</li>"); + } + }) + $('#ConfirmDeleteModal').modal({ + backdrop: 'static', + keyboard: false + }) + .one('click', '#IsConfirmed', function(e) { + $.ajax({ + type: "POST", + dataType: "json", + data: { "address": JSON.stringify(selected_aliases) }, + url: '/api/v1/delete/alias', + jsonp: false, + complete: function (data) { + location.reload(); + } + }); + }) + .one('click', '#isCanceled', function(e) { + $('#ConfirmDeleteModal').modal('hide'); + });; + + } + }); + } + }); }); diff --git a/data/web/js/sorttable.js b/data/web/js/sorttable.js deleted file mode 100644 index cb3e293c..00000000 --- a/data/web/js/sorttable.js +++ /dev/null @@ -1,236 +0,0 @@ -(function() { - var SELECTOR, addEventListener, clickEvents, numberRegExp, sortable, touchDevice, trimRegExp; - - SELECTOR = 'table[data-sortable]'; - - numberRegExp = /^-?[£$¤]?[\d,.]+%?$/; - - trimRegExp = /^\s+|\s+$/g; - - clickEvents = ['click']; - - touchDevice = 'ontouchstart' in document.documentElement; - - if (touchDevice) { - clickEvents.push('touchstart'); - } - - addEventListener = function(el, event, handler) { - if (el.addEventListener != null) { - return el.addEventListener(event, handler, false); - } else { - return el.attachEvent("on" + event, handler); - } - }; - - sortable = { - init: function(options) { - var table, tables, _i, _len, _results; - if (options == null) { - options = {}; - } - if (options.selector == null) { - options.selector = SELECTOR; - } - tables = document.querySelectorAll(options.selector); - _results = []; - for (_i = 0, _len = tables.length; _i < _len; _i++) { - table = tables[_i]; - _results.push(sortable.initTable(table)); - } - return _results; - }, - initTable: function(table) { - var i, th, ths, _i, _len, _ref; - if (((_ref = table.tHead) != null ? _ref.rows.length : void 0) !== 1) { - return; - } - if (table.getAttribute('data-sortable-initialized') === 'true') { - return; - } - table.setAttribute('data-sortable-initialized', 'true'); - ths = table.querySelectorAll('th'); - for (i = _i = 0, _len = ths.length; _i < _len; i = ++_i) { - th = ths[i]; - if (th.getAttribute('data-sortable') !== 'false') { - sortable.setupClickableTH(table, th, i); - } - } - return table; - }, - setupClickableTH: function(table, th, i) { - var eventName, onClick, type, _i, _len, _results; - type = sortable.getColumnType(table, i); - onClick = function(e) { - var compare, item, newSortedDirection, position, row, rowArray, sorted, sortedDirection, tBody, ths, value, _compare, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _m, _ref, _ref1; - if (e.handled !== true) { - e.handled = true; - } else { - return false; - } - sorted = this.getAttribute('data-sorted') === 'true'; - sortedDirection = this.getAttribute('data-sorted-direction'); - if (sorted) { - newSortedDirection = sortedDirection === 'ascending' ? 'descending' : 'ascending'; - } else { - newSortedDirection = type.defaultSortDirection; - } - ths = this.parentNode.querySelectorAll('th'); - for (_i = 0, _len = ths.length; _i < _len; _i++) { - th = ths[_i]; - th.setAttribute('data-sorted', 'false'); - th.removeAttribute('data-sorted-direction'); - } - this.setAttribute('data-sorted', 'true'); - this.setAttribute('data-sorted-direction', newSortedDirection); - tBody = table.tBodies[0]; - rowArray = []; - if (!sorted) { - if (type.compare != null) { - _compare = type.compare; - } else { - _compare = function(a, b) { - return b - a; - }; - } - compare = function(a, b) { - if (a[0] === b[0]) { - return a[2] - b[2]; - } - if (type.reverse) { - return _compare(b[0], a[0]); - } else { - return _compare(a[0], b[0]); - } - }; - _ref = tBody.rows; - for (position = _j = 0, _len1 = _ref.length; _j < _len1; position = ++_j) { - row = _ref[position]; - value = sortable.getNodeValue(row.cells[i]); - if (type.comparator != null) { - value = type.comparator(value); - } - rowArray.push([value, row, position]); - } - rowArray.sort(compare); - for (_k = 0, _len2 = rowArray.length; _k < _len2; _k++) { - row = rowArray[_k]; - tBody.appendChild(row[1]); - } - } else { - _ref1 = tBody.rows; - for (_l = 0, _len3 = _ref1.length; _l < _len3; _l++) { - item = _ref1[_l]; - rowArray.push(item); - } - rowArray.reverse(); - for (_m = 0, _len4 = rowArray.length; _m < _len4; _m++) { - row = rowArray[_m]; - tBody.appendChild(row); - } - } - if (typeof window['CustomEvent'] === 'function') { - return typeof table.dispatchEvent === "function" ? table.dispatchEvent(new CustomEvent('Sortable.sorted', { - bubbles: true - })) : void 0; - } - }; - _results = []; - for (_i = 0, _len = clickEvents.length; _i < _len; _i++) { - eventName = clickEvents[_i]; - _results.push(addEventListener(th, eventName, onClick)); - } - return _results; - }, - getColumnType: function(table, i) { - var row, specified, text, type, _i, _j, _len, _len1, _ref, _ref1, _ref2; - specified = (_ref = table.querySelectorAll('th')[i]) != null ? _ref.getAttribute('data-sortable-type') : void 0; - if (specified != null) { - return sortable.typesObject[specified]; - } - _ref1 = table.tBodies[0].rows; - for (_i = 0, _len = _ref1.length; _i < _len; _i++) { - row = _ref1[_i]; - text = sortable.getNodeValue(row.cells[i]); - _ref2 = sortable.types; - for (_j = 0, _len1 = _ref2.length; _j < _len1; _j++) { - type = _ref2[_j]; - if (type.match(text)) { - return type; - } - } - } - return sortable.typesObject.alpha; - }, - getNodeValue: function(node) { - var dataValue; - if (!node) { - return ''; - } - dataValue = node.getAttribute('data-value'); - if (dataValue !== null) { - return dataValue; - } - if (typeof node.innerText !== 'undefined') { - return node.innerText.replace(trimRegExp, ''); - } - return node.textContent.replace(trimRegExp, ''); - }, - setupTypes: function(types) { - var type, _i, _len, _results; - sortable.types = types; - sortable.typesObject = {}; - _results = []; - for (_i = 0, _len = types.length; _i < _len; _i++) { - type = types[_i]; - _results.push(sortable.typesObject[type.name] = type); - } - return _results; - } - }; - - sortable.setupTypes([ - { - name: 'numeric', - defaultSortDirection: 'descending', - match: function(a) { - return a.match(numberRegExp); - }, - comparator: function(a) { - return parseFloat(a.replace(/[^0-9.-]/g, ''), 10) || 0; - } - }, { - name: 'date', - defaultSortDirection: 'ascending', - reverse: true, - match: function(a) { - return !isNaN(Date.parse(a)); - }, - comparator: function(a) { - return Date.parse(a) || 0; - } - }, { - name: 'alpha', - defaultSortDirection: 'ascending', - match: function() { - return true; - }, - compare: function(a, b) { - return a.localeCompare(b); - } - } - ]); - - setTimeout(sortable.init, 0); - - if (typeof define === 'function' && define.amd) { - define(function() { - return sortable; - }); - } else if (typeof exports !== 'undefined') { - module.exports = sortable; - } else { - window.Sortable = sortable; - } - -}).call(this); diff --git a/data/web/json_api.php b/data/web/json_api.php index f0715466..42918163 100644 --- a/data/web/json_api.php +++ b/data/web/json_api.php @@ -1,169 +1,323 @@ <?php +/* +edit/alias => POST data: + { + address: {a, b, c}, (where a, b, c represent alias addresses) + active: 1 (0 or 1) + } + +delete/alias => POST data: + { + address: {a, b, c}, (where a, b, c represent alias addresses) + } + +*/ +header('Content-Type: application/json'); require_once 'inc/prerequisites.inc.php'; -error_reporting(E_ALL); +error_reporting(0); if (isset($_SESSION['mailcow_cc_role']) || isset($_SESSION['pending_mailcow_cc_username'])) { - if (isset($_GET['action'])) { - $action = $_GET['action']; + if (isset($_GET['query'])) { + + $query = explode('/', $_GET['query']); + $action = (isset($query[0])) ? $query[0] : null; + $category = (isset($query[1])) ? $query[1] : null; + $object = (isset($query[2])) ? $query[2] : null; + switch ($action) { - case "domain_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $data[] = mailbox_get_domain_details($domain); - } - if (!isset($data) || empty($data)) { + case "get": + switch ($category) { + case "domain": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $data[] = mailbox_get_domain_details($domain); + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_domain_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } + break; + case "mailbox": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $mailboxes = mailbox_get_mailboxes($domain); + if (!empty($mailboxes)) { + foreach ($mailboxes as $mailbox) { + $data[] = mailbox_get_mailbox_details($mailbox); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_mailbox_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_mailbox_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + + } + break; + case "resource": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $resources = mailbox_get_resources($domain); + if (!empty($resources)) { + foreach ($resources as $resource) { + $data[] = mailbox_get_resource_details($resource); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_resource_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_resource_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + + } + break; + case "alias-domain": + switch ($object) { + case "all": + $domains = mailbox_get_domains(); + if (!empty($domains)) { + foreach ($domains as $domain) { + $alias_domains = mailbox_get_alias_domains($domain); + if (!empty($alias_domains)) { + foreach ($alias_domains as $alias_domain) { + $data[] = mailbox_get_alias_domain_details($alias_domain); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_alias_domains($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_alias_domains($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } + break; + case "alias": + switch ($object) { + case "all": + $domains = array_merge(mailbox_get_domains(), mailbox_get_alias_domains()); + if (!empty($domains)) { + foreach ($domains as $domain) { + $aliases = mailbox_get_aliases($domain); + if (!empty($aliases)) { + foreach ($aliases as $alias) { + $data[] = mailbox_get_alias_details($alias); + } + } + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = mailbox_get_alias_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(mailbox_get_alias_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } + break; + case "domain-admin": + switch ($object) { + case "all": + $domain_admins = get_domain_admins(); + if (!empty($domain_admins)) { + foreach ($domain_admins as $domain_admin) { + $data[] = get_domain_admin_details($domain_admin); + } + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + } + else { + echo '{}'; + } + break; + + default: + $data = get_domain_admin_details($object); + if (!isset($data) || empty($data)) { + echo '{}'; + } + else { + echo json_encode(get_domain_admin_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); + } + break; + } + break; + case "u2f-registration": + header('Content-Type: application/javascript'); + if (($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") && $_SESSION["mailcow_cc_username"] == $object) { + $data = $u2f->getRegisterData(get_u2f_registrations($object)); + list($req, $sigs) = $data; + $_SESSION['regReq'] = json_encode($req); + echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';'; + } + else { + return; + } + break; + case "u2f-authentication": + header('Content-Type: application/javascript'); + if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) { + $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object))); + $_SESSION['authReq'] = $reqs; + echo 'var req = ' . $reqs . ';'; + } + else { + return; + } + break; + default: echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + break; } - else { - echo '{}'; - } - break; - case "mailbox_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $mailboxes = mailbox_get_mailboxes($domain); - if (!empty($mailboxes)) { - foreach ($mailboxes as $mailbox) { - $data[] = mailbox_get_mailbox_details($mailbox); + break; + case "delete": + switch ($category) { + case "alias": + if (isset($_POST['address'])) { + $address = json_decode($_POST['address'], true); + if (is_array($address)) { + if (mailbox_delete_alias(array('address' => $address)) === false) { + echo json_encode(array( + 'type' => 'error', + 'message' => 'Deletion of item failed' + )); + exit(); + } + echo json_encode(array( + 'type' => 'success', + 'message' => 'Task completed' + )); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + else { + echo json_encode(array( + 'type' => 'error', + 'message' => 'Cannot find address array in post data' + )); + } + break; } - else { - echo '{}'; - } - break; - case "resource_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $resources = mailbox_get_resources($domain); - if (!empty($resources)) { - foreach ($resources as $resource) { - $data[] = mailbox_get_resource_details($resource); + break; + case "edit": + switch ($category) { + case "alias": + if (isset($_POST['address']) && isset($_POST['active'])) { + $address = json_decode($_POST['address'], true); + if (is_array($address)) { + if (mailbox_edit_alias(array('address' => $address, 'active' => ($_POST['active'] == "1") ? $active = 1 : null)) === false) { + echo json_encode(array( + 'type' => 'error', + 'message' => 'Edit item failed' + )); + exit(); + } + echo json_encode(array( + 'type' => 'success', + 'message' => 'Task completed' + )); } } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - } - else { - echo '{}'; - } - break; - case "domain_alias_table_data": - $domains = mailbox_get_domains(); - if (!empty($domains)) { - foreach ($domains as $domain) { - $alias_domains = mailbox_get_alias_domains($domain); - if (!empty($alias_domains)) { - foreach ($alias_domains as $alias_domain) { - $data[] = mailbox_get_alias_domain_details($alias_domain); - } + else { + echo json_encode(array( + 'type' => 'error', + 'message' => 'Cannot find address array in post data' + )); } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } + break; } - else { - echo '{}'; - } - break; - case "alias_table_data": - $domains = array_merge(mailbox_get_domains(), mailbox_get_alias_domains()); - if (!empty($domains)) { - foreach ($domains as $domain) { - $aliases = mailbox_get_aliases($domain); - if (!empty($aliases)) { - foreach ($aliases as $alias) { - $data[] = mailbox_get_alias_details($alias); - } - } - } - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - } - else { - echo '{}'; - } - break; - case "get_mailbox_details": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - $data = mailbox_get_mailbox_details($object); - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode(mailbox_get_mailbox_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - break; - case "get_domain_details": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - $data = mailbox_get_domain_details($object); - if (!isset($data) || empty($data)) { - echo '{}'; - } - else { - echo json_encode(mailbox_get_domain_details($object), JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT); - } - break; - case "get_u2f_reg_challenge": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - if ( - ($_SESSION["mailcow_cc_role"] == "admin" || $_SESSION["mailcow_cc_role"] == "domainadmin") - && - ($_SESSION["mailcow_cc_username"] == $object) - ) { - $data = $u2f->getRegisterData(get_u2f_registrations($object)); - list($req, $sigs) = $data; - $_SESSION['regReq'] = json_encode($req); - echo 'var req = ' . json_encode($req) . '; var sigs = ' . json_encode($sigs) . ';'; - } - else { - echo '{}'; - } - break; - case "get_u2f_auth_challenge": - if (!isset($_GET['object'])) { return false; } - $object = $_GET['object']; - if (isset($_SESSION['pending_mailcow_cc_username']) && $_SESSION['pending_mailcow_cc_username'] == $object) { - $reqs = json_encode($u2f->getAuthenticateData(get_u2f_registrations($object))); - $_SESSION['authReq'] = $reqs; - echo 'var req = ' . $reqs . ';'; - } - else { - echo '{}'; - } - break; - default: - echo '{}'; - break; + break; } } -} \ No newline at end of file +} diff --git a/data/web/lang/lang.de.php b/data/web/lang/lang.de.php index 077882d1..947bb267 100644 --- a/data/web/lang/lang.de.php +++ b/data/web/lang/lang.de.php @@ -9,6 +9,12 @@ $lang['header']['restart_sogo'] = 'SOGo neustarten'; $lang['footer']['restart_sogo'] = 'SOGo neustarten'; $lang['footer']['restart_now'] = 'Jetzt neustarten'; $lang['footer']['restart_sogo_info'] = 'Einige Änderungen an Domains benötigen einen Neustart SOGos. Hier können Sie SOGo neustarten.<br /><br /><b>Wichtig:</b> Ein korrekter Neustart SOGos kann eine Weile in Anspruch nehmen, bitte warten Sie, bis der Prozess vollständig beendet wurde.'; + +$lang['footer']['confirm_delete'] = 'Löschen bestätigen'; +$lang['footer']['delete_these_items'] = 'Sind Sie sicher, dass die folgenden Elemente entfernt werden sollen?'; +$lang['footer']['delete_now'] = 'Jetzt löschen'; +$lang['footer']['cancel'] = 'Abbrechen'; + $lang['dkim']['confirm'] = 'Sind Sie sicher?'; $lang['danger']['dkim_not_found'] = 'DKIM-Key nicht gefunden'; $lang['danger']['dkim_remove_failed'] = 'Kann DKIM-Key nicht entfernen'; @@ -36,6 +42,7 @@ $lang['danger']['object_exists'] = 'Objekt %s existiert bereits'; $lang['danger']['domain_exists'] = 'Domain %s existiert bereits'; $lang['danger']['alias_goto_identical'] = 'Alias- und Ziel-Adresse dürfen nicht identisch sein'; $lang['danger']['aliasd_targetd_identical'] = 'Alias-Domain darf nicht gleich Ziel-Domain sein'; +$lang['danger']['maxquota_empty'] = 'Max. Speicherplatz pro Mailbox darf nicht 0 sein.'; $lang['success']['alias_added'] = 'Alias-Adresse(n) wurden angelegt'; $lang['success']['alias_modified'] = 'Änderungen an Alias %s wurden gespeichert'; $lang['success']['aliasd_modified'] = 'Änderungen an Alias-Domain %s wurden gespeichert'; @@ -70,6 +77,7 @@ $lang['danger']['is_spam_alias'] = '%s lautet bereits eine Spam-Alias-Adresse'; $lang['danger']['quota_not_0_not_numeric'] = 'Speicherplatz muss numerisch und >= 0 sein'; $lang['danger']['domain_not_found'] = 'Domain "%s" nicht gefunden.'; $lang['danger']['max_mailbox_exceeded'] = 'Anzahl an Mailboxen überschritten (%d von %d)'; +$lang['danger']['max_alias_exceeded'] = 'Anzahl an Alias-Adressen überschritten'; $lang['danger']['mailbox_quota_exceeded'] = 'Speicherplatz überschreitet das Limit (max. %d MiB)'; $lang['danger']['mailbox_quota_left_exceeded'] = 'Nicht genügend Speicherplatz vorhanden (Speicherplatz anwendbar: %d MiB)'; $lang['success']['mailbox_added'] = 'Mailbox %s wurde angelegt'; @@ -145,7 +153,7 @@ $lang['user']['spamfilter_default_score'] = 'Standardwert:'; $lang['user']['spamfilter_hint'] = 'Der erste Wert beschreibt den "low spam score", der zweite Wert den "high spam score".'; $lang['user']['spamfilter_table_domain_policy'] = "n.v. (Domainrichtlinie)"; -$lang['user']['tls_policy_warning'] = '<strong>Vorsicht:</strong> Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.<br />Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.'; +$lang['user']['tls_policy_warning'] = '<strong>Vorsicht:</strong> Entscheiden Sie sich unverschlüsselte Verbindungen abzulehnen, kann dies dazu führen, dass Kontakte Sie nicht mehr erreichen.<br />Nachrichten, die die Richtlinie nicht erfüllen, werden durch einen Hard-Fail im Mailsystem abgewiesen.<br />Diese Einstellung ist aktiv für die primäre Mailbox, für alle Alias-Adressen, die dieser Mailbox <b>direkt zugeordnet</b> sind (lediglich eine einzige Ziel-Adresse) und der Adressen, die sich aus Alias-Domains ergeben. Ausgeschlossen sind temporäre Aliasse ("Spam-Alias-Adressen"), Catch-All Alias-Adressen sowie Alias-Adressen mit mehreren Zielen.'; $lang['user']['tls_policy'] = 'Verschlüsselungsrichtlinie'; $lang['user']['tls_enforce_in'] = 'TLS eingehend erzwingen'; $lang['user']['tls_enforce_out'] = 'TLS ausgehend erzwingen'; @@ -246,9 +254,13 @@ $lang['mailbox']['add_domain_alias'] = 'Domain-Alias hinzufügen'; $lang['mailbox']['add_mailbox'] = 'Mailbox hinzufügen'; $lang['mailbox']['add_resource'] = 'Ressource hinzufügen'; $lang['mailbox']['add_alias'] = 'Alias hinzufügen'; +$lang['mailbox']['empty'] = 'Keine Einträge vorhanden'; +$lang['mailbox']['toggle_all'] = 'Alle'; +$lang['mailbox']['quick_actions'] = 'Aktionen'; +$lang['mailbox']['activate'] = 'Aktivieren'; +$lang['mailbox']['deactivate'] = 'Deaktivieren'; $lang['info']['no_action'] = 'Keine Aktion anwendbar'; - $lang['delete']['title'] = 'Objekt entfernen'; $lang['delete']['remove_domain_warning'] = '<b>Warnung:</b> Sie entfernen die Domain <b>%s</b>!'; $lang['delete']['remove_domainalias_warning'] = '<b>Warnung:</b> Sie entfernen die Alias-Domain <b>%s</b>!'; @@ -326,6 +338,9 @@ $lang['add']['subfolder2'] = 'Synchronisation in Unterordner am Ziel'; $lang['add']['mins_interval'] = 'Abrufintervall (Minuten)'; $lang['add']['exclude'] = 'Elemente ausschließen (Regex)'; $lang['add']['delete2duplicates'] = 'Lösche Duplikate im Ziel'; +$lang['add']['delete1'] = 'Lösche Nachricht nach Übertragung vom Quell-Server'; +$lang['edit']['delete2duplicates'] = 'Lösche Duplikate im Ziel'; +$lang['edit']['delete1'] = 'Lösche Nachricht nach Übertragung vom Quell-Server'; $lang['add']['title'] = 'Objekt anlegen'; $lang['add']['domain'] = 'Domain'; @@ -380,6 +395,7 @@ $lang['tfa']['tfa'] = "Zwei-Faktor-Authentifizierung"; $lang['tfa']['set_tfa'] = "Konfiguriere Zwei-Faktor-Authentifizierungsmethode"; $lang['tfa']['yubi_otp'] = "Yubico OTP Authentifizierung"; $lang['tfa']['key_id'] = "Ein Name für diesen YubiKey"; +$lang['tfa']['key_id_totp'] = "Ein eindeutiger Name"; $lang['tfa']['api_register'] = 'mailcow verwendet die Yubico Cloud API. Ein API-Key für den Yubico Stick kann <a href="https://upgrade.yubico.com/getapikey/" target="_blank">hier</a> bezogen werden.'; $lang['tfa']['u2f'] = "U2F Authentifizierung"; $lang['tfa']['hotp'] = "HOTP Authentifizierung"; @@ -390,10 +406,14 @@ $lang['tfa']['disable_tfa'] = "Deaktiviere 2FA bis zur nächsten erfolgreichen A $lang['tfa']['confirm_tfa'] = "Bitte bestätigen Sie Ihr Einmal-Passwort im unteren Feld"; $lang['tfa']['confirm'] = "Bestätigen"; $lang['tfa']['otp'] = "Einmalpasswort"; +$lang['tfa']['totp'] = "Time-based OTP (Google Authenticator etc.)"; $lang['tfa']['trash_login'] = "Login verwerfen"; $lang['tfa']['select'] = "Bitte auswählen"; $lang['tfa']['waiting_usb_auth'] = "<i>Warte auf USB-Gerät...</i><br /><br />Bitte jetzt den vorgesehenen Taster des U2F USB-Gerätes berühren."; $lang['tfa']['waiting_usb_register'] = "<i>Warte auf USB-Gerät...</i><br /><br />Bitte zuerst das obere Passwortfeld ausfüllen und erst dann den vorgesehenen Taster des U2F USB-Gerätes berühren."; +$lang['tfa']['scan_qr_code'] = "Bitte scannen Sie jetzt den angezeigten QR-Code:."; +$lang['tfa']['enter_qr_code'] = "Falls Sie den angezeigten QR-Code nicht scannen können, verwenden Sie bitte nachstehenden Sicherheitsschlüssel"; +$lang['tfa']['confirm_totp_token'] = "Bitte bestätigen Sie die Änderung durch Eingabe eines generierten Tokens"; $lang['admin']['search_domain_da'] = 'Domains durchsuchen'; $lang['admin']['restrictions'] = 'Postfix Restriktionen'; @@ -444,9 +464,20 @@ $lang['admin']['unchanged_if_empty'] = 'Unverändert, wenn leer'; $lang['admin']['yes'] = '✔'; $lang['admin']['no'] = '✘'; $lang['admin']['access'] = 'Zugang'; -$lang['admin']['invalid_max_msg_size'] = 'Maximale Nachrichtengröße ungültig'; // NEEDS TRANSLATION +$lang['admin']['invalid_max_msg_size'] = 'Maximale Nachrichtengröße ungültig'; $lang['admin']['site_not_found'] = 'Kann mailcow Seitenkonfiguration nicht finden'; -$lang['admin']['public_folder_empty'] = 'Öffentlicher Ordner-Name darf nicht leer sein'; // NEEDS TRANSLATION +$lang['admin']['public_folder_empty'] = 'Öffentlicher Ordner-Name darf nicht leer sein'; $lang['admin']['set_rr_failed'] = 'Kann Postfix Restriktionen nicht setzen'; $lang['admin']['no_record'] = 'Kein Eintrag'; +$lang['admin']['filter_table'] = 'Tabelle Filtern'; +$lang['admin']['empty'] = 'Keine Einträge vorhanden'; +$lang['admin']['forwarding_hosts'] = 'Weiterleitungs-Hosts'; +$lang['admin']['forwarding_hosts_hint'] = 'Eingehende Nachrichten werden von den hier gelisteten Hosts bedingungslos akzeptiert. Diese Hosts werden dann nicht mit DNSBLs abgeglichen oder Greylisting unterworfen. Von ihnen empfangener Spam wird nie abgelehnt und immer in den Spam-Ordner einsortiert. Die übliche Verwendung für diese Funktion ist, um Mailserver anzugeben, auf denen eine Weiterleitung zu Ihrem Mailcow-Server eingerichtet wurde.'; +$lang['admin']['forwarding_hosts_add_hint'] = 'Sie können entweder IPv4/IPv6-Adressen, Netzwerke in CIDR-Notation, Hostnamen (die zu IP-Adressen aufgelöst werden), oder Domainnamen (die zu IP-Adressen aufgelöst werden, indem ihr SPF-Record abgefragt wird oder, in dessen Abwesenheit, ihre MX-Records) angeben.'; +$lang['edit']['host'] = 'Host'; +$lang['edit']['source'] = 'Quelle'; +$lang['admin']['add_forwarding_host'] = 'Weiterleitungs-Host hinzufügen'; +$lang['delete']['remove_forwardinghost_warning'] = '<b>Warnung:</b> Sie entfernen den Weiterleitungs-Host <b>%s</b>!'; +$lang['success']['forwarding_host_removed'] = "Weiterleitungs-Host %s wurde entfernt"; +$lang['success']['forwarding_host_added'] = "Weiterleitungs-Host %s wurde hinzugefügt"; ?> diff --git a/data/web/lang/lang.en.php b/data/web/lang/lang.en.php index 2e0487b2..ea573f49 100644 --- a/data/web/lang/lang.en.php +++ b/data/web/lang/lang.en.php @@ -9,6 +9,12 @@ $lang['header']['restart_sogo'] = 'Restart SOGo'; $lang['footer']['restart_sogo'] = 'Restart SOGo'; $lang['footer']['restart_now'] = 'Restart now'; $lang['footer']['restart_sogo_info'] = 'Some tasks, e.g. adding a domain, require you to restart SOGo to catch changes made in the mailcow UI.<br /><br /><b>Important:</b> A graceful restart may take a while to complete, please wait for it to finish.'; + +$lang['footer']['confirm_delete'] = 'Confirm deletion'; +$lang['footer']['delete_these_items'] = 'Are you sure you want to delete the following items?'; +$lang['footer']['delete_now'] = 'Delete now'; +$lang['footer']['cancel'] = 'Cancel'; + $lang['dkim']['confirm'] = "Are you sure?"; $lang['danger']['dkim_not_found'] = "DKIM key not found"; $lang['danger']['dkim_remove_failed'] = "Cannot remove selected DKIM key"; @@ -38,8 +44,9 @@ $lang['danger']['object_exists'] = "Object %s already exists"; $lang['danger']['domain_exists'] = "Domain %s already exists"; $lang['danger']['alias_goto_identical'] = "Alias and goto address must not be identical"; $lang['danger']['aliasd_targetd_identical'] = "Alias domain must not be equal to target domain"; +$lang['danger']['maxquota_empty'] = 'Max. quota per mailbox must not be 0.'; $lang['success']['alias_added'] = "Alias address/es has/have been added"; -$lang['success']['alias_modified'] = "Changes to alias have been saved"; +$lang['success']['alias_modified'] = "Changes to alias/es %s have been saved"; $lang['success']['aliasd_modified'] = "Changes to alias domain have been saved"; $lang['success']['mailbox_modified'] = "Changes to mailbox %s have been saved"; $lang['success']['resource_modified'] = "Changes to mailbox %s have been saved"; @@ -72,12 +79,13 @@ $lang['danger']['is_spam_alias'] = "%s is already known as a spam alias address" $lang['danger']['quota_not_0_not_numeric'] = "Quota must be numeric and >= 0"; $lang['danger']['domain_not_found'] = "Domain not found."; $lang['danger']['max_mailbox_exceeded'] = "Max. mailboxes exceeded (%d of %d)"; +$lang['danger']['max_alias_exceeded'] = 'Max. aliases exceeded'; $lang['danger']['mailbox_quota_exceeded'] = "Quota exceeds the domain limit (max. %d MiB)"; $lang['danger']['mailbox_quota_left_exceeded'] = "Not enough space left (space left: %d MiB)"; $lang['success']['mailbox_added'] = "Mailbox %s has been added"; $lang['success']['resource_added'] = "Resource %s has been added"; $lang['success']['domain_removed'] = "Domain %s has been removed"; -$lang['success']['alias_removed'] = "Alias-Adresse %s has been removed"; +$lang['success']['alias_removed'] = "Alias %s has been removed"; $lang['success']['alias_domain_removed'] = "Alias domain %s has been removed"; $lang['success']['domain_admin_removed'] = "Domain administrator %s has been removed"; $lang['success']['mailbox_removed'] = "Mailbox %s has been removed"; @@ -147,7 +155,7 @@ $lang['user']['spamfilter_default_score'] = 'Default values:'; $lang['user']['spamfilter_hint'] = 'The first value describes the "low spam score", the second represents the "high spam score".'; $lang['user']['spamfilter_table_domain_policy'] = "n/a (domain policy)"; -$lang['user']['tls_policy_warning'] = '<strong>Warning:</strong> If you decide to enforce encrypted mail transfer, you may lose emails.<br />Messages to not satisfy the policy will be bounced with a hard fail by the mail system.'; +$lang['user']['tls_policy_warning'] = '<strong>Warning:</strong> If you decide to enforce encrypted mail transfer, you may lose emails.<br />Messages to not satisfy the policy will be bounced with a hard fail by the mail system.<br />This option applies to your primary email address (login name), all addresses derived from alias domains as well as alias addresses <b>with only this single mailbox</b> as target.'; $lang['user']['tls_policy'] = 'Encryption policy'; $lang['user']['tls_enforce_in'] = 'Enforce TLS incoming'; $lang['user']['tls_enforce_out'] = 'Enforce TLS outgoing'; @@ -249,6 +257,11 @@ $lang['mailbox']['add_mailbox'] = 'Add mailbox'; $lang['mailbox']['add_resource'] = 'Add resource'; $lang['mailbox']['add_alias'] = 'Add alias'; $lang['mailbox']['add_domain_record_first'] = 'Please add a domain first'; +$lang['mailbox']['empty'] = 'No results'; +$lang['mailbox']['toggle_all'] = 'Toggle all'; +$lang['mailbox']['quick_actions'] = 'Quick actions'; +$lang['mailbox']['activate'] = 'Activate'; +$lang['mailbox']['deactivate'] = 'Deactivate'; $lang['info']['no_action'] = 'No action applicable'; @@ -330,6 +343,9 @@ $lang['add']['maxage'] = 'Maximum age of messages that will be polled from remot $lang['add']['subfolder2'] = 'Sync into subfolder on destination'; $lang['add']['exclude'] = 'Exclude objects (regex)'; $lang['add']['delete2duplicates'] = 'Delete duplicates on destination'; +$lang['add']['delete1'] = 'Delete from source when completed'; +$lang['edit']['delete2duplicates'] = 'Delete duplicates on destination'; +$lang['edit']['delete1'] = 'Delete from source when completed'; $lang['add']['title'] = 'Add object'; $lang['add']['domain'] = 'Domain'; @@ -384,6 +400,7 @@ $lang['tfa']['tfa'] = "Two-factor authentication"; $lang['tfa']['set_tfa'] = "Set two-factor authentication method"; $lang['tfa']['yubi_otp'] = "Yubico OTP authentication"; $lang['tfa']['key_id'] = "An identifier for your YubiKey"; +$lang['tfa']['key_id_totp'] = "An identifier for your key"; $lang['tfa']['api_register'] = 'mailcow uses the Yubico Cloud API. Please get an API key for your key <a href="https://upgrade.yubico.com/getapikey/" target="_blank">here</a>'; $lang['tfa']['u2f'] = "U2F authentication"; $lang['tfa']['hotp'] = "HOTP authentication"; @@ -394,10 +411,14 @@ $lang['tfa']['disable_tfa'] = "Disable TFA until next successful login"; $lang['tfa']['confirm_tfa'] = "Please confirm your one-time password in the below field"; $lang['tfa']['confirm'] = "Confirm"; $lang['tfa']['otp'] = "One-time password"; +$lang['tfa']['totp'] = "Time-based OTP (Google Authenticator etc.)"; $lang['tfa']['trash_login'] = "Trash login"; $lang['tfa']['select'] = "Please select"; $lang['tfa']['waiting_usb_auth'] = "<i>Waiting for USB device...</i><br /><br />Please tap the button on your U2F USB device now."; $lang['tfa']['waiting_usb_register'] = "<i>Waiting for USB device...</i><br /><br />Please enter your password above and confirm your U2F registration by tapping the button on your U2F USB device."; +$lang['tfa']['scan_qr_code'] = "Please scan the following code with your authenticator app or enter the code manually."; +$lang['tfa']['enter_qr_code'] = "Your TOTP code if your device cannot scan QR codes"; +$lang['tfa']['confirm_totp_token'] = "Please confirm your changes by entering the generated token"; $lang['admin']['search_domain_da'] = 'Search domains'; $lang['admin']['restrictions'] = 'Postfix Restrictions'; @@ -460,4 +481,15 @@ $lang['admin']['site_not_found'] = 'Cannot locate mailcow site configuration'; $lang['admin']['public_folder_empty'] = 'Public folder name must not be empty'; $lang['admin']['set_rr_failed'] = 'Cannot set Postfix restrictions'; $lang['admin']['no_record'] = 'No record'; +$lang['admin']['filter_table'] = 'Filter table'; +$lang['admin']['empty'] = 'No results'; +$lang['admin']['forwarding_hosts'] = 'Forwarding Hosts'; +$lang['admin']['forwarding_hosts_hint'] = 'Incoming messages are unconditionally accepted from any hosts listed here. These hosts are then not checked against DNSBLs or subjected to greylisting. Spam received from them is never rejected and always filed into the Junk folder. The most common use for this is to specify mail servers on which you have set up a rule that forwards incoming emails to your Mailcow server.'; +$lang['admin']['forwarding_hosts_add_hint'] = 'You can either specify IPv4/IPv6 addresses, networks in CIDR notation, host names (which will be resolved to IP addresses), or domain names (which will be resolved to IP addresses by querying SPF records or, in their absence, MX records).'; +$lang['edit']['host'] = 'Host'; +$lang['edit']['source'] = 'Source'; +$lang['admin']['add_forwarding_host'] = 'Add Forwarding Host'; +$lang['delete']['remove_forwardinghost_warning'] = '<b>Warning:</b> You are about to remove the forwarding host <b>%s</b>!'; +$lang['success']['forwarding_host_removed'] = "Forwarding host %s has been removed"; +$lang['success']['forwarding_host_added'] = "Forwarding host %s has been added"; ?> diff --git a/data/web/lang/lang.ru.php b/data/web/lang/lang.ru.php index e2502b83..d07bbc9f 100644 --- a/data/web/lang/lang.ru.php +++ b/data/web/lang/lang.ru.php @@ -321,6 +321,7 @@ $lang['add']['maxage'] = 'Maximum age of messages that will be polled from remot $lang['add']['subfolder2'] = "Синхронизировать в подпапку по назначению"; $lang['add']['exclude'] = "Исключить объекты (regex)"; $lang['add']['delete2duplicates'] = "Удалить дубликаты в получателях"; +$lang['edit']['delete2duplicates'] = "Удалить дубликаты в получателях"; $lang['add']['title'] = "Добавить объект"; $lang['add']['domain'] = "Домен"; $lang['add']['active'] = "Активный"; diff --git a/data/web/mailbox.php b/data/web/mailbox.php index bc8ca6d2..864ea676 100644 --- a/data/web/mailbox.php +++ b/data/web/mailbox.php @@ -5,113 +5,120 @@ if (isset($_SESSION['mailcow_cc_role']) && ($_SESSION['mailcow_cc_role'] == "adm require_once "inc/header.inc.php"; $_SESSION['return_to'] = $_SERVER['REQUEST_URI']; ?> -<style> -table.footable>tbody>tr.footable-empty>td { - font-size:15px !important; - font-style:italic; -} -.pagination a { - text-decoration: none !important; -} -.panel panel-default { - overflow: visible !important; -} -.table-responsive { - overflow: visible !important; -} -.footer-add-item { - text-align:center; - font-style: italic; - display:block; - padding: 10px; -} -</style> <div class="container"> - <div class="row"> - <div class="col-md-12"> - <div class="panel panel-default"> - <div class="panel-heading"> - <h3 class="panel-title"><?=$lang['mailbox']['domains'];?></h3> - <div class="pull-right"> - <?php - if ($_SESSION['mailcow_cc_role'] == "admin"): - ?> - <a href="/add.php?domain"><span class="glyphicon glyphicon-plus"></span></a> - <?php - endif; - ?> - </div> - </div> - <div class="table-responsive"> - <table id="domain_table" class="table table-striped"></table> - </div> - <span class="footer-add-item"><a href="/add.php?domain"><?=$lang['mailbox']['add_domain'];?></a></span> - </div> - </div> - </div> - <div class="row"> - <div class="col-md-12"> - <div class="panel panel-default"> - <div class="panel-heading"> - <h3 class="panel-title"><?=$lang['mailbox']['mailboxes'];?></h3> - <div class="pull-right"> - <a href="/add.php?mailbox"><span class="glyphicon glyphicon-plus"></span></a> - </div> - </div> - <div class="table-responsive"> - <table id="mailbox_table" class="table table-striped"></table> - </div> - <span class="footer-add-item"><a href="/add.php?mailbox"><?=$lang['mailbox']['add_mailbox'];?></a></span> - </div> - </div> - </div> - <div class="row"> - <div class="col-md-12"> - <div class="panel panel-default"> - <div class="panel-heading"> - <h3 class="panel-title"><?=$lang['mailbox']['resources'];?></h3> - <div class="pull-right"> - <a href="/add.php?resource"><span class="glyphicon glyphicon-plus"></span></a> - </div> - </div> - <div class="table-responsive"> - <table id="resources_table" class="table table-striped"></table> - </div> - <span class="footer-add-item"><a href="/add.php?resource"><?=$lang['mailbox']['add_resource'];?></a></span> </div> - </div> - </div> - <div class="row"> - <div class="col-md-12"> - <div class="panel panel-default"> - <div class="panel-heading"> - <h3 class="panel-title"><?=$lang['mailbox']['domain_aliases'];?></h3> - <div class="pull-right"> - <a href="/add.php?aliasdomain"><span class="glyphicon glyphicon-plus"></span></a> - </div> - </div> - <div class="table-responsive"> - <table id="aliasdomain_table" class="table table-striped"></table> - </div> - <span class="footer-add-item"><a href="/add.php?aliasdomain"><?=$lang['mailbox']['add_domain_alias'];?></a></span> </div> - </div> - </div> + + <ul class="nav nav-tabs" role="tablist"> + <li role="presentation" class="active"><a href="#tab-domains" aria-controls="tab-domains" role="tab" data-toggle="tab"><?=$lang['mailbox']['domains'];?></a></li> + <li role="presentation"><a href="#tab-mailboxes" aria-controls="tab-mailboxes" role="tab" data-toggle="tab"><?=$lang['mailbox']['mailboxes'];?></a></li> + <li role="presentation"><a href="#tab-resources" aria-controls="tab-resources" role="tab" data-toggle="tab"><?=$lang['mailbox']['resources'];?></a></li> + <li class="dropdown"> + <a class="dropdown-toggle" data-toggle="dropdown" href="#"><?=$lang['mailbox']['aliases'];?> + <span class="caret"></span></a> + <ul class="dropdown-menu"> + <li role="presentation"><a href="#tab-mbox-aliases" aria-controls="tab-mbox-aliases" role="tab" data-toggle="tab"><?=$lang['mailbox']['aliases'];?></a></li> + <li role="presentation"><a href="#tab-domain-aliases" aria-controls="tab-domain-aliases" role="tab" data-toggle="tab"><?=$lang['mailbox']['domain_aliases'];?></a></li> + </ul> + </li> + </ul> <div class="row"> <div class="col-md-12"> - <div class="panel panel-default"> - <div class="panel-heading"> - <h3 class="panel-title"><?=$lang['mailbox']['aliases'];?></h3> - <div class="pull-right"> - <a href="/add.php?alias"><span class="glyphicon glyphicon-plus"></span></a> - </div> - </div> - <div class="table-responsive"> - <table id="alias_table" class="table table-striped"></table> + <div class="tab-content" style="padding-top:20px"> + <div role="tabpanel" class="tab-pane active" id="tab-domains"> + <div class="panel panel-default"> + <div class="panel-heading"> + <div class="pull-right"> + <?php + if ($_SESSION['mailcow_cc_role'] == "admin"): + ?> + <a href="/add.php?domain"><span class="glyphicon glyphicon-plus"></span></a> + <?php + endif; + ?> + </div> + <h3 class="panel-title"><?=$lang['mailbox']['domains'];?></h3> + </div> + <div class="table-responsive"> + <table id="domain_table" class="table table-striped"></table> + </div> + <span class="footer-add-item"><a href="/add.php?domain"><?=$lang['mailbox']['add_domain'];?></a></span> + </div> </div> - <span class="footer-add-item"><a href="/add.php?alias"><?=$lang['mailbox']['add_alias'];?></a></span> </div> - </div> - </div> + + <div role="tabpanel" class="tab-pane" id="tab-mailboxes"> + <div class="panel panel-default"> + <div class="panel-heading"> + <div class="pull-right"> + <a href="/add.php?mailbox"><span class="glyphicon glyphicon-plus"></span></a> + </div> + <h3 class="panel-title"><?=$lang['mailbox']['mailboxes'];?></h3> + </div> + <div class="table-responsive"> + <table id="mailbox_table" class="table table-striped"></table> + </div> + <span class="footer-add-item"><a href="/add.php?mailbox"><?=$lang['mailbox']['add_mailbox'];?></a></span> + </div> + </div> + + <div role="tabpanel" class="tab-pane" id="tab-resources"> + <div class="panel panel-default"> + <div class="panel-heading"> + <div class="pull-right"> + <a href="/add.php?resource"><span class="glyphicon glyphicon-plus"></span></a> + </div> + <h3 class="panel-title"><?=$lang['mailbox']['resources'];?></h3> + </div> + <div class="table-responsive"> + <table id="resources_table" class="table table-striped"></table> + </div> + <span class="footer-add-item"><a href="/add.php?resource"><?=$lang['mailbox']['add_resource'];?></a></span> + </div> + </div> + + <div role="tabpanel" class="tab-pane" id="tab-domain-aliases"> + <div class="panel panel-default"> + <div class="panel-heading"> + <div class="pull-right"> + <a href="/add.php?aliasdomain"><span class="glyphicon glyphicon-plus"></span></a> + </div> + <h3 class="panel-title"><?=$lang['mailbox']['domain_aliases'];?></h3> + </div> + <div class="table-responsive"> + <table id="aliasdomain_table" class="table table-striped"></table> + </div> + <span class="footer-add-item"><a href="/add.php?aliasdomain"><?=$lang['mailbox']['add_domain_alias'];?></a></span> + </div> + </div> + + <div role="tabpanel" class="tab-pane" id="tab-mbox-aliases"> + <div class="panel panel-default"> + <div class="panel-heading"> + <a class="pull-right" href="/add.php?alias"><span class="glyphicon glyphicon-plus"></span></a> + <h3 class="panel-title"><?=$lang['mailbox']['aliases'];?></h3> + </div> + <div class="table-responsive"> + <table id="alias_table" class="table table-striped"></table> + </div> + <div class="mass-actions"> + <p id="select_all_aliases" class="mass-select-all"> + ↪ <?=$lang['mailbox']['toggle_all'];?> + </p> + </div> + <div class="footer-add-item"> + <a class="pull-right" href="/add.php?alias"><span class="glyphicon glyphicon-plus"></span></a> + <b><?=$lang['mailbox']['quick_actions'];?>:</b> + <a id="delete_selected_alias" href="#" class="mass-each-action"><?=$lang['mailbox']['remove'];?></a> | + <a id="activate_selected_alias" href="#" class="mass-each-action"><?=$lang['mailbox']['activate'];?></a> | + <a id="deactivate_selected_alias" href="#" class="mass-each-action"><?=$lang['mailbox']['deactivate'];?></a> + </div> + </div> + </div> + + </div> <!-- /tab-content --> + </div> <!-- /col-md-12 --> + </div> <!-- /row --> </div> <!-- /container --> + <script type='text/javascript'> <?php $lang_mailbox = json_encode($lang['mailbox']); diff --git a/data/web/u2f_api.php b/data/web/u2f_api.php deleted file mode 100644 index ddeb1ece..00000000 --- a/data/web/u2f_api.php +++ /dev/null @@ -1,156 +0,0 @@ -<?php -require_once('inc/prerequisites.inc.php'); -$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); -$pdo->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_OBJ); - -$u2f = new u2flib_server\U2F('https://' . $_SERVER['SERVER_NAME']); - -function getRegs($username) { - global $pdo; - $sel = $pdo->prepare("select * from tfa where username = ?"); - $sel->execute(array($username)); - return $sel->fetchAll(); -} -function addReg($username, $reg) { - global $pdo; - $ins = $pdo->prepare("INSERT INTO `tfa` (`username`, `keyHandle`, `publicKey`, `certificate`, `counter`) values (?, ?, ?, ?, ?)"); - $ins->execute(array($username, $reg->keyHandle, $reg->publicKey, $reg->certificate, $reg->counter)); -} -function updateReg($reg) { - global $pdo; - $upd = $pdo->prepare("update tfa set counter = ? where id = ?"); - $upd->execute(array($reg->counter, $reg->id)); -} -?> -<html> -<head> -<script src="js/u2f-api.js"></script> -<?php -if ($_SERVER['REQUEST_METHOD'] === 'POST') { - if ((empty($_POST['u2f_username'])) || (!isset($_POST['action']) && !isset($_POST['u2f_register_data']) && !isset($_POST['u2f_auth_data']))) { - print_r($_POST); - exit(); - } - else { - $username = $_POST['u2f_username']; - if (isset($_POST['action'])) { - switch($_POST['action']) { - case 'register': - try { - $data = $u2f->getRegisterData(getRegs($username)); - list($req, $sigs) = $data; - $_SESSION['regReq'] = json_encode($req); -?> -<script> -var req = <?=json_encode($req);?>; -var sigs = <?=json_encode($sigs);?>; -var username = "<?=$username;?>"; -setTimeout(function() { - console.log("Register: ", req); - u2f.register([req], sigs, function(data) { - var form = document.getElementById('u2f_form'); - var reg = document.getElementById('u2f_register_data'); - var user = document.getElementById('u2f_username'); - var status = document.getElementById('u2f_status'); - console.log("Register callback", data); - if (data.errorCode && data.errorCode != 0) { - var div = document.getElementById('u2f_return_code'); - div.innerHTML = 'Error code: ' + data.errorCode; - return; - } - reg.value = JSON.stringify(data); - user.value = username; - status.value = "1"; - form.submit(); - }); -}, 1000); -</script> -<?php - } - catch( Exception $e ) { - echo "U2F error: " . $e->getMessage(); - } - break; - - case 'authenticate': - try { - $reqs = json_encode($u2f->getAuthenticateData(getRegs($username))); - $_SESSION['authReq'] = $reqs; -?> -<script> -var req = <?=$reqs;?>; -var username = "<?=$username;?>"; -setTimeout(function() { - console.log("sign: ", req); - u2f.sign(req, function(data) { - var form = document.getElementById('u2f_form'); - var auth = document.getElementById('u2f_auth_data'); - var user = document.getElementById('u2f_username'); - console.log("Authenticate callback", data); - auth.value = JSON.stringify(data); - user.value = username; - form.submit(); - }); -}, 1000); -</script> -<?php - } - catch (Exception $e) { - echo "U2F error: " . $e->getMessage(); - } - break; - } - } - if (!empty($_POST['u2f_register_data'])) { - try { - $reg = $u2f->doRegister(json_decode($_SESSION['regReq']), json_decode($_POST['u2f_register_data'])); - addReg($username, $reg); - } - catch (Exception $e) { - echo "U2F error: " . $e->getMessage(); - } - finally { - echo "Success"; - $_SESSION['regReq'] = null; - } - } - if (!empty($_POST['u2f_auth_data'])) { - try { - $reg = $u2f->doAuthenticate(json_decode($_SESSION['authReq']), getRegs($username), json_decode($_POST['u2f_auth_data'])); - updateReg($reg); - } - catch (Exception $e) { - echo "U2F error: " . $e->getMessage(); - } - finally { - echo "Success"; - $_SESSION['authReq'] = null; - } - } - } -?> -</head> -<body> -<div id="u2f_return_code"></div> -<form method="POST" id="u2f_form"> -<input type="hidden" name="u2f_register_data" id="u2f_register_data"/> -<input type="hidden" name="u2f_auth_data" id="u2f_auth_data"/> -<input type="hidden" name="u2f_username" id="u2f_username"/><br/> -<input type="hidden" name="u2f_status" id="u2f_status"/><br/> -</form> -<?php -} -else { -?> -<form method="POST" id="post_form"> -Username: <input name="u2f_username" id="u2f_username"/><br/><hr> -Action: <br /> -<input value="register" name="action" type="radio"/> Register<br/> -<input value="authenticate" name="action" type="radio"/> Authenticate<br/> -<button type="submit">Submit!</button> - </form> -<?php -} -?> -</body> -</html> diff --git a/data/web/user.php b/data/web/user.php index 94f2a762..eaf35923 100644 --- a/data/web/user.php +++ b/data/web/user.php @@ -45,6 +45,7 @@ if (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == 'doma <select id="selectTFA" class="selectpicker" title="<?=$lang['tfa']['select'];?>"> <option value="yubi_otp"><?=$lang['tfa']['yubi_otp'];?></option> <option value="u2f"><?=$lang['tfa']['u2f'];?></option> + <option value="totp"><?=$lang['tfa']['totp'];?></option> <option value="none"><?=$lang['tfa']['none'];?></option> </select> </div> @@ -398,14 +399,14 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' </div> <div class="form-group"> <div class="col-sm-12"> - <button type="submit" id="edit_tls_policy" name="edit_tls_policy" class="btn btn-default"><?=$lang['user']['save_changes'];?></button> + <button type="submit" id="edit_tls_policy" name="edit_tls_policy" class="btn btn-success"><?=$lang['user']['save_changes'];?></button> </div> </div> </form> </div> <div role="tabpanel" class="tab-pane" id="Syncjobs"> <div class="table-responsive"> - <table class="table table-striped sortable-theme-bootstrap" data-sortable id="timelimitedaliases"> + <table class="table table-striped" id="timelimitedaliases"> <thead> <tr> <th class="sort-table" style="min-width: 96px;">Server:Port</th> @@ -416,7 +417,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' <th class="sort-table" style="min-width: 35px;"><?=$lang['user']['last_run'];?></th> <th class="sort-table" style="min-width: 35px;">Log</th> <th class="sort-table" style="max-width: 95px;"><?=$lang['user']['active'];?></th> - <th style="text-align: right; min-width: 200px;" data-sortable="false"><?=$lang['user']['action'];?></th> + <th style="text-align: right; min-width: 200px;"><?=$lang['user']['action'];?></th> </tr> </thead> <tbody> @@ -431,7 +432,7 @@ elseif (isset($_SESSION['mailcow_cc_role']) && $_SESSION['mailcow_cc_role'] == ' <td><?=htmlspecialchars($row['user1']);?></td> <td><?=($row['exclude'] == '') ? '✘' : '<code>' . $row['exclude'] . '</code>';?></td> <td><?=htmlspecialchars($row['mins_interval']);?> min</td> - <td><?=(empty($row['last_run'])) ? '✘' : htmlspecialchars(date($lang['user']['syncjob_full_date'], strtotime($row['last_run'] . ' UTC')));?></td> + <td><?=(empty($row['last_run'])) ? '✘' : htmlspecialchars(date($lang['user']['syncjob_full_date'], strtotime($row['last_run'])));?></td> <td> <?php if (empty($row['returned_text'])) { diff --git a/docker-compose.yml b/docker-compose.yml index 76ca1b6b..f2c80a4d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -55,6 +55,18 @@ services: aliases: - redis + clamd-mailcow: + image: mailcow/clamd + build: ./data/Dockerfiles/clamav + restart: always + dns: + - 172.22.1.254 + dns_search: mailcow-network + networks: + mailcow-network: + aliases: + - clamd + rspamd-mailcow: image: mailcow/rspamd build: ./data/Dockerfiles/rspamd @@ -151,10 +163,11 @@ services: depends_on: - bind9-mailcow volumes: - - ./data/conf/dovecot:/etc/dovecot + - ./data/conf/dovecot:/usr/local/etc/dovecot - ./data/assets/ssl:/etc/ssl/mail/:ro - ./data/conf/sogo/:/etc/sogo/ - vmail-vol-1:/var/vmail + - crypt-vol-1:/mail_crypt/ environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} @@ -184,6 +197,7 @@ services: - ./data/conf/postfix:/opt/postfix/conf - ./data/assets/ssl:/etc/ssl/mail/:ro - postfix-vol-1:/var/spool/postfix + - crypt-vol-1:/var/lib/zeyple environment: - DBNAME=${DBNAME} - DBUSER=${DBUSER} @@ -266,3 +280,4 @@ volumes: redis-vol-1: rspamd-vol-1: postfix-vol-1: + crypt-vol-1: diff --git a/docs/first_steps.md b/docs/first_steps.md deleted file mode 100644 index 7f835bc4..00000000 --- a/docs/first_steps.md +++ /dev/null @@ -1,173 +0,0 @@ -## SSL (and: How to use Let's Encrypt) - -mailcow dockerized comes with a snakeoil CA "mailcow" and a server certificate in `data/assets/ssl`. Please use your own trusted certificates. - -mailcow uses 3 domain names that should be covered by your new certificate: - -- ${MAILCOW_HOSTNAME} -- autodiscover.**example.org** -- autoconfig.**example.org** - -### Obtain multi-SAN certificate by Let's Encrypt - -This is just an example of how to obtain certificates with certbot. There are several methods! - -1\. Get the certbot client: -``` bash -wget https://dl.eff.org/certbot-auto -O /usr/local/sbin/certbot && chmod +x /usr/local/sbin/certbot -``` - -2\. Make sure you set `HTTP_BIND=0.0.0.0` and `HTTP_PORT=80` in `mailcow.conf` or setup a reverse proxy to enable connections to port 80. If you changed HTTP_BIND, then restart Nginx: -``` bash -docker-compose restart nginx-mailcow -``` - -3\. Request the certificate with the webroot method: -``` bash -cd /path/to/git/clone/mailcow-dockerized -source mailcow.conf -certbot certonly \ - --webroot \ - -w ${PWD}/data/web \ - -d ${MAILCOW_HOSTNAME} \ - -d autodiscover.example.org \ - -d autoconfig.example.org \ - --email you@example.org \ - --agree-tos -``` - -4\. Create hard links to the full path of the new certificates. Assuming you are still in the mailcow root folder: -``` bash -mv data/assets/ssl/cert.{pem,pem.backup} -mv data/assets/ssl/key.{pem,pem.backup} -ln $(readlink -f /etc/letsencrypt/live/${MAILCOW_HOSTNAME}/fullchain.pem) data/assets/ssl/cert.pem -ln $(readlink -f /etc/letsencrypt/live/${MAILCOW_HOSTNAME}/privkey.pem) data/assets/ssl/key.pem -``` - -5\. Restart affected containers: -``` -docker-compose restart postfix-mailcow dovecot-mailcow nginx-mailcow -``` - -When renewing certificates, run the last two steps (link + restart) as post-hook in a script. - -## Rspamd Web UI -At first you may want to setup Rspamds web interface which provides some useful features and information. - -1\. Generate a Rspamd controller password hash: -``` -docker-compose exec rspamd-mailcow rspamadm pw -``` - -2\. Replace the default hash in `data/conf/rspamd/override.d/worker-controller.inc` by your newly generated: -``` -enable_password = "myhash"; -``` - -You can use `password = "myhash";` instead of `enable_password` to disable write-access in the web UI. - -3\. Restart rspamd: -``` -docker-compose restart rspamd-mailcow -``` - -Open https://${MAILCOW_HOSTNAME}/rspamd in a browser and login! - -## Optional: Reverse proxy - -You don't need to change the Nginx site that comes with mailcow: dockerized. -mailcow: dockerized trusts the default gateway IP 172.22.1.1 as proxy. This is very important to control access to Rspamd's web UI. - -1\. Make sure you change HTTP_BIND and HTTPS_BIND in `mailcow.conf` to a local address and set the ports accordingly, for example: -``` bash -HTTP_BIND=127.0.0.1 -HTTP_PORT=8080 -HTTPS_PORT=127.0.0.1 -HTTPS_PORT=8443 -``` -** IMPORTANT: Do not use port 8081 ** - -Recreate affected containers by running `docker-compose up -d`. - -2\. Configure your local webserver as reverse proxy: - -### Apache 2.4 -``` apache -<VirtualHost *:443> - ServerName mail.example.org - ServerAlias autodiscover.example.org - ServerAlias autoconfig.example.org - - [...] - # You should proxy to a plain HTTP session to offload SSL processing - ProxyPass / http://127.0.0.1:8080/ - ProxyPassReverse / http://127.0.0.1:8080/ - ProxyPreserveHost Off - your-ssl-configuration-here - [...] - - # If you plan to proxy to a HTTPS host: - #SSLProxyEngine On - - # If you plan to proxy to an untrusted HTTPS host: - #SSLProxyVerify none - #SSLProxyCheckPeerCN off - #SSLProxyCheckPeerName off - #SSLProxyCheckPeerExpire off -</VirtualHost> -``` - -### Nginx -``` -server { - listen 443; - server_name mail.example.org autodiscover.example.org autoconfig.example.org; - - [...] - your-ssl-configuration-here - location / { - proxy_pass http://127.0.0.1:8080/; - proxy_redirect http://127.0.0.1:8080/ $scheme://$host:$server_port/; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - [...] -} -``` - -## Sender and receiver model - -When a mailbox is created, a user is allowed to send mail from and receive mail for his own mailbox address. - - Mailbox me@example.org is created. example.org is a primary domain. - Note: a mailbox cannot be created in an alias domain. - - me@example.org is only known as me@example.org. - me@example.org is allowed to send as me@example.org. - -We can add an alias domain for example.org: - - Alias domain alias.com is added and assigned to primary domain example.org. - me@example.org is now known as me@example.org and me@alias.com. - me@example.org is now allowed to send as me@example.org and me@alias.com. - -We can add aliases for a mailbox to receive mail for and to send from this new address. - -It is important to know, that you are not able to receive mail for `my-alias@my-alias-domain.tld`. You would need to create this particular alias. - - me@example.org is assigned the alias alias@example.org - me@example.org is now known as alias@example.org, me@alias.com, alias@example.org - - me@example.org is NOT known as alias@alias.com. - -Administrators and domain administrators can edit mailboxes to allow specific users to send as other mailbox users ("delegate" them). - -You can choose between mailbox users or completely disable the sender check for domains. - -### SOGo "mail from" addresses - -Mailbox users can, obviously, select their own mailbox address, as well as all alias addresses and aliases that exist through alias domains. - -If you want to select another _existing_ mailbox user as your "mail from" address, this user has to delegate you access through SOGo (see SOGo documentation). Moreover a mailcow (domain) administrator -needs to grant you access as described above. diff --git a/docs/images/logo.svg b/docs/images/logo.svg deleted file mode 100644 index ea3b2796..00000000 --- a/docs/images/logo.svg +++ /dev/null @@ -1,179 +0,0 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<!-- Generator: Adobe Illustrator 17.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> - -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - version="1.1" - id="Layer_1" - x="0px" - y="0px" - width="271.27399" - height="298.871" - viewBox="0 0 271.27398 298.871" - enable-background="new 0 0 1600 1200" - xml:space="preserve" - inkscape:version="0.91 r13725" - sodipodi:docname="logo.svg"><metadata - id="metadata144"><rdf:RDF><cc:Work - rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs - id="defs142" /><sodipodi:namedview - pagecolor="#ffffff" - bordercolor="#666666" - borderopacity="1" - objecttolerance="10" - gridtolerance="10" - guidetolerance="10" - inkscape:pageopacity="0" - inkscape:pageshadow="2" - inkscape:window-width="1203" - inkscape:window-height="1168" - id="namedview140" - showgrid="false" - inkscape:zoom="1.1125147" - inkscape:cx="151.70799" - inkscape:cy="136.82484" - inkscape:window-x="561" - inkscape:window-y="0" - inkscape:window-maximized="0" - inkscape:current-layer="g5" - fit-margin-top="0" - fit-margin-left="0" - fit-margin-right="0" - fit-margin-bottom="0" /><g - id="g3" - transform="translate(-648.292,-401.988)"><g - id="g5"><g - id="email" - transform="translate(0,-58)"><path - style="fill:#5a3620" - inkscape:connector-curvature="0" - id="path10" - d="m 890.306,557.81 29.26,11.373 0,172.027 c 0,9.753 -7.895,17.649 -17.638,17.649 l -235.998,0 c -9.743,0 -17.638,-7.896 -17.638,-17.649 l 0,-172.026 29.259,-8.937" /><path - style="fill:#fee70f;fill-opacity:0.89499996" - inkscape:connector-curvature="0" - id="path12" - d="M 758.871,656.221 649.49,747.45 c 2.507,6.648 8.901,11.409 16.44,11.409 l 235.998,0 c 7.536,0 13.933,-4.761 16.444,-11.409 l -107.402,-91.229 -52.099,0 z" /><g - id="g14"><path - style="fill:#f9e82d;fill-opacity:1" - inkscape:connector-curvature="0" - id="path16" - d="m 810.391,656.686 107.981,90.764 c -0.331,0.881 -0.744,1.726 -1.205,2.536 l 0.028,0.035 c 1.501,-2.596 2.371,-5.594 2.371,-8.81 l 0,-172.004 -109.175,87.479 z" /><path - style="fill:#f9e82d;fill-opacity:1" - inkscape:connector-curvature="0" - id="path18" - d="m 649.49,747.45 108.864,-90.764 -110.061,-87.479 0,172.003 c 0,3.216 0.876,6.214 2.367,8.81 l 0.039,-0.035 c -0.466,-0.809 -0.877,-1.654 -1.209,-2.535 z" /></g></g><path - style="opacity:0.1;fill:#3d5263" - inkscape:connector-curvature="0" - id="path26" - d="m 783.931,446.247 c -66.396,0 -120.223,53.827 -120.223,120.223 0,66.396 53.827,120.221 120.223,120.221 66.397,0 120.222,-53.825 120.222,-120.221 0,-66.395 -53.825,-120.223 -120.222,-120.223 z m -11.96,215.702 c -53.009,0 -95.982,-43.855 -95.982,-97.953 0,-54.098 42.973,-97.952 95.982,-97.952 53.007,0 95.98,43.855 95.98,97.952 -10e-4,54.098 -42.973,97.953 -95.98,97.953 z" /><g - id="g28"><g - id="g30"><polyline - style="fill:#3d5263" - id="polyline32" - points="691.144,492.5 673.257,540.276 686.55,605.582 712.496,631.852 " /><g - id="g34"><g - id="g36"><polyline - style="fill:#fef3df" - id="polyline38" - points="658.248,450.81 673.501,487.076 693.836,496.903 724.04,458.731 " /><g - id="g40"><path - style="fill:#b58765" - inkscape:connector-curvature="0" - id="path42" - d="m 710.634,473.205 c 0,0 -22.482,-25.556 -49.793,-18.975 0,0 4.667,34.118 46.349,44.019 l 2.61,8.533 c 0,0 -65.612,-9.689 -59.339,-67.593 0,0 49.008,-19.884 72.598,15.106" /><polyline - style="fill:#fef3df" - id="polyline44" - points="909.697,450.81 894.447,487.076 874.114,496.903 843.907,458.731 " /><path - style="fill:#b58765" - inkscape:connector-curvature="0" - id="path46" - d="m 857.314,473.205 c 0,0 22.48,-25.556 49.79,-18.975 0,0 -4.664,34.118 -46.347,44.019 l -2.613,8.533 c 0,0 65.611,-9.689 59.339,-67.593 0,0 -49.006,-19.884 -72.6,15.106" /></g></g><path - sodipodi:nodetypes="cccscccccc" - style="fill:#b58765" - inkscape:connector-curvature="0" - id="path48" - d="m 726.619,647.067 55.945,0 16.40428,-204.81407 c -55.814,0 -112.41728,30.01707 -112.41728,77.85207 0,1.454 0.085,2.787 0.121,4.175 0.127,3.934 0.448,7.585 0.856,11.135 1.689,14.816 5.451,27.177 8.461,43.383 1.452,7.831 5.002,23.374 5.002,23.374 0.056,0.408 0.165,0.804 0.224,1.211 2.535,16.546 11.832,32.027 25.404,43.684 z" /><path - style="fill:#b58765" - inkscape:connector-curvature="0" - id="path50" - d="m 781.992,433.489 0,213.577 55.944,0 c 13.572,-11.657 22.867,-27.138 25.406,-43.684 0.058,-0.407 0.163,-0.803 0.221,-1.211 0,0 3.549,-15.543 5.002,-23.374 3.011,-16.206 6.774,-28.567 8.464,-43.381 0.405,-3.552 0.724,-7.203 0.846,-11.137 0.042,-1.388 0.126,-2.721 0.126,-4.175 0,-47.834 -40.191,-86.615 -96.009,-86.615 z" /><g - id="g52"><g - id="g54"><path - style="fill:#fef3df" - inkscape:connector-curvature="0" - id="path56" - d="m 860.944,613.502 c 0,28.321 -35.091,51.289 -78.383,51.289 -43.299,0 -78.388,-22.968 -78.388,-51.289 0,-28.325 35.089,-51.289 78.388,-51.289 43.292,0 78.383,22.964 78.383,51.289 z" /></g></g><g - id="g58"><g - id="g60"><g - id="g62"><path - style="fill:#5a3620" - inkscape:connector-curvature="0" - id="path64" - d="m 747.044,605.582 c 0,6.215 -5.04,11.256 -11.261,11.256 -6.21,0 -11.253,-5.041 -11.253,-11.256 0,-6.223 5.043,-11.257 11.253,-11.257 6.22,0 11.261,5.034 11.261,11.257 z" /></g></g><g - id="g66"><g - id="g68"><path - style="fill:#5a3620" - inkscape:connector-curvature="0" - id="path70" - d="m 840.856,605.582 c 0,6.215 -5.037,11.256 -11.257,11.256 -6.218,0 -11.259,-5.041 -11.259,-11.256 0,-6.223 5.041,-11.257 11.259,-11.257 6.22,0 11.257,5.034 11.257,11.257 z" /></g></g></g><g - id="g72"><path - style="fill:#87654a" - inkscape:connector-curvature="0" - id="path74" - d="m 875.228,525.835 c 0.354,-3.113 0.634,-6.311 0.743,-9.754 0.035,-1.218 0.109,-2.384 0.109,-3.661 0,-40.785 -33.369,-74.043 -80.237,-75.775 l -7.335,0.005 c -0.003,0 -0.003,0 -0.006,0 -0.007,0.018 -28.632,88.422 76.583,140.268 0.946,-4.317 2.078,-9.585 2.73,-13.088 2.64,-14.196 5.934,-25.021 7.413,-37.995 z" /></g><g - id="g76"><g - id="g78"><g - id="g80"><g - id="g82"><path - style="fill:#5a3620" - inkscape:connector-curvature="0" - id="path84" - d="m 843.907,519.681 c 0,6.964 -5.65,12.611 -12.618,12.611 -6.963,0 -12.614,-5.646 -12.614,-12.611 0,-6.97 5.651,-12.614 12.614,-12.614 6.968,0 12.618,5.644 12.618,12.614 z" /></g></g></g><g - id="g86"><g - id="g88"><g - id="g90"><path - style="fill:#5a3620" - inkscape:connector-curvature="0" - id="path92" - d="m 752.028,519.681 c 0,6.964 -5.649,12.611 -12.612,12.611 -6.969,0 -12.612,-5.646 -12.612,-12.611 0,-6.97 5.642,-12.614 12.612,-12.614 6.964,0 12.612,5.644 12.612,12.614 z" /></g></g></g><g - id="g94"><g - id="g96"><path - style="fill:#ffffff" - inkscape:connector-curvature="0" - id="path98" - d="m 748.75,515.894 c 0,2.558 -2.071,4.629 -4.63,4.629 -2.558,0 -4.633,-2.072 -4.633,-4.629 0,-2.552 2.076,-4.626 4.633,-4.626 2.559,0 4.63,2.073 4.63,4.626 z" /></g></g><g - id="g100"><g - id="g102"><path - style="fill:#ffffff" - inkscape:connector-curvature="0" - id="path104" - d="m 839.771,515.894 c 0,2.558 -2.073,4.629 -4.629,4.629 -2.558,0 -4.631,-2.072 -4.631,-4.629 0,-2.552 2.072,-4.626 4.631,-4.626 2.555,0 4.629,2.073 4.629,4.626 z" /></g></g></g></g><path - style="fill:#fef3df" - inkscape:connector-curvature="0" - id="path106" - d="m 734.557,443.625 c 0,0 -18.236,-25.199 0,-41.637 0,0 13.125,32.012 40.242,31.502" /><path - style="fill:#fef3df" - inkscape:connector-curvature="0" - id="path108" - d="m 834.496,443.625 c 0,0 18.236,-25.199 0,-41.637 0,0 -13.126,32.012 -40.242,31.502" /><path - style="fill:#f1f2f2" - inkscape:connector-curvature="0" - id="path110" - d="m 786.264,431.965 c -66.396,0 -120.223,53.827 -120.223,120.223 0,66.396 53.827,120.221 120.223,120.221 66.397,0 120.222,-53.825 120.222,-120.221 10e-4,-66.395 -53.825,-120.223 -120.222,-120.223 z m -11.96,215.702 c -53.009,0 -95.982,-43.855 -95.982,-97.953 0,-54.098 42.973,-97.952 95.982,-97.952 53.007,0 95.979,43.855 95.979,97.952 0,54.098 -42.972,97.953 -95.979,97.953 z" /></g><g - id="g112"><path - style="fill:#ffffff" - inkscape:connector-curvature="0" - id="path114" - d="m 781.737,436.751 c 66.396,0 120.221,53.827 120.221,120.223 0,30.718 -11.526,58.74 -30.482,79.991 21.636,-21.74 35.01,-51.708 35.01,-84.803 0,-66.395 -53.825,-120.222 -120.222,-120.222 -35.678,0 -67.721,15.549 -89.739,40.233 21.772,-21.879 51.91,-35.422 85.212,-35.422 z" /></g></g><path - d="m 648.292,644.7595 0,46.15088 c 0,5.49435 7.88,9.94862 17.6,9.94862 l 236.073,0 c 9.72,0 17.6,-4.45427 17.6,-9.94862 l 0,-14.07618 c 10e-4,0 -175.814,20.0804 -271.273,-32.0747 z" - id="path124" - inkscape:connector-curvature="0" - style="opacity:0.1;fill:#3d5263" /></g><g - id="g126" /></g></svg> \ No newline at end of file diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index f4aea569..00000000 --- a/docs/index.md +++ /dev/null @@ -1,48 +0,0 @@ -# mailcow: dockerized - 🐮 + 🐋 = 💕 - -[](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=JWBSYHF4SMC68) - -## Screenshots - -You can find screenshots [on Imgur](http://imgur.com/a/oewYt). - -## Overview - -mailcow dockerized comes with **11 containers** linked in **one bridged network**. - -- Dovecot -- Memcached -- Redis -- MySQL -- Bind9 (Resolver) (formerly PDNS Recursor) -- PHP-FPM -- Postfix -- Nginx -- Rmilter -- Rspamd -- SOGo - -**6 volumes** to keep dynamic data - take care of them! - -- vmail-vol-1 -- dkim-vol-1 -- redis-vol-1 -- mysql-vol-1 -- rspamd-vol-1 -- postfix-vol-1 - -The integrated **mailcow UI** allows administrative work on your mail server instance as well as separated domain administrator and mailbox user access: - -- DKIM key management -- Black- and whitelists per domain and per user -- Spam score managment per-user (reject spam, mark spam, greylist) -- Allow mailbox users to create temporary spam aliases -- Prepend mail tags to subject or move mail to subfolder (per-user) -- Allow mailbox users to toggle incoming and outgoing TLS enforcement -- Allow users to reset SOGo ActiveSync device caches -- imapsync to migrate or pull remote mailboxes regularly -- TFA: Yubi OTP and U2F USB (Google Chrome and derivates only) -- Add domains, mailboxes, aliases, domain aliases and SOGo resources - - -*[Looking for a farm to host your cow?](https://www.servercow.de)* diff --git a/docs/install.md b/docs/install.md deleted file mode 100644 index 7c383a63..00000000 --- a/docs/install.md +++ /dev/null @@ -1,122 +0,0 @@ -## Install mailcow - -You need Docker and Docker Compose. - -1\. Learn how to install [Docker](https://docs.docker.com/engine/installation/linux/) and [Docker Compose](https://docs.docker.com/compose/install/). - -Quick installation for most operation systems: - -- Docker -``` -curl -sSL https://get.docker.com/ | sh -``` - -- Docker-Compose -``` -curl -L https://github.com/docker/compose/releases/download/$(curl -Ls https://www.servercow.de/docker-compose/latest.php)/docker-compose-$(uname -s)-$(uname -m) > /usr/local/bin/docker-compose -chmod +x /usr/local/bin/docker-compose -``` - -Please use the latest Docker engine available and do not use the engine that ships with your distros repository. - -2\. Clone the master branch of the repository -``` -git clone https://github.com/andryyy/mailcow-dockerized && cd mailcow-dockerized -``` - -3\. Generate a configuration file. Use a FQDN (`host.domain.tld`) as hostname when asked. -``` -./generate_config.sh -``` - -4\. Change configuration if you want or need to. -``` -nano mailcow.conf -``` -If you plan to use a reverse proxy, you can, for example, bind HTTPS to 127.0.0.1 on port 8443 and HTTP to 127.0.0.1 on port 8080. - -5\. Pull the images and run the composer file. The paramter `-d` will start mailcow: dockerized detached: -``` -docker-compose pull -docker-compose up -d -``` - -Done! - -You can now access **https://${MAILCOW_HOSTNAME}** with the default credentials `admin` + password `moohoo`. - -The database will be initialized right after a connection to MySQL can be established. - -## Update mailcow - -There is no update routine. You need to refresh your pulled repository clone and apply your local changes (if any). Actually there are many ways to merge local changes. - -### Step 1, method 1 -Stash all local changes, pull changes from the remote master branch and apply your stash on top of it. You will most likely see warnings about non-commited changes; you can ignore them: - -``` -# Stash local changes -git stash -# Re-pull master -git pull -# Apply stash and remove it -git stash pop -``` - -### Step 1, method 2 -Fetch new data from GitHub, commit changes and merge remote repository: - -``` -# Get updates/changes -git fetch -# Add all changed files to local clone -git add -A -# Commit changes, ignore git complaining about username and mail address -git commit -m "Local config aat $(date)" -# Merge changes -git merge -``` - -If git complains about conflicts, solve them! Example: -``` -CONFLICT (content): Merge conflict in data/web/index.php -``` - -Open `data/web/index.php`, solve the conflict, close the file and run `git add -A` + `git commit -m "Solved conflict"`. - -### Step 1, method 3 - -Thanks to fabreg @ GitHub! - -In case both methods do not work (for many reason like you're unable to fix the CONFLICTS or any other reasons) you can simply start all over again. - -Keep in mind that all local changes _to configuration files_ will be lost. However, your volumes will not be removed. - -- Copy mailcow.conf somewhere outside the mailcow-dockerized directory -- Stop and remove mailcow containers: `docker-compose down` -- Delete the directory or rename it -- Clone the remote repository again (`git clone https://github.com/andryyy/mailcow-dockerized && cd mailcow-dockerized`). **Pay attention** to this step - the folder must have the same name of the previous one! -- Copy back your previous `mailcow.conf` into the mailcow-dockerizd folder - -If you forgot to stop Docker before deleting the cloned directoy, you can use the following commands: -``` -docker stop $(docker ps -a -q) -docker rm $(docker ps -a -q) -``` - -### Step 2 - -Pull new images (if any) and recreate changed containers: - -``` -docker-compose pull -docker-compose up -d --remove-orphans -``` - -### Step 3 -Clean-up dangling (unused) images and volumes: - -``` -docker rmi -f $(docker images -f "dangling=true" -q) -docker volume rm $(docker volume ls -qf dangling=true) -``` diff --git a/docs/u_and_e.md b/docs/u_and_e.md deleted file mode 100644 index d0a52cc8..00000000 --- a/docs/u_and_e.md +++ /dev/null @@ -1,428 +0,0 @@ -## mailcow UI configuration - -Several configuration parameters of the mailcow UI can be changed by creating a file `data/web/inc/vars.local.inc.php` which overrides defaults settings found in `data/web/inc/vars.inc.php`. - -The local configuration file is persistent over updates of mailcow. Try not to change values inside `data/web/inc/vars.inc.php`, but use them as template for the local override. - -mailcow UI configuration parameters can be to... - -- ...change the default language* -- ...change the default bootstrap theme -- ...set a password complexity regex -- ...add mailcow app buttons to the login screen -- ...set a pagination trigger -- ...set action after submitting forms (stay in form, return to previous page) - -\* To change SOGos default language, you will need to edit `data/conf/sogo/sogo.conf` and replace "English" by your preferred language. - -## Anonymize headers - -Save as `data/conf/postfix/mailcow_anonymize_headers.pcre`: - -``` -/^\s*Received:[^\)]+\)\s+\(Authenticated sender:(.+)/ - REPLACE Received: from localhost (localhost [127.0.0.1]) (Authenticated sender:$1 -/^\s*User-Agent/ IGNORE -/^\s*X-Enigmail/ IGNORE -/^\s*X-Mailer/ IGNORE -/^\s*X-Originating-IP/ IGNORE -/^\s*X-Forward/ IGNORE -``` - -Add this to `data/conf/postfix/main.cf`: -``` -smtp_header_checks = pcre:/opt/postfix/conf/mailcow_anonymize_headers.pcre -``` - -## Backup and restore maildir (simple tar file) - -### Backup - -This line backups the vmail directory to a file backup_vmail.tar.gz in the mailcow root directory: -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -DATE=$(date +"%Y%m%d_%H%M%S") -docker run --rm -it -v $(docker inspect --format '{{ range .Mounts }}{{ if eq .Destination "/var/vmail" }}{{ .Name }}{{ end }}{{ end }}' $(docker-compose ps -q dovecot-mailcow)):/vmail -v ${PWD}:/backup debian:jessie tar cvfz /backup/backup_vmail.tar.gz /vmail -``` - -You can change the path by adjusting ${PWD} (which equals to the current directory) to any path you have write-access to. -Set the filename `backup_vmail.tar.gz` to any custom name, but leave the path as it is. Example: `[...] tar cvfz /backup/my_own_filename_.tar.gz` - -### Restore -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -DATE=$(date +"%Y%m%d_%H%M%S") -docker run --rm -it -v $(docker inspect --format '{{ range .Mounts }}{{ if eq .Destination "/var/vmail" }}{{ .Name }}{{ end }}{{ end }}' $(docker-compose ps -q dovecot-mailcow)):/vmail -v ${PWD}:/backup debian:jessie tar xvfz /backup/backup_vmail.tar.gz -``` - -## Docker Compose Bash completion - -For the tab-tab... :-) - -``` -curl -L https://raw.githubusercontent.com/docker/compose/$(docker-compose version --short)/contrib/completion/bash/docker-compose -o /etc/bash_completion.d/docker-compose -``` -## Black and Whitelist - -Edit a domain as (domain) administrator to add an item to the filter table. - -Beware that a mailbox user can login to mailcow and override a domain policy filter item. - -## Customize Dockerfiles - -Make your changes in `data/Dockerfiles/$service` and build the image locally: - -``` -docker build data/Dockerfiles/service -t andryyy/mailcow-dockerized:$service -``` - -Now auto-recreate modified containers: - -``` -docker-compose up -d -``` - -## Disable sender addresses verification - -This option is not best-practice and should only be implemented when there is no other option available to archive whatever you are trying to do. - -Simply create a file `data/conf/postfix/check_sasl_access` and enter the following content. This user must exist in your installation and needs to authenticate before sending mail. -``` -user-to-allow-everything@example.com OK -``` - -Open `data/conf/postfix/main.cf` and find `smtpd_sender_restrictions`. Prepend `check_sasl_access hash:/opt/postfix/conf/check_sasl_access` like this: -``` -smtpd_sender_restrictions = check_sasl_access hash:/opt/postfix/conf/check_sasl_access reject_authenticated_sender_login_mismatch [...] -``` - -Run postmap on check_sasl_access: - -``` -docker-compose exec postfix-mailcow postmap /opt/postfix/conf/check_sasl_access -``` - -Restart the Postfix container. - -## Install Roundcube - -Download Roundcube 1.3.x (beta at the time of Feb 2017) to the web htdocs directory and extract it (here `rc/`): -``` -cd data/web/rc -wget -O - https://github.com/roundcube/roundcubemail/releases/download/1.3-beta/roundcubemail-1.3-beta-complete.tar.gz | tar xfvz - -# Change folder name -mv roundcubemail-1.3* rc -# Change permissions -chown -R root: rc/ -``` - -Create a file `data/web/rc/config/config.inc.php` with the following content. - -**Change the `des_key` parameter to a random value.** It is used to temporarily store your IMAP password. - -``` -<?php -error_reporting(0); -if (!file_exists('/tmp/mime.types')) { -file_put_contents("/tmp/mime.types", fopen("http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types", 'r')); -} -$config = array(); -$config['db_dsnw'] = 'mysql://' . getenv('DBUSER') . ':' . getenv('DBPASS') . '@mysql/' . getenv('DBNAME'); -$config['default_host'] = 'tls://dovecot'; -$config['default_port'] = '143'; -$config['smtp_server'] = 'tls://postfix'; -$config['smtp_port'] = 587; -$config['smtp_user'] = '%u'; -$config['smtp_pass'] = '%p'; -$config['support_url'] = ''; -$config['product_name'] = 'Roundcube Webmail'; -$config['des_key'] = 'rcmail-!24ByteDESkey*Str'; -$config['log_dir'] = '/dev/null'; -$config['temp_dir'] = '/tmp'; -$config['plugins'] = array( - 'archive', -); -$config['skin'] = 'larry'; -$config['mime_types'] = '/tmp/mime.types'; -$config['imap_conn_options'] = array( -'ssl' => array('verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true) -); -$config['enable_installer'] = false; -$config['smtp_conn_options'] = array( -'ssl' => array('verify_peer' => false, 'verify_peer_name' => false, 'allow_self_signed' => true) -); -``` - -Point your browser to `https://myserver/rc/installer` and follow the instructions. -Initialize the database and leave the installer. - -**Delete the directory `data/web/rc/installer` after a successful installation!** - -### Enable change password function in Roundcube - -Open `data/web/rc/config/config.inc.php` and enable the password plugin: - -``` -... -$config['plugins'] = array( - 'archive', - 'password', -); -... -``` - -Open `data/web/rc/plugins/password/password.php`, search for `case 'ssha':` and add above: - -``` - case 'ssha256': - $salt = rcube_utils::random_bytes(8); - $crypted = base64_encode( hash('sha256', $password . $salt, TRUE ) . $salt ); - $prefix = '{SSHA256}'; - break; -``` - -Open `data/web/rc/plugins/password/config.inc.php` and change the following parameters (or add them at the bottom of that file): - -``` -$config['password_driver'] = 'sql'; -$config['password_algorithm'] = 'ssha256'; -$config['password_algorithm_prefix'] = '{SSHA256}'; -$config['password_query'] = "UPDATE mailbox SET password = %P WHERE username = %u"; -``` - -## MySQL - -### Connect -``` -source mailcow.conf -docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -``` - -### Backup -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -DATE=$(date +"%Y%m%d_%H%M%S") -docker-compose exec mysql-mailcow mysqldump --default-character-set=utf8mb4 -u${DBUSER} -p${DBPASS} ${DBNAME} > backup_${DBNAME}_${DATE}.sql -``` - -### Restore -``` -cd /path/to/mailcow-dockerized -source mailcow.conf -docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} < backup_file.sql -``` - -## Debugging - -You can use `docker-compose logs $service-name` for all containers. - -Run `docker-compose logs` for all logs at once. - -Follow the log output by running docker-compose with `logs -f`. - -## Redirect port 80 to 443 - -Since February the 28th 2017 mailcow does come with port 80 and 443 enabled. - -Open `mailcow.conf` and set `HTTP_BIND=0.0.0.0`. - -Open `data/conf/nginx/site.conf` and add a new "catch-all" site at the top of that file: - -``` -server { - listen 80 default_server; - include /etc/nginx/conf.d/server_name.active; - return 301 https://$host$request_uri; -} -``` - -Restart the stack, changed containers will be updated: - -`docker-compose up -d` - -## Redis - -### Client - -``` -docker-compose exec redis-mailcow redis-cli -``` - -## Remove persistent data - -- Remove volume `mysql-vol-1` to remove all MySQL data. -- Remove volume `redis-vol-1` to remove all Redis data. -- Remove volume `vmail-vol-1` to remove all contents of `/var/vmail` mounted to `dovecot-mailcow`. -- Remove volume `dkim-vol-1` to remove all DKIM keys. -- Remove volume `rspamd-vol-1` to remove all Rspamd data. - -Running `docker-compose down -v` will **destroy all mailcow: dockerized volumes** and delete any related containers. - -## Reset admin password -Reset mailcow admin to `admin:moohoo`: - -1\. Drop admin table -``` -source mailcow.conf -docker-compose exec mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DROP TABLE admin;" -``` - -2\. Open mailcow UI to auto-init the db - -## Rspamd - -### Learn spam and ham - -Rspamd learns mail as spam or ham when you move a message in or out of the junk folder to any mailbox besides trash. -This is archived by using the Dovecot plugin "antispam" and a simple parser script. - -Rspamd also auto-learns mail when a high or low score is detected (see https://rspamd.com/doc/configuration/statistic.html#autolearning) - -The bayes statistics are written to Redis as keys `BAYES_HAM` and `BAYES_SPAM`. - -You can also use Rspamd's web ui to learn ham and/or spam. - -### Learn ham or spam from existing directory - -You can use a one-liner to learn mail in plain-text (uncompressed) format: -``` -# Ham -for file in /my/folder/cur/*; do docker exec -i $(docker-compose ps -q rspamd-mailcow) rspamc learn_ham < $file; done -# Spam -for file in /my/folder/.Junk/cur/*; do docker exec -i $(docker-compose ps -q rspamd-mailcow) rspamc learn_spam < $file; done -``` - -Consider attaching a local folder as new volume to `rspamd-mailcow` in `docker-compose.yml` and learn given files inside the container. This can be used as workaround to parse compressed data with zcat. Example: - -``` -for file in /data/old_mail/.Junk/cur/*; do rspamc learn_spam < zcat $file; done -``` - -### CLI tools - -``` -docker-compose exec rspamd-mailcow rspamc --help -docker-compose exec rspamd-mailcow rspamadm --help -``` - -See [Rspamd documentation](https://rspamd.com/doc/index.html) - -## Adjust service configurations - -The most important configuration files are mounted from the host into the related containers: - -``` -data/conf -├── bind9 -│ └── named.conf -├── dovecot -│ ├── dovecot.conf -│ ├── dovecot-master.passwd -│ ├── sieve_after -│ └── sql -│ ├── dovecot-dict-sql.conf -│ └── dovecot-mysql.conf -├── mysql -│ └── my.cnf -├── nginx -│ ├── dynmaps.conf -│ ├── site.conf -│ └── templates -│ ├── listen_plain.template -│ ├── listen_ssl.template -│ └── server_name.template -├── pdns -│ ├── pdns_custom.lua -│ └── recursor.conf -├── postfix -│ ├── main.cf -│ ├── master.cf -│ ├── postscreen_access.cidr -│ ├── smtp_dsn_filter -│ └── sql -│ ├── mysql_relay_recipient_maps.cf -│ ├── mysql_tls_enforce_in_policy.cf -│ ├── mysql_tls_enforce_out_policy.cf -│ ├── mysql_virtual_alias_domain_catchall_maps.cf -│ ├── mysql_virtual_alias_domain_maps.cf -│ ├── mysql_virtual_alias_maps.cf -│ ├── mysql_virtual_domains_maps.cf -│ ├── mysql_virtual_mailbox_maps.cf -│ ├── mysql_virtual_relay_domain_maps.cf -│ ├── mysql_virtual_sender_acl.cf -│ └── mysql_virtual_spamalias_maps.cf -├── rmilter -│ └── rmilter.conf -├── rspamd -│ ├── dynmaps -│ │ ├── authoritative.php -│ │ ├── settings.php -│ │ ├── tags.php -│ │ └── vars.inc.php -> ../../../web/inc/vars.inc.php -│ ├── local.d -│ │ ├── dkim.conf -│ │ ├── metrics.conf -│ │ ├── options.inc -│ │ ├── redis.conf -│ │ ├── rspamd.conf.local -│ │ └── statistic.conf -│ ├── lua -│ │ └── rspamd.local.lua -│ └── override.d -│ ├── logging.inc -│ ├── worker-controller.inc -│ └── worker-normal.inc -└── sogo - ├── sieve.creds - └── sogo.conf - -``` - -Just change the according configuration file on the host and restart the related service: -``` -docker-compose restart service-mailcow -``` - -## Tagging - -Mailbox users can tag their mail address like in `me+facebook@example.org` and choose between to setups to handle this tag: - -1\. Move this message to a subfolder "facebook" (will be created lower case if not existing) - -2\. Prepend the tag to the subject: "[facebook] Subject" - -## Two-factor authentication - -So far two methods for TFA are implemented. Both work with the fantastic [Yubikey](https://www.yubico.com). - -While Yubi OTP needs an active internet connection and an API ID and key, U2F will work with any FIDO U2F USB key out of the box, but can only be used when mailcow is accessed over HTTPS. - -Both methods support multiple YubiKeys. - -As administrator you are able to temporary disable a domain administrators TFA login until they successfully logged in. - -The key used to login will be displayed in green, while other keys remain grey. - -### Yubi OTP - -The Yubi API ID and Key will be checked against the Yubico Cloud API. When setting up TFA you will be asked for your personal API account for this key. -The API ID, API key and the first 12 characters (your YubiKeys ID in modhex) are stored in the MySQL table as secret. - -### U2F - -Only Google Chrome (+derivates) and Opera support U2F authentication to this day natively. -For Firefox you will need to install the "U2F Support Add-on" as provided on [mozilla.org](https://addons.mozilla.org/en-US/firefox/addon/u2f-support-add-on/). - -U2F works without an internet connection. - -## Why Bind? - -For DNS blacklist lookups and DNSSEC. - -Most systems use either a public or a local caching DNS resolver. -That's a very bad idea when it comes to filter spam using DNS-based blackhole lists (DNSBL) or similar technics. -Most if not all providers apply a rate limit based on the DNS resolver that is used to query their service. -Using a public resolver like Googles 4x8, OpenDNS or any other shared DNS resolver like your ISPs will hit that limit very soon. diff --git a/mailcow-reset-admin.sh b/mailcow-reset-admin.sh new file mode 100755 index 00000000..7ce1def8 --- /dev/null +++ b/mailcow-reset-admin.sh @@ -0,0 +1,36 @@ +#/bin/bash +if [[ ! -f mailcow.conf ]]; then + echo "Cannot find mailcow.conf, make sure this script is run from within the mailcow folder." + exit 1 +fi + +echo -n "Checking MySQL service... " +docker-compose ps -q mysql-mailcow > /dev/null 2>&1 + +if [[ $? -ne 0 ]]; then + echo "failed" + echo "MySQL (mysql-mailcow) is not up and running, exiting..." + exit 1 +fi + +echo "OK" +read -r -p "Are you sure you want to reset the mailcow administrator account? [y/N] " response +response=${response,,} # tolower +if [[ "$response" =~ ^(yes|y)$ ]]; then + echo -e "\nWorking, please wait..." + source mailcow.conf + docker-compose exec -T mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM admin;" + docker-compose exec -T mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "INSERT INTO admin (username, password, superadmin, created, modified, active) VALUES ('admin', '{SSHA256}K8eVJ6YsZbQCfuJvSUbaQRLr0HPLz5rC9IAp0PAFl0tmNDBkMDc0NDAyOTAxN2Rk', 1, NOW(), NOW(), 1);" + docker-compose exec -T mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM domain_admins WHERE username='admin';" + docker-compose exec -T mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "INSERT INTO domain_admins (username, domain, created, active) VALUES ('admin', 'ALL', NOW(), 1);" + docker-compose exec -T mysql-mailcow mysql -u${DBUSER} -p${DBPASS} ${DBNAME} -e "DELETE FROM tfa WHERE username='admin';" + echo " +Reset credentials: +--- +Username: admin +Password: moohoo +TFA: none +" +else + echo "Operation canceled." +fi diff --git a/mailcow-setup-relayhost.sh b/mailcow-setup-relayhost.sh new file mode 100755 index 00000000..283590df --- /dev/null +++ b/mailcow-setup-relayhost.sh @@ -0,0 +1,76 @@ +#/bin/bash +if [[ ! -f mailcow.conf ]]; then + echo "Cannot find mailcow.conf, make sure this script is run from within the mailcow folder." + exit 1 +fi + +echo -n "Checking Postfix service... " +docker-compose ps -q postfix-mailcow > /dev/null 2>&1 + +if [[ $? -ne 0 ]]; then + echo "failed" + echo "Postfix (postifx-mailcow) is not up and running, exiting..." + exit 1 +fi + +echo "OK" + +if [[ -z ${1} ]]; then + echo "Usage:" + echo + echo "Setup a relayhost:" + echo "${0} relayhost port (username) (password)" + echo "Username and password are optional parameters." + echo + echo "Reset to defaults:" + echo "${0} reset" + exit 1 +fi + +if [[ ${1} == "reset" ]]; then + # Reset modified values to their defaults + sed -i "s/^relayhost\ \=.*/relayhost\ \=/" data/conf/postfix/main.cf + sed -i "s/^smtp\_sasl\_password\_maps.*/smtp\_sasl\_password\_maps\ \=/" data/conf/postfix/main.cf + sed -i "s/^smtp\_sasl\_security\_options.*/smtp\_sasl\_security\_options\ \=\ noplaintext\,\ noanonymous/" data/conf/postfix/main.cf + sed -i "s/^smtp\_sasl\_auth\_enable.*/smtp\_sasl\_auth\_enable\ \=\ no/" data/conf/postfix/main.cf + # Also delete the plaintext password file + rm -f data/conf/postfix/smarthost_passwd* + docker-compose exec postfix-mailcow postfix reload + # Exit with dc exit code + exit $? +else + # Try a simple connection to host:port but don't recieve any data + # Abort after 3 seconds + if ! nc -z -v -w3 ${1} ${2} 2>/dev/null; then + echo "Connection to relayhost ${1} failed, aborting..." + exit 1 + fi + # Use exact hostname as relayhost, don't lookup the MX record of relayhost + sed -i "s/relayhost\ \=.*/relayhost\ \=\ \[${1}\]\:${2}/" data/conf/postfix/main.cf + if grep -q "smtp_sasl_password_maps" data/conf/postfix/main.cf + then + sed -i "s/^smtp\_sasl\_password\_maps.*/smtp\_sasl\_password\_maps\ \=\ hash\:\/opt\/postfix\/conf\/smarthost\_passwd/" data/conf/postfix/main.cf + else + echo "smtp_sasl_password_maps = hash:/opt/postfix/conf/smarthost_passwd" >> data/conf/postfix/main.cf + fi + if grep -q "smtp_sasl_auth_enable" data/conf/postfix/main.cf + then + sed -i "s/^smtp\_sasl\_auth\_enable.*/smtp\_sasl\_auth\_enable\ \=\ yes/" data/conf/postfix/main.cf + else + echo "smtp_sasl_auth_enable = yes" >> data/conf/postfix/main.cf + fi + if grep -q "smtp_sasl_security_options" data/conf/postfix/main.cf + then + sed -i "s/^smtp\_sasl\_security\_options.*/smtp\_sasl\_security\_options\ \=/" data/conf/postfix/main.cf + else + echo "smtp_sasl_security_options =" >> data/conf/postfix/main.cf + fi + if [[ ! -z ${3} ]]; then + echo ${1} ${3}:${4} > data/conf/postfix/smarthost_passwd + docker-compose exec postfix-mailcow postmap /opt/postfix/conf/smarthost_passwd + fi + docker-compose exec postfix-mailcow chown root:postfix /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db + docker-compose exec postfix-mailcow chmod 660 /opt/postfix/conf/smarthost_passwd /opt/postfix/conf/smarthost_passwd.db + docker-compose exec postfix-mailcow postfix reload + exit $? +fi diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 1ab6bec8..00000000 --- a/mkdocs.yml +++ /dev/null @@ -1,25 +0,0 @@ -site_name: "mailcow: dockerized" -repo_url: https://github.com/andryyy/mailcow-dockerized -remote_branch: gh-pages -theme: material -extra: - social: - - type: 'github' - link: 'https://github.com/andryyy/mailcow-dockerized' - palette: - primary: 'indigo' - accent: 'yellow' - logo: 'images/logo.svg' - -markdown_extensions: - - admonition - - codehilite(guess_lang=true) - - footnotes - - meta - - toc(permalink=true) - -pages: - - 'This is mailcow': 'index.md' - - 'Installation': 'install.md' - - 'First Steps': 'first_steps.md' - - 'Usage & Examples': 'u_and_e.md'