Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement node registration #47

Merged
merged 17 commits into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlinVersion")

api('tech.relaycorp:relaynet:1.36.3')
implementation('tech.relaycorp:relaynet-testing:1.0.0')

// Handshake nonce signatures
implementation("org.bouncycastle:bcpkix-jdk15on:1.66")
Expand Down
8 changes: 5 additions & 3 deletions jacoco.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,22 @@ jacocoTestCoverageVerification {
limit {
counter = "CLASS"
value = "MISSEDCOUNT"
maximum = "0".toBigDecimal()
// Workaround for https://github.com/jacoco/jacoco/issues/654
maximum = "1".toBigDecimal()
}

limit {
counter = "METHOD"
value = "MISSEDCOUNT"
maximum = "0".toBigDecimal()
// Workaround for https://github.com/jacoco/jacoco/issues/654
maximum = "1".toBigDecimal()
}

limit {
counter = "BRANCH"
value = "MISSEDCOUNT"
// Workaround for https://github.com/jacoco/jacoco/issues/1036
maximum = "1".toBigDecimal()
maximum = "2".toBigDecimal()
}
}
}
Expand Down
133 changes: 110 additions & 23 deletions src/main/kotlin/tech/relaycorp/poweb/PoWebClient.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
package tech.relaycorp.poweb

import io.ktor.client.HttpClient
import io.ktor.client.engine.HttpClientEngine
import io.ktor.client.engine.okhttp.OkHttp
import io.ktor.client.features.ResponseException
import io.ktor.client.features.websocket.DefaultClientWebSocketSession
import io.ktor.client.features.websocket.WebSockets
import io.ktor.client.features.websocket.webSocket
import io.ktor.client.request.header
import io.ktor.client.request.post
import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType
import io.ktor.http.cio.websocket.CloseReason
import io.ktor.http.cio.websocket.Frame
import io.ktor.http.cio.websocket.close
import io.ktor.http.cio.websocket.readBytes
import io.ktor.http.content.ByteArrayContent
import io.ktor.http.content.OutgoingContent
import io.ktor.http.content.TextContent
import io.ktor.http.contentType
import io.ktor.util.KtorExperimentalAPI
import io.ktor.util.toByteArray
import kotlinx.coroutines.channels.ClosedReceiveChannelException
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
Expand All @@ -27,11 +32,14 @@ import tech.relaycorp.relaynet.bindings.pdc.ParcelCollection
import tech.relaycorp.relaynet.bindings.pdc.StreamingMode
import tech.relaycorp.relaynet.messages.InvalidMessageException
import tech.relaycorp.relaynet.messages.control.ParcelDelivery
import tech.relaycorp.relaynet.messages.control.PrivateNodeRegistration
import tech.relaycorp.relaynet.wrappers.x509.Certificate
import java.io.Closeable
import java.io.EOFException
import java.net.ConnectException
import java.net.SocketException
import java.security.MessageDigest
import java.security.PublicKey

/**
* PoWeb client.
Expand All @@ -46,9 +54,10 @@ import java.net.SocketException
public class PoWebClient internal constructor(
internal val hostName: String,
internal val port: Int,
internal val useTls: Boolean
internal val useTls: Boolean,
ktorEngine: HttpClientEngine = OkHttp.create {}
) : Closeable {
internal var ktorClient = HttpClient(OkHttp) {
internal var ktorClient = HttpClient(ktorEngine) {
install(WebSockets)
}

Expand All @@ -62,6 +71,42 @@ public class PoWebClient internal constructor(
*/
override fun close(): Unit = ktorClient.close()

/**
* Request a Private Node Registration Authorization (PNRA).
*
* @param nodePublicKey The public key of the private node requesting authorization
*/
@Throws(
ServerConnectionException::class,
ServerBindingException::class,
ClientBindingException::class
)
public suspend fun preRegisterNode(nodePublicKey: PublicKey): ByteArray {
val keyDigest = getSHA256DigestHex(nodePublicKey.encoded)
val response = post("/pre-registrations", TextContent(keyDigest, ContentType.Text.Plain))

requireContentType(PNRA_CONTENT_TYPE, response.contentType())

return response.content.toByteArray()
}

/**
* Register a private node.
*
* @param pnrrSerialized The Private Node Registration Request
*/
public suspend fun registerNode(pnrrSerialized: ByteArray): PrivateNodeRegistration {
val response = post("/nodes", ByteArrayContent(pnrrSerialized, PNRR_CONTENT_TYPE))

requireContentType(PNR_CONTENT_TYPE, response.contentType())

return try {
PrivateNodeRegistration.deserialize(response.content.toByteArray())
} catch (exc: InvalidMessageException) {
throw ServerBindingException("The server returned a malformed registration", exc)
}
}

