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

Jay*_*ard 5 kotlin vert.x

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

因此,如果我这样做:

val publicUrl = context.request().absoluteURI() 
Run Code Online (Sandbox Code Playgroud)

我最终得到一个URL http://10.10.103.22:8080/some/page而不是https://app.mydomain.com/some/page。该网址的所有内容都不对!

我发现了一个头,理应告诉我更多的原始请求,如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中共享。

Jay*_*ard 7

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

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

  • X-Forwarded-Proto(或者X-Forwarded-Scheme: https,也许oddballs等X-Forwarded-Ssl: onFront-End-Https: on
  • X-Forwarded-Host (作为“ myhost.com”或“ myhost.com:port”)
  • X-Forwarded-Port

而且,如果您要解析并返回一个不是当前URL的URL,则还需要考虑以下几点:

  • 不带主机的部分主机,例如“ / something / here”或“ under / me”解析到服务器的公共协议,主机,端口以及绝对路径或相对路径
  • 部分带有主机/端口,例如“ //somehost.com:8983/thing”将为此服务器添加相同的方案(http / https),其余部分保留
  • 完整,完全合格的URL将保持不变,因此可以安全地传递给此函数(“ http:// ...”,“ https:// ...”),并且不会被修改

这是一对扩展功能RoutingContext,可以处理所有这些情况,并在不存在负载平衡器/代理标头时退回,因此在直接连接到服务器以及通过中介的情况下都可以使用。您输入绝对或相对URL(到当前页面),它将返回该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()
}
Run Code Online (Sandbox Code Playgroud)

哪个函数调用完成实际工作的内部函数(并且由于无需模拟,因此更易于测试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)
}
Run Code Online (Sandbox Code Playgroud)

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

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 }
}
Run Code Online (Sandbox Code Playgroud)