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

我使用的是Kotlin的Vertx 3,有时我需要从公共URL的角度返回一个特定的URI,这与Vertx-web请求认为我的URL不一样。 这可能是由于我的负载均衡器或代理接收到一个URL,然后通过内部URL转发到我的应用程序。

所以如果我这样做:

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

我最终得到一个像http://10.10.103.22:8080/some/page而不是https://app.mydomain.com/some/page的URL。 关于该网址的一切都是错误的!

我发现了一个标题,可以告诉我更多关于原始请求的信息,例如X-Forwarded-Host但是它只包含app.mydomain.com ,有时它的端口是app.mydomain:80但是还不足以解决所有问题部分网址,我结束了像http://app.mydomain.com:8080/some/page仍然不是正确的公共URL。

我还需要处理的不仅仅是我当前的URL,而且是对等URL,就像在页面上的“something / page1”一样,在同一台服务器上转到“something / page2”。 当我试图解析到另一个URL,因为公共URL的重要部分是无法获得提到的相同的问题。

Vertx-web中有没有一种方法可以确定这个公共URL,或者一些常用的方法来解决这个问题?

我在Kotlin编码,所以这个语言的例子都很棒!

注意: 这个问题是由作者故意编写和回答的( 自我回答问题 ),所以在SO中共享有趣问题的解决方案。

这是一个更复杂的问题,如果大多数应用服务器尚未提供URL外部化功能,其逻辑是相同的。

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

  • 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

如果您想解析并返回一个不是当前网址的网址,您还需要考虑:

  • 部分没有主机,例如“/ something / here”或“under / me”解析服务器的公共协议,主机,端口以及那些绝对或相对路径
  • 部分与主机/端口,例如“//somehost.com:8983/thing”将添加相同的方案(HTTP / HTTPS)作为这台服务器,并保持休息
  • 完整的,完全限定的URL被原封不动地返回,所以它们可以安全地传递给这个函数(“http:// …”,“https:// …”),并且不会被修改

下面是一对RoutingContext的扩展函数,它将处理所有这些情况,并在负载均衡器/代理头不存在时回退,因此可以在直接连接到服务器的情况下以及通过中介的情况下工作。 你传递绝对或相对的URL(到当前页面),它将返回相同的公共版本。

 // 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().filter { 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 = 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, String?> { 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) } fun String.mustStartWith(prefix: Char): String { return if (this.startsWith(prefix)) { this } else { prefix + this } }