[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[libeufin] branch master updated: logging
From: |
gnunet |
Subject: |
[libeufin] branch master updated: logging |
Date: |
Mon, 28 Nov 2022 14:15:51 +0100 |
This is an automated email from the git hooks/post-receive script.
ms pushed a commit to branch master
in repository libeufin.
The following commit(s) were added to refs/heads/master by this push:
new bfd94d80 logging
bfd94d80 is described below
commit bfd94d8055b3b154a45744e8b9a289bce7bc38fa
Author: MS <ms@taler.net>
AuthorDate: Mon Nov 28 14:15:38 2022 +0100
logging
---
nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt | 2 +-
.../tech/libeufin/nexus/bankaccount/BankAccount.kt | 6 +-
.../tech/libeufin/nexus/ebics/EbicsClient.kt | 18 +++--
.../kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt | 4 +-
.../{SchedulingTest.kt => DownloadAndSubmit.kt} | 90 ++++++++++++----------
.../tech/libeufin/sandbox/EbicsProtocolBackend.kt | 50 +++++++-----
util/src/main/kotlin/Ebics.kt | 26 ++++++-
7 files changed, 118 insertions(+), 78 deletions(-)
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
index 52bd0eab..a5d46530 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/Main.kt
@@ -88,7 +88,7 @@ class Serve : CliktCommand("Run nexus HTTP server") {
}
}
-class ParseCamt : CliktCommand("Parse a camt file") {
+class ParseCamt : CliktCommand("Parse CAMT file, outputs JSON in libEufin
internal representation.") {
private val logLevel by option()
private val filename by argument("FILENAME", "File in CAMT format")
override fun run() {
diff --git
a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
index c3ac4313..b0365000 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/bankaccount/BankAccount.kt
@@ -149,7 +149,6 @@ data class CamtTransactionsCount(
fun processCamtMessage(
bankAccountId: String, camtDoc: Document, code: String
): CamtTransactionsCount {
- logger.info("processing CAMT message")
var newTransactions = 0
var downloadedTransactions = 0
transaction {
@@ -371,7 +370,10 @@ suspend fun fetchBankAccountTransactions(
val connectionName = conn.connectionId
}
}
- // abstracts over the connection type: ebics or others.
+ /**
+ * Collects transactions from the bank and stores the (camt)
+ * document into the database.
+ */
getConnectionPlugin(res.connectionType).fetchTransactions(
fetchSpec,
client,
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
index e5c78434..5516e88c 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsClient.kt
@@ -121,9 +121,9 @@ suspend fun doEbicsDownloadTransaction(
val transactionID =
initResponse.transactionID ?: throw NexusError(
HttpStatusCode.InternalServerError,
- "initial response must contain transaction ID"
+ "Initial response must contain transaction ID"
)
-
+ logger.debug("Bank acknowledges EBICS download initialization.
Transaction ID: $transactionID.")
val encryptionInfo = initResponse.dataEncryptionInfo
?: throw NexusError(HttpStatusCode.InternalServerError, "initial
response did not contain encryption info")
@@ -139,10 +139,10 @@ suspend fun doEbicsDownloadTransaction(
?: throw NexusError(HttpStatusCode.FailedDependency, "missing segment
number in EBICS download init response")
// Transfer phase
-
for (x in 2 .. numSegments) {
val transferReqStr =
createEbicsRequestForDownloadTransferPhase(subscriberDetails,
transactionID, x, numSegments)
+ logger.debug("EBICS download transfer phase of ${transactionID}:
sending segment $x")
val transferResponseStr =
client.postToBank(subscriberDetails.ebicsUrl, transferReqStr)
val transferResponse =
parseAndValidateEbicsResponse(subscriberDetails, transferResponseStr)
when (transferResponse.technicalReturnCode) {
@@ -171,6 +171,7 @@ suspend fun doEbicsDownloadTransaction(
"transfer response for download transaction does not contain
data transfer"
)
payloadChunks.add(transferOrderDataEncChunk)
+ logger.debug("Download transfer phase of ${transactionID}: bank
acknowledges $x")
}
val respPayload = decryptAndDecompressResponse(subscriberDetails,
encryptionInfo, payloadChunks)
@@ -193,10 +194,13 @@ suspend fun doEbicsDownloadTransaction(
)
}
}
+ logger.debug("Bank acknowledges EBICS download receipt. Transaction ID:
$transactionID.")
return EbicsDownloadSuccessResult(respPayload)
}
-
+/**
+ * Currently only 1-segment requests.
+ */
suspend fun doEbicsUploadTransaction(
client: HttpClient,
subscriberDetails: EbicsClientSubscriberDetails,
@@ -221,8 +225,7 @@ suspend fun doEbicsUploadTransaction(
HttpStatusCode.InternalServerError,
"init response must have transaction ID"
)
-
- logger.debug("INIT phase passed!")
+ logger.debug("Bank acknowledges EBICS upload initialization. Transaction
ID: $transactionID.")
/* now send actual payload */
val payload = createEbicsRequestForUploadTransferPhase(
@@ -231,14 +234,12 @@ suspend fun doEbicsUploadTransaction(
preparedUploadData,
0
)
-
val txRespStr = client.postToBank(
subscriberDetails.ebicsUrl,
payload
)
val txResp = parseAndValidateEbicsResponse(subscriberDetails, txRespStr)
-
when (txResp.technicalReturnCode) {
EbicsReturnCode.EBICS_OK -> {
}
@@ -248,6 +249,7 @@ suspend fun doEbicsUploadTransaction(
)
}
}
+ logger.debug("Bank acknowledges EBICS upload transfer. Transaction ID:
$transactionID")
}
suspend fun doEbicsHostVersionQuery(client: HttpClient, ebicsBaseUrl: String,
ebicsHostId: String): EbicsHevDetails {
diff --git a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
index 3d72fcb8..28c34b68 100644
--- a/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
+++ b/nexus/src/main/kotlin/tech/libeufin/nexus/ebics/EbicsNexus.kt
@@ -73,7 +73,7 @@ private data class EbicsFetchSpec(
fun storeCamt(bankConnectionId: String, camt: String, historyType: String) {
val camt53doc = XMLUtil.parseStringIntoDom(camt)
val msgId =
camt53doc.pickStringWithRootNs("/*[1]/*[1]/root:GrpHdr/root:MsgId")
- logger.info("msg id $msgId")
+ logger.info("camt document '$msgId' received.")
transaction {
val conn = NexusBankConnectionEntity.findByName(bankConnectionId)
if (conn == null) {
@@ -102,7 +102,6 @@ private suspend fun fetchEbicsC5x(
orderParams: EbicsOrderParams,
subscriberDetails: EbicsClientSubscriberDetails
) {
- logger.debug("Requesting $historyType")
val response = try {
doEbicsDownloadTransaction(
client,
@@ -529,6 +528,7 @@ class EbicsBankConnectionProtocol: BankConnectionProtocol {
messageId = paymentInitiation.messageId
)
)
+ logger.debug("Sending Pain.001:
${paymentInitiation.paymentInformationId}")
if (!XMLUtil.validateFromString(painMessage)) throw NexusError(
HttpStatusCode.InternalServerError, "Pain.001 message is
invalid."
)
diff --git a/nexus/src/test/kotlin/SchedulingTest.kt
b/nexus/src/test/kotlin/DownloadAndSubmit.kt
similarity index 62%
rename from nexus/src/test/kotlin/SchedulingTest.kt
rename to nexus/src/test/kotlin/DownloadAndSubmit.kt
index 3d592bbe..9a387523 100644
--- a/nexus/src/test/kotlin/SchedulingTest.kt
+++ b/nexus/src/test/kotlin/DownloadAndSubmit.kt
@@ -1,4 +1,6 @@
import io.ktor.application.*
+import io.ktor.client.*
+import io.ktor.client.request.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.request.*
@@ -6,17 +8,31 @@ import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.server.testing.*
import kotlinx.coroutines.runBlocking
+import org.jetbrains.exposed.sql.transactions.transaction
import org.junit.Ignore
import org.junit.Test
import org.w3c.dom.Document
import tech.libeufin.nexus.*
+import tech.libeufin.nexus.bankaccount.addPaymentInitiation
+import tech.libeufin.nexus.bankaccount.fetchBankAccountTransactions
+import tech.libeufin.nexus.ebics.EbicsBankConnectionProtocol
+import tech.libeufin.nexus.server.FetchLevel
+import tech.libeufin.nexus.server.FetchSpecAllJson
+import tech.libeufin.nexus.server.FetchSpecJson
+import tech.libeufin.nexus.server.Pain001Data
import tech.libeufin.sandbox.*
import tech.libeufin.util.*
import tech.libeufin.util.ebics_h004.EbicsRequest
import tech.libeufin.util.ebics_h004.EbicsResponse
import tech.libeufin.util.ebics_h004.EbicsTypes
+/**
+ * This source is NOT a test case -- as it uses no assertions --
+ * but merely a tool to download and submit payments to the bank
+ * via Nexus.
+ * /
+ */
/**
* Data to make the test server return for EBICS
* phases. Currently only init is supported.
@@ -76,64 +92,56 @@ fun getCustomEbicsServer(r: EbicsResponses, endpoint:
String = "/ebicsweb"): App
* and having had access to runTask and TaskSchedule, that
* are now 'private'.
*/
-@Ignore
+// @Ignore
class SchedulingTest {
/**
* Instruct the server to return invalid CAMT content.
*/
@Test
- fun inject() {
+ fun download() {
withNexusAndSandboxUser {
- val payload = """
- Invalid Camt Document
- """.trimIndent()
- withTestApplication(
- getCustomEbicsServer(EbicsResponses(payload))
- ) {
+ withTestApplication(sandboxApp) {
+ val conn = EbicsBankConnectionProtocol()
runBlocking {
- runTask(
+ conn.fetchTransactions(
+ fetchSpec = FetchSpecAllJson(
+ level = FetchLevel.REPORT,
+ "foo"
+ ),
client,
- TaskSchedule(
- 0L,
- "test-schedule",
- "fetch",
- "bank-account",
- "mock-bank-account",
- params =
"{\"level\":\"report\",\"rangeType\":\"all\"}"
- )
+ "foo",
+ "mock-bank-account"
)
}
}
}
}
- /**
- * Create two payments and asks for C52.
- */
+
@Test
- fun ordinary() {
- withNexusAndSandboxUser { // DB prep
- for (t in 1 .. 2) {
- wireTransfer(
- "bank",
- "foo",
- "default",
- "1HJX78AH7WAGBDJTCXJ4JKX022DBCHERA051KH7D3EC48X09G4RG",
- "TESTKUDOS:5",
- "xxx"
- )
- }
+ fun upload() {
+ withNexusAndSandboxUser {
withTestApplication(sandboxApp) {
+ val conn = EbicsBankConnectionProtocol()
runBlocking {
- runTask(
+ // Create Pain.001 to be submitted.
+ addPaymentInitiation(
+ Pain001Data(
+ creditorIban = getIban(),
+ creditorBic = "SANDBOXX",
+ creditorName = "Tester",
+ subject = "test payment",
+ sum = Amount(1),
+ currency = "TESTKUDOS"
+ ),
+ transaction {
+ NexusBankAccountEntity.findByName(
+ "mock-bank-account"
+ ) ?: throw Exception("Test failed")
+ }
+ )
+ conn.submitPaymentInitiation(
client,
- TaskSchedule(
- 0L,
- "test-schedule",
- "fetch",
- "bank-account",
- "mock-bank-account",
- params =
"{\"level\":\"report\",\"rangeType\":\"all\"}"
- )
+ 1L
)
}
}
diff --git
a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
index 528e1023..103b8759 100644
--- a/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
+++ b/sandbox/src/main/kotlin/tech/libeufin/sandbox/EbicsProtocolBackend.kt
@@ -544,17 +544,19 @@ private fun constructCamtResponse(
baseBalance = lastBalance
)
+ val camtData = buildCamtString(
+ type,
+ bankAccount.iban,
+ history,
+ balancePrcd = lastBalance,
+ balanceClbd = freshBalance
+ )
+ logger.debug("camt.052 document '${camtData.messageId}' generated.")
return listOf(
- buildCamtString(
- type,
- bankAccount.iban,
- history,
- balancePrcd = lastBalance,
- balanceClbd = freshBalance
- ).camtMessage
+ camtData.camtMessage
)
}
- SandboxAssert(type == 53, "Didn't catch unsupported Camt type")
+ SandboxAssert(type == 53, "Didn't catch unsupported CAMT type")
logger.debug("Finding C$type records")
/**
@@ -569,7 +571,7 @@ private fun constructCamtResponse(
* time range given as a function's parameter.
*/
if (dateRange != null) {
- logger.debug("Querying c$type with date range: $dateRange")
+ logger.debug("Querying C$type with date range: $dateRange")
BankAccountStatementEntity.find {
BankAccountStatementsTable.creationTime.between(
dateRange.first,
@@ -696,8 +698,8 @@ private fun parsePain001(paymentRequest: String):
PainParseResult {
* Process a payment request in the pain.001 format.
*/
private fun handleCct(paymentRequest: String) {
- logger.debug("Handling CCT")
val parseResult = parsePain001(paymentRequest)
+ logger.debug("Handling Pain.001: ${parseResult.pmtInfId}")
transaction(Connection.TRANSACTION_SERIALIZABLE, repetitionAttempts = 10) {
val maybeExist = BankAccountTransactionEntity.find {
BankAccountTransactionsTable.pmtInfId eq parseResult.pmtInfId
@@ -761,7 +763,6 @@ private fun handleCct(paymentRequest: String) {
* to the querying subscriber.
*/
private fun handleEbicsC52(requestContext: RequestContext): ByteArray {
- logger.debug("Handling C52 request")
// Ignoring any dateRange parameter. (FIXME)
val report = constructCamtResponse(52, requestContext.subscriber,
dateRange = null)
SandboxAssert(
@@ -776,8 +777,6 @@ private fun handleEbicsC52(requestContext: RequestContext):
ByteArray {
}
private fun handleEbicsC53(requestContext: RequestContext): ByteArray {
- logger.debug("Handling C53 request")
-
// Fetch date range.
val orderParams =
requestContext.requestObject.header.static.orderDetails?.orderParams // as
EbicsRequest.StandardOrderParams
val dateRange = if (orderParams != null) {
@@ -1094,7 +1093,6 @@ private fun handleEbicsHkd(requestContext:
RequestContext): ByteArray {
return str.toByteArray()
}
-
private data class RequestContext(
val ebicsHost: EbicsHostEntity,
val subscriber: EbicsSubscriberEntity,
@@ -1108,6 +1106,10 @@ private data class RequestContext(
val downloadTransaction: EbicsDownloadTransactionEntity?
)
+/**
+ * Get segmentation values and the EBICS transaction ID, before
+ * handing the response to 'createForDownloadTransferPhase()'.
+ */
private fun handleEbicsDownloadTransactionTransfer(requestContext:
RequestContext): EbicsResponse {
val segmentNumber =
requestContext.requestObject.header.mutable.segmentNumber?.value ?:
throw EbicsInvalidRequestError()
@@ -1128,7 +1130,7 @@ private fun
handleEbicsDownloadTransactionTransfer(requestContext: RequestContex
private fun handleEbicsDownloadTransactionInitialization(requestContext:
RequestContext): EbicsResponse {
val orderType =
requestContext.requestObject.header.static.orderDetails?.orderType ?:
throw EbicsInvalidRequestError()
- logger.debug("handling initialization for order type $orderType")
+ val nonce = requestContext.requestObject.header.static.nonce
val response = when (orderType) {
"HTD" -> handleEbicsHtd(requestContext)
"HKD" -> handleEbicsHkd(requestContext)
@@ -1139,10 +1141,14 @@ private fun
handleEbicsDownloadTransactionInitialization(requestContext: Request
else -> throw EbicsInvalidXmlError()
}
val transactionID = EbicsOrderUtil.generateTransactionId()
+ logger.debug(
+ "Handling download initialization for order type $orderType, " +
+ "nonce: ${nonce?.toHexString() ?: "not given"}, " +
+ "transaction ID: $transactionID"
+ )
val compressedResponse = DeflaterInputStream(response.inputStream()).use {
it.readAllBytes()
}
-
val enc = CryptoUtil.encryptEbicsE002(compressedResponse,
requestContext.clientEncPub)
val encodedResponse = Base64.getEncoder().encodeToString(enc.encryptedData)
@@ -1169,11 +1175,14 @@ private fun
handleEbicsDownloadTransactionInitialization(requestContext: Request
)
}
-
private fun handleEbicsUploadTransactionInitialization(requestContext:
RequestContext): EbicsResponse {
val orderType =
requestContext.requestObject.header.static.orderDetails?.orderType ?:
throw EbicsInvalidRequestError()
val transactionID = EbicsOrderUtil.generateTransactionId()
+ logger.debug("Handling upload initialization for order $orderType, " +
+ "transactionID $transactionID, nonce: " +
+ (requestContext.requestObject.header.static.nonce?.toHexString()
?: "not given")
+ )
val oidn = requestContext.subscriber.nextOrderID++
if (EbicsOrderUtil.checkOrderIDOverflow(oidn)) throw NotImplementedError()
val orderID = EbicsOrderUtil.computeOrderIDFromNumber(oidn)
@@ -1197,7 +1206,6 @@ private fun
handleEbicsUploadTransactionInitialization(requestContext: RequestCo
val plainSigData =
InflaterInputStream(decryptedSignatureData.inputStream()).use {
it.readAllBytes()
}
- logger.debug("creating upload transaction for transactionID
$transactionID")
EbicsUploadTransactionEntity.new(transactionID) {
this.host = requestContext.ebicsHost
this.subscriber = requestContext.subscriber
@@ -1209,7 +1217,7 @@ private fun
handleEbicsUploadTransactionInitialization(requestContext: RequestCo
}
val sigObj =
XMLUtil.convertStringToJaxb<UserSignatureData>(plainSigData.toString(Charsets.UTF_8))
for (sig in sigObj.value.orderSignatureList ?: listOf()) {
- logger.debug("inserting order signature for orderID $orderID and
orderType $orderType")
+ logger.debug("inserting order signature for orderID $orderID, order
type $orderType, transaction '$transactionID'")
EbicsOrderSignatureEntity.new {
this.orderID = orderID
this.orderType = orderType
@@ -1238,7 +1246,6 @@ private fun
handleEbicsUploadTransactionTransmission(requestContext: RequestCont
)
val unzippedData =
InflaterInputStream(zippedData.inputStream()).use {
it.readAllBytes() }
- // logger.debug("got upload data:
${unzippedData.toString(Charsets.UTF_8)}")
val sigs = EbicsOrderSignatureEntity.find {
(EbicsOrderSignaturesTable.orderID eq uploadTransaction.orderID)
and
@@ -1415,9 +1422,12 @@ suspend fun ApplicationCall.ebicsweb() {
requestObject.header.static.transactionID ?: throw
EbicsInvalidRequestError()
if (requestContext.downloadTransaction == null)
throw EbicsInvalidRequestError()
+ logger.debug("Handling download receipt for EBICS
transaction: " +
+ requestTransactionID)
val receiptCode =
requestObject.body.transferReceipt?.receiptCode ?:
throw EbicsInvalidRequestError()
EbicsResponse.createForDownloadReceiptPhase(requestTransactionID, receiptCode
== 0)
+
}
}
signEbicsResponse(ebicsResponse, requestContext.hostAuthPriv)
diff --git a/util/src/main/kotlin/Ebics.kt b/util/src/main/kotlin/Ebics.kt
index 5165e6df..ae13c72e 100644
--- a/util/src/main/kotlin/Ebics.kt
+++ b/util/src/main/kotlin/Ebics.kt
@@ -156,7 +156,6 @@ private fun signOrder(
return userSignatureData
}
-
fun createEbicsRequestForDownloadReceipt(
subscriberDetails: EbicsClientSubscriberDetails,
transactionID: String
@@ -232,11 +231,12 @@ fun createEbicsRequestForUploadInitialization(
orderParams: EbicsOrderParams,
preparedUploadData: PreparedUploadData
): String {
+ val nonce = getNonce(128)
val req = EbicsRequest.createForUploadInitializationPhase(
preparedUploadData.transactionKey,
preparedUploadData.userSignatureDataEncrypted,
subscriberDetails.hostId,
- getNonce(128),
+ nonce,
subscriberDetails.partnerId,
subscriberDetails.userId,
DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
@@ -246,6 +246,15 @@ fun createEbicsRequestForUploadInitialization(
orderType,
makeOrderParams(orderParams)
)
+ /**
+ * FIXME: this log should be made by the caller.
+ * That way, all the EBICS transaction steps would be logged in only one
function,
+ * as opposed to have them spread through the helpers here. This function
+ * returning a string blocks now, since the caller should parse and
stringify
+ * again the message, only to get its nonce.
+ */
+ logger.debug("Created EBICS $orderType document for upload
initialization," +
+ " nonce: ${nonce.toHexString()}")
val doc = XMLUtil.convertJaxbToDocument(req)
XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv)
return XMLUtil.convertDomToString(doc)
@@ -257,11 +266,12 @@ fun createEbicsRequestForDownloadInitialization(
orderType: String,
orderParams: EbicsOrderParams
): String {
+ val nonce = getNonce(128)
val req = EbicsRequest.createForDownloadInitializationPhase(
subscriberDetails.userId,
subscriberDetails.partnerId,
subscriberDetails.hostId,
- getNonce(128),
+ nonce,
DatatypeFactory.newInstance().newXMLGregorianCalendar(GregorianCalendar()),
subscriberDetails.bankEncPub ?: throw EbicsProtocolError(
HttpStatusCode.BadRequest,
@@ -274,6 +284,15 @@ fun createEbicsRequestForDownloadInitialization(
orderType,
makeOrderParams(orderParams)
)
+ /**
+ * FIXME: this log should be made by the caller.
+ * That way, all the EBICS transaction steps would be logged in only one
function,
+ * as opposed to have them spread through the helpers here. This function
+ * returning a string blocks now, since the caller should parse and
stringify
+ * again the message, only to get its nonce.
+ */
+ logger.debug("Created EBICS $orderType document for download
initialization," +
+ " nonce: ${nonce.toHexString()}")
val doc = XMLUtil.convertJaxbToDocument(req)
XMLUtil.signEbicsDocument(doc, subscriberDetails.customerAuthPriv)
return XMLUtil.convertDomToString(doc)
@@ -296,7 +315,6 @@ fun createEbicsRequestForDownloadTransferPhase(
return XMLUtil.convertDomToString(doc)
}
-
fun createEbicsRequestForUploadTransferPhase(
subscriberDetails: EbicsClientSubscriberDetails,
transactionID: String,
--
To stop receiving notification emails like this one, please contact
gnunet@gnunet.org.
[Prev in Thread] |
Current Thread |
[Next in Thread] |
- [libeufin] branch master updated: logging,
gnunet <=