diff --git a/docker/containers/web-server/Dockerfile b/docker/containers/web-server/Dockerfile index 75b09744aa9c284855cb84e6cbe0340e3027fdb3..db0a24c565fd84312209140c8d9a0de79a50e1c2 100644 --- a/docker/containers/web-server/Dockerfile +++ b/docker/containers/web-server/Dockerfile @@ -1,7 +1,7 @@ # # GENERATED BY init.sh - template: templates/web-server-Dockerfile - 42dce773c83597a7d05af398bdd66d15 # -FROM php:8.3-apache +FROM php:8.4-apache # install packages RUN apt-get update && apt-get install -y git unzip zip diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 4fa133ff15feb1815a239591c1f718fa7cc5d21c..951f3e7cf594772d1e7999d547fedb86a016ab89 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -19,7 +19,7 @@ services: build: context: . dockerfile: ./containers/web-server/Dockerfile - image: "php:8.3-apache" + image: "php:8.4-apache" container_name: 'my_new_app-server' ports: - '${APP_PORT}:80' diff --git a/docker/init.sh b/docker/init.sh index d75223516bd09e854db9378231f85b42b1ba4e70..04fc725623d657387c79c9ad4644cffe7c5bbc95 100755 --- a/docker/init.sh +++ b/docker/init.sh @@ -24,20 +24,28 @@ # 2024-07-29 v1.17 <www.axel-hahn.de> hide unnecessary menu items; reorder functions # 2024-08-14 v1.18 <www.axel-hahn.de> update container view # 2024-09-20 v1.19 <www.axel-hahn.de> detect dockerd-rootless (hides menu item to set permissions) +# 2024-10-16 v1.20 <axel.hahn@unibe.ch> add db import and export +# 2024-10-25 v1.21 <axel.hahn@unibe.ch> create missing subdir dbdumps +# 2024-10-30 v1.22 <axel.hahn@unibe.ch> added: Open Mysql client in container +# 2024-10-30 v1.23 <axel.hahn@unibe.ch> added: show menu hints why some menu items are visible +# 2024-11-20 v1.24 <axel.hahn@unibe.ch> fix menu with started database less app; apply template permissions on target file; add $WEBURL; remove $frontendurl +# 2024-11-20 v1.25 <axel.hahn@unibe.ch> fix menu startup containers +# 2024-11-21 v1.26 <axel.hahn@unibe.ch> Reset colors in _checkConfig # ====================================================================== cd "$( dirname "$0" )" || exit 1 +_version="1.26" + # init used vars gittarget= -frontendurl= +WEBURL= _self=$( basename "$0" ) # shellcheck source=/dev/null . "${_self}.cfg" || exit 1 -_version="1.19" # git@git-repo.iml.unibe.ch:iml-open-source/docker-php-starterkit.git selfgitrepo="docker-php-starterkit.git" @@ -55,14 +63,19 @@ fgReset="\e[0m" # running containers DC_WEB_UP=0 DC_DB_UP=0 +DC_ALL_UP=0 # repo of docker-php-starterkit is here? DC_REPO=1 DC_CONFIG_CHANGED=0 +# absolute urls for web app DC_WEB_URL="" +DC_DUMP_DIR=dbdumps +DC_SHOW_MENUHINTS=0 + isDockerRootless=0 ps -ef | grep dockerd-rootless | grep -q $USER && isDockerRootless=1 @@ -70,6 +83,19 @@ ps -ef | grep dockerd-rootless | grep -q $USER && isDockerRootless=1 # FUNCTIONS # ---------------------------------------------------------------------- +# check config for changes in newer versions +function _checkConfig(){ + + # --- v1.24 + if [ -z "$WEBURL" ]; then + echo -e "${fgBrown}INFO: add 'WEBURL=\"/\"' in your ${_self}.cfg. It is a new var since v1.24${fgReset}" + WEBURL="/" + fi + if [ -n "$frontendurl" ]; then + echo -e "${fgBrown}INFO: Remove frontendurl=$frontendurl in your ${_self}.cfg. It is obsolete since v1.24${fgReset}" + fi + +} # ---------------------------------------------------------------------- # STATUS FUNCTIONS @@ -97,13 +123,38 @@ function _getStatus_docker(){ DC_WEB_UP=0 DC_DB_UP=0 + DC_ALL_UP=0 + grep -q "${APP_NAME}-server" <<< "$_out" && DC_WEB_UP=1 grep -q "${APP_NAME}-db" <<< "$_out" && DC_DB_UP=1 + + if [ "$DB_ADD" != "false" ] && [ ! -d "${DC_DUMP_DIR}" ]; then + echo "INFO: creating subdir ${DC_DUMP_DIR} to import/ export databases ..." + mkdir "${DC_DUMP_DIR}" || exit 1 + return + fi + + if [ "${DC_WEB_UP}" = "1" ] && [ "${DC_DB_UP}" = "1" ]; then + DC_ALL_UP=1 + fi + + if [ "$DB_ADD" = "false" ] && [ "${DC_WEB_UP}" = "1" ]; then + DC_ALL_UP=1 + fi + } +# Get web url of the application +# It is for support of Nginx Docker Proxy +# https://github.com/axelhahn/nginx-docker-proxy +# It returns http://localhost:<port> or a https://<appname> plus $WEBURL function _getWebUrl(){ - DC_WEB_URL="$frontendurl" - grep -q "${APP_NAME}-server" /etc/hosts && DC_WEB_URL="https://${APP_NAME}-server/" + if grep -q "^[0-9\.]* ${APP_NAME}-server" /etc/hosts; then + DC_WEB_URL="https://${APP_NAME}-server$WEBURL" + else + DC_WEB_URL=http://localhost:${APP_PORT}$WEBURL + fi + set +vx } # ---------------------------------------------------------------------- @@ -123,7 +174,16 @@ function h3(){ # helper for menu: print an inverted key function _key(){ - echo -en "\e[4;7m ${1} \e[0m" + echo -en "$fgInvert ${1} $fgReset" +} + +# helper for menu: show hint text +# param int FLag _bAll (i true the txt will be hidden) +# param string message to show +function menuhint(){ + local _bAll="$1" + shift 1 + test $DC_SHOW_MENUHINTS -ne 0 && test "$_bAll" -eq "0" && ( echo -e "$fgBlue $*$fgReset" ) } # show menu in interactive mode and list keys in help with param -h @@ -137,38 +197,61 @@ function showMenu(){ echo if [ $DC_REPO -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Git data of starterkit were found" echo "${_spacer}$( _key g ) - remove git data of starterkit" echo fi if [ $isDockerRootless -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Because rootless docker was found" echo "${_spacer}$( _key i ) - init application: set permissions" + echo fi if [ $DC_CONFIG_CHANGED -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Config was changed" echo "${_spacer}$( _key t ) - generate files from templates" + echo fi if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Config is unchanged" echo "${_spacer}$( _key T ) - remove generated files" + echo fi - echo - if [ $DC_WEB_UP -eq 0 ] || [ $_bAll -eq 1 ]; then + if [ $DC_ALL_UP -eq 0 ] || [ $_bAll -eq 1 \ + ]; then if [ $DC_CONFIG_CHANGED -eq 0 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "A container is down and config is unchanged" echo "${_spacer}$( _key u ) - startup containers docker-compose ... up -d" echo "${_spacer}$( _key U ) - startup containers docker-compose ... up -d --build" echo echo "${_spacer}$( _key r ) - remove containers docker-compose rm -f" + echo fi fi - if [ $DC_WEB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then + if [ $DC_WEB_UP -eq 1 ] || [ $DC_DB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "A container is up" echo "${_spacer}$( _key s ) - shutdown containers docker-compose stop" echo echo "${_spacer}$( _key m ) - more infos" echo "${_spacer}$( _key o ) - open app [${APP_NAME}] $DC_WEB_URL" echo "${_spacer}$( _key c ) - console (bash)" + echo + fi + if [ $DC_WEB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then + menuhint $_bAll "Web container is up" echo "${_spacer}$( _key p ) - console check with php linter" + echo fi - echo + if [ $DC_DB_UP -eq 1 ] || [ $_bAll -eq 1 ]; then + echo + menuhint $_bAll "Database container is up" + echo "${_spacer}$( _key d ) - Dump container database" + echo "${_spacer}$( _key D ) - Import Dump into container database" + echo "${_spacer}$( _key M ) - Open Mysql client in database container" + echo + fi + menuhint $_bAll "Always available" echo "${_spacer}$( _key q ) - quit" } @@ -313,7 +396,7 @@ function _fix_no-db(){ local iStart; typeset -i iStart iStart=$( grep -Fn "$CUTTER_NO_DATABASE" "${_file}" | cut -f 1 -d ':' )-1 if [ $iStart -gt 0 ]; then - sed -ni "1,${iStart}p" "${_file}" + sed -n "$sed_no_backup" "1,${iStart}p" "${_file}" fi fi } @@ -375,14 +458,15 @@ function _generateFiles(){ # write file from line 2 to a tmp file sed -n '2,$p' "$mytpl" >"$_tmpfile" + chmod "$( stat -c %a "$mytpl" )" "$_tmpfile" # add generator # sed -i "s#{{generator}}#generated by $0 - template: $mytpl - $( date )#g" $_tmpfile local _md5; _md5=$( md5sum $_tmpfile | awk '{ print $1 }' ) - sed -i "s#{{generator}}#GENERATED BY $_self - template: $mytpl - $_md5#g" $_tmpfile + sed -i "$sed_no_backup" "s#{{generator}}#GENERATED BY $_self - template: $mytpl - $_md5#g" $_tmpfile # apply all replacements to the tmp file - eval sed -i "$params" "$_tmpfile" || exit + eval sed "$sed_no_backup" "$params" "$_tmpfile" || exit _fix_no-db $_tmpfile @@ -395,7 +479,7 @@ function _generateFiles(){ echo -n "$mytpl - changes detected - writing [$target] ... " mkdir -p "$( dirname ../"$target" )" || exit 2 mv "$_tmpfile" "../$target" || exit 2 - echo OK + echo -e "${fgGreen}OK${fgReset}" echo fi else @@ -424,7 +508,7 @@ function _removeGeneratedFiles(){ echo -n "REMOVING " ls -l "../$target" || exit 2 rm -f "../$target" || exit 2 - echo OK + echo -e "${fgGreen}OK${fgReset}" else echo "SKIP: $target" fi @@ -498,10 +582,94 @@ function _wait(){ echo } +# DB TOOL - dump db from container +function _dbDump(){ + local _iKeepDumps; + typeset -i _iKeepDumps=5 + local _iStart; + typeset -i _iStart=$_iKeepDumps+1; + + if [ $DC_DB_UP -eq 0 ]; then + echo "Database container is not running. Aborting." + return + fi + outfile=${DC_DUMP_DIR}/${MYSQL_DB}_$( date +%Y%m%d_%H%M%S ).sql + echo -n "dumping ${MYSQL_DB} ... " + if docker exec -i "${APP_NAME}-db" mysqldump -uroot -p${MYSQL_ROOT_PASS} ${MYSQL_DB} > "$outfile"; then + echo -n "OK ... Gzip ... " + if gzip "${outfile}"; then + echo "OK" + ls -l "$outfile.gz" + + # CLEANUP + echo + echo "--- Cleanup: keep $_iKeepDumps files." + ls -1t ${DC_DUMP_DIR}/* | sed -n "$_iStart,\$p" | while read -r delfile + do + echo "CLEANUP: Deleting $delfile ... " + rm -f "$delfile" + done + echo + echo -n "Size of dump directory: " + du -hs ${DC_DUMP_DIR} | awk '{ print $1 }' + + else + echo "ERROR" + rm -f "$outfile" + fi + else + echo "ERROR" + rm -f "$outfile" + fi +} + +# DB TOOL - import local database dump into container +function _dbImport(){ + echo "--- Available dumps:" + ls -ltr ${DC_DUMP_DIR}/*.gz | sed "s#^# #g" + if [ $DC_DB_UP -eq 0 ]; then + echo "Database container is not running. Aborting." + return + fi + echo -n "Dump file to import into ${MYSQL_DB} > " + read -r dumpfile + if [ -z "$dumpfile" ]; then + echo "Abort - no value was given." + return + fi + if [ ! -f "$dumpfile" ]; then + echo "Abort - wrong filename." + return + fi + + echo -n "Importing $dumpfile ... " + + # Mac OS compatibility + # if zcat "$dumpfile" | docker exec -i "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}" + if cat "$dumpfile" | zcat | docker exec -i "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}" + then + echo "OK" + else + echo "ERROR" + fi +} + # ---------------------------------------------------------------------- # MAIN # ---------------------------------------------------------------------- +_checkConfig + +# Mac OS compatibility +case "$OSTYPE" in + darwin*|bsd*) + sed_no_backup=" -i '' " + ;; + *) + sed_no_backup="-i" + ;; +esac + action=$1; shift 1 while true; do @@ -612,6 +780,18 @@ while true; do echo "Start your docker container first." fi ;; + d) + h2 "DB tools :: dump" + _dbDump + ;; + D) + h2 "DB tools :: import" + _dbImport + ;; + M) + h2 "DB tools :: mysql client" + docker exec -it "${APP_NAME}-db" mysql -uroot -p${MYSQL_ROOT_PASS} "${MYSQL_DB}" + ;; o) h2 "Open app ..." xdg-open "$DC_WEB_URL" diff --git a/docker/init.sh.cfg b/docker/init.sh.cfg index e30c69e0a9664ebff0532f299a6af60826959057..c66e959ddd215ece6fa988b277b4dbc19a325a51 100644 --- a/docker/init.sh.cfg +++ b/docker/init.sh.cfg @@ -17,7 +17,7 @@ APP_APT_PACKAGES="git unzip zip" #APP_APACHE_MODULES="rewrite" APP_APACHE_MODULES="" -APP_PHP_VERSION=8.3 +APP_PHP_VERSION=8.4 # APP_PHP_MODULES="curl pdo_mysql mbstring xml zip xdebug" # APP_PHP_MODULES="curl mbstring xml zip xdebug" APP_PHP_MODULES="xdebug" @@ -61,9 +61,8 @@ DOCKER_USER_UID=33 # document root inside web-server container WEBROOT=/var/www/${APP_NAME}/public_html +WEBURL=/admin/ CUTTER_NO_DATABASE="CUT-HERE-FOR-NO-DATABASE" -frontendurl=http://localhost:${APP_PORT}/ - # ---------------------------------------------------------------------- diff --git "a/docs/10_\360\237\223\221_Description.md" "b/docs/10_\360\237\223\221_Description.md" index 372e2f38a6168e613b1b932853b13353859d53a6..7fbf9f88e1518065a2ee47400586a90a1df6abfd 100644 --- "a/docs/10_\360\237\223\221_Description.md" +++ "b/docs/10_\360\237\223\221_Description.md" @@ -1,6 +1,19 @@ +## Why? + +At our institute we have about 50 domains for redirections only. +If all rewrite rules are inside wbserver definitions then no non-sysadmin would be able to get an overview over all redirect domains and their rules to point anywhere with what redirect code. + +All domains with redirects only point to the same web that executes the redirects. + +The redirects are handled in JSON files. So it can be put into a repository and can reproduce the history of changes. + +Next to the redirec functionality for the public web there is a web ui for showing all domains, redirects in a sortable, searchable table. Domains and redirects can be tested. + + + ## Requirements -* PHP 8 (up to PHP 8.3) +* PHP 8 (up to PHP 8.4) * Webserver (docs describe usage for Apache httpd) ## Features diff --git "a/docs/20_\342\226\266\357\270\217_Installation.md" "b/docs/20_\342\226\266\357\270\217_Installation.md" index 37b0e1ab2e027512d75931467406b3b74d0284ca..000ccd9d06cfd92a0263346a9f0a2cde87b4f67a 100644 --- "a/docs/20_\342\226\266\357\270\217_Installation.md" +++ "b/docs/20_\342\226\266\357\270\217_Installation.md" @@ -2,8 +2,16 @@ ### Get the files -The repository contains a subfolder *public_html*. Run `git clone` or extract the downloaded archive 1 level above webroot. The document root of the web must point -to the public_html directory. The config folder is outside the webroot. +The repository contains a subfolder *public_html*. Run `git clone` or extract the downloaded archive 1 level above webroot. + +In this example the software will be installed into a webroot `/var/www/links/public_html`: + +```shell +mkdir /var/www/ +git clone https://git-repo.iml.unibe.ch/iml-open-source/redirect-handler.git links +``` + +The document root of the web must point to the public_html directory. The config folder is outside the webroot. ```text . @@ -23,21 +31,89 @@ to the public_html directory. The config folder is outside the webroot. βββ readme.md ``` -### Redirect all requests to index.php +### Webserver + +This is the basic idea how it works: + +* Create on or multiple vhosts with document root /var/www/links/public_html +* optional: one extra domain eg. links.example.com points to /var/www/links/public_html too - and is restricted with ip restriction, basic auth, whatever. + +#### Redirect all requests to index.php Redirect all requests to the index.php. Activate the .htaccess or (better) add the config to the vhost config. ```text -RewriteEngine On -RewriteBase / -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d -RewriteRule ^.*$ /index.php [L] +<VirtualHost *:80> + ServerName redirects.example.com + ServerAlias www.redirect-domain-1.com ... www.redirect-domain-N.com + + DocumentRoot "/var/www/links/public_html" + ServerSignature Off + + ErrorLog "/var/log/apache2/links_error.log" + CustomLog "/var/log/apache2/links_access.log" combined + + # --- Allow access on webroot + <Directory "/var/www/links/public_html"> + Options -Indexes -FollowSymLinks -MultiViews + AllowOverride None + Require all granted + </Directory> + + ## Rewrite rules + RewriteEngine On + + RewriteRule ^(.*)$ index.php [QSA,L] + +</VirtualHost> ``` In the DNS point all hostnames with redirects only to this server (i.e. with a CNAME). -If you don't have a single vhost in the webserver then additionally add the -domains to "catch" as ServerAlias. +#### Web ui + +The web ui is a viewer only - no configuration can be changed. +It shows all domains, redirects and is helpful to keep an overview. + +This web points to the same document root - should be protected. In the example below is an ip restriction with an additional basic auth (snippet only). + +```text +<VirtualHost *:443> + ServerName links.example.com + DocumentRoot "/var/www/links/public_html" + ServerSignature Off + + ErrorLog "/var/log/apache2/links_error.log" + CustomLog "/var/log/apache2/links_access.log" combined + + # --- Allow access on webroot + <Directory "/var/www/links/public_html"> + Options -Indexes -FollowSymLinks -MultiViews + AllowOverride None + Require all granted + + <RequireAll> + # ip restriction: networks with access + <RequireAny> + Require ip 192.168.100.0/24 + Require ip 192.168.200.0/24 + </RequireAny> + + # and additional basic auth + Require valid-user + AuthType Basic + # ... basic auth config here + </RequireAll> + + </Directory> + + SSLEngine on + SSLCertificateFile "/etc/ssl/certs/links.example.com.fullchain.cer" + SSLCertificateKeyFile "/etc/ssl/certs/links.example.com.key.pem" + SSLCertificateChainFile "/etc/ssl/certs/links.example.com.fullchain.cer" + SSLCACertificatePath "/etc/ssl/certs" + +</VirtualHost> +``` \ No newline at end of file diff --git "a/docs/30_\342\232\231\357\270\217_Configuration.md" "b/docs/30_\342\232\231\357\270\217_Configuration.md" index 9045a308fd74ee3c2befba62caf6be3faa0789a0..cb6580c058bb023e3b507fdd9a849518c335d381 100644 --- "a/docs/30_\342\232\231\357\270\217_Configuration.md" +++ "b/docs/30_\342\232\231\357\270\217_Configuration.md" @@ -21,6 +21,8 @@ Let's start with an example for a `redirects_<FQDN>.json`: ```json { + "comment": "Example redirect web", + "httponly": true, "direct":{ "/heise": {"code": 307, "target": "https://www.heise.de" } }, @@ -33,10 +35,14 @@ Let's start with an example for a `redirects_<FQDN>.json`: } ``` -There are 2 required sections to define redirects: +The keys are -* direct -* regex +| Key | Type | comment | +| --- |--- | --- +| comment | string | optional: short description for this domain | +| httponly | bool | optional: flag if the redirects work on http only. Default: false (redirect definitions are the same for http and https) | +| direct | array | required: list of redirects with strings | +| regex | array | required: list of redirects with regex | The section "direct" will be scanned first and has priority. The json will be read into a hash ... if you define a direct rule twice then diff --git "a/docs/40_\360\237\226\245\357\270\217_Web_ui.md" "b/docs/40_\360\237\226\245\357\270\217_Web_ui.md" index c69d22a1be4209d3f63264bd8fdd9896b8b4b67c..5ee6e8be789841f82c8adca79087c5852e9715fb 100644 --- "a/docs/40_\360\237\226\245\357\270\217_Web_ui.md" +++ "b/docs/40_\360\237\226\245\357\270\217_Web_ui.md" @@ -25,7 +25,7 @@ In the table you see the columns * **Host** - the hostname/ FQDN that has a redirect rule * π· Domain with config entries - * β»οΈ Alias pointng to the config of another domain + * βͺοΈ Alias pointng to the config of another domain * **Ip address** - the found ip of the fqdn. * π’ green = domain is on the same ip like the redirect tool. * π yellow = domain is on another domain (= the config from here does not work because redirect is handled on another system) @@ -36,12 +36,22 @@ In the table you see the columns * **Code** - http response code for redirection * **Target** - target url -## Http header +### Test connection + +You can click a link to a hostname, a linked redirect or target url. In an overlay window you get the status for the http test. + +### Http header If click on the link of the host you get a live view of a response of a http HEAD request to the webroot of this domain. The redirects of the type "direct" have a linked "From" field value. If you click on it you get a live view of the http response header of `https://[Server]/[From]`. + + +If the connect failed there is no http response header to show. To get an idea what is wrong you get the curl error message. The you can estimate the problem - is it a missing DNS entry, a network problem, timeout, whatever. + + + ## Config If you click on the linked text "config" you get the content of the config file for this domain with all its redirects. diff --git a/docs/images/admin_ui.png b/docs/images/admin_ui.png index a7f9671a7a69da673f55571a7e127a94848f8b09..e3c36b2946632c22b21a9ee80a81e40b0022dde8 100644 Binary files a/docs/images/admin_ui.png and b/docs/images/admin_ui.png differ diff --git a/docs/images/admin_ui_box_connect_failed.png b/docs/images/admin_ui_box_connect_failed.png new file mode 100644 index 0000000000000000000000000000000000000000..c30be6b0137b6197e39a0364bb465da350567a41 Binary files /dev/null and b/docs/images/admin_ui_box_connect_failed.png differ diff --git a/docs/images/admin_ui_box_http_ok.png b/docs/images/admin_ui_box_http_ok.png new file mode 100644 index 0000000000000000000000000000000000000000..ea2e0c841d756ef3707abfd0c5a93a4494652577 Binary files /dev/null and b/docs/images/admin_ui_box_http_ok.png differ diff --git a/docs/style.css b/docs/style.css index 45383c3288f17a74d88fc2ffca48307f794d4592..90a0bc938f147608a1d4fd11952e4825f076862c 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,6 +1,6 @@ /* override css elements of daux.io blue theme - version 2023-11-10 + version 2024-10-31 */ :root { /* Axels Overrides */ @@ -8,22 +8,24 @@ --link-color: #228; --brand-color: var(--color-text); --brand-background: var(--body-background); - --code-tag-border-color: #d8d8d8; + --code-tag-background-color: #f0f3f3; + --code-tag-border-color: #dee; + --code-tag-box-shadow: none; --hr-color: none; + --pager-background-color: #f8fafa; + --pager-border-color: none; --search-field-background: none; --search-field-border-color: none; --sidebar-background: var(--body-background); --sidebar-border-color: none; - --sidebar-link-active-background: #e8f4f6; - --sidebar-link-active-background: #eee; + --sidebar-link-active-background: #f0f4f6; + --toc--inner-border-color: none; /* Axels custom values */ - --axel_bg-toc: var(--body-background); + --axel_bg-toc: #f8fafa; --axel_bg-toc-head: #f8f8f8; --axel_brand-background: none; --axel_brand-pre-background: rgb(255, 0, 51); - ; --axel_brand-pre-background-hover: rgb(255, 0, 51); - ; --axel_h1_header: none; --axel_h1: #111; --axel_h1-bg: none; @@ -34,12 +36,13 @@ --axel_h2-hero-bottom: 2px solid #912; --axel_h3: #333; --axel_h3-bottom: 0px solid #ddd; - --axel_h4: #444; - --axel_hero_bg: #f8f8f8; + --axel_h4: #666; + --axel_h5: #888; + --axel_hero_bg: #faf8f6; --axel_img-border: 2px dashed #ccc; --axel_nav-bg: #fcfcfc; --axel_nav-buttomborder: #ddd; - --axel_pre-background: #f8f8f8; + --axel_pre-background: #faf8f6; --axel-th-background: #e0e4e8; --axel-article-nav-border-top: 0px dotted #ddd; } @@ -58,6 +61,7 @@ --sidebar-background: var(--body-background); --sidebar-border-color: none; --sidebar-link-active-background: #333; + --sidebar-link-color: var(--link-color); /* Axels custom values */ --axel_bg-toc: var(--body-background); --axel_bg-toc-head: #333; @@ -76,6 +80,8 @@ --axel_h2-hero-bottom: 2px solid #712; --axel_h3: #589; --axel_h3-bottom: 0px solid #333; + --axel_h4: #478; + --axel_h5: #278; --axel_hero_bg: #242424; --axel_img-border: 2px dashed #555; --axel_nav-bg: #242424; @@ -122,7 +128,25 @@ a.Brand { /* ---------- page content ---------- */ .s-content { - padding-top: 1em; + padding-top: 6em; +} + +/** +h1::before{color: #aaa;content: 'h1: ';} +h2::before{color: #aaa;content: 'h2: ';} +h3::before{color: #aaa;content: 'h3: ';} +h4::before{color: #aaa;content: 'h4: ';} +h5::before{color: #aaa;content: 'h5: ';} +h6::before{color: #aaa;content: 'h6: ';} +*/ +h2::before{color: #888;content: ': : ';} +h3::before{color: #ccc;content: '> ';} +h4::before{color: #ccc;content: '_ ';} + +.s-content h1::before{ + color: #f00; + content: 'FEHLER: Keine UΜberschrift 1 in einer Markdown-Datei fΓΌr Daux verwenden! Mit H2 beginnen!'; + content: '!! h1 !! '; } .s-content h1 { @@ -144,8 +168,14 @@ a.Brand { border-bottom: var(--axel_h2-bottom); } -h1:first-of-type { +.Page__header > h1:first-of-type { margin-top: 0em; + margin-left: -1em; + padding-left: 1em; + position: fixed; + min-width: 100%; + background: var(--body-background); + box-shadow: 0 2em 1em var(--body-background); } h2:first-of-type { @@ -170,6 +200,13 @@ img{ .s-content > h4 { color: var(--axel_h4); + font-size: 140%; + font-weight: bold; + margin: 2em 0; +} + +.s-content > h5 { + color: var(--axel_h5); font-size: 135%; font-weight: bold; margin: 2em 0; @@ -189,6 +226,8 @@ ul.TableOfContents a{ } .s-content pre { background: var(--axel_pre-background); + border-radius: 0.5em; + padding: 1rem; } /* FIX smaller fnt size in tables */ @@ -240,13 +279,6 @@ div.hero h2 { } /* ---------- TOC ---------- */ -@media(min-width:1700px) { - .TableOfContentsContainer { - position: fixed; - right: 2em; - top: 1em; - } -} .TableOfContentsContainer { background-color: var(--axel_bg-toc); @@ -256,14 +288,18 @@ div.hero h2 { .s-content .TableOfContentsContainer h4 { background-color: var(--axel_bg-toc-head); border-top-left-radius: 1em; + border-bottom: 2px solid var(--axel_bg-toc-bottom-border); font-size: 1.1em; margin: 0; - padding: 0; + padding: 0.3em; + display: none; } .TableOfContentsContainer__content { - border-width: 1px; + border-width: 0px; font-size: 0.5em; + height: inherit; + overflow: auto; } ul.TableOfContents ul { @@ -271,17 +307,27 @@ ul.TableOfContents ul { padding-left: 1em; } +.TableOfContents a:hover{ + text-decoration: underline; +} + +@media(min-width:1700px) { + .TableOfContentsContainer { + background: none; + position: fixed; + right: 2em; + top: 4em; + height: 90%; + } +} + /* ----- Icons on links --- */ .EditOn a::before{ content: 'βοΈ '; } -.Links a[href^="https://github.com/"]::before { - content: 'π '; -} - -.Links a[href^="https://git-repo.iml.unibe.ch/"]::before { +.Links a::before { content: 'π '; } diff --git a/public_html/admin/functions.js b/public_html/admin/functions.js index 7650f5d31ce6382ddfb9581ec680a12c795a0871..b5feeee70541c9c91792d2b95ca4ef8fea5bcf69 100644 --- a/public_html/admin/functions.js +++ b/public_html/admin/functions.js @@ -44,6 +44,12 @@ async function showInOverlay(oLink){ if (response.ok) { show(body); + + // colorize clicked link + var myspan=document.getElementsByClassName("status")[0]; + var myclass=myspan.className.replace("status ",""); + oLink.className=myclass; + oLink.title=myspan.innerText; } else { show("HTTP-Error: " + response.status); } diff --git a/public_html/admin/index.php b/public_html/admin/index.php index f09c4897bb47ad6434516e32bac79df9227e3d15..185914d25e921b7a67cf44faf8dacd7bad7e3291 100644 --- a/public_html/admin/index.php +++ b/public_html/admin/index.php @@ -14,47 +14,46 @@ * 2022-02-03 v0.1 <axel.hahn@iml.unibe.ch> initial version * 2022-05-31 v0.2 <axel.hahn@iml.unibe.ch> optical changes; use debugredirect=1 if url is a local domain * 2023-08-28 v1.0 <axel.hahn@unibe.ch> Welcome message if there is no config yet + * 2025-01-13 v1.1 <axel.hahn@unibe.ch> test links http and https * ---------------------------------------------------------------------- */ require_once '../classes/redirect.admin.class.php'; -$oR=new redirectadmin(); -$sHtml=''; -$sErrors=''; +$oR = new redirectadmin(); +$sHtml = ''; +$sErrors = ''; -$aIco=[ - 'h1'=>'β©', +$aIco = [ + 'h2_err' => 'β οΈ', + 'h2_config' => 'π οΈ', + 'h2_file' => 'π', - 'h2_head'=>'βοΈ', - 'h2_err'=>'β οΈ', - 'h2_config'=>'π οΈ', - 'h2_file'=>'π', + 'ip_ok' => 'π’', + 'ip_warn' => 'π ', + 'ip_err' => 'β', - 'ip_ok'=>'π’', - 'ip_warn'=>'π ', - 'ip_err'=>'β', + 'type_config' => 'π·', + 'type_alias' => 'βͺοΈ', - 'type_config'=>'π·', - 'type_alias'=>'β»οΈ', - - 'url'=>'π', - 'welcome'=>'πͺ', + 'url' => 'π', + 'welcome' => 'πͺ', ]; // ---------------------------------------------------------------------- // FUNCTIONS // ---------------------------------------------------------------------- -function getId($sDomain){ - return 'id_'.md5($sDomain); +function getId($sDomain) +{ + return 'id_' . md5($sDomain); } // ---------------------------------------------------------------------- // MAIN // ---------------------------------------------------------------------- -if (!$oR->isEnabled()){ - $sHtml.='<div class="content"> +if (!$oR->isEnabled()) { + $sHtml .= '<div class="content"> <h3>Nothing to see here.</h3> <div class="error"> The Admin interface is disabled. @@ -66,148 +65,154 @@ if (!$oR->isEnabled()){ // ---------- return content for ajax requests // ---------- TEST URL - $sUrl=(isset($_GET['url']) && $_GET['url']) ? $_GET['url'] : ''; + $sUrl = (isset($_GET['url']) && $_GET['url']) ? $_GET['url'] : ''; - if ($sUrl){ - $sResult=$oR->httpGet($sUrl,1); - echo '<h2>Response of a http HEAD request to '.$aIco['url'].' <a href="'.$sUrl.'">'.$sUrl.'</a></h2>' - . $oR->renderHttpResponseHeader($sResult) - ; + if ($sUrl) { + $aResult = $oR->httpGet($sUrl, 1); + echo '<h2>Response of a http HEAD request to ' . $aIco['url'] . ' <a href="' . $sUrl . '">' . $sUrl . '</a></h2>' + . $oR->renderHttpResponseHeader($aResult) + ; return true; } # ---------- return content for ajax requests - $sCfgfile=(isset($_GET['cfgfile']) && $_GET['cfgfile']) ? $_GET['cfgfile'] : ''; - if($sCfgfile){ - $sFilename=__DIR__.'/../../config/'.$sCfgfile; - echo '<h2>'.$aIco['h2_file'].' File: '.htmlentities($sCfgfile).'</h2>' + $sCfgfile = (isset($_GET['cfgfile']) && $_GET['cfgfile']) ? $_GET['cfgfile'] : ''; + if ($sCfgfile) { + $sFilename = __DIR__ . '/../../config/' . $sCfgfile; + echo '<h2>' . $aIco['h2_file'] . ' File: ' . htmlentities($sCfgfile) . '</h2>' . (file_exists($sFilename) - ? '<pre>'. file_get_contents($sFilename).'</pre>' + ? '<pre>' . file_get_contents($sFilename) . '</pre>' : '<div class="error">ERROR: config file was not found.</div>' ) ; exit(0); } - $sMyIp=gethostbyname($_SERVER['SERVER_NAME']); - if(!$sMyIp){ - $sErrors.='<li>Ip address of current host ['.$_SERVER['SERVER_NAME'].'] was not found.</li>'; + $sMyIp = gethostbyname($_SERVER['SERVER_NAME']); + if (!$sMyIp) { + $sErrors .= '<li>Ip address of current host [' . $_SERVER['SERVER_NAME'] . '] was not found.</li>'; } // ---------- GET CONFIG DATA - $aHosts=$oR->getHosts(); + $aHosts = $oR->getHosts(); // ---------- SHOW ERRORS - if(count($aHosts['_errors'])) { - $sErrors.= '<li>' . implode('</li><li>', $aHosts['_errors']).'</li>'; + if (count($aHosts['_errors'])) { + $sErrors .= '<li>' . implode('</li><li>', $aHosts['_errors']) . '</li>'; } unset($aHosts['_errors']); // ---------- LOOP OVER ALL ENTRIES - $sTable=''; - foreach($aHosts as $sHost => $aCfg){ - $sTdFirst='<tr class="cfgtype-'.$aCfg['type'].'">' - .'<td>' - .'<span style="display: none">'.$sHost.'</span>' - .$aIco['type_'.$aCfg['type']] - .' <a href="?url=http://'.$sHost.'/' - .($aCfg['ip']===$sMyIp ? '?debugredirect=1' : '' ) - .'" title="click to test http://'.$sHost.'/" onclick="showInOverlay(this); return false;">'.$sHost.'</a></td>' - .'<td>' - .($aCfg['ip'] - ? ($aCfg['ip']===$sMyIp - ? '<span title="">'.$aIco['ip_ok'].' '.$aCfg['ip']. '</span>' - : '<span title="Warning: this is not the ip address of the current host ('.$sMyIp.')">'.$aIco['ip_warn'].' '.$aCfg['ip']. '</span>' - ) - : '<span class="error">'.$aIco['ip_err'].' ERROR: unknown host</span>') - .'</td>' - .'<td>' - .($aCfg['type']=="config" - ? '<a href="?cfgfile=redirects_'.$sHost.'.json" onclick="showInOverlay(this); return false;" title="show config for host '.$sHost.'">'.$aCfg['type'].'</a> ' - : '<a href="?cfgfile=redirects_'.$aCfg['target'].'.json" onclick="showInOverlay(this); return false;" title="show config for alias '.$sHost.' pointing to host '.$aCfg['target'].'">'.$aCfg['type'].'</a> ' + $sTable = ''; + foreach ($aHosts as $sHost => $aCfg) { + $bHttpOnly = isset($aCfg['redirects']['httponly']) && $aCfg['redirects']['httponly']; + + $sUrlpart = '://' . $sHost . '/' + . ($aCfg['ip'] === $sMyIp ? '?debugredirect=1' : '') + ; + $sTdFirst = '<tr class="cfgtype-' . $aCfg['type'] . '">' + . '<td>' + . '<span style="display: none">' . $sHost . '</span>' + . '<span style="float: right;">' + . ' <a href="?url=http' . $sUrlpart . '" title="click to test http://' . $sHost . '/" onclick="showInOverlay(this); return false;">http</a> ' + . ($bHttpOnly ? '' : '<a href="?url=https' . $sUrlpart . '" title="click to test https://' . $sHost . '/" onclick="showInOverlay(this); return false;">https</a>') + . '</span>' + . $aIco['type_' . $aCfg['type']] . " $sHost" + . '</td>' + . '<td>' + . ($aCfg['ip'] + ? ($aCfg['ip'] === $sMyIp + ? '<span title="">' . $aIco['ip_ok'] . ' ' . $aCfg['ip'] . '</span>' + : '<span title="Warning: this is not the ip address of the current host (' . $sMyIp . ')">' . $aIco['ip_warn'] . ' ' . $aCfg['ip'] . '</span>' + ) + : '<span class="error">' . $aIco['ip_err'] . ' ERROR: unknown host</span>') + . '</td>' + . '<td>' + . ($aCfg['type'] == "config" + ? '<a href="?cfgfile=redirects_' . $sHost . '.json" onclick="showInOverlay(this); return false;" title="show config for host ' . $sHost . '">' . $aCfg['type'] . '</a> ' + : '<a href="?cfgfile=redirects_' . $aCfg['target'] . '.json" onclick="showInOverlay(this); return false;" title="show config for alias ' . $sHost . ' pointing to host ' . $aCfg['target'] . '">' . $aCfg['type'] . '</a> ' ) . '</td>' - ; - /* - if(!$aCfg['ip']){ - $sErrors.='<li>Host was not found in DNS: '.$sHost.'</li>'; - } - */ - if (isset($aCfg['redirects'])){ - $iCount=0; - foreach(['direct', 'regex'] as $sType){ - if (count($aCfg['redirects'][$sType])){ - foreach($aCfg['redirects'][$sType] as $sFrom=>$aTo){ + ; + if (isset($aCfg['redirects'])) { + $iCount = 0; + foreach (['direct', 'regex'] as $sType) { + if (count($aCfg['redirects'][$sType])) { + foreach ($aCfg['redirects'][$sType] as $sFrom => $aTo) { $iCount++; - $sTable.=$sTdFirst - .'<td class="type-'.$sType.'">'.$sType.'</td>' - .'<td class="type-'.$sType.'">' - .($sType == 'direct' - ? '<a href="?url=http://'.$sHost.$sFrom.'" title="click to test http://'.$sHost.'/'.$sFrom.'" onclick="showInOverlay(this); return false;">'.$sFrom.'</a>' - : $sFrom - ) - .'</td>' - .'<td class="http-'.$aTo['code'].'">'.$aTo['code'].'</td>' - .'<td>'.$aIco['url'].' <a href="?url='.$aTo['target'].'" title="click to test '.$aTo['target'].'" onclick="showInOverlay(this); return false;">'.$aTo['target'].'</a></td>' - .'</tr>'; + $sTable .= $sTdFirst + . '<td class="type-' . $sType . '">' . $sType . '</td>' + . '<td class="type-' . $sType . '">' + . ($sType == 'direct' + ? '<span style="float: right;">' + . '<a href="?url=http://' . $sHost . $sFrom . '" title="click to test http://' . $sHost . '/' . $sFrom . '" onclick="showInOverlay(this); return false;">http</a> ' + . '<a href="?url=https://' . $sHost . $sFrom . '" title="click to test http://' . $sHost . '/' . $sFrom . '" onclick="showInOverlay(this); return false;">https</a> ' + . '</span>' + : '' + ) + . $sFrom + . '</td>' + . '<td class="http-' . $aTo['code'] . '">' . $aTo['code'] . '</td>' + . '<td>' . $aIco['url'] . ' <a href="?url=' . $aTo['target'] . '" title="click to test ' . $aTo['target'] . '" onclick="showInOverlay(this); return false;">' . $aTo['target'] . '</a></td>' + . '</tr>'; } } - + } } else { - // type = alias - // $sHtml.='<tr>'.$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <a href="#'.getId($aCfg['target']).'">'.$aCfg['target'].'</a>' : '').'</td></tr>'; - $sTable.=$sTdFirst.'<td></td><td></td><td></td><td>'.(isset($aCfg['target']) ? 'see config for <em>'.$aCfg['target'].'</em>' : '').'</td></tr>'; + $sTable .= $sTdFirst . '<td></td><td></td><td></td><td>' + . (isset($aCfg['target']) ? 'see config for <em>' . $aCfg['target'] . '</em>' : '') + . '</td></tr>'; } - + } - $sTable=$sTable + $sTable = $sTable ? '<table class="mydatatable"><thead> <tr> - <th>Host</th> - <th>Ip address</th> - <th>Setup</th> - <th>Type</th> - <th>From</th> - <th>Code</th> - <th>Target</th> + <th>Host</th> + <th>Ip address</th> + <th>Setup</th> + <th>Type</th> + <th>From</th> + <th class="th-code">Code</th> + <th>Target</th> </tr> - </thead><tbody>'.$sTable.'</tbody></table></div>' + </thead><tbody>' . $sTable . '</tbody></table></div>' . '<br><br>' - .'<div class="content legend">' + . '<div class="content legend">' . '<strong>Legend</strong>:<br>' . '<table><tbody>' - .'<tr>' + . '<tr>' . '<td colspan="2"><br>Icons:</td>' - .'</tr>' - .'<tr>' - . '<td>' - . $aIco['type_config'].' Domain with config entries<br>' - . $aIco['type_alias'].' Alias pointng to the config of another domain<br>' - . $aIco['url'].' clickable url to show result of a http head request<br>' - . '</td>' - . '<td>' - . $aIco['ip_ok'].' Domain is on the same ip like the redirect tool.<br>' - . $aIco['ip_warn'].' Domain is on another domain (= the config from here does not work because redirect is handled on another system)<br>' - . $aIco['ip_err'].' Hostname was not found in DNS<br>' - . '</td>' . '</tr>' - .'<tr>' + . '<tr>' + . '<td>' + . $aIco['type_config'] . ' Domain with config entries<br>' + . $aIco['type_alias'] . ' Alias pointng to the config of another domain<br>' + . $aIco['url'] . ' clickable url to show result of a http head request<br>' + . '</td>' + . '<td>' + . $aIco['ip_ok'] . ' Domain is on the same ip like the redirect tool.<br>' + . $aIco['ip_warn'] . ' Domain is on another domain (= the config from here does not work because redirect is handled on another system)<br>' + . $aIco['ip_err'] . ' Hostname was not found in DNS<br>' + . '</td>' + . '</tr>' + . '<tr>' . '<td colspan="2"><br>Redirect status codes:</td>' - .'<tr>' - . '<td colspan="2">' - . '301 Moved Permanently - The address is outdated! However, the new address of the resource is still being forwarded.<br>' - . '307 Temporary Redirect - The URL is currently being redirected temporarily (method is retained) - but the old address remains valid.<br>' - . '308 Permanent Redirect - The address is outdated! The new address of the resource is forwarded with the same method.<br>' - . '</td>' - .'</tr>' - .'</tbody></table>' - .'</div>' - - : '<h3>'.$aIco['welcome'].' Welcome!</h3> + . '<tr>' + . '<td colspan="2">' + . '301 Moved Permanently - The address is outdated! However, the new address of the resource is still being forwarded.<br>' + . '307 Temporary Redirect - The URL is currently being redirected temporarily (method is retained) - but the old address remains valid.<br>' + . '308 Permanent Redirect - The address is outdated! The new address of the resource is forwarded with the same method.<br>' + . '</td>' + . '</tr>' + . '</tbody></table>' + . '</div>' + + : '<h3>' . $aIco['welcome'] . ' Welcome!</h3> <p> Thank you for the installation!<br> Now is a good moment to create your first config. @@ -218,66 +223,61 @@ if (!$oR->isEnabled()){ <li>Relaod this page</li> </ul> <p> - See the <a href="'.$oR->urlDocs.'Configuration.html" target="_blank">Docs</a> for details. + See the <a href="' . $oR->urlDocs . 'Configuration.html" target="_blank">Docs</a> for details. </p> ' - ; + ; - $sHtml.=' - <!-- - <h2>'.$aIco['h2_head'].' Http head tester</h2> - <div class="content"> - <form> - '.$aIco['url'].' <input type="text" name="url" size="100" value="'.$sUrl.'" placeholder="Enter url or click a link in the table below."/> - <button>Http HEAD</button> - </form> - </div> - <br> - --> - <h2>'.$aIco['h2_config'].' Domains and their redirects</h2> - <div class="content">' - - /* - .'<h2>Config array</h2> - <pre>'.print_r($aHosts, 1).'</pre>' - */ - .$sTable - ; + $sHtml .= ' + <h2>' . $aIco['h2_config'] . ' Domains and their redirects</h2> + <div class="content">' + . $sTable + . '</div>' + ; - $sErrors = $sErrors - ? '<h2>'.$aIco['h2_err'].' Found errors</h2>' - .'<div class="content">' - .'<ol class="error">' - .$sErrors - .'</ol>' - .'</div>' + $sErrors = $sErrors + ? '<!-- <h2>' . $aIco['h2_err'] . ' Found errors</h2> -->' + . '<ol class="error">' + . $sErrors + . '</ol>' : '' - ; + ; - $sHtml.='<footer><a href="'.$oR->urlRepo.'">Source</a> | <a href="'.$oR->urlDocs.'">Docs</a></footer>' - ; + $sHtml .= '<footer><a href="' . $oR->urlRepo . '">Source</a> | <a href="' . $oR->urlDocs . '">Docs</a></footer>' + ; } // ---------- OUTPUT ?><!doctype html> <html> - <head> - <title>Redirects</title> - <link rel="stylesheet" type="text/css" href="https://cdn.datatables.net/v/dt/dt-1.11.4/datatables.min.css"/> - <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> - <script type="text/javascript" src="https://cdn.datatables.net/v/dt/dt-1.11.4/datatables.min.js"></script> - <link rel="stylesheet" href="main.css"> - </head> - <body> - <h1><?php echo $aIco['h1']?> <a href="?">Redirects :: admin</a></h1> +<head> + <title>Redirects</title> + + <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js" + integrity="sha512-v2CJ7UaYy4JwqLDIrZUI/4hqeoQieOmAZNXBeQyjo21dadnwR+8ZaIJVT8EE2iyI61OV8e6M8PP2/4hpQINQ/g==" + crossorigin="anonymous" referrerpolicy="no-referrer"></script> + + <link rel="stylesheet" type="text/css" href="//cdn.datatables.net/2.2.1/css/dataTables.dataTables.min.css" /> + <script type="text/javascript" src="//cdn.datatables.net/2.2.1/js/dataTables.min.js"></script> + + <link rel="stylesheet" href="main.css"> +</head> + +<body> + <h1><a href="?"> Redirects :: admin</a></h1> + + <main> <?php echo $sErrors . $sHtml; ?> - <div id="divoverlay" class="overlay" onclick="this.style.display='none';"></div> - - <script type="text/javascript" src="functions.js"></script> - <script> - - </script> - </body> -</html> + </main> + + <div id="divoverlay" class="overlay" onclick="this.style.display='none';"></div> + + <script type="text/javascript" src="functions.js"></script> + <script> + + </script> +</body> + +</html> \ No newline at end of file diff --git a/public_html/admin/main.css b/public_html/admin/main.css index 7b23959c2995b327c6ab6692c426817bcac4b9ed..d8fd9f535634c3a1a9373040e24213b387d28175 100644 --- a/public_html/admin/main.css +++ b/public_html/admin/main.css @@ -1,47 +1,121 @@ -a{color:royalblue;} -body{background: #f8f8f8; color: #234; font-family: arial; margin: 0;} -h1{background:rgba(0,0,0,0.05); margin: 0 0 1em;; padding: 0.5em;} -h1 a{color:#234; text-decoration: none;} -h2{background: #d0e0e8; color:#458; margin: 1em 0 0.5em; border-top: 2px solid #fff; border-left: 5px solid #fff; border-top-left-radius: 0.5em; padding: 0.5em; margin: 0 0 1em;} -h3{color:#ccc; font-size: 250%} - -pre{background: rgba(0,0,0,0.02);padding: 0.3em 1em; border: 1px solid rgba(0,0,0,0.1); margin: 2em 0 3em;; border-bottom: 2px solid rgba(0,0,0,0.2);} - -tr:hover{background: #f4f0f8 !important;} -footer{background:rgba(0,0,0,0.03); margin-top: 4em; text-align: right;padding: 1em;} - -.content{margin: 0 1em;} -.legend{background: #fff; padding: 1em;} -.error{background: #fcc; padding: 0.2em 1.5em;} -.warning{color:#651; background:#fec; padding: 0.2em 1em;} - -.cfgtype-alias{color:#89a;} -.http-301::after{color:#a55; content: ' (Moved Permanently)'} -.http-307::after{color:#488; content: ' (Temporary Redirect)'} -.http-308::after{color:#a95; content: ' (Permanent Redirect)'} -.type-direct{color:#383; } -.type-regex{color:#838; } - -.status{padding: 0.5em 1em; position: relative;top: -0.8em; border-left: 2px solid; border-left_: 1.5em solid;font-size: 125%;} -.status-ok{color:#080; background:#cec; } -.status-redirect{color:#651; background:#fec;} -.status-error{color:#800; background:#ecc;} -.statuscode{background: rgba(255,255,255,0.8); font-size: 150%;} - -.location{background:#cde; border: 1px solid rgba(0,0,0,0.2); font-size: 125%; color:#236;} -.location::before{content:' > '; color:#800; background-color: #fff;} -.debug{color:#197;} - -.overlay{position: fixed; margin: 0; width: 100%; height: 100%; top: 0; left: 0; background: rgba(0,0,0,0.3);overflow: scroll; display: none;} -.overlay>div{margin: 3% 10%; background: #f8f8f8; padding: 1em;box-shadow: 0 0 3em #000; } +:root{ + + --col-border-1: #6aa; + + --body-bg: linear-gradient(-10deg, #ddd, #fff, #ddd, #e5e5e5) fixed; + --body-color: #335; + + --top-bg: linear-gradient(0deg, #000, #234, #222); + --top-color: #e55; + --top-bottom: var(--col-border-1); + + --link-color: #46d; + + --h2-bg: #c8e0e0; + --h2-color: #458; + --h2-border: var(--col-border-1); + + --h3-color: #ccc; + + --footer-bg: rgba(0,0,0,0.05); + + --box-content-bg: #fff; + --box-legend-bg: #fff4e8; + + --txt-error-bg: #ebb; + --txt-error-color: #800; + --txt-ok-bg: #aeb; + --txt-ok-color: #080; + --txt-warning-bg: #fda; + --txt-warning-color: #651; + + --txt-alias-color: #89a; + + --http-301-color: #a55; + --http-307-color: #488; + --http-308-color: #a95; + + --type-direct-color:#383; + --type-regex-color:#838; + + --txt-statuscode-bg: rgba(255,255,255,0.8); + + --txt-location-bg: #cde; + --txt-location-color: #236; + + --txt-location-before-bg: #fff; + --txt-debug-color: #197; + + --overlay-bg: rgba(0,0,0,0.3); + --overlay-box-bg: #f8f8f8; + --overlay-box-shadow: 0 0 3em #000; + + --spin-color1: #ccc; + --spin-color2: var(--col-border-1); + +} + + +a{color: var(--link-color);} + +body { + background: var(--body-bg); + color: var(--body-color); + font-size: 1.2em; + font-family: arial; + margin: 0; + padding: 0; + height: 100%,; +} + +h1{margin: 0 0 0.5em;; padding: 0;} +h1 a{background: var(--top-bg); color: var(--top-color); text-decoration: none;border-bottom: 5px solid var(--top-bottom); display: block; padding: 0.5em;} + +h2{background: var(--h2-bg); color: var('--h2-color'); margin: 1em 0 0.5em; border-top: 2px solid var(--h2-border); border-left: 5px solid var(--h2-border); border-top-left-radius: 0.5em; padding: 0.5em; margin: 0;} +h3{color:var('--h3-color'); font-size: 250%} + +pre{background: rgba(0,0,0,0.02);padding: 0.3em 1em; border: 1px solid rgba(0,0,0,0.1); margin: 2em 0 3em;; border-bottom: 2px solid rgba(0,0,0,0.2); border-bottom-right-radius: 1em;} + +.dataTable tr:hover{background: #f4f0f8 !important;} +footer{background: var(--footer-bg); margin-top: 4em; text-align: right; padding: 1em;} +main{margin: 0 2em; padding: 0em;} + +.content{background: var(--box-content-bg); padding: 1em;} +.legend{background: var(--box-legend-bg); border-top: 1px dashed; padding: 1em;} +.error{background: var(--txt-error-bg); color: var(--txt-error-color); padding: 0.2em 0.5em;} +.warning{background: var(--txt-warning-bg); color: var(--txt-warning-color); } + +ol.error{padding-left: 2em;} + +.th-code{min-width: 12em;} + +.cfgtype-alias{color:var(--txt-alias-color);} +.http-301::after{color: var(--http-301-color); content: ' (Moved Permanently)'} +.http-307::after{color:var(--http-307-color); content: ' (Temporary Redirect)'} +.http-308::after{color:var(--http-308-color); content: ' (Permanent Redirect)'} +.type-direct{color: var(--type-direct-color); } +.type-regex{color: var(--type-regex-color); } + +.status{padding: 0.5em 1em; position: relative; top: -1.4em; left: -0.9em; border-left: 5px solid; font-size: 125%; border-bottom-right-radius: 0.7em;} +.status-ok{background: var(--txt-ok-bg); color: var(--txt-ok-color);} +.status-redirect{background: var(--txt-warning-bg); color: var(--txt-warning-color);} +.status-error{background: var(--txt-error-bg); color: var(--txt-error-color);} +.statuscode{background: var(--txt-statuscode-bg); font-size: 150%;} + +.location{background:var(--txt-location-bg); font-size: 125%; color:var(--txt-location-color);} +.location::before{content:' π '; background-color: var(--txt-location-before-bg);} +.debug{color:var(--txt-debug-color);} + +.overlay{position: fixed; margin: 0; width: 100%; height: 100%; top: 0; left: 0; background: var(--overlay-bg); overflow: scroll; display: none;} +.overlay>div{margin: 3% 10%; background: var(--overlay-box-bg); padding: 1em;box-shadow: var(--overlay-box-shadow); } .spin { display: inline-block; width: 50px; height: 50px; - border: 3px solid #ccc; + border: 3px solid var(--spin-color1); border-radius: 50%; - border-top-color: #1c87c9; + border-top-color: var(--spin-color2); animation: spin 1s ease-in-out infinite; -webkit-animation: spin 1s ease-in-out infinite; } diff --git a/public_html/classes/redirect.admin.class.php b/public_html/classes/redirect.admin.class.php index 9a1ed976c69ddc31599ab96b986100258b15b1fd..36e07590543d8724a937691feaf4f623be8c7833 100644 --- a/public_html/classes/redirect.admin.class.php +++ b/public_html/classes/redirect.admin.class.php @@ -19,6 +19,7 @@ require_once 'redirect.class.php'; * 2022-05-31 v1.7 ah optical changes * 2023-08-28 v1.8 ah remove php warning if there is no config yet * 2024-10-04 v1.9 ah php8 only: typed variables + * 2025-01-13 v1.10 ah fetch curl error */ /** @@ -57,13 +58,15 @@ class redirectadmin extends redirect } /** - * Make a single http(s) get request and return the response body + * Make a single http(s) get request and return an array with response header, body, curl error info * @param string $url url to fetch * @param boolean $bHeaderOnly optional: true=make HEAD request; default: false (=GET) - * @return string + * @return array */ - public function httpGet(string $url, bool $bHeaderOnly = false): bool|string + public function httpGet(string $url, bool $bHeaderOnly = false): array { + $aResult = []; + $ch = curl_init($url); foreach ($this->_getCurlOptions() as $sCurlOption => $sCurlValue) { curl_setopt($ch, $sCurlOption, $sCurlValue); @@ -73,29 +76,46 @@ class redirectadmin extends redirect curl_setopt($ch, CURLOPT_NOBODY, 1); } $res = curl_exec($ch); + + $sHeader = ''; + $sBody = ''; + $aResponse = explode("\r\n\r\n", $res, 2); + list($sHeader, $sBody) = count($aResponse) > 1 + ? $aResponse + : [$aResponse[0], '']; + + $aResult = [ + 'url' => $url, + 'response_header' => $sHeader, + 'response_body' => $sBody, + 'curlinfo' => curl_getinfo($ch), + 'curlerrorcode' => curl_errno($ch), + 'curlerrormsg' => curl_error($ch), + ]; + curl_close($ch); - return ($res); + return $aResult; } /** * Get html code for a response header of a request * - * @param string $sHeader + * @param array $aResponse response array from httpGet() method * @return string */ - public function renderHttpResponseHeader(string $sHeader): string + public function renderHttpResponseHeader(array $aResponse): string { + $sHeader=$aResponse['response_header']; $sReturn = $sHeader; - if (!$sReturn) { - $sReturn = '<pre><span class="status status-error">Request failed. </span><br>' - . 'No data... no response.<br>' - . 'Maybe ... ' - . '<ul>' - . '<li>the nostname does not exist ... </li>' - . '<li>or there is a network problem ... or </li>' - . '<li>the webservice on the target system does not run.</li>' - . '</ul>' - . '</pre>' + // $sReturn.="<pre>".print_r($aResponse, 1)."</pre>"; + + if ($aResponse['curlerrorcode']) { + $sReturn = '<pre><br>' + .'<span class="status status-error">Request failed.<br></span>' + .'Curl error #'.$aResponse['curlerrorcode'] .':<br>' + . '<strong>'.$aResponse['curlerrormsg'].'</strong><br><br>' + .'</pre>' + .'π <a href="https://curl.se/libcurl/c/libcurl-errors.html" target="_blank">Curl error codes</a>' ; } else { diff --git a/readme.md b/readme.md index da271843ca6810186ea9b715a5dafc14612803c4..f03652a5f7c2a4647b42edde0113de3e76d658a0 100644 --- a/readme.md +++ b/readme.md @@ -1,7 +1,7 @@ -# IML Redirect # +# IML Redirect -Redirect urls of any domain that points here. +Redirect tool for multiple domains (that point here). Author: Axel Hahn; Institute for Medical Education; University of Bern @@ -11,4 +11,16 @@ Author: Axel Hahn; Institute for Medical Education; University of Bern - - - - \ No newline at end of file +## Screenshots + +Admin web ui with all configured domains and their redirects: + + + +Urls, Redirects and target links can be tested. You see the http reponse header. Redirects will be highlighted. + + + +If a connection fails you get the curl error. + +