Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions app/Http/Controllers/Api/ApplicationsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -1091,7 +1091,7 @@ private function create_application(Request $request, $type)

$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";

return $url;
Expand Down Expand Up @@ -1332,7 +1332,7 @@ private function create_application(Request $request, $type)

$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";

return $url;
Expand Down Expand Up @@ -1545,7 +1545,7 @@ private function create_application(Request $request, $type)

$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";

return $url;
Expand Down Expand Up @@ -2551,7 +2551,7 @@ public function update_by_uuid(Request $request)

$errors = [];
$urls = $urls->map(function ($url) use (&$errors) {
if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";

return $url;
Expand Down
2 changes: 1 addition & 1 deletion app/Support/ValidationPatterns.php
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,7 @@ public static function validateApplicationDomains(mixed $value): array
continue;
}

if (! filter_var($url, FILTER_VALIDATE_URL)) {
if (! isValidDomainUrl($url)) {
$errors[] = "Invalid URL: {$url}";

continue;
Expand Down
48 changes: 48 additions & 0 deletions bootstrap/helpers/domains.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,54 @@
use App\Models\ServiceApplication;
use Illuminate\Support\Collection;

function isValidDomainUrl(string $url): bool
{
Comment thread
andrasbacsai marked this conversation as resolved.
$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;
}
Comment thread
andrasbacsai marked this conversation as resolved.

function checkDomainUsage(ServiceApplication|Application|null $resource = null, ?string $domain = null)
{
$conflicts = [];
Expand Down
4 changes: 4 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
3 changes: 3 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
6 changes: 3 additions & 3 deletions templates/service-templates-latest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
6 changes: 3 additions & 3 deletions templates/service-templates.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
29 changes: 29 additions & 0 deletions tests/Unit/IsValidDomainUrlTest.php
Original file line number Diff line number Diff line change
@@ -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();
});
Comment thread
andrasbacsai marked this conversation as resolved.

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();
});
4 changes: 4 additions & 0 deletions tests/Unit/ValidationPatternsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,7 @@
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();
});