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