mirror of
https://github.com/tiennm99/coolify.git
synced 2026-08-20 00:23:27 +00:00
fix: accept underscores in domain hostnames for API URL validation (#10663)
This commit is contained in:
@@ -1091,7 +1091,7 @@ class ApplicationsController extends Controller
|
||||
|
||||
$errors = [];
|
||||
$urls = $urls->map(function ($url) use (&$errors) {
|
||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
if (! isValidDomainUrl($url)) {
|
||||
$errors[] = "Invalid URL: {$url}";
|
||||
|
||||
return $url;
|
||||
@@ -1332,7 +1332,7 @@ class ApplicationsController extends Controller
|
||||
|
||||
$errors = [];
|
||||
$urls = $urls->map(function ($url) use (&$errors) {
|
||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
if (! isValidDomainUrl($url)) {
|
||||
$errors[] = "Invalid URL: {$url}";
|
||||
|
||||
return $url;
|
||||
@@ -1545,7 +1545,7 @@ class ApplicationsController extends Controller
|
||||
|
||||
$errors = [];
|
||||
$urls = $urls->map(function ($url) use (&$errors) {
|
||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
if (! isValidDomainUrl($url)) {
|
||||
$errors[] = "Invalid URL: {$url}";
|
||||
|
||||
return $url;
|
||||
@@ -2551,7 +2551,7 @@ class ApplicationsController extends Controller
|
||||
|
||||
$errors = [];
|
||||
$urls = $urls->map(function ($url) use (&$errors) {
|
||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
if (! isValidDomainUrl($url)) {
|
||||
$errors[] = "Invalid URL: {$url}";
|
||||
|
||||
return $url;
|
||||
|
||||
@@ -557,7 +557,7 @@ class ValidationPatterns
|
||||
continue;
|
||||
}
|
||||
|
||||
if (! filter_var($url, FILTER_VALIDATE_URL)) {
|
||||
if (! isValidDomainUrl($url)) {
|
||||
$errors[] = "Invalid URL: {$url}";
|
||||
|
||||
continue;
|
||||
|
||||
@@ -4,6 +4,54 @@ use App\Models\Application;
|
||||
use App\Models\ServiceApplication;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
function isValidDomainUrl(string $url): bool
|
||||
{
|
||||
$components = parse_url($url);
|
||||
|
||||
if ($components === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$scheme = $components['scheme'] ?? '';
|
||||
$host = $components['host'] ?? '';
|
||||
|
||||
if (! in_array(strtolower($scheme), ['http', 'https'], true) || $host === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$urlToValidate = $scheme.'://';
|
||||
|
||||
if (isset($components['user'])) {
|
||||
$urlToValidate .= $components['user'];
|
||||
|
||||
if (isset($components['pass'])) {
|
||||
$urlToValidate .= ':'.$components['pass'];
|
||||
}
|
||||
|
||||
$urlToValidate .= '@';
|
||||
}
|
||||
|
||||
$urlToValidate .= str_replace('_', '-', $host);
|
||||
|
||||
if (isset($components['port'])) {
|
||||
$urlToValidate .= ':'.$components['port'];
|
||||
}
|
||||
|
||||
if (isset($components['path'])) {
|
||||
$urlToValidate .= $components['path'];
|
||||
}
|
||||
|
||||
if (isset($components['query'])) {
|
||||
$urlToValidate .= '?'.$components['query'];
|
||||
}
|
||||
|
||||
if (isset($components['fragment'])) {
|
||||
$urlToValidate .= '#'.$components['fragment'];
|
||||
}
|
||||
|
||||
return filter_var($urlToValidate, FILTER_VALIDATE_URL) !== false;
|
||||
}
|
||||
|
||||
function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
|
||||
{
|
||||
$conflicts = [];
|
||||
|
||||
@@ -2553,6 +2553,10 @@
|
||||
"is_preserve_repository_enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false."
|
||||
},
|
||||
"include_source_commit_in_build": {
|
||||
"type": "boolean",
|
||||
"description": "Include source commit information in the build. Default is false."
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -1663,6 +1663,9 @@ paths:
|
||||
is_preserve_repository_enabled:
|
||||
type: boolean
|
||||
description: 'Preserve git repository during application update. If false, the existing repository will be removed and replaced with the new one. If true, the existing repository will be kept and the new one will be ignored. Default is false.'
|
||||
include_source_commit_in_build:
|
||||
type: boolean
|
||||
description: 'Include source commit information in the build. Default is false.'
|
||||
type: object
|
||||
responses:
|
||||
'200':
|
||||
|
||||
@@ -803,7 +803,7 @@
|
||||
"category": "backend",
|
||||
"logo": "svgs/convex.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-06-12T10:45:52+02:00",
|
||||
"template_last_updated_at": "2026-05-09T19:26:30+05:30",
|
||||
"port": "6791"
|
||||
},
|
||||
"cryptgeon": {
|
||||
@@ -1779,7 +1779,7 @@
|
||||
"category": "devtools",
|
||||
"logo": "svgs/gitea.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-06-06T00:11:24+02:00"
|
||||
"template_last_updated_at": "2026-06-01T07:54:27-05:00"
|
||||
},
|
||||
"gitea-with-mariadb": {
|
||||
"documentation": "https://docs.gitea.com?utm_source=coolify.io",
|
||||
@@ -2361,7 +2361,7 @@
|
||||
"category": "automation",
|
||||
"logo": "svgs/inngest.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
|
||||
"template_last_updated_at": null,
|
||||
"port": "8288"
|
||||
},
|
||||
"invoice-ninja": {
|
||||
|
||||
@@ -803,7 +803,7 @@
|
||||
"category": "backend",
|
||||
"logo": "svgs/convex.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-06-12T10:45:52+02:00",
|
||||
"template_last_updated_at": "2026-05-09T19:26:30+05:30",
|
||||
"port": "6791"
|
||||
},
|
||||
"cryptgeon": {
|
||||
@@ -1779,7 +1779,7 @@
|
||||
"category": "devtools",
|
||||
"logo": "svgs/gitea.svg",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-06-06T00:11:24+02:00"
|
||||
"template_last_updated_at": "2026-06-01T07:54:27-05:00"
|
||||
},
|
||||
"gitea-with-mariadb": {
|
||||
"documentation": "https://docs.gitea.com?utm_source=coolify.io",
|
||||
@@ -2361,7 +2361,7 @@
|
||||
"category": "automation",
|
||||
"logo": "svgs/inngest.png",
|
||||
"minversion": "0.0.0",
|
||||
"template_last_updated_at": "2026-07-02T13:25:47+02:00",
|
||||
"template_last_updated_at": null,
|
||||
"port": "8288"
|
||||
},
|
||||
"invoice-ninja": {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
it('accepts hostnames containing underscores', function () {
|
||||
// Regression: PHP's FILTER_VALIDATE_URL rejects underscores in the host,
|
||||
// which blocked valid service domains (e.g. Docker service naming) from
|
||||
// being saved and getting Let's Encrypt certificates. See issue #10597.
|
||||
expect(isValidDomainUrl('https://myapp_service.example.com'))->toBeTrue();
|
||||
expect(isValidDomainUrl('http://my_app.example.com'))->toBeTrue();
|
||||
expect(isValidDomainUrl('https://a_b_c.example.com/path'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('accepts ordinary domains and URLs', function () {
|
||||
expect(isValidDomainUrl('https://example.com'))->toBeTrue();
|
||||
expect(isValidDomainUrl('http://sub.example.com:8080/path?q=1'))->toBeTrue();
|
||||
expect(isValidDomainUrl('https://example.com/a_b'))->toBeTrue();
|
||||
});
|
||||
|
||||
it('rejects strings that are not valid URLs', function () {
|
||||
expect(isValidDomainUrl('not a url'))->toBeFalse();
|
||||
expect(isValidDomainUrl('example.com'))->toBeFalse();
|
||||
expect(isValidDomainUrl('ht_tp://example.com'))->toBeFalse();
|
||||
expect(isValidDomainUrl(''))->toBeFalse();
|
||||
});
|
||||
|
||||
it('rejects URLs that do not use HTTP or HTTPS schemes', function () {
|
||||
expect(isValidDomainUrl('ftp://example.com'))->toBeFalse();
|
||||
expect(isValidDomainUrl('javascript://example.com'))->toBeFalse();
|
||||
expect(isValidDomainUrl('data://example.com'))->toBeFalse();
|
||||
});
|
||||
@@ -187,3 +187,7 @@ it('normalizes application domain scheme and host without lowercasing path query
|
||||
expect(ValidationPatterns::normalizeApplicationDomains($domains))
|
||||
->toBe('https://example.com/MixedCase/Path?Token=ABC#Fragment,http://sub.example.com/Api/V1');
|
||||
});
|
||||
|
||||
it('validates application domains with underscores in the hostname', function () {
|
||||
expect(ValidationPatterns::validateApplicationDomains('https://myapp_service.example.com'))->toBeEmpty();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user