1
0
Fork 0
mirror of https://github.com/ACINQ/eclair.git synced 2025-02-24 22:58:23 +01:00

Improve error handling via rejections

This commit is contained in:
Andrea 2019-03-13 18:50:50 +01:00
parent cecb6bcdd8
commit c9e573a4c2
No known key found for this signature in database
GPG key ID: FFB3470FFF04CA76
2 changed files with 137 additions and 128 deletions

View file

@ -4,7 +4,6 @@ import akka.http.scaladsl.unmarshalling.Unmarshaller
import fr.acinq.bitcoin.BinaryData
import fr.acinq.bitcoin.Crypto.PublicKey
import fr.acinq.eclair.payment.PaymentRequest
import fr.acinq.eclair.wire.NodeAddress
import scala.util.{Failure, Success, Try}
object FormParamExtractors {
@ -18,15 +17,6 @@ object FormParamExtractors {
}
}
// assumes IPv4 like XXX.YYY.ZZZ.EEE:1234
implicit val inetAddressUnmarshaller: Unmarshaller[String, NodeAddress] = Unmarshaller.strict { rawAddress =>
val Array(host: String, port: String) = rawAddress.split(":")
NodeAddress.fromParts(host, port.toInt) match {
case Success(address) => address
case Failure(thr) => throw thr
}
}
implicit val binaryDataUnmarshaller: Unmarshaller[String, BinaryData] = Unmarshaller.strict { hex =>
BinaryData(hex)
}

View file

