Skip to content

Commit 31df750

Browse files
committed
feat: improve factory resolver URL parsing
Signed-off-by: Oleksii Orel <oorel@redhat.com>
1 parent 9dffc0e commit 31df750

File tree

6 files changed

+255
-16
lines changed

6 files changed

+255
-16
lines changed

wsmaster/che-core-api-factory-azure-devops/src/main/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParser.java

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,34 @@ private Optional<String> getServerUrl(String repositoryUrl) {
126126
+ substring.substring(
127127
0, substring.contains(":") ? substring.indexOf(":") : substring.indexOf("/")));
128128
}
129+
// Use URI parsing to properly handle IPv6 addresses
130+
try {
131+
URI uri = URI.create(repositoryUrl);
132+
if (uri.getScheme() != null && uri.getHost() != null) {
133+
String authority = uri.getRawAuthority();
134+
if (authority == null) {
135+
String host = uri.getHost();
136+
boolean ipv6 = host != null && host.contains(":");
137+
String hostForUrl = ipv6 ? "[" + host + "]" : host;
138+
int port = uri.getPort();
139+
authority = port == -1 ? hostForUrl : hostForUrl + ":" + port;
140+
}
141+
142+
if (authority != null) {
143+
String serverUrl = uri.getScheme() + "://" + authority;
144+
// Remove path and query from the server URL
145+
int authorityIdx = repositoryUrl.indexOf(authority);
146+
if (authorityIdx >= 0) {
147+
int pathIndex = authorityIdx + authority.length();
148+
if (pathIndex < repositoryUrl.length() && repositoryUrl.charAt(pathIndex) == '/') {
149+
return Optional.of(serverUrl);
150+
}
151+
}
152+
}
153+
}
154+
} catch (IllegalArgumentException e) {
155+
// Fall through to old logic if URI parsing fails
156+
}
129157
// Otherwise, extract the base url from the given repository url by cutting the url after the
130158
// first slash.
131159
Matcher serverUrlMatcher = compile("[^/|:]/").matcher(repositoryUrl);
@@ -137,16 +165,28 @@ private Optional<String> getServerUrl(String repositoryUrl) {
137165
}
138166

