Changeset View
Changeset View
Standalone View
Standalone View
src/app/Http/Controllers/API/NGINXController.php
- This file was added.
<?php | |||||
namespace App\Http\Controllers\API; | |||||
machniak: Shouldn't this go under /V4? I know we have e.g. AuthController here, but everything really… | |||||
use App\Http\Controllers\Controller; | |||||
use Illuminate\Http\Request; | |||||
use Illuminate\Support\Facades\Auth; | |||||
use Illuminate\Support\Facades\Hash; | |||||
use Illuminate\Support\Facades\Validator; | |||||
use Illuminate\Support\Facades\Cache; | |||||
use Illuminate\Support\Str; | |||||
class NGINXController extends Controller | |||||
{ | |||||
/** | |||||
* Authentication request. | |||||
* | |||||
* @todo: Separate IMAP(+STARTTLS) from IMAPS, same for SMTP/submission. | |||||
* | |||||
* @param \Illuminate\Http\Request $request The API request. | |||||
* | |||||
* @return \Illuminate\Http\Response The response | |||||
*/ | |||||
public function authenticate(Request $request) | |||||
{ | |||||
/** | |||||
* Auth-Login-Attempt: 1 | |||||
* Auth-Method: plain | |||||
* Auth-Pass: simple123 | |||||
* Auth-Protocol: imap | |||||
* Auth-Ssl: on | |||||
* Auth-User: john@kolab.org | |||||
* Client-Ip: 127.0.0.1 | |||||
* Host: 127.0.0.1 | |||||
* | |||||
* Auth-SSL: on | |||||
* Auth-SSL-Verify: SUCCESS | |||||
* Auth-SSL-Subject: /CN=example.com | |||||
* Auth-SSL-Issuer: /CN=example.com | |||||
* Auth-SSL-Serial: C07AD56B846B5BFF | |||||
* Auth-SSL-Fingerprint: 29d6a80a123d13355ed16b4b04605e29cb55a5ad | |||||
*/ | |||||
\Log::info("Authentication attempt"); | |||||
\Log::debug($request->headers); | |||||
$login = $request->headers->get('Auth-User', null); | |||||
if (empty($login)) { | |||||
return $this->byebye($request, __LINE__); | |||||
} | |||||
// validate user exists, otherwise bye bye | |||||
$user = \App\User::where('email', $login)->first(); | |||||
if (!$user) { | |||||
return $this->byebye($request, __LINE__); | |||||
} | |||||
// TODO: validate the user's domain is A-OK (active, confirmed, not suspended, ldapready) | |||||
// TODO: validate the user is A-OK (active, not suspended, ldapready, imapready) | |||||
// validate password, otherwise bye bye | |||||
$password = $request->headers->get('Auth-Pass', null); | |||||
Done Inline ActionsThis should be checked before selecting the User. machniak: This should be checked before selecting the User. | |||||
if (empty($password)) { | |||||
return $this->byebye($request, __LINE__); | |||||
} | |||||
$result = Hash::check($password, $user->password); | |||||
Done Inline ActionsA sanity check for empty Client-Ip? machniak: A sanity check for empty Client-Ip? | |||||
if (!$result) { | |||||
// TODO: Log, notify user. | |||||
return $this->byebye($request, __LINE__); | |||||
} | |||||
$clientIP = $request->headers->get('Client-Ip', null); | |||||
// validate country of origin against restrictions, otherwise bye bye | |||||
$countryCodes = json_decode($user->getSetting('limit_geo', "[]")); | |||||
\Log::debug("Countries for {$user->email}: " . var_export($countryCodes, true)); | |||||
// TODO: Consider "new geographical area notification". | |||||
if (!empty($countryCodes)) { | |||||
// fake the country is NL, and the limitation is CH | |||||
if ($clientIP == '127.0.0.1' && $login == "piet@kolab.org") { | |||||
$country = "NL"; | |||||
} else { | |||||
// TODO: GeoIP reliance | |||||
$country = "CH"; | |||||
} | |||||
if (!in_array($country, $countryCodes)) { | |||||
// TODO: Log, notify user. | |||||
return $this->byebye($request, __LINE__); | |||||
} | |||||
} | |||||
// TODO: Apply some sort of limit for Auth-Login-Attempt -- docs say it is the number of | |||||
// attempts over the same authAttempt. | |||||
// Check 2fa | |||||
if ($user->getSetting('2fa_plz', false)) { | |||||
$authAttempt = \App\AuthAttempt::recordAuthAttempt($user, $clientIP); | |||||
if (!\App\AuthAttempt::waitFor2FA($authAttempt)) { | |||||
return $this->byebye($request, __LINE__); | |||||
} | |||||
} | |||||
// All checks passed | |||||
switch ($request->headers->get('Auth-Protocol')) { | |||||
case "imap": | |||||
return $this->authenticateIMAP($request, $user->getSetting('guam_plz', false), $password); | |||||
case "smtp": | |||||
return $this->authenticateSMTP($request, $password); | |||||
default: | |||||
return $this->byebye($request); | |||||
} | |||||
} | |||||
private function authenticateIMAP(Request $request, $prefGuam, $password) | |||||
{ | |||||
if ($prefGuam) { | |||||
if ($request->headers->get('Auth-Ssl') == 'on') { | |||||
$port = 9993; | |||||
} else { | |||||
$port = 9143; | |||||
} | |||||
} else { | |||||
if ($request->headers->get('Auth-Ssl') == 'on') { | |||||
$port = 11993; | |||||
} else { | |||||
$port = 12143; | |||||
} | |||||
} | |||||
$response = response("")->withHeaders( | |||||
[ | |||||
"Auth-Status" => "OK", | |||||
"Auth-Server" => "127.0.0.1", | |||||
"Auth-Port" => $port, | |||||
"Auth-Pass" => $password | |||||
] | |||||
); | |||||
\Log::debug("Response with headers:\n{$response->headers}"); | |||||
return $response; | |||||
} | |||||
private function authenticateSMTP(Request $request, $password) | |||||
{ | |||||
$response = response("")->withHeaders( | |||||
[ | |||||
"Auth-Status" => "OK", | |||||
"Auth-Server" => "127.0.0.1", | |||||
"Auth-Port" => 10465, | |||||
"Auth-Pass" => $password | |||||
] | |||||
); | |||||
\Log::debug("Response with headers:\n{$response->headers}"); | |||||
return $response; | |||||
} | |||||
private function byebye(Request $request, $code = null) | |||||
{ | |||||
$response = response("")->withHeaders( | |||||
[ | |||||
// TODO code only for development | |||||
"Auth-Status" => "NO {$code}", | |||||
"Auth-Wait" => 3 | |||||
] | |||||
); | |||||
\Log::debug("Response with headers:\n{$response->headers}"); | |||||
return $response; | |||||
} | |||||
} |
Shouldn't this go under /V4? I know we have e.g. AuthController here, but everything really should go unser /V4, imo.