@ -11,7 +11,7 @@ import FormParamExtractors._
import akka.NotUsed
import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import akka.http.scaladsl.model.HttpMethods.POST
import akka.http.scaladsl.model.StatusCodes
import akka.http.scaladsl.model.{ContentTypes, HttpRequest, HttpResponse, StatusCodes}
import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public}
import akka.http.scaladsl.model.headers.{`Access-Control-Allow-Headers`, `Access-Control-Allow-Methods`, `Cache-Control`}
import akka.http.scaladsl.model.ws.{Message, TextMessage}
@ -44,7 +44,7 @@ trait NewService extends Directives with Logging with MetaService {
implicit val ec = appKit.system.dispatcher
implicit val mat: ActorMaterializer
implicit val timeout = Timeout(60 seconds)
implicit val timeout = Timeout(60 seconds) // used by akka ask
// a named and typed URL parameter used across several routes, 32-bytes hex-encoded
val channelIdNamedParameter = "channelId".as[BinaryData](sha256HashUnmarshaller)
@ -58,11 +58,17 @@ trait NewService extends Directives with Logging with MetaService {
complete(StatusCodes.InternalServerError, s"Error: $t")
}
val apiRejectionHandler = RejectionHandler.newBuilder()
.handle {
case UnknownMethodRejection => complete(StatusCodes.BadRequest, "Wrong method")
case UnknownParamsRejection(msg) => complete(StatusCodes.BadRequest, msg)
}
.result()
val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") ::
`Access-Control-Allow-Methods`(POST) ::
`Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil
lazy val makeSocketHandler: Flow[Message, TextMessage.Strict, NotUsed] = {
// create a flow transforming a queue of string -> string
@ -83,118 +89,127 @@ trait NewService extends Directives with Logging with MetaService {
.map(TextMessage.apply)
}
val timeoutResponse: HttpRequest => HttpResponse = { r =>
HttpResponse(StatusCodes.RequestTimeout).withEntity(ContentTypes.`application/json`, """{ "result": null, "error": { "code": 408, "message": "request timed out"} } """)
}
val route: Route = {
respondWithDefaultHeaders(customHeaders) {
handleExceptions(apiExceptionHandler) {
authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ =>
post {
path("getinfo") {
complete(getInfoResponse)
} ~
path("help") {
complete(help)
} ~
path("connect") {
formFields("nodeId".as[PublicKey].?, "host".as[String].?, "port".as[Int].?, "uri".as[String].?) { (nodeId, host, port, uri) =>
complete(connect(nodeId, host, port, uri))
}
} ~
path("open") {
formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) {
(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) =>
complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags))
}
} ~
path("close") {
formFields(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) =>
complete(close(channelId, scriptPubKey_opt))
}
} ~
path("forceclose") {
formFields(channelIdNamedParameter) { channelId =>
complete(forceClose(channelId.toString))
}
} ~
path("updaterelayfee") {
formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) =>
complete(updateRelayFee(channelId.toString, feeBase, feeProportional))
}
} ~
path("peers") {
complete(peersInfo())
} ~
path("channels") {
formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt =>
complete(channelsInfo(toRemoteNodeId_opt))
}
} ~
path("channel") {
formFields(channelIdNamedParameter) { channelId =>
complete(channelInfo(channelId))
}
} ~
path("allnodes") {
complete(allnodes())
} ~
path("allchannels") {
complete(allchannels())
} ~
path("allupdates") {
formFields("nodeId".as[PublicKey].?) { nodeId_opt =>
complete(allupdates(nodeId_opt))
}
} ~
path("receive") {
formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) =>
complete(receive(desc, amountMsat, expire))
}
} ~
path("parseinvoice") {
formFields("invoice".as[PaymentRequest]) { invoice =>
complete(invoice)
}
} ~
path("findroute") {
formFields("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) =>
complete(findRoute(nodeId, amount, invoice))
}
} ~
path("send") {
formFields("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) =>
complete(send(nodeId, amountMsat, paymentHash, invoice))
}
} ~
path("checkpayment") {
formFields("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) =>
complete(checkpayment(paymentHash, invoice))
}
} ~
path("audit") {
formFields("from".as[Long].?, "to".as[Long].?) { (from, to) =>
complete(audit(from, to))
}
} ~
path("networkfees") {
formFields("from".as[Long].?, "to".as[Long].?) { (from, to) =>
complete(networkFees(from, to))
}
} ~
path("channelstats") {
complete(channelStats())
} ~
path("ws") {
handleWebSocketMessages(makeSocketHandler)
handleRejections(apiRejectionHandler){
withRequestTimeoutResponse(timeoutResponse){
authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator) { _ =>
post {
path("getinfo") {
complete(getInfoResponse)
} ~
path("help") {
complete(help)
} ~
path("connect") {
formFields("nodeId".as[PublicKey].?, "host".as[String].?, "port".as[Int].?, "uri".as[String].?) { (nodeId, host, port, uri) =>
connect(nodeId, host, port, uri)
}
} ~
path("open") {
formFields("nodeId".as[PublicKey], "fundingSatoshis".as[Long], "pushMsat".as[Long].?, "fundingFeerateSatByte".as[Long].?, "channelFlags".as[Int].?) {
(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags) =>
complete(open(nodeId, fundingSatoshis, pushMsat, fundingFeerateSatByte, channelFlags))
}
} ~
path("close") {
formFields(channelIdNamedParameter, "scriptPubKey".as[BinaryData](binaryDataUnmarshaller).?) { (channelId, scriptPubKey_opt) =>
complete(close(channelId, scriptPubKey_opt))
}
} ~
path("forceclose") {
formFields(channelIdNamedParameter) { channelId =>
complete(forceClose(channelId.toString))
}
} ~
path("updaterelayfee") {
formFields(channelIdNamedParameter, "feeBaseMsat".as[Long], "feeProportionalMillionths".as[Long]) { (channelId, feeBase, feeProportional) =>
complete(updateRelayFee(channelId.toString, feeBase, feeProportional))
}
} ~
path("peers") {
complete(peersInfo())
} ~
path("channels") {
formFields("toRemoteNodeId".as[PublicKey].?) { toRemoteNodeId_opt =>
complete(channelsInfo(toRemoteNodeId_opt))
}
} ~
path("channel") {
formFields(channelIdNamedParameter) { channelId =>
complete(channelInfo(channelId))
}
} ~
path("allnodes") {
complete(allnodes())
} ~
path("allchannels") {
complete(allchannels())
} ~
path("allupdates") {
formFields("nodeId".as[PublicKey].?) { nodeId_opt =>
complete(allupdates(nodeId_opt))
}
} ~
path("receive") {
formFields("description".as[String], "amountMsat".as[Long].?, "expireIn".as[Long].?) { (desc, amountMsat, expire) =>
complete(receive(desc, amountMsat, expire))
}
} ~
path("parseinvoice") {
formFields("invoice".as[PaymentRequest]) { invoice =>
complete(invoice)
}
} ~
path("findroute") {
formFields("nodeId".as[PublicKey].?, "amountMsat".as[Long].?, "invoice".as[PaymentRequest].?) { (nodeId, amount, invoice) =>
findRoute(nodeId, amount, invoice)
}
} ~
path("send") {
formFields("amountMsat".as[Long].?, "paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "nodeId".as[PublicKey].?, "invoice".as[PaymentRequest].?) { (amountMsat, paymentHash, nodeId, invoice) =>
complete(send(nodeId, amountMsat, paymentHash, invoice))
}
} ~
path("checkpayment") {
formFields("paymentHash".as[BinaryData](sha256HashUnmarshaller).?, "invoice".as[PaymentRequest].?) { (paymentHash, invoice) =>
checkpayment(paymentHash, invoice)
}
} ~
path("audit") {
formFields("from".as[Long].?, "to".as[Long].?) { (from, to) =>
complete(audit(from, to))
}
} ~
path("networkfees") {
formFields("from".as[Long].?, "to".as[Long].?) { (from, to) =>
complete(networkFees(from, to))
}
} ~
path("channelstats") {
complete(channelStats())
} ~
path("ws") {
handleWebSocketMessages(makeSocketHandler)
} ~
path(Segment) { _ => reject() }
}
}
}
}
}
}
}
def connect(nodeId_opt: Option[PublicKey], host_opt:Option[String], port_opt: Option[Int], uri_opt: Option[String]): Future[String] = (nodeId_opt, host_opt, port_opt, uri_opt) match {
case (None, None, None, Some(uri)) => (appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String]
case (Some(nodeId), Some(host), Some(port), None) => (appKit.switchboard ? Peer.Connect(NodeURI.parse(s"$nodeId@$host:$port"))).mapTo[String]
case _ => throw IllegalApiParams("connect")
def connect(nodeId_opt: Option[PublicKey], host_opt:Option[String], port_opt: Option[Int], uri_opt: Option[String]): Route = (nodeId_opt, host_opt, port_opt, uri_opt) match {
case (None, None, None, Some(uri)) => complete((appKit.switchboard ? Peer.Connect(NodeURI.parse(uri))).mapTo[String])
case (Some(nodeId), Some(host), Some(port), None) => complete((appKit.switchboard ? Peer.Connect(NodeURI.parse(s"$nodeId@$host:$port"))).mapTo[String])
case _ => reject(UnknownParamsRejection("Wrong arguments for 'connect'"))
}
def open(nodeId: PublicKey, fundingSatoshis: Long, pushMsat: Option[Long], fundingFeerateSatByte: Option[Long], flags: Option[Int]): Future[String] = {
@ -255,35 +270,35 @@ trait NewService extends Directives with Logging with MetaService {
}
}
def findRoute(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], invoice_opt: Option[PaymentRequest]): Future[RouteResponse] = (nodeId_opt, amount_opt, invoice_opt) match {
def findRoute(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], invoice_opt: Option[PaymentRequest]): Route = (nodeId_opt, amount_opt, invoice_opt) match {
case (None, None, Some(invoice@PaymentRequest(_, Some(amountMsat), _, targetNodeId, _, _))) =>
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat.toLong, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse]
complete((appKit.router ? RouteRequest(appKit.nodeParams.nodeId, targetNodeId, amountMsat.toLong, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse])
case (None, Some(amountMsat), Some(invoice)) =>
(appKit.router ? RouteRequest(appKit.nodeParams.nodeId, invoice.nodeId, amountMsat, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse]
case (Some(nodeId), Some(amountMsat), None) => (appKit.router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat)).mapTo[RouteResponse]
case _ => throw IllegalApiParams("findroute")
complete((appKit.router ? RouteRequest(appKit.nodeParams.nodeId, invoice.nodeId, amountMsat, assistedRoutes = invoice.routingInfo)).mapTo[RouteResponse])
case (Some(nodeId), Some(amountMsat), None) => complete((appKit.router ? RouteRequest(appKit.nodeParams.nodeId, nodeId, amountMsat)).mapTo[RouteResponse])
case _ => reject(UnknownParamsRejection("Wrong params for method 'findroute'"))
}
def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[PaymentResult] = {
def send(nodeId_opt: Option[PublicKey], amount_opt: Option[Long], paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Route = {
val (targetNodeId, paymentHash, amountMsat) = (nodeId_opt, amount_opt, paymentHash_opt, invoice_opt) match {
case (Some(nodeId), Some(amount), Some(ph), None) => (nodeId, ph, amount)
case (None, None, None, Some(invoice@PaymentRequest(_, Some(amount), _, target, _, _))) => (target, invoice.paymentHash, amount.toLong)
case (None, Some(amount), None, Some(invoice@PaymentRequest(_, Some(_), _, target, _, _))) => (target, invoice.paymentHash, amount) // invoice amount is overridden
case _ => throw IllegalApiParams("send")
case _ => return reject(UnknownParamsRejection("Wrong params for method 'send'"))
}
val sendPayment = SendPayment(amountMsat, paymentHash, targetNodeId, assistedRoutes = invoice_opt.map(_.routingInfo).getOrElse(Seq.empty)) // TODO add minFinalCltvExpiry
(appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map {
complete((appKit.paymentInitiator ? sendPayment).mapTo[PaymentResult].map {
case s: PaymentSucceeded => s
case f: PaymentFailed => f.copy(failures = PaymentLifecycle.transformForUser(f.failures))
}
})
}
def checkpayment(paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Future[Boolean] = (paymentHash_opt, invoice_opt) match {
case (Some(ph), None) => (appKit.paymentHandler ? CheckPayment(ph)).mapTo[Boolean]
case (None, Some(invoice)) => (appKit.paymentHandler ? CheckPayment(invoice.paymentHash)).mapTo[Boolean]
case _ => throw IllegalApiParams("checkpayment", "Wrong params list, call 'help' to know more about it")
def checkpayment(paymentHash_opt: Option[BinaryData], invoice_opt: Option[PaymentRequest]): Route = (paymentHash_opt, invoice_opt) match {
case (Some(ph), None) => complete((appKit.paymentHandler ? CheckPayment(ph)).mapTo[Boolean])
case (None, Some(invoice)) => complete((appKit.paymentHandler ? CheckPayment(invoice.paymentHash)).mapTo[Boolean])
case _ => reject(UnknownParamsRejection("Wrong params for method 'checkpayment'"))
}
def audit(from_opt: Option[Long], to_opt: Option[Long]): Future[AuditResponse] = {
@ -366,5 +381,9 @@ trait NewService extends Directives with Logging with MetaService {
}
case class IllegalApiParams(apiMethod: String, msg: String = "Wrong params list, call 'help' to know more about it", thr: Option[Throwable] = None) extends RuntimeException(s"Error calling $apiMethod: $msg")
case object UnknownMethodRejection extends Rejection
case class UnknownParamsRejection(message: String) extends Rejection
case class RpcValidationRejection(message: String) extends Rejection
case class ExceptionRejection(message: String) extends Rejection
}