Improved image proxy
authorAlexander Ebert <ebert@woltlab.com>
Wed, 18 Oct 2017 14:06:04 +0000 (16:06 +0200)
committerAlexander Ebert <ebert@woltlab.com>
Wed, 18 Oct 2017 14:06:04 +0000 (16:06 +0200)
Closes #2447

com.woltlab.wcf/option.xml
constants.php
wcfsetup/install/files/lib/system/html/output/node/HtmlOutputNodeImg.class.php
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml

index 5568748776f6f73975f416729992598d241518f4..7a3beef0c268dd0558c2a1a045a1c5cdc034840a 100644 (file)
@@ -1069,6 +1069,11 @@ Pinterest</defaultvalue>
                                <optiontype>boolean</optiontype>
                                <defaultvalue>0</defaultvalue>
                        </option>
+                       <option name="image_proxy_insecure_only">
+                               <categoryname>message.general.image</categoryname>
+                               <optiontype>boolean</optiontype>
+                               <defaultvalue>0</defaultvalue>
+                       </option>
                        <option name="image_proxy_expiration">
                                <categoryname>message.general.image</categoryname>
                                <optiontype>integer</optiontype>
@@ -1077,6 +1082,10 @@ Pinterest</defaultvalue>
                                <minvalue>7</minvalue>
                                <suffix>days</suffix>
                        </option>
+                       <option name="image_proxy_host_whitelist">
+                               <categoryname>message.general.image</categoryname>
+                               <optiontype>textarea</optiontype>
+                       </option>
                        <!-- /message.general.image -->
                        
                        <!-- message.censorship -->
index 5e5ef776bf1ad43234b19ff874ed873b86d8fdf3..a617e027bb38ab601df23f3b7529b09289a28177 100644 (file)
@@ -132,7 +132,9 @@ define('EDIT_HISTORY_EXPIRATION', 90);
 define('ENABLE_SHARE_BUTTONS', 1);
 define('SHARE_BUTTONS_PROVIDERS', '');
 define('MODULE_IMAGE_PROXY', 0);
+define('IMAGE_PROXY_INSECURE_ONLY', 0);
 define('IMAGE_PROXY_EXPIRATION', 14);
+define('IMAGE_PROXY_HOST_WHITELIST', '');
 define('ENABLE_CENSORSHIP', 0);
 define('CENSORED_WORDS', '');
 define('REGISTER_ENABLE_PASSWORD_SECURITY_CHECK', 0);
index e494420dc49ffdbfaa1f43e3203cd30862435e46..5e2eeb0d978c4cb01259b0310f92d5047dd49feb 100644 (file)
@@ -2,8 +2,10 @@
 namespace wcf\system\html\output\node;
 use wcf\data\smiley\Smiley;
 use wcf\data\smiley\SmileyCache;
+use wcf\system\application\ApplicationHandler;
 use wcf\system\html\node\AbstractHtmlNodeProcessor;
 use wcf\system\request\LinkHandler;
+use wcf\system\request\RouteHandler;
 use wcf\system\WCF;
 use wcf\util\exception\CryptoException;
 use wcf\util\CryptoUtil;
@@ -81,6 +83,22 @@ class HtmlOutputNodeImg extends AbstractHtmlOutputNode {
                                                continue;
                                        }
                                        
+                                       if (IMAGE_PROXY_INSECURE_ONLY && $urlComponents['scheme'] === 'https') {
+                                               // proxy is enabled for insecure connections only
+                                               continue;
+                                       }
+                                       
+                                       if ($this->bypassProxy($urlComponents['host'])) {
+                                               // check if page was requested over a secure connection
+                                               // but the link is insecure
+                                               if (RouteHandler::secureConnection() && $urlComponents['scheme'] === 'http') {
+                                                       // rewrite protocol to `https`
+                                                       $element->setAttribute('src', preg_replace('~^http~', 'https', $src));
+                                               }
+                                               
+                                               continue;
+                                       }
+                                       
                                        $element->setAttribute('data-valid', 'true');
                                        
                                        if (!empty($urlComponents['path']) && preg_match('~\.svg~', basename($urlComponents['path']))) {
@@ -113,6 +131,63 @@ class HtmlOutputNodeImg extends AbstractHtmlOutputNode {
                }
        }
        
