1 /* <lambda>null2 * Copyright 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.build.importMaven 18 19 import io.ktor.client.HttpClient 20 import io.ktor.client.engine.okhttp.OkHttp 21 import io.ktor.client.request.header 22 import io.ktor.client.request.request 23 import io.ktor.client.statement.HttpResponse 24 import io.ktor.client.statement.bodyAsChannel 25 import io.ktor.http.Headers 26 import io.ktor.http.HttpMethod 27 import io.ktor.http.contentType 28 import io.ktor.http.isSuccess 29 import io.ktor.server.application.call 30 import io.ktor.server.engine.embeddedServer 31 import io.ktor.server.netty.Netty 32 import io.ktor.server.request.path 33 import io.ktor.server.response.respondBytes 34 import io.ktor.server.routing.get 35 import io.ktor.server.routing.routing 36 import io.ktor.util.toByteArray 37 import kotlinx.coroutines.runBlocking 38 import org.apache.logging.log4j.kotlin.logger 39 import java.net.URI 40 import java.net.URL 41 42 /** 43 * Creates a local proxy server for given the artifactory url. 44 * 45 * @see MavenRepositoryProxy.Companion.startAll 46 */ 47 class MavenRepositoryProxy private constructor( 48 delegateHost: String, 49 val downloadObserver: DownloadObserver? 50 ) { 51 init { 52 check(delegateHost.startsWith("http")) { 53 "Unsupported url: $delegateHost. Only http(s) urls are supported" 54 } 55 } 56 57 private val logger = logger("MavenProxy[$delegateHost]") 58 59 private val delegateHost = delegateHost.trimEnd { 60 it == '/' 61 } 62 63 fun <T> start(block: (URI) -> T): T { 64 val client = HttpClient(OkHttp) 65 val server = embeddedServer(Netty, port = 0 /*random port*/) { 66 routing { 67 get("/{...}") { 68 val path = this.call.request.path() 69 val displayUrl = "$delegateHost$path" 70 val incomingHeaders = this.call.request.headers 71 logger.trace { 72 "Request ($displayUrl)" 73 } 74 75 try { 76 val (clientResponse, responseBytes) = requestFromDelegate( 77 path, 78 client, 79 incomingHeaders 80 ) 81 call.respondBytes( 82 bytes = responseBytes, 83 contentType = clientResponse.contentType(), 84 status = clientResponse.status 85 ).also { 86 logger.trace { 87 "Success ($displayUrl)" 88 } 89 } 90 } catch (ex: Throwable) { 91 logger.error(ex) { 92 "Failed ($displayUrl): ${ex.message}" 93 } 94 throw ex 95 } 96 } 97 } 98 } 99 return try { 100 server.start(wait = false) 101 val url = runBlocking { 102 server.resolvedConnectors().first().let { 103 URL( 104 it.type.name.lowercase(), 105 // Always use `localhost` for local loopback given this is secure for `http` URLs. 106 "localhost", 107 it.port, 108 "" 109 ) 110 // Always use `localhost` for local loopback given this is secure for `http` URIs. 111 URI("${it.type.name.lowercase()}://localhost:${it.port}") 112 } 113 } 114 block(url) 115 } finally { 116 runCatching { 117 client.close() 118 } 119 runCatching { 120 server.stop() 121 } 122 } 123 } 124 125 private suspend fun requestFromDelegate( 126 path: String, 127 client: HttpClient, 128 incomingHeaders: Headers 129 ): Pair<HttpResponse, ByteArray> { 130 val delegatedUrl = "$delegateHost$path" 131 val clientResponse = client.request(delegatedUrl) { 132 incomingHeaders.forEach { key, value -> 133 // don't copy host header since we are proxying from localhost. 134 if (key != "Host") { 135 header(key, value) 136 } 137 } 138 method = HttpMethod.Get 139 } 140 val responseBytes = clientResponse.bodyAsChannel().toByteArray() 141 if (clientResponse.status.isSuccess()) { 142 downloadObserver?.onDownload( 143 path = path.dropWhile { it == '/' }, 144 bytes = responseBytes 145 ) 146 } 147 return Pair(clientResponse, responseBytes) 148 } 149 150 companion object { 151 /** 152 * Creates proxy servers for all given artifactory urls. 153 * 154 * It will call the given [block] with local servers that can be provided to gradle as maven 155 * repositories. 156 */ 157 fun <T> startAll( 158 repositoryUrls: List<String>, 159 downloadObserver: DownloadObserver?, 160 block: (List<URI>) -> T 161 ): T { 162 val proxies = repositoryUrls.map { url -> 163 MavenRepositoryProxy( 164 delegateHost = url, 165 downloadObserver = downloadObserver 166 ) 167 } 168 return startAll( 169 proxies = proxies, 170 previousUris = emptyList(), 171 block = block 172 ) 173 } 174 175 /** 176 * Recursively start all proxy servers 177 */ 178 private fun <T> startAll( 179 proxies: List<MavenRepositoryProxy>, 180 previousUris: List<URI>, 181 block: (List<URI>) -> T 182 ): T { 183 if (proxies.isEmpty()) { 184 return block(previousUris) 185 } 186 val first = proxies.first() 187 return first.start { myUri -> 188 startAll( 189 proxies = proxies.drop(1), 190 previousUris = previousUris + myUri, 191 block = block 192 ) 193 } 194 } 195 } 196 } 197