Skip to content

Commit

Permalink
Add insecure option to download schema (#4021)
Browse files Browse the repository at this point in the history
* Add insecure option to download schema

* Add comment for SSLContext
  • Loading branch information
kdk96 authored Apr 13, 2022
1 parent c671348 commit 223ca3a
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 13 deletions.
1 change: 1 addition & 0 deletions apollo-gradle-plugin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {
testImplementation(groovy.util.Eval.x(project, "x.dep.truth"))
testImplementation(groovy.util.Eval.x(project, "x.dep.assertj"))
testImplementation(groovy.util.Eval.x(project, "x.dep.okHttp.mockWebServer"))
testImplementation(groovy.util.Eval.x(project, "x.dep.okHttp.tls"))
}

if (true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import com.apollographql.apollo3.compiler.introspection.toIntrospectionSchema
import com.apollographql.apollo3.compiler.toJson
import okio.Buffer
import org.gradle.api.DefaultTask
import org.gradle.api.file.DirectoryProperty
import org.gradle.api.provider.Property
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.Internal
Expand Down Expand Up @@ -64,6 +63,11 @@ abstract class ApolloDownloadSchemaTask : DefaultTask() {
@set:Option(option = "header", description = "headers in the form 'Name: Value'")
var header = emptyList<String>() // cannot be abstract for @Option to work

@get:Optional
@get:Input
@get:Option(option = "insecure", description = "flag to disable endpoint TLS certificate verification")
abstract val insecure: Property<Boolean>

init {
/**
* We cannot know in advance if the backend schema changed so don't cache or mark this task up-to-date
Expand Down Expand Up @@ -96,11 +100,13 @@ abstract class ApolloDownloadSchemaTask : DefaultTask() {
graph = key.split(":")[1]
}

val insecure = insecure.getOrElse(false)
when {
endpointUrl != null -> {
introspectionSchema = SchemaDownloader.downloadIntrospection(
endpoint = endpointUrl,
headers = headers,
insecure = insecure,
).toIntrospectionSchema()
}
graph != null -> {
Expand All @@ -111,7 +117,8 @@ abstract class ApolloDownloadSchemaTask : DefaultTask() {
graph = graph,
key = key,
variant = graphVariant ?: "current",
endpoint = registryUrl.orNull ?: "https://graphql.api.apollographql.com/api/graphql"
endpoint = registryUrl.getOrElse("https://graphql.api.apollographql.com/api/graphql"),
insecure = insecure,
).let { Buffer().writeUtf8(it) }.parseAsGQLDocument().valueAssertNoErrors()
}
else -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import com.apollographql.apollo3.compiler.fromJson
object SchemaDownloader {
fun downloadIntrospection(
endpoint: String,
headers: Map<String, String>
headers: Map<String, String>,
insecure: Boolean,
): String {

val body = mapOf(
"query" to introspectionQuery,
"operationName" to "IntrospectionQuery"
)
val response = SchemaHelper.executeQuery(body, endpoint, headers)
val response = SchemaHelper.executeQuery(body, endpoint, headers, insecure)

return response.body.use { responseBody ->
responseBody!!.string()
Expand All @@ -23,7 +24,8 @@ object SchemaDownloader {
key: String,
graph: String,
variant: String,
endpoint: String = "https://graphql.api.apollographql.com/api/graphql"
endpoint: String = "https://graphql.api.apollographql.com/api/graphql",
insecure: Boolean,
): String {
val query = """
query DownloadSchema(${'$'}graphID: ID!, ${'$'}variant: String!) {
Expand All @@ -40,7 +42,7 @@ object SchemaDownloader {
""".trimIndent()
val variables = mapOf("graphID" to graph, "variant" to variant)

val response = SchemaHelper.executeQuery(query, variables, endpoint, mapOf("x-api-key" to key))
val response = SchemaHelper.executeQuery(query, variables, endpoint, mapOf("x-api-key" to key), insecure)

val responseString = response.body.use { it?.string() }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,21 @@ import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.internal.platform.Platform
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.util.concurrent.TimeUnit
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSession
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager

internal object SchemaHelper {
private fun newOkHttpClient(): OkHttpClient {
private fun newOkHttpClient(insecure: Boolean): OkHttpClient {
val connectTimeoutSeconds = System.getProperty("okHttp.connectTimeout", "600").toLong()
val readTimeoutSeconds = System.getProperty("okHttp.readTimeout", "600").toLong()
return OkHttpClient.Builder()
val clientBuilder = OkHttpClient.Builder()
.connectTimeout(connectTimeoutSeconds, TimeUnit.SECONDS)
.addInterceptor { chain ->
chain.request().newBuilder()
Expand All @@ -25,9 +33,15 @@ internal object SchemaHelper {

}
.readTimeout(readTimeoutSeconds, TimeUnit.SECONDS)
.build()

if (insecure) {
clientBuilder.applyInsecureTrustManager()
}

return clientBuilder.build()
}
internal fun executeQuery(map: Map<String, Any?>, url: String, headers: Map<String, String>): Response {

internal fun executeQuery(map: Map<String, Any?>, url: String, headers: Map<String, String>, insecure: Boolean): Response {
val body = map.toJson().toByteArray().toRequestBody("application/json".toMediaTypeOrNull())
val request = Request.Builder()
.post(body)
Expand All @@ -39,7 +53,7 @@ internal object SchemaHelper {
.url(url)
.build()

val response = newOkHttpClient()
val response = newOkHttpClient(insecure)
.newCall(request)
.execute()

Expand All @@ -49,10 +63,68 @@ internal object SchemaHelper {

return response
}

/**
* @param variables a map representing the variable as Json values
*/
internal fun executeQuery(query: String, variables: Map<String, Any>, url: String, headers: Map<String, String>): Response {
return executeQuery(mapOf("query" to query, "variables" to variables), url, headers)
internal fun executeQuery(
query: String,
variables: Map<String, Any>,
url: String,
headers: Map<String, String>,
insecure: Boolean = false,
): Response {
return executeQuery(mapOf("query" to query, "variables" to variables), url, headers, insecure)
}

private fun OkHttpClient.Builder.applyInsecureTrustManager() = apply {
val insecureTrustManager = InsecureTrustManager()
sslSocketFactory(createSslSocketFactory(insecureTrustManager), insecureTrustManager)
hostnameVerifier(InsecureHostnameVerifier())
}

@Suppress("TooGenericExceptionCaught")
private fun createSslSocketFactory(trustManager: X509TrustManager): SSLSocketFactory {
try {
val sslContext = try {
SSLContext.getInstance("SSL")
} catch (_: Exception) {
// get a SSLContext.
// There are a lot of subtle differences in the different protocols but this is used on the insecure path
// we are ok taking any of them
Platform.get().newSSLContext()
}

sslContext.init(null, arrayOf(trustManager), SecureRandom())
return sslContext.socketFactory
} catch (e: Exception) {
throw IllegalStateException("Cannot init SSLContext", e)
}
}


@Suppress("CustomX509TrustManager")
private class InsecureTrustManager : X509TrustManager {

@Suppress("TrustAllX509TrustManager")
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
// accept all
}

@Suppress("TrustAllX509TrustManager")
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
// accept all
}

override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
}

private class InsecureHostnameVerifier : HostnameVerifier {

@Suppress("BadHostnameVerifier")
override fun verify(hostname: String?, session: SSLSession?): Boolean {
// accept all
return true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.apollographql.apollo3.gradle.util.TestUtils
import com.apollographql.apollo3.gradle.util.TestUtils.withSimpleProject
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
import okhttp3.tls.HandshakeCertificates
import okhttp3.tls.HeldCertificate
import org.gradle.testkit.runner.TaskOutcome
import org.junit.Assert.assertEquals
import org.junit.Test
Expand Down Expand Up @@ -118,4 +120,25 @@ class DownloadSchemaTests {
assertEquals(schemaString1, schema.readText())
}
}

@Test
fun `manually downloading a schema from self signed endpoint is working`() {
withSimpleProject(apolloConfiguration = "") { dir ->
val mockResponse = MockResponse().setBody(schemaString1)
mockServer.enqueue(mockResponse)

val selfSignedCertificate = HeldCertificate.Builder().build()
val certs = HandshakeCertificates.Builder().heldCertificate(selfSignedCertificate).build()
mockServer.useHttps(certs.sslSocketFactory(), tunnelProxy = false)

val schema = File("build/testProject/schema.json")

TestUtils.executeGradle(dir, "downloadApolloSchema",
"--schema=${schema.absolutePath}",
"--endpoint=${mockServer.url("/")}",
"--insecure")

assertEquals(schemaString1, schema.readText())
}
}
}
1 change: 1 addition & 0 deletions gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ ext.dep = [
mockWebServer: "com.squareup.okhttp3:mockwebserver:$versions.okHttp",
okHttp : "com.squareup.okhttp3:okhttp:$versions.okHttp",
logging : "com.squareup.okhttp3:logging-interceptor:$versions.okHttp",
tls : "com.squareup.okhttp3:okhttp-tls:$versions.okHttp",
],
okio : "com.squareup.okio:okio:$versions.okio",
okioNodeJs: "com.squareup.okio:okio-nodefilesystem:$versions.okio",
Expand Down

0 comments on commit 223ca3a

Please sign in to comment.