/**
* Deliver a parcel.
*
Expand All @@ -70,30 +115,18 @@ public class PoWebClient internal constructor(
@Throws(
ServerConnectionException::class,
ServerBindingException::class,
RefusedParcelException::class,
RejectedParcelException::class,
ClientBindingException::class
)
public suspend fun deliverParcel(parcelSerialized: ByteArray) {
val body = ByteArrayContent(parcelSerialized, PARCEL_CONTENT_TYPE)
try {
ktorClient.post<Unit>("$baseURL/parcels") {
body = ByteArrayContent(parcelSerialized, PARCEL_CONTENT_TYPE)
}
} catch (exc: SocketException) {
// Java on macOS throws a SocketException but all other platforms throw a
// ConnectException (a subclass of SocketException)
throw ServerConnectionException("Failed to connect to $baseURL", exc)
} catch (exc: ResponseException) {
val status = exc.response!!.status
when (status.value) {
403 -> throw RefusedParcelException("Parcel was refused by the server ($status)")
in 400..499 -> throw ClientBindingException(
"The server reports that the client violated binding ($status)"
)
in 500..599 -> throw ServerConnectionException(
"The server was unable to fulfil the request ($status)"
)
else -> throw ServerBindingException("Received unexpected status ($status)")
}
post("/parcels", body)
} catch (exc: ClientBindingException) {
throw if (exc.statusCode == 403)
RejectedParcelException("The server rejected the parcel")
else
exc
}
}

Expand Down Expand Up @@ -166,6 +199,49 @@ public class PoWebClient internal constructor(
}
}

@Throws(
ServerConnectionException::class,
ServerBindingException::class,
ClientBindingException::class
)
internal suspend fun post(path: String, requestBody: OutgoingContent): HttpResponse {
val url = "$baseURL$path"
val response: HttpResponse = try {
ktorClient.post(url) {
body = requestBody
}
} catch (exc: SocketException) {
// Java on macOS throws a SocketException but all other platforms throw a
// ConnectException (a subclass of SocketException)
throw ServerConnectionException("Failed to connect to $url", exc)
}

if (response.status.value in 200..299) {
return response
}
throw when (response.status.value) {
in 400..499 -> ClientBindingException(
"The server reports that the client violated binding (${response.status})",
response.status.value
)
in 500..599 -> ServerConnectionException(
"The server was unable to fulfil the request (${response.status})"
)
else -> ServerBindingException("Received unexpected status (${response.status})")
}
}

private fun requireContentType(
requiredContentType: ContentType,
actualContentType: ContentType?
) {
if (actualContentType != requiredContentType) {
throw ServerBindingException(
"The server returned an invalid Content-Type ($actualContentType)"
)
}
}

internal suspend fun wsConnect(
path: String,
headers: List<Pair<String, String>>? = null,
Expand All @@ -189,6 +265,12 @@ public class PoWebClient internal constructor(
private const val DEFAULT_REMOTE_PORT = 443

private val PARCEL_CONTENT_TYPE = ContentType("application", "vnd.relaynet.parcel")
internal val PNRA_CONTENT_TYPE =
ContentType("application", "vnd.relaynet.node-registration.authorization")
internal val PNRR_CONTENT_TYPE =
ContentType("application", "vnd.relaynet.node-registration.request")
internal val PNR_CONTENT_TYPE =
ContentType("application", "vnd.relaynet.node-registration.registration")

/**
* Connect to a private gateway from a private endpoint.
Expand All @@ -208,6 +290,11 @@ public class PoWebClient internal constructor(
*/
public fun initRemote(hostName: String, port: Int = DEFAULT_REMOTE_PORT): PoWebClient =
PoWebClient(hostName, port, true)

private fun getSHA256DigestHex(plaintext: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256")
return digest.digest(plaintext).joinToString("") { "%02x".format(it) }
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions src/main/kotlin/tech/relaycorp/poweb/PoWebException.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class ServerBindingException(message: String, cause: Throwable? = null) :
/**
* The server refused to accept a parcel.
*/
public class RefusedParcelException(message: String) : PoWebException(message)
public class RejectedParcelException(message: String) : PoWebException(message)

/**
* Base class for exceptions (supposedly) caused by the client.
Expand All @@ -43,7 +43,8 @@ public abstract class ClientException internal constructor(message: String) :
*
* Retrying later is unlikely to make a difference.
*/
public class ClientBindingException(message: String) : ClientException(message)
public class ClientBindingException(message: String, public val statusCode: Int) :
ClientException(message)

/**
* The client made a mistake while specifying the nonce signer(s).
Expand Down
Loading