我有一个 Vertx 请求,我需要计算一个外部可见的 (public) URL

I have a Vertx request and I need to calculate an externally visible (public) URL

我在 Kotlin 中使用 Vertx 3,有时我需要从 public URL 的角度 return 一个特定的 URI,这与Vertx-web 请求认为我的 URL 是。这可能是因为我的负载均衡器或代理接收了一个 URL,然后转发到我的内部 URL.

上的应用程序

所以如果我这样做:

val publicUrl = context.request().absoluteURI() 

我最终得到了 URL,比如 http://10.10.103.22:8080/some/page 而不是 https://app.mydomain.com/some/page。 URL!

一切都错了

我找到了一个 header,据说可以告诉我更多关于原始请求的信息,例如 X-Forwarded-Host 但它只包含 app.mydomain.com 或者有时它有端口 app.mydomain:80 但是这不足以弄清楚 URL 的所有部分,我最终得到类似 http://app.mydomain.com:8080/some/page 的东西,它仍然不是正确的 public URL.

我不仅需要处理当前的 URL,还需要处理同行 URL 的问题,例如在页面“something/page1”上转到“something/page2”在同一台服务器上。当我尝试解决另一个 URL 时提到的相同问题,因为 public URL 的重要部分无法获得。

Vertx-web 中是否有方法来确定这个 public URL,或者一些惯用的方法来解决这个问题?

我正在用 Kotlin 编写代码,所以该语言的任何示例都很棒!

注: 这个问题是作者(Self-Answered Questions)特意写出来的,所以有兴趣的问题的解决办法都在SO里分享.

这是一个更复杂的问题,如果它们还没有提供 URL 外部化功能,那么对于大多数应用服务器来说,逻辑是相同的。

要正确执行此操作,您需要处理所有这些 headers:

  • X-Forwarded-Proto(或 X-Forwarded-Scheme: https,可能还有像 X-Forwarded-Ssl: onFront-End-Https: on 这样的怪人)
  • X-Forwarded-Host(如"myhost.com"或"myhost.com:port")
  • X-Forwarded-Port

如果你想解析 return 一个不是当前的 URL 你还需要考虑:

  • 部分没有主机,例如“/something/here”或"under/me"解析到服务器public协议、主机、端口以及绝对或相对路径
  • 部分 host/port,例如“//somehost.com:8983/thing”将添加与此服务器相同的方案 (http/https) 并保留其余
  • full,完全合格的 URL 是 returned 不变的,所以它们可以安全地传递给这个函数("http://...","https://...")和不会被修改

这是 RoutingContext 的一对扩展函数,它们将处理所有这些情况,并在负载均衡器/代理 headers 不存在时回退,因此在两种直接连接情况下都可以使用到服务器和那些通过中介的人。您传入绝对或相对 URL(到当前页面),它将 return 相同的 public 版本。

// return current URL as public URL
fun RoutingContext.externalizeUrl(): String {
    return externalizeUrl(URI(request().absoluteURI()).pathPlusParmsOfUrl())
}

// resolve a related URL as a public URL
fun RoutingContext.externalizeUrl(resolveUrl: String): String {
    val cleanHeaders = request().headers().filterNot { it.value.isNullOrBlank() }
            .map { it.key to it.value }.toMap()
    return externalizeURI(URI(request().absoluteURI()), resolveUrl, cleanHeaders).toString()
}

它调用一个内部函数来完成实际工作(并且更易于测试,因为不需要模拟 RoutingContext):

internal fun externalizeURI(requestUri: URI, resolveUrl: String, headers: Map<String, String>): URI {
    // special case of not touching fully qualified resolve URL's
    if (resolveUrl.startsWith("http://") || resolveUrl.startsWith("https://")) return URI(resolveUrl)

    val forwardedScheme = headers.get("X-Forwarded-Proto")
            ?: headers.get("X-Forwarded-Scheme")
            ?: requestUri.getScheme()

    // special case of //host/something URL's
    if (resolveUrl.startsWith("//")) return URI("$forwardedScheme:$resolveUrl")

    val (forwardedHost, forwardedHostOptionalPort) =
            dividePort(headers.get("X-Forwarded-Host") ?: requestUri.getHost())

    val fallbackPort = requestUri.getPort().let { explicitPort ->
        if (explicitPort <= 0) {
            if ("https" == forwardedScheme) 443 else 80
        } else {
            explicitPort
        }
    }
    val requestPort: Int = headers.get("X-Forwarded-Port")?.toInt()
            ?: forwardedHostOptionalPort
            ?: fallbackPort
    val finalPort = when {
        forwardedScheme == "https" && requestPort == 443 -> ""
        forwardedScheme == "http" && requestPort == 80 -> ""
        else -> ":$requestPort"
    }

    val restOfUrl = requestUri.pathPlusParmsOfUrl()
    return URI("$forwardedScheme://$forwardedHost$finalPort$restOfUrl").resolve(resolveUrl)
}

以及一些相关的辅助函数:

internal fun URI.pathPlusParmsOfUrl(): String {
    val path = this.getRawPath().let { if (it.isNullOrBlank()) "" else it.mustStartWith('/') }
    val query = this.getRawQuery().let { if (it.isNullOrBlank()) "" else it.mustStartWith('?') }
    val fragment = this.getRawFragment().let { if (it.isNullOrBlank()) "" else it.mustStartWith('#') }
    return "$path$query$fragment"
}

internal fun dividePort(hostWithOptionalPort: String): Pair<String, Int?> {
    val parts = if (hostWithOptionalPort.startsWith('[')) { // ipv6
        Pair(hostWithOptionalPort.substringBefore(']') + ']', hostWithOptionalPort.substringAfter("]:", ""))
    } else { // ipv4
        Pair(hostWithOptionalPort.substringBefore(':'), hostWithOptionalPort.substringAfter(':', ""))
    }
    return Pair(parts.first, if (parts.second.isNullOrBlank()) null else parts.second.toInt())
}

fun String.mustStartWith(prefix: Char): String {
    return if (this.startsWith(prefix)) { this } else { prefix + this }
}