У меня есть запрос Vertx, и мне нужно вычислить внешний видимый (общедоступный) URL-адрес

Я использую Vertx 3 с Kotlin, и порой мне нужно вернуть определенный URI с точки зрения общедоступного URL-адреса, который не совпадает с тем, что веб-запрос Vertx считает моим URL-адресом. Вероятно, это связано с тем, что мой балансировщик нагрузки или прокси-сервер получают один URL-адрес, а затем перенаправление в мое приложение по внутреннему URL-адресу.

Поэтому, если я это сделаю:

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

Я получаю URL-адрес, например http://10.10.103.22:8080/some/page а не https://app.mydomain.com/some/page . В этом URL-адресе нет ничего плохого!

Я нашел заголовок, который предположительно расскажет мне больше об исходном запросе, таком как X-Forwarded-Host но он включает только app.mydomain.com или иногда имеет порт app.mydomain:80 но этого недостаточно, чтобы выяснить все части URL-адреса, я получаю что-то вроде http://app.mydomain.com:8080/some/page которое по-прежнему не является правильным общедоступным URL-адресом.

Мне также нужно обрабатывать не только мой текущий URL-адрес, но и URL-адрес партнера, например, на странице «something / page1» перейти к «something / page2» на том же сервере. Те же проблемы, о которых я упоминал, когда пытаюсь разрешить другой URL-адрес, потому что важные части общедоступного URL недоступны.

Есть ли метод в Vertx-web, который мне не хватает, чтобы определить этот общедоступный URL-адрес или какой-то идиоматический способ решить эту проблему?

Я кодирую в Kotlin, поэтому любые примеры для этого языка великолепны!

Примечание. Этот вопрос намеренно написан автором и отвечает на него ( Self-Answered Questions ), так что решения для интересных проблем разделяются в SO.

Это более сложная проблема, и логика для большинства серверов приложений одинакова, если они еще не предоставляют функцию внешнего URL-адреса.

Чтобы сделать это правильно, вам необходимо обработать все эти заголовки:

  • X-Forwarded-Proto (или X-Forwarded-Scheme: https , и, возможно, такие странные вещи, как X-Forwarded-Ssl: on , Front-End-Https: on )
  • X-Forwarded-Host (как «myhost.com» или «myhost.com:port»)
  • X-Forwarded-Port

И если вы хотите разрешить и вернуть URL-адрес, который не является текущим, вам также необходимо учесть:

  • частичный без хоста, например «/ 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 } }