139167
private Optional<Matcher> getPatternMatcherByUrl(String url) {
140-
String host = URI.create(url).getHost();
141-
Matcher matcher = compile(format(azureDevOpsPatternTemplate, host)).matcher(url);
168+
URI uri = URI.create(url);
169+
String host = uri.getHost();
170+
// Handle IPv6 addresses: escape brackets for regex
171+
final String hostForRegex;
172+
if (host != null && host.startsWith("[") && host.endsWith("]")) {
173+
// IPv6 address - escape the brackets
174+
hostForRegex = "\\[" + Pattern.quote(host.substring(1, host.length() - 1)) + "\\]";
175+
} else if (host != null) {
176+
// Regular hostname - escape special regex characters
177+
hostForRegex = Pattern.quote(host);
178+
} else {
179+
hostForRegex = "";
180+
}
181+
Matcher matcher = compile(format(azureDevOpsPatternTemplate, hostForRegex)).matcher(url);
142182
if (matcher.matches()) {
143183
return Optional.of(matcher);
144184
} else {
145-
matcher = compile(format(azureSSHDevOpsPatternTemplate, host)).matcher(url);
185+
matcher = compile(format(azureSSHDevOpsPatternTemplate, hostForRegex)).matcher(url);
146186
if (matcher.matches()) {
147187
return Optional.of(matcher);
148188
} else {
149-
matcher = compile(format(azureSSHDevOpsServerPatternTemplate, host)).matcher(url);
189+
matcher = compile(format(azureSSHDevOpsServerPatternTemplate, hostForRegex)).matcher(url);
150190
}
151191
return matcher.matches() ? Optional.of(matcher) : Optional.empty();
152192
}

wsmaster/che-core-api-factory-azure-devops/src/test/java/org/eclipse/che/api/factory/server/azure/devops/AzureDevOpsURLParserTest.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,25 @@ public void shouldParseWithUrlBranch() {
6060
assertEquals(azureDevOpsUrl.getBranch(), "main");
6161
}
6262

63+
@Test
64+
public void shouldParseServerUrlWithIpv6Host() {
65+
// given
66+
azureDevOpsURLParser =
67+
new AzureDevOpsURLParser(
68+
mock(DevfileFilenamesProvider.class),
69+
mock(PersonalAccessTokenManager.class),
70+
"https://[2001:db8::1]/");
71+
72+
// when
73+
AzureDevOpsUrl azureDevOpsUrl =
74+
azureDevOpsURLParser.parse("https://[2001:db8::1]/MyOrg/MyProject/_git/MyRepo", null);
75+
76+
// then
77+
assertEquals(azureDevOpsUrl.getOrganization(), "MyOrg");
78+
assertEquals(azureDevOpsUrl.getProject(), "MyProject");
79+
assertEquals(azureDevOpsUrl.getRepository(), "MyRepo");
80+
}
81+
6382
@Test(dataProvider = "parsing")
6483
public void testParse(
6584
String url,

wsmaster/che-core-api-factory-github-common/src/main/java/org/eclipse/che/api/factory/server/github/AbstractGithubURLParser.java

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,34 @@ private Optional<String> getServerUrl(String repositoryUrl) {
161161
String substring = repositoryUrl.substring(4);
162162
return Optional.of("https://" + substring.substring(0, substring.indexOf(":")));
163163
}
164+
// Use URI parsing to properly handle IPv6 addresses
165+
try {
166+
URI uri = URI.create(repositoryUrl);
167+
if (uri.getScheme() != null && uri.getHost() != null) {
168+
String authority = uri.getRawAuthority();
169+
if (authority == null) {
170+
String host = uri.getHost();
171+
boolean ipv6 = host != null && host.contains(":");
172+
String hostForUrl = ipv6 ? "[" + host + "]" : host;
173+
int port = uri.getPort();
174+
authority = port == -1 ? hostForUrl : hostForUrl + ":" + port;
175+
}
176+
177+
if (authority != null) {
178+
String serverUrl = uri.getScheme() + "://" + authority;
179+
// Remove path and query from the server URL
180+
int authorityIdx = repositoryUrl.indexOf(authority);
181+
if (authorityIdx >= 0) {
182+
int pathIndex = authorityIdx + authority.length();
183+
if (pathIndex < repositoryUrl.length() && repositoryUrl.charAt(pathIndex) == '/') {
184+
return Optional.of(serverUrl);
185+
}
186+
}
187+
}
188+
}
189+
} catch (IllegalArgumentException e) {
190+
// Fall through to old logic if URI parsing fails
191+
}
164192
// Otherwise, extract the base url from the given repository url by cutting the url after the
165193
// first slash.
166194
Matcher serverUrlMatcher = compile("[^/|:]/").matcher(repositoryUrl);
@@ -385,11 +413,36 @@ private Optional<Matcher> getPatternMatcherByUrl(String url) {
385413
: url);
386414
String scheme = uri.getScheme();
387415
String host = uri.getHost();
388-
Matcher matcher = compile(format(githubPatternTemplate, scheme + "://" + host)).matcher(url);
416+
// Build the authority part (host + port)
417+
String authority = host != null ? host : "";
418+
if (uri.getPort() > 0) {
419+
authority += ":" + uri.getPort();
420+
}
421+
// Escape special regex characters, handling IPv6 brackets
422+
final String hostForRegex;
423+
if (host != null && host.startsWith("[") && host.endsWith("]")) {
424+
// IPv6 address - escape the brackets
425+
String ipv6 = host.substring(1, host.length() - 1);
426+
String escapedAuthority = "\\[" + Pattern.quote(ipv6) + "\\]";
427+
if (uri.getPort() > 0) {
428+
escapedAuthority += ":" + uri.getPort();
429+
}
430+
hostForRegex = Pattern.quote(scheme + "://") + escapedAuthority;
431+
} else {
432+
hostForRegex = Pattern.quote(scheme + "://" + authority);
433+
}
434+
Matcher matcher = compile(format(githubPatternTemplate, hostForRegex)).matcher(url);
389435
if (matcher.matches()) {
390436
return Optional.of(matcher);
391437
} else {
392-
matcher = compile(format(githubSSHPatternTemplate, host)).matcher(url);
438+
final String hostOnlyForRegex;
439+
if (host != null && host.startsWith("[") && host.endsWith("]")) {
440+
// For SSH pattern, use the IP without brackets
441+
hostOnlyForRegex = Pattern.quote(host.substring(1, host.length() - 1));
442+
} else {
443+
hostOnlyForRegex = host != null ? Pattern.quote(host) : "";
444+
}
445+
matcher = compile(format(githubSSHPatternTemplate, hostOnlyForRegex)).matcher(url);
393446
return matcher.matches() ? Optional.of(matcher) : Optional.empty();
394447
}
395448
}

wsmaster/che-core-api-factory-github/src/test/java/org/eclipse/che/api/factory/server/github/GithubURLParserTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ public void shouldParseWithUrlBranch() throws ApiException {
100100
assertEquals(githubUrl.getBranch(), "master");
101101
}
102102

103+
@Test
104+
public void shouldParseGithubServerUrlWithIpv6Host() throws ApiException {
105+
// given
106+
githubUrlParser =
107+
new GithubURLParser(
108+
personalAccessTokenManager,
109+
devfileFilenamesProvider,
110+
githubApiClient,
111+
"https://[2001:db8::1]",
112+
false);
113+
when(githubApiClient.isConnected(eq("https://[2001:db8::1]"))).thenReturn(true);
114+
when(devfileFilenamesProvider.getConfiguredDevfileFilenames())
115+
.thenReturn(asList("devfile.yaml", ".devfile.yaml"));
116+
117+
// when
118+
GithubUrl githubUrl = githubUrlParser.parse("https://[2001:db8::1]/eclipse/che", null);
119+
120+
// then
121+
assertEquals(githubUrl.getUsername(), "eclipse");
122+
assertEquals(githubUrl.getRepository(), "che");
123+
}
124+
103125
/** Check URLs are valid with regexp */
104126
@Test(dataProvider = "UrlsProvider")
105127
public void checkRegexp(String url) {

wsmaster/che-core-api-factory-gitlab-common/src/main/java/org/eclipse/che/api/factory/server/gitlab/AbstractGitlabUrlParser.java

Lines changed: 91 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -71,17 +71,57 @@ public AbstractGitlabUrlParser(
7171
String trimmedEndpoint = trimEnd(serverUrl, '/');
7272
URI uri = URI.create(trimmedEndpoint);
7373
String schema = uri.getScheme();
74-
String host =
75-
trimmedEndpoint.substring(
76-
trimmedEndpoint.indexOf("://") + 3,
77-
uri.getPort() > 0
78-
? trimmedEndpoint.indexOf(String.valueOf(uri.getPort())) - 1
79-
: trimmedEndpoint.length());
74+
// We support GitLab endpoints that include an extra path segment, e.g.:
75+
// https://gitlab-server.com/scm
76+
// and want to match repo URLs like:
77+
// https://gitlab-server.com/scm/group/project.git
78+
//
79+
// In that case, we treat "/scm" as part of the fixed endpoint prefix, not as part of the
80+
// repository path.
81+
// Review feedback: use "hostname" naming instead of "endpointWithoutScheme".
82+
final String hostname = uri.getAuthority();
83+
final String endpointPath = uri.getPath() == null ? "" : uri.getPath();
84+
85+
// What if URI#getAuthority() is null? Fallback to string parsing.
86+
final String authority;
87+
if (hostname != null) {
88+
authority = hostname;
89+
} else {
90+
final int schemeSeparatorIdx = trimmedEndpoint.indexOf("://");
91+
final String withoutScheme =
92+
schemeSeparatorIdx >= 0
93+
? trimmedEndpoint.substring(schemeSeparatorIdx + 3)
94+
: trimmedEndpoint;
95+
final int firstSlashIdx = withoutScheme.indexOf('/');
96+
authority = firstSlashIdx >= 0 ? withoutScheme.substring(0, firstSlashIdx) : withoutScheme;
97+
}
98+
99+
// authority may be:
100+
// - gitlab-server.com
101+
// - gitlab-server.com:8443
102+
// - [2001:db8::1]
103+
// - [2001:db8::1]:8443
104+
final String hostOnly;
105+
if (authority.startsWith("[")) {
106+
int closingBracket = authority.indexOf(']');
107+
hostOnly = closingBracket > 0 ? authority.substring(0, closingBracket + 1) : authority;
108+
} else {
109+
int colonIdx = authority.indexOf(':');
110+
hostOnly = colonIdx > 0 ? authority.substring(0, colonIdx) : authority;
111+
}
112+
113+
// HTTP(S) patterns should match the configured endpoint prefix (including any extra path
114+
// segment). SSH pattern should match host only.
115+
// For HTTP(S) patterns we include any configured endpointPath and keep the port (if any) as a
116+
// part of the "host" group. This allows GitlabUrl#getProviderUrl() to return values like
117+
// "https://host:8443/scm" even though GitlabUrl models "host" and "port" separately.
118+
final String httpHostForRegex = Pattern.quote(authority + endpointPath);
119+
final String sshHostForRegex = Pattern.quote(hostOnly);
80120
for (String gitlabUrlPatternTemplate : gitlabUrlPatternTemplates) {
81121
gitlabUrlPatterns.add(
82-
compile(format(gitlabUrlPatternTemplate, schema, host, uri.getPort())));
122+
compile(format(gitlabUrlPatternTemplate, schema, httpHostForRegex, uri.getPort())));
83123
}
84-
gitlabUrlPatterns.add(compile(format(gitlabSSHPatternTemplate, host)));
124+
gitlabUrlPatterns.add(compile(format(gitlabSSHPatternTemplate, sshHostForRegex)));
85125
}
86126
}
87127

