From 0dfbc7d53369bf744fd74c0e7f8947be6187c764 Mon Sep 17 00:00:00 2001 From: Victor Nogueira Date: Mon, 8 Aug 2022 18:19:18 +0300 Subject: [PATCH] Auto-forward all workspace open ports when using Latest JetBrains IDEs --- .../jetbrains/backend-plugin/build.gradle.kts | 7 +- .../jetbrains/remote/GitpodCLIService.kt | 24 +++- .../GitpodClientProjectSessionTracker.kt | 23 +++- .../jetbrains/remote/GitpodPortsService.kt | 81 +++++++++++ .../latest/GitpodPortForwardingService.kt | 129 ++++++++++++++++++ .../remote/latest/GitpodTerminalService.kt | 51 +------ .../resources-latest/META-INF/extensions.xml | 1 + .../src/main/resources/META-INF/plugin.xml | 1 + .../remote/GitpodPortsServiceTest.kt | 67 +++++++++ 9 files changed, 324 insertions(+), 60 deletions(-) create mode 100644 components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodPortsService.kt create mode 100644 components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodPortForwardingService.kt create mode 100644 components/ide/jetbrains/backend-plugin/src/test/kotlin/io/gitpod/jetbrains/remote/GitpodPortsServiceTest.kt diff --git a/components/ide/jetbrains/backend-plugin/build.gradle.kts b/components/ide/jetbrains/backend-plugin/build.gradle.kts index 3769309c2c9ba2..83dee90147d839 100644 --- a/components/ide/jetbrains/backend-plugin/build.gradle.kts +++ b/components/ide/jetbrains/backend-plugin/build.gradle.kts @@ -13,7 +13,7 @@ plugins { // Kotlin support - check the latest version at https://plugins.gradle.org/plugin/org.jetbrains.kotlin.jvm id("org.jetbrains.kotlin.jvm") version "1.7.0" // gradle-intellij-plugin - read more: https://github.com/JetBrains/gradle-intellij-plugin - id("org.jetbrains.intellij") version "1.6.0" + id("org.jetbrains.intellij") version "1.8.0" // detekt linter - read more: https://detekt.github.io/detekt/gradle.html id("io.gitlab.arturbosch.detekt") version "1.17.1" // ktlint linter - read more: https://github.com/JLLeitschuh/ktlint-gradle @@ -108,7 +108,10 @@ tasks { } test { - useJUnitPlatform() + // Currently, we need to indicate where are the test classes. + // Read more: https://youtrack.jetbrains.com/issue/IDEA-278926/All-inheritors-of-UsefulTestCase-are-invisible-for-Gradle#focus=Comments-27-5561012.0-0 + isScanForTestClasses = false + include("**/*Test.class") } runPluginVerifier { diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodCLIService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodCLIService.kt index 49fd84e2f88832..63b6f39892212f 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodCLIService.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodCLIService.kt @@ -16,6 +16,10 @@ import com.intellij.openapi.project.Project import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream import com.intellij.openapi.util.io.FileUtilRt import com.intellij.util.application +import com.intellij.util.withFragment +import com.intellij.util.withPath +import com.intellij.util.withQuery +import com.jetbrains.rd.util.URI import io.netty.buffer.Unpooled import io.netty.channel.ChannelHandlerContext import io.netty.handler.codec.http.FullHttpRequest @@ -31,6 +35,7 @@ import java.nio.file.Path class GitpodCLIService : RestService() { private val manager = service() + private val portsService = service() override fun getServiceName() = SERVICE_NAME @@ -68,13 +73,30 @@ class GitpodCLIService : RestService() { if (url.isNullOrBlank()) { return "url is missing" } + + val resolvedUrl = resolveExternalUrl(url) + return withClient(request, context) { project -> - BrowserUtil.browse(url, project) + BrowserUtil.browse(resolvedUrl, project) } } return "invalid operation" } + private fun resolveExternalUrl(url: String): String { + val uri = URI.create(url) + val optionalLocalHostUriMetadata = portsService.extractLocalHostUriMetaDataForPortMapping(uri) + + return when { + optionalLocalHostUriMetadata.isEmpty -> url + else -> portsService.getLocalHostUriFromHostPort(optionalLocalHostUriMetadata.get().port) + .withPath(uri.path) + .withQuery(uri.query) + .withFragment(uri.fragment) + .toString() + } + } + private fun withClient(request: FullHttpRequest, context: ChannelHandlerContext, action: (project: Project?) -> Unit): String? { ApplicationManager.getApplication().executeOnPooledThread { getClientSessionAndProjectAsync().let { (session, project) -> diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodClientProjectSessionTracker.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodClientProjectSessionTracker.kt index 2d634e901c238f..d00efb4b83584c 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodClientProjectSessionTracker.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodClientProjectSessionTracker.kt @@ -33,11 +33,13 @@ import org.jetbrains.ide.BuiltInServerManager import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture +@Suppress("UnstableApiUsage", "OPT_IN_USAGE") class GitpodClientProjectSessionTracker( private val session: ClientProjectSession ) : Disposable { private val manager = service() + private val portsService = service() private lateinit var info: Info.WorkspaceInfoResponse private val lifetime = Lifetime.Eternal.createNested() @@ -54,19 +56,26 @@ class GitpodClientProjectSessionTracker( } } - private fun isExposedServedPort(port: Status.PortsStatus?): Boolean { + private fun isExposedServedPort(port: PortsStatus?): Boolean { if (port === null) { return false } return port.served && port.hasExposed() } + private fun getForwardedPortUrl(port: PortsStatus): String { + return when { + portsService.isForwarded(port.localPort) -> portsService.getLocalHostUriFromHostPort(port.localPort).toString() + else -> port.exposed.url + } + } + private fun showOpenServiceNotification(port: PortsStatus, offerMakePublic: Boolean = false) { val message = "A service is available on port ${port.localPort}" val notification = manager.notificationGroup.createNotification(message, NotificationType.INFORMATION) - val openBrowserAction = NotificationAction.createSimple("Open Browser") { - openBrowser(port.exposed.url) + val openBrowserAction = NotificationAction.createSimple("Open browser") { + openBrowser(getForwardedPortUrl(port)) } notification.addAction(openBrowserAction) @@ -76,7 +85,7 @@ class GitpodClientProjectSessionTracker( makePortPublic(info.workspaceId, port) } } - val makePublicAction = NotificationAction.createSimple("Make Public", makePublicLambda) + val makePublicAction = NotificationAction.createSimple("Make public", makePublicLambda) notification.addAction(makePublicAction) } @@ -113,7 +122,7 @@ class GitpodClientProjectSessionTracker( val backendPort = BuiltInServerManager.getInstance().waitForStart().port val serverPort = StartupUtil.getServerFuture().await().port val ignorePorts = listOf(backendPort, serverPort, 5990) - val portsStatus = hashMapOf() + val portsStatus = hashMapOf() val status = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel) while (isActive) { @@ -147,7 +156,7 @@ class GitpodClientProjectSessionTracker( } if (port.exposed.onExposed.number == Status.OnPortExposedAction.open_browser_VALUE || port.exposed.onExposed.number == Status.OnPortExposedAction.open_preview_VALUE) { - openBrowser(port.exposed.url) + openBrowser(getForwardedPortUrl(port)) continue } @@ -157,7 +166,7 @@ class GitpodClientProjectSessionTracker( } if (port.exposed.onExposed.number == Status.OnPortExposedAction.notify_private_VALUE) { - showOpenServiceNotification(port, port.exposed.visibilityValue !== PortVisibility.public_visibility_VALUE) + showOpenServiceNotification(port, port.exposed.visibilityValue != PortVisibility.public_visibility_VALUE) continue } } diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodPortsService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodPortsService.kt new file mode 100644 index 00000000000000..95a4b936442a7e --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/GitpodPortsService.kt @@ -0,0 +1,81 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote + +import com.intellij.openapi.diagnostic.thisLogger +import com.jetbrains.rd.util.URI +import org.apache.http.client.utils.URIBuilder +import java.util.Optional +import java.util.regex.Pattern + +class GitpodPortsService { + companion object { + /** Host used by forwarded ports on JetBrains Client. */ + const val FORWARDED_PORT_HOST = "127.0.0.1" + } + private val hostToClientForwardedPortMap: MutableMap = mutableMapOf() + + fun isForwarded(hostPort: Int): Boolean = hostToClientForwardedPortMap.containsKey(hostPort) + + private fun getForwardedPort(hostPort: Int): Optional = Optional.ofNullable(hostToClientForwardedPortMap[hostPort]) + + fun setForwardedPort(hostPort: Int, clientPort: Int) { + hostToClientForwardedPortMap[hostPort] = clientPort + } + + fun removeForwardedPort(hostPort: Int) { + hostToClientForwardedPortMap.remove(hostPort) + } + + fun getLocalHostUriFromHostPort(hostPort: Int): URI { + val optionalForwardedPort = getForwardedPort(hostPort) + + val port = if (optionalForwardedPort.isPresent) { + optionalForwardedPort.get() + } else { + thisLogger().warn( + "gitpod: Tried to get the forwarded port of $hostPort, which was not forwarded. " + + "Returning $hostPort itself." + ) + hostPort + } + + return URIBuilder() + .setScheme("http") + .setHost(FORWARDED_PORT_HOST) + .setPort(port) + .build() + } + + interface LocalHostUriMetadata { + val address: String + val port: Int + } + + fun extractLocalHostUriMetaDataForPortMapping(uri: URI): Optional { + if (uri.scheme != "http" && uri.scheme != "https") return Optional.empty() + + val localhostMatch = Pattern.compile("^(localhost|127(?:\\.[0-9]+){0,2}\\.[0-9]+|0+(?:\\.0+){0,2}\\.0+|\\[(?:0*:)*?:?0*1?])(?::(\\d+))?\$").matcher(uri.authority) + + if (!localhostMatch.find()) return Optional.empty() + + var address = localhostMatch.group(1) + if (address.startsWith('[') && address.endsWith(']')) { + address = address.substring(1, address.length - 2) + } + + var port = 443 + try { + port = localhostMatch.group(2).toInt() + } catch (throwable: Throwable){ + if (uri.scheme == "http") port = 80 + } + + return Optional.of(object: LocalHostUriMetadata { + override val address = address + override val port = port + }) + } +} diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodPortForwardingService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodPortForwardingService.kt new file mode 100644 index 00000000000000..669c3da12d9226 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodPortForwardingService.kt @@ -0,0 +1,129 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote.latest + +import com.intellij.openapi.components.service +import com.intellij.openapi.diagnostic.thisLogger +import com.intellij.openapi.project.Project +import com.intellij.remoteDev.util.onTerminationOrNow +import com.intellij.util.application +import com.jetbrains.codeWithMe.model.RdPortType +import com.jetbrains.rd.platform.util.lifetime +import com.jetbrains.rd.util.lifetime.LifetimeStatus +import com.jetbrains.rdserver.portForwarding.ForwardedPortInfo +import com.jetbrains.rdserver.portForwarding.PortForwardingManager +import com.jetbrains.rdserver.portForwarding.remoteDev.PortEventsProcessor +import io.gitpod.jetbrains.remote.GitpodManager +import io.gitpod.jetbrains.remote.GitpodPortsService +import io.gitpod.supervisor.api.Status +import io.gitpod.supervisor.api.StatusServiceGrpc +import io.grpc.stub.ClientCallStreamObserver +import io.grpc.stub.ClientResponseObserver +import io.ktor.utils.io.* +import java.util.concurrent.CompletableFuture +import java.util.concurrent.TimeUnit + +@Suppress("UnstableApiUsage") +class GitpodPortForwardingService(private val project: Project) { + companion object { + const val FORWARDED_PORT_LABEL = "gitpod" + } + + private val portsService = service() + + init { start() } + + private fun start() { + if (application.isHeadlessEnvironment) return + + observePortsListWhileProjectIsOpen() + } + + private fun observePortsListWhileProjectIsOpen() = application.executeOnPooledThread { + while (project.lifetime.status == LifetimeStatus.Alive) { + try { + observePortsList().get() + } catch (throwable: Throwable) { + when (throwable) { + is InterruptedException, is CancellationException -> break + else -> thisLogger().error( + "gitpod: Got an error while trying to get ports list from Supervisor. " + + "Going to try again in a second.", + throwable + ) + } + } + + TimeUnit.SECONDS.sleep(1) + } + } + + private fun observePortsList(): CompletableFuture { + val completableFuture = CompletableFuture() + + val statusServiceStub = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel) + + val portsStatusRequest = Status.PortsStatusRequest.newBuilder().setObserve(true).build() + + val portsStatusResponseObserver = object : + ClientResponseObserver { + override fun beforeStart(request: ClientCallStreamObserver) { + project.lifetime.onTerminationOrNow { request.cancel("gitpod: Project terminated.", null) } + } + override fun onNext(response: Status.PortsStatusResponse) { + application.invokeLater { updateForwardedPortsList(response) } + } + override fun onCompleted() { completableFuture.complete(null) } + override fun onError(throwable: Throwable) { completableFuture.completeExceptionally(throwable) } + } + + statusServiceStub.portsStatus(portsStatusRequest, portsStatusResponseObserver) + + return completableFuture + } + + private fun updateForwardedPortsList(response: Status.PortsStatusResponse) { + val portForwardingManager = PortForwardingManager.getInstance(project) + val forwardedPortsList = portForwardingManager.getForwardedPortsWithLabel(FORWARDED_PORT_LABEL) + + for (port in response.portsList) { + val hostPort = port.localPort + val isServed = port.served + + if (isServed && !forwardedPortsList.containsKey(hostPort)) { + val portEventsProcessor = object : PortEventsProcessor { + override fun onPortForwarded(hostPort: Int, clientPort: Int) { + portsService.setForwardedPort(hostPort, clientPort) + thisLogger().info("gitpod: Forwarded port $hostPort to client's port $clientPort.") + } + + override fun onPortForwardingEnded(hostPort: Int) { + thisLogger().info("gitpod: Finished forwarding port $hostPort.") + } + + override fun onPortForwardingFailed(hostPort: Int, reason: String) { + thisLogger().error("gitpod: Failed to forward port $hostPort: $reason") + } + } + + val portInfo = ForwardedPortInfo( + hostPort, + RdPortType.HTTP, + FORWARDED_PORT_LABEL, + emptyList(), + portEventsProcessor + ) + + portForwardingManager.forwardPort(portInfo) + } + + if (!isServed && forwardedPortsList.containsKey(hostPort)) { + portForwardingManager.removePort(hostPort) + portsService.removeForwardedPort(hostPort) + thisLogger().info("gitpod: Stopped forwarding port $hostPort.") + } + } + } +} diff --git a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodTerminalService.kt b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodTerminalService.kt index 7367897a47d7ec..40c37d22994407 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodTerminalService.kt +++ b/components/ide/jetbrains/backend-plugin/src/main/kotlin/io/gitpod/jetbrains/remote/latest/GitpodTerminalService.kt @@ -6,12 +6,7 @@ package io.gitpod.jetbrains.remote.latest import com.intellij.openapi.client.ClientProjectSession import com.intellij.openapi.diagnostic.thisLogger -import com.intellij.openapi.rd.createLifetime -import com.intellij.remoteDev.util.onTerminationOrNow import com.intellij.util.application -import com.jetbrains.rdserver.portForwarding.PortForwardingDiscovery -import com.jetbrains.rdserver.portForwarding.PortForwardingManager -import com.jetbrains.rdserver.portForwarding.remoteDev.PortEventsProcessor import com.jetbrains.rdserver.terminal.BackendTerminalManager import io.gitpod.jetbrains.remote.GitpodManager import io.gitpod.supervisor.api.Status @@ -27,15 +22,13 @@ import java.util.concurrent.CompletableFuture import java.util.concurrent.TimeUnit @Suppress("UnstableApiUsage") -class GitpodTerminalService(private val session: ClientProjectSession) { +class GitpodTerminalService(session: ClientProjectSession) { private companion object { var hasStarted = false - val forwardedPortsList: MutableSet = mutableSetOf() } private val terminalView = TerminalView.getInstance(session.project) private val backendTerminalManager = BackendTerminalManager.getInstance(session.project) - private val portForwardingManager = PortForwardingManager.getInstance(session.project) private val terminalServiceFutureStub = TerminalServiceGrpc.newFutureStub(GitpodManager.supervisorChannel) private val statusServiceStub = StatusServiceGrpc.newStub(GitpodManager.supervisorChannel) @@ -88,7 +81,6 @@ class GitpodTerminalService(private val session: ClientProjectSession) { val terminal = aliasToTerminalMap[terminalAlias] ?: continue createAttachedSharedTerminal(terminal) - autoForwardAllPortsFromTerminal(terminal) } } @@ -177,45 +169,4 @@ class GitpodTerminalService(private val session: ClientProjectSession) { "gp tasks attach ${supervisorTerminal.alias}" ) } - - private fun autoForwardAllPortsFromTerminal(supervisorTerminal: TerminalOuterClass.Terminal) { - val projectLifetime = session.project.createLifetime() - - val discoveryCallback = object : PortForwardingDiscovery { - /** - * @return Whether port should be forwarded or not. - * We shouldn't try to forward ports that are already forwarded. - */ - override fun onPortDiscovered(hostPort: Int): Boolean = !forwardedPortsList.contains(hostPort) - - override fun getEventsProcessor(hostPort: Int) = object : PortEventsProcessor { - override fun onPortForwarded(hostPort: Int, clientPort: Int) { - forwardedPortsList.add(hostPort) - thisLogger().info("gitpod: Forwarded port $hostPort from Supervisor's Terminal " + - "${supervisorTerminal.pid} to client's port $clientPort.") - - projectLifetime.onTerminationOrNow { - if (forwardedPortsList.contains(hostPort)) { - forwardedPortsList.remove(hostPort) - portForwardingManager.removePort(hostPort) - thisLogger().info("gitpod: Removing forwarded port $hostPort from Supervisor's Terminal " + - "${supervisorTerminal.pid}") - } - } - } - - override fun onPortForwardingFailed(hostPort: Int, reason: String) { - thisLogger().error("gitpod: Failed to forward port $hostPort from Supervisor's Terminal " + - "${supervisorTerminal.pid}: $reason") - } - - override fun onPortForwardingEnded(hostPort: Int) { - thisLogger().info("gitpod: Port $hostPort from Supervisor's Terminal " + - "${supervisorTerminal.pid} is not being forwarded anymore.") - } - } - } - - portForwardingManager.forwardPortsOfPid(projectLifetime, supervisorTerminal.pid, discoveryCallback, true) - } } diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources-latest/META-INF/extensions.xml b/components/ide/jetbrains/backend-plugin/src/main/resources-latest/META-INF/extensions.xml index d705f363926944..c98241d241d8c9 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/resources-latest/META-INF/extensions.xml +++ b/components/ide/jetbrains/backend-plugin/src/main/resources-latest/META-INF/extensions.xml @@ -6,5 +6,6 @@ + diff --git a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml index ca19490a92ab7b..bbb6f3991a9cdc 100644 --- a/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml +++ b/components/ide/jetbrains/backend-plugin/src/main/resources/META-INF/plugin.xml @@ -24,6 +24,7 @@ + diff --git a/components/ide/jetbrains/backend-plugin/src/test/kotlin/io/gitpod/jetbrains/remote/GitpodPortsServiceTest.kt b/components/ide/jetbrains/backend-plugin/src/test/kotlin/io/gitpod/jetbrains/remote/GitpodPortsServiceTest.kt new file mode 100644 index 00000000000000..5388eb00427d37 --- /dev/null +++ b/components/ide/jetbrains/backend-plugin/src/test/kotlin/io/gitpod/jetbrains/remote/GitpodPortsServiceTest.kt @@ -0,0 +1,67 @@ +// Copyright (c) 2022 Gitpod GmbH. All rights reserved. +// Licensed under the GNU Affero General Public License (AGPL). +// See License-AGPL.txt in the project root for license information. + +package io.gitpod.jetbrains.remote + +import com.intellij.testFramework.fixtures.BasePlatformTestCase +import io.gitpod.jetbrains.remote.GitpodPortsService.LocalHostUriMetadata +import java.net.URI + +class GitpodPortsServiceTest : BasePlatformTestCase() { + fun testExtractLocalHostUriMetaDataForPortMapping() { + val portsService = GitpodPortsService() + + val urlToMetadataMap = mapOf( + "https://localhost:80" to object: LocalHostUriMetadata { + override val address = "localhost" + override val port = 80 + }, + "https://localhost" to object: LocalHostUriMetadata { + override val address = "localhost" + override val port = 443 + }, + "http://localhost:12354" to object: LocalHostUriMetadata { + override val address = "localhost" + override val port = 12354 + }, + "https://127.0.0.1:3000" to object: LocalHostUriMetadata { + override val address = "127.0.0.1" + override val port = 3000 + }, + "http://127.0.0.1:5000" to object: LocalHostUriMetadata { + override val address = "127.0.0.1" + override val port = 5000 + }, + "http://[::1]:8080" to object: LocalHostUriMetadata { + override val address = "::" + override val port = 8080 + }, + ) + + urlToMetadataMap.forEach { (url, expected) -> + val uri = URI.create(url) + val actualLocalHostUriMetadataOptional = portsService.extractLocalHostUriMetaDataForPortMapping(uri) + val actual = actualLocalHostUriMetadataOptional.get() + + assertEquals(expected.address, actual.address) + assertEquals(expected.port, actual.port) + } + + val urlsThatShouldReturnEmpty = listOf( + "https://localhost:123b", + "http://192.168.0.1:4000", + "https://example.com?cb=localhost", + "https://example.com?cb=http://localhost", + "https://example.com?cb=https://localhost:8080", + "https://example.com?cb=https://127.0.0.1:8080" + ) + + urlsThatShouldReturnEmpty.forEach { url -> + val uri = URI.create(url) + val localHostUriMetaDataForPort = portsService.extractLocalHostUriMetaDataForPortMapping(uri) + + assertTrue(localHostUriMetaDataForPort.isEmpty) + } + } +}