+       /**
+        * Validates the domain name against the list of own domains
+        * and whitelisted ones with wildcard support.
+        * 
+        * @param       string          $hostname
+        * @return      boolean
+        */
+       protected function bypassProxy($hostname) {
+               static $hosts = null;
+               static $validHosts = [];
+               
+               if ($hosts === null) {
+                       $whitelist = explode("\n", StringUtil::unifyNewlines(IMAGE_PROXY_HOST_WHITELIST));
+                       foreach ($whitelist as $host) {
+                               $isWildcard = false;
+                               if (mb_strpos($host, '*') !== false) {
+                                       $host = preg_replace('~^(\*\.)+~', '', $host);
+                                       if (mb_strpos($host, '*') !== false || $host === '') {
+                                               // bad host
+                                               continue;
+                                       }
+                                       
+                                       $isWildcard = true;
+                               }
+                               
+                               $host = mb_strtolower($host);
+                               if (!isset($hosts[$host])) $hosts[$host] = $isWildcard;
+                       }
+                       
+                       foreach (ApplicationHandler::getInstance()->getApplications() as $application) {
+                               $host = mb_strtolower($application->domainName);
+                               if (!isset($hosts[$host])) $hosts[$host] = false;
+                       }
+               }
+               
+               $hostname = mb_strtolower($hostname);
+               if (isset($hosts[$hostname]) || isset($validHosts[$hostname])) {
+                       return true;
+               }
+               else {
+                       // check wildcard hosts
+                       foreach ($hosts as $host => $isWildcard) {
+                               if ($isWildcard && mb_strpos($hostname, $host) !== false) {
+                                       // the prepended dot will ensure that `example.com` matches only
+                                       // on domains like `foo.example.com` but not on `bar-example.com`
+                                       if (StringUtil::endsWith($hostname, '.' . $host)) {
+                                               $validHosts[$hostname] = $hostname;
+                                               
+                                               return true;
+                                       }
+                               }
+                       }
+               }
+               
+               return false;
+       }
+       
        /**
         * Returns the link to fetch the image using the image proxy.
         *
index 5be5d15d5a68461d884f11b0bead59fca09212c5..c53b1db33e8b7b10845df21276f30e5221fb319c 100644 (file)
@@ -1375,6 +1375,9 @@ Als Benachrichtigungs-URL in der Konfiguration der sofortigen Zahlungsbestätigu
                <item name="wcf.acp.option.module_article"><![CDATA[Artikel]]></item>
                <item name="wcf.acp.option.module_image_proxy"><![CDATA[Zwischenspeicherung von externen Bilder aktivieren]]></item>
                <item name="wcf.acp.option.image_proxy_expiration"><![CDATA[Speicherzeit]]></item>
+               <item name="wcf.acp.option.image_proxy_insecure_only"><![CDATA[Nur Bilder aus unverschlüsselten Quellen zwischenspeichern]]></item>
+               <item name="wcf.acp.option.image_proxy_host_whitelist"><![CDATA[Ausnahmen von der Zwischenspeicherung]]></item>
+               <item name="wcf.acp.option.image_proxy_host_whitelist.description"><![CDATA[Die aufgeführten Domains werden von der Zwischenspeicherung ausgenommen, die eigene Domain ist implizit enthalten. Der Abgleich erfolgt auf Basis der strikten Übereinstimmung, optional können Subdomains mit einem Platzhalter berücksichtigt werden: <kbd>*.example.com</kbd> umfasst sowohl <kbd>example.com</kbd> als auch Subdomains wie <kbd>foo.example.com</kbd> oder <kbd>www.example.com</kbd>.<br>Bitte nur eine Domain pro Zeile eingeben.]]></item>
                <item name="wcf.acp.option.share_buttons_providers"><![CDATA[Anbieter zum Teilen von Inhalten]]></item>
                <item name="wcf.acp.option.show_style_changer"><![CDATA[Stil-Auswahl anzeigen]]></item>
                <item name="wcf.acp.option.language_use_informal_variant"><![CDATA[Informelle Anrede verwenden]]></item>
index 9dffeb36abd52a031fd834a0a5037a0716845af9..ae7331a51d00831b9761fd19fd774e0fe1ac3a44 100644 (file)
@@ -1368,6 +1368,9 @@ When prompted for the notification URL for the instant payment notifications, pl
                <item name="wcf.acp.option.module_article"><![CDATA[Articles]]></item>
                <item name="wcf.acp.option.module_image_proxy"><![CDATA[Enable image proxy]]></item>
                <item name="wcf.acp.option.image_proxy_expiration"><![CDATA[Storage Time Period]]></item>
+               <item name="wcf.acp.option.image_proxy_insecure_only"><![CDATA[Store images from insecure sources only]]></item>
+               <item name="wcf.acp.option.image_proxy_host_whitelist"><![CDATA[Image proxy whitelist]]></item>
+               <item name="wcf.acp.option.image_proxy_host_whitelist.description"><![CDATA[The listed domains will not be handled by the image proxy, the current domain is implicitly added. Hostnames are exact matches only, a leading wildcard can be used to exclude an entire domain: <kbd>*.example.com</kbd> matches <kbd>example.com</kbd> and subdomains such as <kbd>foo.example.com</kbd> or <kbd>www.example.com</kbd>.<br>Enter one domain per line only.]]></item>
                <item name="wcf.acp.option.share_buttons_providers"><![CDATA[Share Button Providers]]></item>
                <item name="wcf.acp.option.show_style_changer"><![CDATA[Enable style changer]]></item>
                <item name="wcf.acp.option.language_use_informal_variant"><![CDATA[Use informal language variant]]></item>