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

时间:2016-09-19 01:29:15

标签: kotlin vert.x

我将Vertx 3与Kotlin一起使用,有时我需要从公共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这样的东西仍然不是正确的公共网址。

我还需要处理不仅仅是我当前的网址,还需要处理对等网址,就像在页面上一样#34;某事/ page1"转到" something / page2"在同一台服务器上当我尝试解析到另一个URL时提到了同样的问题,因为无法获得公共URL的重要部分。

Vertx-web中是否有一种方法缺少确定此公开网址或某种惯用方法来解决此问题?

我在Kotlin编码,因此该语言的任何示例都很棒!

注意: 此问题是由作者(Self-Answered Questions)故意编写和回答的,因此可以在SO中分享有趣问题的解决方案。

1 个答案:

答案 0 :(得分:6)

这是一个更复杂的问题,如果大多数App服务器尚未提供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

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

  • 部分没有主持人,例如" / something / here"或者"在/ 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().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 }
}