@@ -152,13 +192,25 @@ private Optional<Matcher> getPatternMatcherByUrl(String url) {
152192
: url);
153193
String scheme = uri.getScheme();
154194
String host = uri.getHost();
195+
// Handle IPv6 addresses: escape brackets for regex
196+
final String hostForRegex;
197+
if (host != null && host.contains(":")) {
198+
// IPv6 address - URLs contain host in square brackets.
199+
hostForRegex = "\\[" + Pattern.quote(host) + "\\]";
200+
} else if (host != null) {
201+
// Regular hostname - escape special regex characters
202+
hostForRegex = Pattern.quote(host);
203+
} else {
204+
hostForRegex = "";
205+
}
155206
return gitlabUrlPatternTemplates.stream()
156-
.map(t -> compile(format(t, scheme, host, uri.getPort())).matcher(url))
207+
.map(t -> compile(format(t, scheme, hostForRegex, uri.getPort())).matcher(url))
157208
.filter(Matcher::matches)
158209
.findAny()
159210
.or(
160211
() -> {
161-
Matcher matcher = compile(format(gitlabSSHPatternTemplate, host)).matcher(url);
212+
Matcher matcher =
213+
compile(format(gitlabSSHPatternTemplate, hostForRegex)).matcher(url);
162214
if (matcher.matches()) {
163215
return Optional.of(matcher);
164216
}
@@ -171,6 +223,35 @@ private Optional<String> getServerUrl(String repositoryUrl) {
171223
String substring = repositoryUrl.substring(4);
172224
return Optional.of("https://" + substring.substring(0, substring.indexOf(":")));
173225
}
226+
// Use URI parsing to properly handle IPv6 addresses
227+
try {
228+
URI uri = URI.create(repositoryUrl);
229+
if (uri.getScheme() != null && uri.getHost() != null) {
230+
String authority = uri.getRawAuthority();
231+
if (authority == null) {
232+
String host = uri.getHost();
233+
boolean ipv6 = host != null && host.contains(":");
234+
String hostForUrl = ipv6 ? "[" + host + "]" : host;
235+
int port = uri.getPort();
236+
authority = port == -1 ? hostForUrl : hostForUrl + ":" + port;
237+
}
238+
239+
if (authority != null) {
240+
String serverUrl = uri.getScheme() + "://" + authority;
241+
// Remove path and query from the server URL
242+
int authorityIdx = repositoryUrl.indexOf(authority);
243+
if (authorityIdx >= 0) {
244+
int pathIndex = authorityIdx + authority.length();
245+
if (pathIndex < repositoryUrl.length() && repositoryUrl.charAt(pathIndex) == '/') {
246+
return Optional.of(serverUrl);
247+
}
248+
}
249+
}
250+
}
251+
} catch (IllegalArgumentException e) {
252+
// Fall through to old logic if URI parsing fails
253+
}
254+
// Fallback for non-standard URLs
174255
Matcher serverUrlMatcher = compile("[^/|:]/").matcher(repositoryUrl);
175256
if (serverUrlMatcher.find()) {
176257
return Optional.of(

wsmaster/che-core-api-factory-gitlab/src/test/java/org/eclipse/che/api/factory/server/gitlab/GitlabUrlParserTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,30 @@ public void shouldGetProviderUrlWithExtraSegment() throws ApiException {
8989
assertEquals(gitlabUrl.getProviderUrl(), "https://gitlab-server.com/scm");
9090
}
9191

92+
@Test
93+
public void shouldGetProviderUrlWithExtraSegmentOnIpv6Endpoint() throws ApiException {
94+
gitlabUrlParser =
95+
new GitlabUrlParser(
96+
"https://[2001:db8::1]/scm",
97+
devfileFilenamesProvider,
98+
mock(PersonalAccessTokenManager.class));
99+
GitlabUrl gitlabUrl =
100+
gitlabUrlParser.parse("https://[2001:db8::1]/scm/user/project/test.git", null);
101+
assertEquals(gitlabUrl.getProviderUrl(), "https://[2001:db8::1]/scm");
102+
}
103+
104+
@Test
105+
public void shouldGetProviderUrlWithExtraSegmentOnIpv6EndpointWithPort() throws ApiException {
106+
gitlabUrlParser =
107+
new GitlabUrlParser(
108+
"https://[2001:db8::1]:8443/scm",
109+
devfileFilenamesProvider,
110+
mock(PersonalAccessTokenManager.class));
111+
GitlabUrl gitlabUrl =
112+
gitlabUrlParser.parse("https://[2001:db8::1]:8443/scm/user/project/test.git", null);
113+
assertEquals(gitlabUrl.getProviderUrl(), "https://[2001:db8::1]:8443/scm");
114+
}
115+
92116
@Test
93117
public void shouldParseWithUrlBranch() throws ApiException {
94118
GitlabUrl gitlabUrl =

0 commit comments

Comments
 (0)