2023-10-13 12:25:30 +00:00
< ? php
namespace App\Actions\Proxy ;
2024-10-03 19:29:55 +00:00
use App\Enums\ProxyTypes ;
2023-10-13 12:25:30 +00:00
use App\Models\Server ;
2024-11-06 14:16:12 +00:00
use Illuminate\Support\Facades\Log ;
2023-10-13 12:25:30 +00:00
use Lorisleiva\Actions\Concerns\AsAction ;
2024-10-03 19:29:55 +00:00
use Symfony\Component\Yaml\Yaml ;
2023-10-13 12:25:30 +00:00
class CheckProxy
{
use AsAction ;
2024-06-10 20:43:34 +00:00
2024-10-03 19:29:55 +00:00
// It should return if the proxy should be started (true) or not (false)
public function handle ( Server $server , $fromUI = false ) : bool
2023-10-13 12:25:30 +00:00
{
2024-06-10 20:43:34 +00:00
if ( ! $server -> isFunctional ()) {
2024-05-22 10:20:36 +00:00
return false ;
}
2024-05-24 09:17:23 +00:00
if ( $server -> isBuildServer ()) {
if ( $server -> proxy ) {
$server -> proxy = null ;
$server -> save ();
}
2024-06-10 20:43:34 +00:00
2024-05-24 09:17:23 +00:00
return false ;
}
2024-05-22 18:42:08 +00:00
$proxyType = $server -> proxyType ();
2025-04-05 12:32:51 +00:00
if (( is_null ( $proxyType ) || $proxyType === 'NONE' || $server -> proxy -> force_stop ) && ! $fromUI ) {
2024-03-11 14:08:05 +00:00
return false ;
}
2024-06-10 20:43:34 +00:00
if ( ! $server -> isProxyShouldRun ()) {
2023-10-17 17:00:23 +00:00
if ( $fromUI ) {
2025-01-07 14:31:43 +00:00
throw new \Exception ( 'Proxy should not run. You selected the Custom Proxy.' );
} else {
return false ;
2023-10-17 17:00:23 +00:00
}
2023-10-13 12:25:30 +00:00
}
2025-04-02 14:34:24 +00:00
// Determine proxy container name based on environment
$proxyContainerName = $server -> isSwarm () ? 'coolify-proxy_traefik' : 'coolify-proxy' ;
2023-11-29 13:59:06 +00:00
if ( $server -> isSwarm ()) {
2025-04-02 14:34:24 +00:00
$status = getContainerStatus ( $server , $proxyContainerName );
2023-11-29 13:59:06 +00:00
$server -> proxy -> set ( 'status' , $status );
2023-10-13 12:25:30 +00:00
$server -> save ();
2025-01-07 14:31:43 +00:00
if ( $status === 'running' ) {
return false ;
}
2023-11-29 13:59:06 +00:00
2025-01-07 14:31:43 +00:00
return true ;
} else {
2025-04-02 14:34:24 +00:00
$status = getContainerStatus ( $server , $proxyContainerName );
2025-01-07 14:31:43 +00:00
if ( $status === 'running' ) {
$server -> proxy -> set ( 'status' , 'running' );
$server -> save ();
2024-10-03 19:29:55 +00:00
2025-01-07 14:31:43 +00:00
return false ;
}
if ( $server -> settings -> is_cloudflare_tunnel ) {
return false ;
}
$ip = $server -> ip ;
if ( $server -> id === 0 ) {
$ip = 'host.docker.internal' ;
}
$portsToCheck = [ '80' , '443' ];
2025-03-25 17:28:01 +00:00
foreach ( $portsToCheck as $port ) {
2025-04-02 14:34:24 +00:00
// Use the smart port checker that handles dual-stack properly
if ( $this -> isPortConflict ( $server , $port , $proxyContainerName )) {
2025-03-25 17:28:01 +00:00
if ( $fromUI ) {
throw new \Exception ( " Port $port is in use.<br>You must stop the process using this port.<br><br>Docs: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/docs'>https://coolify.io/docs</a><br>Discord: <a target='_blank' class='dark:text-white hover:underline' href='https://coolify.io/discord'>https://coolify.io/discord</a> " );
} else {
return false ;
}
}
}
2025-01-07 14:31:43 +00:00
try {
if ( $server -> proxyType () !== ProxyTypes :: NONE -> value ) {
$proxyCompose = CheckConfiguration :: run ( $server );
if ( isset ( $proxyCompose )) {
$yaml = Yaml :: parse ( $proxyCompose );
$portsToCheck = [];
if ( $server -> proxyType () === ProxyTypes :: TRAEFIK -> value ) {
$ports = data_get ( $yaml , 'services.traefik.ports' );
} elseif ( $server -> proxyType () === ProxyTypes :: CADDY -> value ) {
$ports = data_get ( $yaml , 'services.caddy.ports' );
}
if ( isset ( $ports )) {
foreach ( $ports as $port ) {
$portsToCheck [] = str ( $port ) -> before ( ':' ) -> value ();
}
2024-10-03 19:29:55 +00:00
}
}
2025-01-07 14:31:43 +00:00
} else {
$portsToCheck = [];
2023-11-29 13:59:06 +00:00
}
2025-01-07 14:31:43 +00:00
} catch ( \Exception $e ) {
Log :: error ( 'Error checking proxy: ' . $e -> getMessage ());
2024-10-03 19:29:55 +00:00
}
2025-01-07 14:31:43 +00:00
if ( count ( $portsToCheck ) === 0 ) {
2025-01-07 13:52:08 +00:00
return false ;
}
2025-01-07 14:31:43 +00:00
return true ;
}
2023-10-13 12:25:30 +00:00
}
2025-04-02 14:34:24 +00:00
/**
* Smart port checker that handles dual - stack configurations
* Returns true only if there ' s a real port conflict ( not just dual - stack )
*/
private function isPortConflict ( Server $server , string $port , string $proxyContainerName ) : bool
{
// First check if our own proxy is using this port (which is fine)
try {
$getProxyContainerId = " docker ps -a --filter name= $proxyContainerName --format ' { { .ID}}' " ;
$containerId = trim ( instant_remote_process ([ $getProxyContainerId ], $server ));
if ( ! empty ( $containerId )) {
$checkProxyPort = " docker inspect $containerId --format ' { { json .NetworkSettings.Ports}}' | grep ' \" $port /tcp \" ' " ;
try {
instant_remote_process ([ $checkProxyPort ], $server );
// Our proxy is using the port, which is fine
return false ;
} catch ( \Throwable $e ) {
// Our container exists but not using this port
}
}
} catch ( \Throwable $e ) {
// Container not found or error checking, continue with regular checks
}
// Command sets for different ways to check ports, ordered by preference
$commandSets = [
// Set 1: Use ss to check listener counts by protocol stack
[
2025-04-05 10:30:14 +00:00
'available' => 'command -v ss >/dev/null 2>&1' ,
2025-04-02 14:34:24 +00:00
'check' => [
// Get listening process details
2025-04-05 11:56:07 +00:00
" ss_output= \$ (ss -Htuln state listening sport = : $port 2>/dev/null) && echo \" \$ ss_output \" " ,
2025-04-02 14:34:24 +00:00
// Count IPv4 listeners
2025-04-05 11:32:58 +00:00
" echo \" \$ ss_output \" | grep -c ': $port ' " ,
2025-04-02 14:34:24 +00:00
],
],
// Set 2: Use netstat as alternative to ss
[
2025-04-05 10:30:14 +00:00
'available' => 'command -v netstat >/dev/null 2>&1' ,
2025-04-02 14:34:24 +00:00
'check' => [
// Get listening process details
2025-04-05 11:13:15 +00:00
" netstat_output= \$ (netstat -tuln 2>/dev/null) && echo \" \$ netstat_output \" | grep ': $port ' " ,
2025-04-02 14:34:24 +00:00
// Count listeners
2025-04-05 11:32:58 +00:00
" echo \" \$ netstat_output \" | grep ': $port ' | grep -c 'LISTEN' " ,
2025-04-02 14:34:24 +00:00
],
],
// Set 3: Use lsof as last resort
[
2025-04-05 10:30:14 +00:00
'available' => 'command -v lsof >/dev/null 2>&1' ,
2025-04-02 14:34:24 +00:00
'check' => [
// Get process using the port
" lsof -i : $port -P -n | grep 'LISTEN' " ,
// Count listeners
" lsof -i : $port -P -n | grep 'LISTEN' | wc -l " ,
],
],
];
// Try each command set until we find one available
foreach ( $commandSets as $set ) {
try {
// Check if the command is available
instant_remote_process ([ $set [ 'available' ]], $server );
// Run the actual check commands
$output = instant_remote_process ( $set [ 'check' ], $server , true );
// Parse the output lines
$lines = explode ( " \n " , trim ( $output ));
// Get the detailed output and listener count
$details = trim ( $lines [ 0 ] ? ? '' );
$count = intval ( trim ( $lines [ 1 ] ? ? '0' ));
// If no listeners or empty result, port is free
if ( $count == 0 || empty ( $details )) {
return false ;
}
// Try to detect if this is our coolify-proxy
if ( strpos ( $details , 'docker' ) !== false || strpos ( $details , $proxyContainerName ) !== false ) {
// It's likely our docker or proxy, which is fine
return false ;
}
// Check for dual-stack scenario - typically 1-2 listeners (IPv4+IPv6)
// If exactly 2 listeners and both have same port, likely dual-stack
if ( $count <= 2 ) {
// Check if it looks like a standard dual-stack setup
$isDualStack = false ;
// Look for IPv4 and IPv6 in the listing (ss output format)
if ( preg_match ( '/LISTEN.*:' . $port . '\s/' , $details ) &&
( preg_match ( '/\*:' . $port . '\s/' , $details ) ||
preg_match ( '/:::' . $port . '\s/' , $details ))) {
$isDualStack = true ;
}
// For netstat format
if ( strpos ( $details , '0.0.0.0:' . $port ) !== false &&
strpos ( $details , ':::' . $port ) !== false ) {
$isDualStack = true ;
}
// For lsof format (IPv4 and IPv6)
if ( strpos ( $details , '*:' . $port ) !== false &&
preg_match ( '/\*:' . $port . '.*IPv4/' , $details ) &&
preg_match ( '/\*:' . $port . '.*IPv6/' , $details )) {
$isDualStack = true ;
}
if ( $isDualStack ) {
return false ; // This is just a normal dual-stack setup
}
}
// If we get here, it's likely a real port conflict
return true ;
} catch ( \Throwable $e ) {
// This command set failed, try the next one
continue ;
}
}
// Fallback to simpler check if all above methods fail
try {
// Just try to bind to the port directly to see if it's available
$checkCommand = " nc -z -w1 127.0.0.1 $port >/dev/null 2>&1 && echo 'in-use' || echo 'free' " ;
$result = instant_remote_process ([ $checkCommand ], $server , true );
return trim ( $result ) === 'in-use' ;
} catch ( \Throwable $e ) {
// If everything fails, assume the port is free to avoid false positives
return false ;
}
}
2023-10-13 12:25:30 +00:00
}