Skip to content

Commit 9ff2f83

Browse files
tompropm47t-bast
authored
Refactor and simplify API dsl (#1690)
Refactor the API handlers. Split handlers and directives in several files to make them more composable. Co-authored-by: Pierre-Marie Padiou <[email protected]> Co-authored-by: Bastien Teinturier <[email protected]>
1 parent ea8f940 commit 9ff2f83

22 files changed

+1037
-430
lines changed

eclair-core/eclair-cli

+12-4
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ and COMMAND is one of the available commands:
3333
- connect
3434
- disconnect
3535
- peers
36-
- nodes
3736
- audit
3837
3938
=== Channel ===
@@ -45,6 +44,8 @@ and COMMAND is one of the available commands:
4544
- allchannels
4645
- allupdates
4746
- channelstats
47+
48+
=== Fees ===
4849
- networkfees
4950
- updaterelayfee
5051
@@ -53,6 +54,7 @@ and COMMAND is one of the available commands:
5354
- findroutetonode
5455
- findroutebetweennodes
5556
- networkstats
57+
- nodes
5658
5759
=== Invoice ===
5860
- createinvoice
@@ -62,15 +64,21 @@ and COMMAND is one of the available commands:
6264
- parseinvoice
6365
6466
=== Payment ===
65-
- getnewaddress
6667
- usablebalances
67-
- onchainbalance
6868
- payinvoice
6969
- sendtonode
7070
- sendtoroute
71-
- sendonchain
7271
- getsentinfo
7372
- getreceivedinfo
73+
74+
=== Message ===
75+
- signmessage
76+
- verifymessage
77+
78+
=== OnChain ===
79+
- getnewaddress
80+
- sendonchain
81+
- onchainbalance
7482
- onchaintransactions
7583
7684
Examples

eclair-node/src/main/scala/fr/acinq/eclair/api/Service.scala

+26-340
Large diffs are not rendered by default.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2019 ACINQ SAS
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 fr.acinq.eclair.api.directives
18+
19+
import akka.http.scaladsl.server.Directive0
20+
import akka.http.scaladsl.server.directives.Credentials
21+
import fr.acinq.eclair.api.Service
22+
23+
import scala.concurrent.Future
24+
import scala.concurrent.duration.DurationInt
25+
26+
trait AuthDirective {
27+
this: Service with EclairDirectives =>
28+
29+
/**
30+
* A directive0 that passes whenever valid basic credentials are provided. We
31+
* are not interested in the extracted username.
32+
*
33+
* @return
34+
*/
35+
def authenticated: Directive0 = authenticateBasicAsync(realm = "Access restricted", userPassAuthenticator).tflatMap { _ => pass }
36+
37+
38+
private def userPassAuthenticator(credentials: Credentials): Future[Option[String]] = credentials match {
39+
case p@Credentials.Provided(id) if p.verify(password) => Future.successful(Some(id))
40+
case _ => akka.pattern.after(1 second, using = actorSystem.scheduler)(Future.successful(None))(actorSystem.dispatcher) // force a 1 sec pause to deter brute force
41+
}
42+
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright 2019 ACINQ SAS
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 fr.acinq.eclair.api.directives
18+
19+
import akka.http.scaladsl.model.HttpMethods.POST
20+
import akka.http.scaladsl.model.headers.CacheDirectives.{`max-age`, `no-store`, public}
21+
import akka.http.scaladsl.model.headers._
22+
import akka.http.scaladsl.server.Directive0
23+
import akka.http.scaladsl.server.Directives.respondWithDefaultHeaders
24+
25+
trait DefaultHeaders {
26+
27+
/**
28+
* Adds customHeaders to all http responses.
29+
*/
30+
def eclairHeaders:Directive0 = respondWithDefaultHeaders(customHeaders)
31+
32+
33+
private val customHeaders = `Access-Control-Allow-Headers`("Content-Type, Authorization") ::
34+
`Access-Control-Allow-Methods`(POST) ::
35+
`Cache-Control`(public, `no-store`, `max-age`(0)) :: Nil
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
* Copyright 2019 ACINQ SAS
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 fr.acinq.eclair.api.directives
18+
19+
import akka.http.scaladsl.server.{Directive0, Directive1, Directives}
20+
import akka.util.Timeout
21+
import fr.acinq.eclair.api.Service
22+
23+
import scala.concurrent.duration.DurationInt
24+
25+
class EclairDirectives extends Directives with TimeoutDirective with ErrorDirective with AuthDirective with DefaultHeaders with ExtraDirectives { this: Service =>
26+
27+
/**
28+
* Prepares inner routes to be exposed as public API with default headers, error handlers and basic authentication.
29+
*/
30+
private def securedHandler:Directive0 = eclairHeaders & handled & authenticated
31+
32+
/**
33+
* Provides a Timeout to the inner route either from request param or the default.
34+
*/
35+
private def standardHandler:Directive1[Timeout] = toStrictEntity(5 seconds) & withTimeout
36+
37+
/**
38+
* Handles POST requests with given simple path. The inner route is wrapped in a standard handler and provides a Timeout as parameter.
39+
*/
40+
def postRequest(p:String):Directive1[Timeout] = securedHandler & post & path(p) & standardHandler
41+
42+
/**
43+
* Handles GET requests with given simple path. The inner route is wrapped in a standard handler and provides a Timeout as parameter.
44+
*/
45+
def getRequest(p:String):Directive1[Timeout] = securedHandler & get & path(p) & standardHandler
46+
47+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/*
2+
* Copyright 2019 ACINQ SAS
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 fr.acinq.eclair.api.directives
18+
19+
import akka.http.scaladsl.model.{ContentTypes, HttpEntity, HttpResponse, StatusCodes}
20+
import akka.http.scaladsl.server.{Directive0, ExceptionHandler, RejectionHandler}
21+
import fr.acinq.eclair.api.Service
22+
23+
trait ErrorDirective {
24+
this: Service with EclairDirectives =>
25+
26+
/**
27+
* Handles API exceptions and rejections. Produces json formatted
28+
* error responses.
29+
*/
30+
def handled: Directive0 = handleExceptions(apiExceptionHandler) &
31+
handleRejections(apiRejectionHandler)
32+
33+
34+
import fr.acinq.eclair.api.serde.JsonSupport.{formats, marshaller, serialization}
35+
36+
private val apiExceptionHandler = ExceptionHandler {
37+
case t: IllegalArgumentException =>
38+
logger.error(s"API call failed with cause=${t.getMessage}", t)
39+
complete(StatusCodes.BadRequest, ErrorResponse(t.getMessage))
40+
case t: Throwable =>
41+
logger.error(s"API call failed with cause=${t.getMessage}", t)
42+
complete(StatusCodes.InternalServerError, ErrorResponse(t.getMessage))
43+
}
44+
45+
// map all the rejections to a JSON error object ErrorResponse
46+
private val apiRejectionHandler = RejectionHandler.default.mapRejectionResponse {
47+
case res@HttpResponse(_, _, ent: HttpEntity.Strict, _) =>
48+
res.withEntity(
49+
HttpEntity(ContentTypes.`application/json`, serialization.writePretty(ErrorResponse(ent.data.utf8String)))
50+
)
51+
}
52+
53+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2019 ACINQ SAS
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 fr.acinq.eclair.api.directives
18+
19+
case class ErrorResponse(error: String)

eclair-node/src/main/scala/fr/acinq/eclair/api/ExtraDirectives.scala eclair-node/src/main/scala/fr/acinq/eclair/api/directives/ExtraDirectives.scala

+16-14
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,19 @@
1414
* limitations under the License.
1515
*/
1616

17-
package fr.acinq.eclair.api
17+
package fr.acinq.eclair.api.directives
1818

19+
import fr.acinq.eclair.api.serde.JsonSupport.serialization
20+
import akka.http.scaladsl.common.{NameReceptacle, NameUnmarshallerReceptacle}
1921
import akka.http.scaladsl.marshalling.ToResponseMarshaller
2022
import akka.http.scaladsl.model.StatusCodes.NotFound
2123
import akka.http.scaladsl.model.{ContentTypes, HttpResponse}
2224
import akka.http.scaladsl.server.{Directive1, Directives, MalformedFormFieldRejection, Route}
2325
import fr.acinq.bitcoin.ByteVector32
2426
import fr.acinq.bitcoin.Crypto.PublicKey
2527
import fr.acinq.eclair.ApiTypes.ChannelIdentifier
26-
import fr.acinq.eclair.api.FormParamExtractors._
27-
import fr.acinq.eclair.api.JsonSupport._
28+
import fr.acinq.eclair.api.serde.FormParamExtractors._
29+
import fr.acinq.eclair.api.serde.JsonSupport._
2830
import fr.acinq.eclair.payment.PaymentRequest
2931
import fr.acinq.eclair.{MilliSatoshi, ShortChannelId}
3032

@@ -34,17 +36,17 @@ import scala.util.{Failure, Success}
3436
trait ExtraDirectives extends Directives {
3537

3638
// named and typed URL parameters used across several routes
37-
val shortChannelIdFormParam = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller)
38-
val shortChannelIdsFormParam = "shortChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller)
39-
val channelIdFormParam = "channelId".as[ByteVector32](sha256HashUnmarshaller)
40-
val channelIdsFormParam = "channelIds".as[List[ByteVector32]](sha256HashesUnmarshaller)
41-
val nodeIdFormParam = "nodeId".as[PublicKey]
42-
val nodeIdsFormParam = "nodeIds".as[List[PublicKey]](pubkeyListUnmarshaller)
43-
val paymentHashFormParam = "paymentHash".as[ByteVector32](sha256HashUnmarshaller)
44-
val fromFormParam = "from".as[Long]
45-
val toFormParam = "to".as[Long]
46-
val amountMsatFormParam = "amountMsat".as[MilliSatoshi]
47-
val invoiceFormParam = "invoice".as[PaymentRequest]
39+
val shortChannelIdFormParam: NameUnmarshallerReceptacle[ShortChannelId] = "shortChannelId".as[ShortChannelId](shortChannelIdUnmarshaller)
40+
val shortChannelIdsFormParam: NameUnmarshallerReceptacle[List[ShortChannelId]] = "shortChannelIds".as[List[ShortChannelId]](shortChannelIdsUnmarshaller)
41+
val channelIdFormParam: NameUnmarshallerReceptacle[ByteVector32] = "channelId".as[ByteVector32](sha256HashUnmarshaller)
42+
val channelIdsFormParam: NameUnmarshallerReceptacle[List[ByteVector32]] = "channelIds".as[List[ByteVector32]](sha256HashesUnmarshaller)
43+
val nodeIdFormParam: NameReceptacle[PublicKey] = "nodeId".as[PublicKey]
44+
val nodeIdsFormParam: NameUnmarshallerReceptacle[List[PublicKey]] = "nodeIds".as[List[PublicKey]](pubkeyListUnmarshaller)
45+
val paymentHashFormParam: NameUnmarshallerReceptacle[ByteVector32] = "paymentHash".as[ByteVector32](sha256HashUnmarshaller)
46+
val fromFormParam: NameReceptacle[Long] = "from".as[Long]
47+
val toFormParam: NameReceptacle[Long] = "to".as[Long]
48+
val amountMsatFormParam: NameReceptacle[MilliSatoshi] = "amountMsat".as[MilliSatoshi]
49+
val invoiceFormParam: NameReceptacle[PaymentRequest] = "invoice".as[PaymentRequest]
4850

4951
// custom directive to fail with HTTP 404 (and JSON response) if the element was not found
5052
def completeOrNotFound[T](fut: Future[Option[T]])(implicit marshaller: ToResponseMarshaller[T]): Route = onComplete(fut) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright 2019 ACINQ SAS
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 fr.acinq.eclair.api.directives
18+
19+
import akka.http.scaladsl.model.{ContentTypes, HttpRequest, HttpResponse, StatusCodes}
20+
import akka.http.scaladsl.server.{Directive0, Directive1, Directives}
21+
import akka.util.Timeout
22+
import fr.acinq.eclair.api.serde.FormParamExtractors._
23+
import fr.acinq.eclair.api.serde.JsonSupport._
24+
import fr.acinq.eclair.api.serde.JsonSupport
25+
26+
import scala.concurrent.duration.DurationInt
27+
28+
trait TimeoutDirective extends Directives {
29+
30+
import JsonSupport.{formats, serialization}
31+
32+
33+
/**
34+
* Extracts a given request timeout from an optional form field. Provides either the
35+
* extracted Timeout or a default Timeout to the inner route.
36+
*/
37+
def withTimeout:Directive1[Timeout] = extractTimeout.tflatMap { timeout =>
38+
withTimeoutRequest(timeout._1) & provide(timeout._1)
39+
}
40+
41+
private val timeoutResponse: HttpRequest => HttpResponse = { _ =>
42+
HttpResponse(StatusCodes.RequestTimeout).withEntity(
43+
ContentTypes.`application/json`, serialization.writePretty(ErrorResponse("request timed out"))
44+
)
45+
}
46+
47+
private def withTimeoutRequest(t: Timeout): Directive0 = withRequestTimeout(t.duration + 2.seconds) &
48+
withRequestTimeoutResponse(timeoutResponse)
49+
50+
private def extractTimeout: Directive1[Timeout] = formField("timeoutSeconds".as[Timeout].?).tflatMap { opt =>
51+
provide(opt._1.getOrElse(Timeout(30 seconds)))
52+
}
53+
54+
}

0 commit comments

Comments
 (0)