Handle Domain::$name as lowercase punycode due to IDN complexity

This commit is contained in:
the-djmaze 2024-03-11 20:11:13 +01:00
parent 56ede16833
commit 7999894bda
7 changed files with 89 additions and 129 deletions

View file

@ -115,11 +115,10 @@ class Http
if (!\strlen($sHost)) {
$sName = static::GetServer('SERVER_NAME', '');
$iPort = (int) static::GetServer('SERVER_PORT', 80);
$sHost = (\in_array($iPort, array(80, 433))) ? $sName : $sName.':'.$iPort;
}
if ($bWithoutWWW && 'www.' === \substr(\strtolower($sHost), 0, 4)) {
if ($bWithoutWWW && \str_starts_with($sHost, 'www.')) {
$sHost = \substr($sHost, 4);
}

View file

@ -685,19 +685,6 @@ abstract class Utils
return \sha1($sAdditionalSalt . \random_bytes(16));
}
public static function ValidateDomain(string $sDomain, bool $bSimple = false) : bool
{
$aMatch = array();
if ($bSimple) {
return \preg_match('/.+(\.[a-zA-Z]+)$/', $sDomain, $aMatch) && !empty($aMatch[1]);
}
return \preg_match('/.+(\.[a-zA-Z]+)$/', $sDomain, $aMatch) && !empty($aMatch[1]) && \in_array($aMatch[1], \explode(' ',
'.academy .actor .agency .audio .bar .beer .bike .blue .boutique .cab .camera .camp .capital .cards .careers .cash .catering .center .cheap .city .cleaning .clinic .clothing .club .coffee .community .company .computer .construction .consulting .contractors .cool .credit .dance .dating .democrat .dental .diamonds .digital .direct .directory .discount .domains .education .email .energy .equipment .estate .events .expert .exposed .fail .farm .fish .fitness .florist .fund .futbol .gallery .gift .glass .graphics .guru .help .holdings .holiday .host .hosting .house .institute .international .kitchen .land .life .lighting .limo .link .management .market .marketing .media .menu .moda .partners .parts .photo .photography .photos .pics .pink .press .productions .pub .red .rentals .repair .report .rest .sexy .shoes .social .solar .solutions .space .support .systems .tattoo .tax .technology .tips .today .tools .town .toys .trade .training .university .uno .vacations .vision .vodka .voyage .watch .webcam .wiki .work .works .wtf .zone .aero .asia .biz .cat .com .coop .edu .gov .info .int .jobs .mil .mobi .museum .name .net .org .pro .tel .travel .xxx .xyz '.
'.ac .ad .ae .af .ag .ai .al .am .an .ao .aq .ar .as .at .au .aw .ax .az .ba .bb .bd .be .bf .bg .bh .bi .bj .bm .bn .bo .br .bs .bt .bv .bw .by .bz .ca .cc .cd .cf .cg .ch .ci .ck .cl .cm .cn .co .cr .cs .cu .cv .cx .cy .cz .dd .de .dj .dk .dm .do .dz .ec .ee .eg .er .es .et .eu .fi .fj .fk .fm .fo .fr .ga .gb .gd .ge .gf .gg .gh .gi .gl .gm .gn .gp .gq .gr .gs .gt .gu .gw .gy .hk .hm .hn .hr .ht .hu .id .ie .il .im .in .io .iq .ir .is .it .je .jm .jo .jp .ke .kg .kh .ki .km .kn .kp .kr .kw .ky .kz .la .lb .lc .li .lk .lr .ls .lt .lu .lv .ly .ma .mc .md .me .mg .mh .mk .ml .mm .mn .mo .mp .mq .mr .ms .mt .mu .mv .mw .mx .my .mz .na .nc .ne .nf .ng .ni .nl .no .np .nr .nu .nz .om .pa .pe .pf .pg .ph .pk .pl .pm .pn .pr .ps .pt .pw .py .qa .re .ro .rs .ru . .rw .sa .sb .sc .sd .se .sg .sh .si .sj .sk .sl .sm .sn .so .sr .st .su .sv .sy .sz .tc .td .tf .tg .th .tj .tk .tl .tm .tn .to .tp .tr .tt .tv .tw .tz .ua .ug .uk .us .uy .uz .va .vc .ve .vg .vi .vn .vu .wf .ws .ye .yt .za .zm .zw'
));
}
public static function ValidateIP(string $sIp) : bool
{
return !empty($sIp) && $sIp === \filter_var($sIp, FILTER_VALIDATE_IP);
@ -726,9 +713,6 @@ abstract class Utils
{
$aParts = \explode('@', $sStr);
$sDomain = \array_pop($aParts);
if ($bLowerCase) {
$sDomain = \mb_strtolower($sDomain);
}
if (\strlen($sDomain) && \preg_match('/[^\x20-\x7E]/', $sDomain)) {
try
{
@ -736,6 +720,9 @@ abstract class Utils
}
catch (\Throwable $oException) {}
}
if ($bLowerCase) {
$sDomain = \strtolower($sDomain);
}
$aParts[] = $sDomain;
return \implode('@', $aParts);
}

View file

@ -33,55 +33,44 @@ trait UserAuth
$this->Plugins()->RunHook('login.credentials.step-1', array(&$sEmail));
if (!\str_contains($sEmail, '@')) {
$this->logWrite('The email address "' . $sEmail . '" is not complete', \LOG_INFO, 'LOGIN');
$bAdded = false;
$this->logWrite("The email address '{$sEmail}' is incomplete", \LOG_INFO, 'LOGIN');
$oDomain = null;
if ($this->Config()->Get('login', 'determine_user_domain', false)) {
$sUserHost = \trim($this->Http()->GetHost(true, true));
$this->logWrite('Determined user domain: ' . $sUserHost, \LOG_INFO, 'LOGIN');
$sUserHost = \strtolower(\idn_to_ascii($this->Http()->GetHost(false, true)));
$this->logWrite("Determined user domain: {$sUserHost}", \LOG_INFO, 'LOGIN');
$aDomainParts = \explode('.', $sUserHost);
$iLimit = \min(\count($aDomainParts), 14);
$oDomainProvider = $this->DomainProvider();
while (0 < $iLimit--) {
$sLine = \implode('.', $aDomainParts);
$oDomain = $oDomainProvider->Load($sLine, false);
if ($oDomain) {
$bAdded = true;
$this->logWrite('Check "' . $sLine . '": OK (' . $sEmail . ' > ' . $sEmail . '@' . $sLine . ')',
\LOG_INFO, 'LOGIN');
$sEmail .= '@' . $sLine;
$this->logWrite("Check '{$sLine}': OK", \LOG_INFO, 'LOGIN');
break;
} else {
$this->logWrite('Check "' . $sLine . '": NO', \LOG_INFO, 'LOGIN');
$this->logWrite("Check '{$sLine}': NO", \LOG_INFO, 'LOGIN');
}
\array_shift($aDomainParts);
}
if (!$bAdded) {
if (!$oDomain) {
$oDomain = $oDomainProvider->Load($sUserHost, true);
if ($oDomain) {
$bAdded = true;
$this->logWrite('Check "' . $sUserHost . '" with wildcard: OK (' . $sEmail . ' > ' . $sEmail . '@' . $sUserHost . ')',
\LOG_INFO, 'LOGIN');
$sEmail .= '@' . $sUserHost;
$this->logWrite("Check '{$sUserHost}' with wildcard: OK", \LOG_INFO, 'LOGIN');
} else {
$this->logWrite('Check "' . $sUserHost . '" with wildcard: NO', \LOG_INFO, 'LOGIN');
$this->logWrite("Check '{$sUserHost}' with wildcard: NO", \LOG_INFO, 'LOGIN');
}
}
if (!$bAdded) {
$this->logWrite('Domain was not found!', \LOG_INFO, 'LOGIN');
if (!$oDomain) {
$this->logWrite("Domain '{$sUserHost}' was not determined!", \LOG_INFO, 'LOGIN');
}
}
if (!$bAdded) {
if (!$oDomain) {
$sDefDomain = \trim($this->Config()->Get('login', 'default_domain', ''));
if (\strlen($sDefDomain)) {
if ('HTTP_HOST' === $sDefDomain || 'SERVER_NAME' === $sDefDomain) {
@ -89,10 +78,8 @@ trait UserAuth
} else if ('gethostname' === $sDefDomain) {
$sDefDomain = \gethostname();
}
$this->logWrite('Default domain "' . $sDefDomain . '" was used. (' . $sEmail . ' > ' . $sEmail . '@' . $sDefDomain . ')',
\LOG_INFO, 'LOGIN');
$sEmail .= '@' . $sDefDomain;
$this->logWrite("Default domain '{$sDefDomain}' will be used.", \LOG_INFO, 'LOGIN');
} else {
$this->logWrite('Default domain not configured.', \LOG_INFO, 'LOGIN');
}

View file

@ -1,4 +1,8 @@
<?php
/**
* Domain name is handled in lowercase punycode.
* Because internationalized domain names can have uppercase or titlecase characters.
*/
namespace RainLoop\Model;
@ -20,8 +24,7 @@ class Domain implements \JsonSerializable
function __construct(string $sName)
{
$this->Name = $sName;
$this->Name = \strtolower(\idn_to_ascii($sName));
$this->IMAP = new \MailSo\Imap\Settings;
$this->SMTP = new \MailSo\Smtp\Settings;
$this->Sieve = new \MailSo\Sieve\Settings;
@ -96,7 +99,7 @@ class Domain implements \JsonSerializable
public function SetAliasName(string $sAliasName) : void
{
$this->aliasName = $sAliasName;
$this->aliasName = \strtolower(\idn_to_ascii($sAliasName));
}
public function ValidateWhiteList(string $sEmail, string $sLogin = '') : bool
@ -232,14 +235,14 @@ class Domain implements \JsonSerializable
{
$aResult = array(
// '@Object' => 'Object/Domain',
'name' => $this->Name,
'name' => \idn_to_utf8($this->Name),
'IMAP' => $this->IMAP,
'SMTP' => $this->SMTP,
'Sieve' => $this->Sieve,
'whiteList' => $this->whiteList
);
if ($this->aliasName) {
$aResult['aliasName'] = $this->aliasName;
$aResult['aliasName'] = \idn_to_utf8($this->aliasName);
}
return $aResult;
}

View file

@ -10,6 +10,7 @@ abstract class Autoconfig
public static function discover(string $emailaddress) : ?array
{
$domain = \MailSo\Base\Utils::GetDomainFromEmail($emailaddress);
// $domain = \strtolower(\idn_to_ascii($domain));
// First try
$autoconfig = static::resolve($domain, $emailaddress);
if ($autoconfig) {
@ -89,7 +90,7 @@ abstract class Autoconfig
private static function publicsuffixes() : array
{
$oCache = \RainLoop\Api::Actions()->Cacher();
$list = $oCache->Get('public_suffix_list') ?: null;
$list = $oCache->Get('public_suffix_list');
if ($list) {
$list = \json_decode($list, true);
if ($list[1] < \time()) {

View file

@ -1,4 +1,8 @@
<?php
/**
* Domain names are handled in lowercase punycode.
* Because internationalized domain names can have uppercase or titlecase characters.
*/
namespace RainLoop\Providers\Domain;
@ -6,7 +10,7 @@ use MailSo\Cache\CacheClient;
class DefaultDomain implements DomainInterface
{
const CACHE_KEY = '/WildCard/DomainCache/';
const CACHE_KEY = '/WildCardDomainsCache/';
protected string $sDomainPath;
@ -20,30 +24,26 @@ class DefaultDomain implements DomainInterface
private static function decodeFileName(string $sName) : string
{
return ('default' === $sName)
? '*'
: \str_replace('_wildcard_', '*', \MailSo\Base\Utils::IdnToUtf8($sName, true));
return ('default' === $sName) ? '*' : \str_replace('_wildcard_', '*', $sName);
}
private static function encodeFileName(string $sName) : string
{
return ('*' === $sName)
? 'default'
: \str_replace('*', '_wildcard_', \MailSo\Base\Utils::IdnToAscii($sName, true));
return ('*' === $sName) ? 'default' : \str_replace('*', '_wildcard_', \strtolower(\idn_to_ascii($sName)));
}
private function getWildcardDomainsLine() : string
{
if ($this->oCacher) {
$sResult = $this->oCacher->Get(static::CACHE_KEY);
if (\strlen($sResult)) {
if (\is_string($sResult)) {
return $sResult;
}
}
$aNames = array();
// $aList = \glob($this->sDomainPath.'/*.{ini,json}', GLOB_BRACE);
// $aList = \glob($this->sDomainPath.'/*.{ini,json,alias}', GLOB_BRACE);
$aList = \glob($this->sDomainPath.'/*.json');
foreach ($aList as $sFile) {
$sName = \substr(\basename($sFile), 0, -5);
@ -72,16 +72,14 @@ class DefaultDomain implements DomainInterface
$sResult = \implode(' ', \array_unique($aNames));
}
if ($this->oCacher) {
$this->oCacher->Set(static::CACHE_KEY, $sResult);
}
$this->oCacher && $this->oCacher->Set(static::CACHE_KEY, $sResult);
return $sResult;
}
public function Load(string $sName, bool $bFindWithWildCard = false, bool $bCheckDisabled = true, bool $bCheckAliases = true) : ?\RainLoop\Model\Domain
{
$sName = \MailSo\Base\Utils::IdnToUtf8($sName, true);
$sName = \strtolower(\idn_to_ascii($sName));
if ($bCheckDisabled && \in_array($sName, $this->getDisabled())) {
return null;
}
@ -130,7 +128,7 @@ class DefaultDomain implements DomainInterface
public function SaveAlias(string $sName, string $sAlias) : bool
{
$sAlias = \MailSo\Base\Utils::IdnToUtf8($sAlias, true);
$sAlias = \strtolower(\idn_to_ascii($sAlias));
$sRealFileName = static::encodeFileName($sName);
\RainLoop\Utils::saveFile($this->sDomainPath.'/'.$sRealFileName.'.alias', $sAlias);
$this->oCacher && $this->oCacher->Delete(static::CACHE_KEY);
@ -147,7 +145,7 @@ class DefaultDomain implements DomainInterface
// RainLoop use comma, we use newline
$sItem = \strtok($sFile, ",\n");
while (false !== $sItem) {
$aDisabled[] = \MailSo\Base\Utils::IdnToUtf8($sItem, true);
$aDisabled[] = \strtolower(\idn_to_ascii($sItem));
$sItem = \strtok(",\n");
}
return $aDisabled;
@ -156,7 +154,7 @@ class DefaultDomain implements DomainInterface
public function Disable(string $sName, bool $bDisable) : bool
{
$sName = \MailSo\Base\Utils::IdnToUtf8($sName, true);
$sName = \strtolower(\idn_to_ascii($sName));
if ($sName) {
$aResult = $this->getDisabled();
if ($bDisable) {
@ -209,20 +207,23 @@ class DefaultDomain implements DomainInterface
if ($bAlias) {
if ($bIncludeAliases) {
$aAliases[$sName] = array(
'name' => $sName,
'name' => \idn_to_utf8($sName),
'punycode' => $sName,
'disabled' => \in_array($sName, $aDisabledNames),
'alias' => true
);
}
} else if (false !== \strpos($sName, '*')) {
$aWildCards[$sName] = array(
'name' => $sName,
'name' => \idn_to_utf8($sName),
'punycode' => $sName,
'disabled' => \in_array($sName, $aDisabledNames),
'alias' => false
);
} else {
$aResult[$sName] = array(
'name' => $sName,
'name' => \idn_to_utf8($sName),
'punycode' => $sName,
'disabled' => \in_array($sName, $aDisabledNames),
'alias' => false
);

View file

@ -9,10 +9,10 @@ abstract class IDN
{
public static function anyToAscii(string $string) : string
{
if (\preg_match('#[:/]#', $string)) {
if (\strspn($string, ':/')) {
return static::uri($string, true);
}
if (\strpos($string, '@')) {
if (\str_contains($string, '@')) {
return static::emailAddress($string, true);
}
return \idn_to_ascii($string);
@ -20,91 +20,73 @@ abstract class IDN
public static function anyToUtf8(string $string) : string
{
if (\preg_match('#[:/]#', $string)) {
if (\strspn($string, ':/')) {
return static::uri($string, false);
}
if (\strpos($string, '@')) {
if (\str_contains($string, '@')) {
return static::emailAddress($string, false);
}
return \idn_to_utf8($string);
}
/**
* Converts IDN domain part to lowercased punycode
*/
public static function emailToAscii(string $address) : string
{
return static::emailAddress($address, true);
}
public static function emailToUtf8(string $address) : string
private static function domain(string $domain, bool $toAscii) : string
{
return static::emailAddress($address, false);
}
public static function uriToAscii(string $address) : string
{
return static::uri($address, true);
}
public static function uriToUtf8(string $address) : string
{
return static::uri($address, false);
return $toAscii ? \strtolower(\idn_to_ascii($domain)) : \idn_to_utf8($domain);
/*
$domain = \explode('.', $domain);
foreach ($domain as $k => $v) {
$conv = $toAscii ? \idn_to_ascii($v) : \idn_to_utf8($v);
if ($conv) $domain[$k] = $conv;
}
return \implode('.', $domain);
*/
}
private static function emailAddress(string $address, bool $toAscii) : string
{
if (\str_contains($address, '@')) {
$local = \explode('@', $address);
$domain = \array_pop($local);
$local = \implode('@', $local);
$arr = \explode('.', $domain);
foreach ($arr as $k => $v) {
$conv = $toAscii ? \idn_to_ascii($v) : \idn_to_utf8($v);
if ($conv) $arr[$k] = $conv;
}
return $local . '@' . \implode('.', $arr);
if (!\str_contains($address, '@')) {
throw new \RuntimeException("Invalid email address: {$address}");
}
return $toAscii ? \idn_to_ascii($address) : \idn_to_utf8($address);
$local = \explode('@', $address);
$domain = static::domain(\array_pop($local), $toAscii);
return \implode('@', $local) . '@' . $domain;
}
private static function uri(string $address, bool $toAscii) : string
{
if (\preg_match('#[:\./]#', $address)) {
$parsed = \parse_url($address);
if (isset($parsed['host'])) {
$arr = \explode('.', $parsed['host']);
foreach ($arr as $k => $v) {
$conv = $toAscii ? \idn_to_ascii($v) : \idn_to_utf8($v);
if ($conv) $arr[$k] = $conv;
}
$parsed['host'] = \implode('.', $arr);
$parsed = \parse_url($address);
if (isset($parsed['host'])) {
$parsed['host'] = static::domain($parsed['host'], $toAscii);
$url = '//';
if (!empty($parsed['scheme'])) { $url = $parsed['scheme'] . (\strtolower($parsed['scheme']) === 'mailto' ? ':' : '://'); }
$url = empty($parsed['scheme']) ? '//'
: $parsed['scheme'] . (\strtolower($parsed['scheme']) === 'mailto' ? ':' : '://');
/*
if (!empty($parsed['user']) || !empty($parsed['pass'])) {
$ret_url .= $parts_arr['user'];
if (!empty($parts_arr['pass'])) {
$ret_url .= ':' . $parts_arr['pass'];
}
$ret_url .= '@';
if (!empty($parsed['user']) || !empty($parsed['pass'])) {
$ret_url .= $parts_arr['user'];
if (!empty($parts_arr['pass'])) {
$ret_url .= ':' . $parts_arr['pass'];
}
$ret_url .= '@';
}
*/
if (!empty($parsed['host'])) { $url .= $parsed['host']; }
if (!empty($parsed['port'])) { $url .= ':'.$parsed['port']; }
if (!empty($parsed['path'])) { $url .= $parsed['path']; }
if (!empty($parsed['query'])) { $url .= '?'.$parsed['query']; }
if (!empty($parsed['fragment'])) { $url .= '#'.$parsed['fragment']; }
return $url;
}
// parse_url seems to have failed, try without it
$arr = \explode('.', $address);
foreach ($arr as $k => $v) {
$conv = $toAscii ? \idn_to_ascii($v) : \idn_to_utf8($v);
if ($conv) $arr[$k] = $conv;
}
return \implode('.', $arr);
if (!empty($parsed['host'])) { $url .= $parsed['host']; }
if (!empty($parsed['port'])) { $url .= ':'.$parsed['port']; }
if (!empty($parsed['path'])) { $url .= $parsed['path']; }
if (!empty($parsed['query'])) { $url .= '?'.$parsed['query']; }
if (!empty($parsed['fragment'])) { $url .= '#'.$parsed['fragment']; }
return $url;
}
return $toAscii ? \idn_to_ascii($address) : \idn_to_utf8($address);
// parse_url seems to have failed, try without it
return static::domain($address, $toAscii);
}
}