Merge pull request #6976 from mailcow/fix/autodiscover-passwordless
feat: Implement passwordless autodiscover endpoint
This commit is contained in:
commit
609ce6b0d6
@ -60,101 +60,31 @@ $pdo = new PDO($dsn, $database_user, $database_pass, $opt);
|
|||||||
$iam_provider = identity_provider('init');
|
$iam_provider = identity_provider('init');
|
||||||
$iam_settings = identity_provider('get');
|
$iam_settings = identity_provider('get');
|
||||||
|
|
||||||
$login_user = strtolower(trim($_SERVER['PHP_AUTH_USER']));
|
// Passwordless autodiscover - no authentication required
|
||||||
$login_pass = trim(htmlspecialchars_decode($_SERVER['PHP_AUTH_PW']));
|
// Email will be extracted from the request body
|
||||||
|
$login_user = null;
|
||||||
|
$login_role = null;
|
||||||
|
|
||||||
if (empty($_SERVER['PHP_AUTH_USER']) || empty($_SERVER['PHP_AUTH_PW'])) {
|
header("Content-Type: application/xml");
|
||||||
$json = json_encode(
|
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
||||||
array(
|
|
||||||
"time" => time(),
|
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
|
||||||
"user" => "none",
|
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
|
||||||
"service" => "Error: must be authenticated"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
|
||||||
header('WWW-Authenticate: Basic realm="' . $_SERVER['HTTP_HOST'] . '"');
|
|
||||||
header('HTTP/1.0 401 Unauthorized');
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
$login_role = check_login($login_user, $login_pass, array('service' => 'EAS'));
|
|
||||||
|
|
||||||
if ($login_role === "user") {
|
|
||||||
header("Content-Type: application/xml");
|
|
||||||
echo '<?xml version="1.0" encoding="utf-8" ?>' . PHP_EOL;
|
|
||||||
?>
|
?>
|
||||||
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
<Autodiscover xmlns="http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006">
|
||||||
<?php
|
<?php
|
||||||
if(!$data) {
|
if(!$data) {
|
||||||
try {
|
|
||||||
$json = json_encode(
|
|
||||||
array(
|
|
||||||
"time" => time(),
|
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
|
||||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
|
||||||
"service" => "Error: invalid or missing request data"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
|
||||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
|
||||||
}
|
|
||||||
catch (RedisException $e) {
|
|
||||||
$_SESSION['return'][] = array(
|
|
||||||
'type' => 'danger',
|
|
||||||
'msg' => 'Redis: '.$e
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
list($usec, $sec) = explode(' ', microtime());
|
|
||||||
?>
|
|
||||||
<Response>
|
|
||||||
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="2477272013">
|
|
||||||
<ErrorCode>600</ErrorCode>
|
|
||||||
<Message>Invalid Request</Message>
|
|
||||||
<DebugData />
|
|
||||||
</Error>
|
|
||||||
</Response>
|
|
||||||
</Autodiscover>
|
|
||||||
<?php
|
|
||||||
exit(0);
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$discover = new SimpleXMLElement($data);
|
|
||||||
$email = $discover->Request->EMailAddress;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
$email = $_SERVER['PHP_AUTH_USER'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$username = trim($email);
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->prepare("SELECT `name` FROM `mailbox` WHERE `username`= :username");
|
|
||||||
$stmt->execute(array(':username' => $username));
|
|
||||||
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
}
|
|
||||||
catch(PDOException $e) {
|
|
||||||
die("Failed to determine name from SQL");
|
|
||||||
}
|
|
||||||
if (!empty($MailboxData['name'])) {
|
|
||||||
$displayname = $MailboxData['name'];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$displayname = $email;
|
|
||||||
}
|
|
||||||
try {
|
try {
|
||||||
$json = json_encode(
|
$json = json_encode(
|
||||||
array(
|
array(
|
||||||
"time" => time(),
|
"time" => time(),
|
||||||
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
"user" => $_SERVER['PHP_AUTH_USER'],
|
"user" => "none",
|
||||||
"ip" => $_SERVER['REMOTE_ADDR'],
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
"service" => $autodiscover_config['autodiscoverType']
|
"service" => "Error: invalid or missing request data"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$redis->lPush('AUTODISCOVER_LOG', $json);
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Invalid request by " . $_SERVER['REMOTE_ADDR']);
|
||||||
}
|
}
|
||||||
catch (RedisException $e) {
|
catch (RedisException $e) {
|
||||||
$_SESSION['return'][] = array(
|
$_SESSION['return'][] = array(
|
||||||
@ -163,7 +93,143 @@ if ($login_role === "user") {
|
|||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$discover = new SimpleXMLElement($data);
|
||||||
|
$email = $discover->Request->EMailAddress;
|
||||||
|
} catch (Exception $e) {
|
||||||
|
// If parsing fails, return error
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => "none",
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => "Error: could not parse email from request"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Malformed XML by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
$username = trim((string)$email);
|
||||||
|
try {
|
||||||
|
$stmt = $pdo->prepare("SELECT `mailbox`.`name`, `mailbox`.`active` FROM `mailbox`
|
||||||
|
INNER JOIN `domain` ON `mailbox`.`domain` = `domain`.`domain`
|
||||||
|
WHERE `mailbox`.`username` = :username
|
||||||
|
AND `mailbox`.`active` = '1'
|
||||||
|
AND `domain`.`active` = '1'");
|
||||||
|
$stmt->execute(array(':username' => $username));
|
||||||
|
$MailboxData = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
}
|
||||||
|
catch(PDOException $e) {
|
||||||
|
// Database error - return error response with complete XML
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>500</ErrorCode>
|
||||||
|
<Message>Database Error</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mailbox not found or not active - return generic error to prevent user enumeration
|
||||||
|
if (empty($MailboxData)) {
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => $email,
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => "Error: mailbox not found or inactive"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
$redis->publish("F2B_CHANNEL", "Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
error_log("Autodiscover: Invalid mailbox attempt by " . $_SERVER['REMOTE_ADDR']);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
// Silently fail
|
||||||
|
}
|
||||||
|
list($usec, $sec) = explode(' ', microtime());
|
||||||
|
?>
|
||||||
|
<Response>
|
||||||
|
<Error Time="<?=date('H:i:s', $sec) . substr($usec, 0, strlen($usec) - 2);?>" Id="<?=rand(1000000000, 9999999999);?>">
|
||||||
|
<ErrorCode>600</ErrorCode>
|
||||||
|
<Message>Invalid Request</Message>
|
||||||
|
<DebugData />
|
||||||
|
</Error>
|
||||||
|
</Response>
|
||||||
|
</Autodiscover>
|
||||||
|
<?php
|
||||||
|
exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($MailboxData['name'])) {
|
||||||
|
$displayname = $MailboxData['name'];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$displayname = $email;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$json = json_encode(
|
||||||
|
array(
|
||||||
|
"time" => time(),
|
||||||
|
"ua" => $_SERVER['HTTP_USER_AGENT'],
|
||||||
|
"user" => $email,
|
||||||
|
"ip" => $_SERVER['REMOTE_ADDR'],
|
||||||
|
"service" => $autodiscover_config['autodiscoverType']
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$redis->lPush('AUTODISCOVER_LOG', $json);
|
||||||
|
$redis->lTrim('AUTODISCOVER_LOG', 0, 100);
|
||||||
|
}
|
||||||
|
catch (RedisException $e) {
|
||||||
|
$_SESSION['return'][] = array(
|
||||||
|
'type' => 'danger',
|
||||||
|
'msg' => 'Redis: '.$e
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ($autodiscover_config['autodiscoverType'] == 'imap') {
|
||||||
?>
|
?>
|
||||||
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
<Response xmlns="http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a">
|
||||||
<User>
|
<User>
|
||||||
@ -238,6 +304,3 @@ if ($login_role === "user") {
|
|||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
</Autodiscover>
|
</Autodiscover>
|
||||||
<?php
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
|
|||||||
122
helper-scripts/dev_tests/view_autodiscover.sh
Executable file
122
helper-scripts/dev_tests/view_autodiscover.sh
Executable file
@ -0,0 +1,122 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Autodiscover XML Debug Script
|
||||||
|
# Usage: ./view_autodiscover.sh [OPTIONS] [email@domain.com]
|
||||||
|
|
||||||
|
# Function to display help
|
||||||
|
show_help() {
|
||||||
|
cat << EOF
|
||||||
|
Autodiscover XML Debug Script
|
||||||
|
|
||||||
|
Usage: $0 [OPTIONS] [email@domain.com]
|
||||||
|
|
||||||
|
OPTIONS:
|
||||||
|
-h, --help Show this help message
|
||||||
|
-d, --domain FQDN Override autodiscover domain (default: autodiscover.DOMAIN)
|
||||||
|
Example: -d mail.example.com
|
||||||
|
|
||||||
|
EXAMPLES:
|
||||||
|
$0 user@example.com
|
||||||
|
Test autodiscover for user@example.com using autodiscover.example.com
|
||||||
|
|
||||||
|
$0 -d mail.example.com user@example.com
|
||||||
|
Test autodiscover for user@example.com using mail.example.com
|
||||||
|
|
||||||
|
$0 -d localhost:8443 user@example.com
|
||||||
|
Test autodiscover using localhost:8443 (useful for development)
|
||||||
|
|
||||||
|
EOF
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Initialize variables
|
||||||
|
EMAIL=""
|
||||||
|
DOMAIN_OVERRIDE=""
|
||||||
|
|
||||||
|
# Parse command line arguments
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case $1 in
|
||||||
|
-h|--help)
|
||||||
|
show_help
|
||||||
|
;;
|
||||||
|
-d|--domain)
|
||||||
|
DOMAIN_OVERRIDE="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
-*)
|
||||||
|
echo "Error: Unknown option $1"
|
||||||
|
echo "Use -h or --help for usage information"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
EMAIL="$1"
|
||||||
|
shift
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
# Check if xmllint is available
|
||||||
|
if ! command -v xmllint &> /dev/null; then
|
||||||
|
echo "WARNING: xmllint not found. Output will not be formatted."
|
||||||
|
echo "Install with: apt install libxml2-utils (Debian/Ubuntu) or yum install libxml2 (CentOS/RHEL)"
|
||||||
|
echo ""
|
||||||
|
USE_XMLLINT=false
|
||||||
|
else
|
||||||
|
USE_XMLLINT=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get email address from user input if not provided
|
||||||
|
if [ -z "$EMAIL" ]; then
|
||||||
|
read -p "Enter email address to test: " EMAIL
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate email format
|
||||||
|
if [[ ! "$EMAIL" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
|
||||||
|
echo "Error: Invalid email address format"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract domain from email
|
||||||
|
EMAIL_DOMAIN="${EMAIL#*@}"
|
||||||
|
|
||||||
|
# Determine autodiscover URL
|
||||||
|
if [ -n "$DOMAIN_OVERRIDE" ]; then
|
||||||
|
AUTODISCOVER_URL="https://${DOMAIN_OVERRIDE}/Autodiscover/Autodiscover.xml"
|
||||||
|
echo "Testing Autodiscover for: $EMAIL"
|
||||||
|
echo "Override domain: $DOMAIN_OVERRIDE"
|
||||||
|
else
|
||||||
|
AUTODISCOVER_URL="https://autodiscover.${EMAIL_DOMAIN}/Autodiscover/Autodiscover.xml"
|
||||||
|
echo "Testing Autodiscover for: $EMAIL"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "URL: $AUTODISCOVER_URL"
|
||||||
|
echo "============================================"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Make the request
|
||||||
|
RESPONSE=$(curl -k -s -X POST "$AUTODISCOVER_URL" \
|
||||||
|
-H "Content-Type: text/xml" \
|
||||||
|
-d "<?xml version=\"1.0\" encoding=\"utf-8\"?>
|
||||||
|
<Autodiscover xmlns=\"http://schemas.microsoft.com/exchange/autodiscover/request/2006\">
|
||||||
|
<Request>
|
||||||
|
<EMailAddress>$EMAIL</EMailAddress>
|
||||||
|
<AcceptableResponseSchema>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a</AcceptableResponseSchema>
|
||||||
|
</Request>
|
||||||
|
</Autodiscover>")
|
||||||
|
|
||||||
|
# Check if response is empty
|
||||||
|
if [ -z "$RESPONSE" ]; then
|
||||||
|
echo "Error: No response received from server"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Format and display output
|
||||||
|
if [ "$USE_XMLLINT" = true ]; then
|
||||||
|
echo "$RESPONSE" | xmllint --format - 2>&1
|
||||||
|
else
|
||||||
|
echo "$RESPONSE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "============================================"
|
||||||
|
echo "Response length: ${#RESPONSE} bytes"
|
||||||
Loading…
Reference in New Issue
Block a user