commit 7cc650a542901f9e93de322201b3fcc8f8758e44
parent 12afd0f86be5c4c028107eb3633cc462c1c63544
Author: MS <ms@taler.net>
Date: Fri, 15 Sep 2023 14:10:12 +0200
Remove the UtilError type.
This type used to interfere with Web applications
responses. This change makes Util more HTTP agnostic,
letting Web apps decide how to handle their errors.
Additionally, this change allows testing Util helpers
without checking the type of the thrown exception.
Diffstat:
17 files changed, 111 insertions(+), 585 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,4 +1,4 @@
-.idea
+.idea/
/nexus/bin/
/sandbox/bin/
/util/bin/
diff --git a/bank/src/main/kotlin/tech/libeufin/bank/Main.kt b/bank/src/main/kotlin/tech/libeufin/bank/Main.kt
@@ -60,6 +60,7 @@ const val GENERIC_UNDEFINED = -1 // Filler for ECs that don't exist yet.
// TYPES
+// FIXME: double-check the enum numeric value.
enum class FracDigits(howMany: Int) {
TWO(2),
EIGHT(8)
@@ -150,8 +151,14 @@ class LibeufinBankException(
*/
fun ApplicationCall.myAuth(requiredScope: TokenScope): Customer? {
// Extracting the Authorization header.
- val header = getAuthorizationRawHeader(this.request)
- val authDetails = getAuthorizationDetails(header)
+ val header = getAuthorizationRawHeader(this.request) ?: throw badRequest(
+ "Authorization header not found.",
+ GENERIC_HTTP_HEADERS_MALFORMED
+ )
+ val authDetails = getAuthorizationDetails(header) ?: throw badRequest(
+ "Authorization is invalid.",
+ GENERIC_HTTP_HEADERS_MALFORMED
+ )
return when (authDetails.scheme) {
"Basic" -> doBasicAuth(authDetails.content)
"Bearer" -> doTokenAuth(authDetails.content, requiredScope)
diff --git a/util/src/main/kotlin/CamtJsonMapping.kt b/util/src/main/kotlin/CamtJsonMapping.kt
@@ -8,7 +8,6 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize
import com.fasterxml.jackson.databind.annotation.JsonSerialize
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.ser.std.StdSerializer
-import tech.libeufin.util.internalServerError
enum class CreditDebitIndicator {
DBIT,
@@ -54,10 +53,6 @@ data class CurrencyAmount(
val value: String
)
-fun CurrencyAmount.toPlainString(): String {
- return "${this.currency}:${this.value}"
-}
-
@JsonInclude(JsonInclude.Include.NON_NULL)
data class CashAccount(
val name: String?,
@@ -285,14 +280,14 @@ data class CamtBankAccountEntry(
val batches: List<Batch>?
) {
// Checks that the given list contains only one element and returns it.
- private fun <T>checkAndGetSingleton(maybeTxs: List<T>?): T {
- if (maybeTxs == null || maybeTxs.size > 1) throw internalServerError(
- "Only a singleton transaction is " +
- "allowed inside ${this.javaClass}."
- )
+ private fun <T>checkAndGetSingleton(maybeTxs: List<T>?): T? {
+ if (maybeTxs == null || maybeTxs.size > 1) {
+ logger.error("Only a singleton transaction is allowed inside ${this.javaClass}.")
+ return null
+ }
return maybeTxs[0]
}
- private fun getSingletonTxDtls(): TransactionDetails {
+ private fun getSingletonTxDtls(): TransactionDetails? {
/**
* Types breakdown until the meaningful payment information is reached.
*
@@ -315,9 +310,9 @@ data class CamtBankAccountEntry(
* type, that is also -- so far -- required to be a singleton
* inside Batch.
*/
- val batch: Batch = checkAndGetSingleton(this.batches)
+ val batch: Batch = checkAndGetSingleton(this.batches) ?: return null
val batchTransactions = batch.batchTransactions
- val tx: BatchTransaction = checkAndGetSingleton(batchTransactions)
+ val tx: BatchTransaction = checkAndGetSingleton(batchTransactions) ?: return null
val details: TransactionDetails = tx.details
return details
}
@@ -329,18 +324,8 @@ data class CamtBankAccountEntry(
* and never participate in the application logic.
*/
@JsonIgnore
- fun getSingletonSubject(): String {
- val maybeSubject = getSingletonTxDtls().unstructuredRemittanceInformation
- if (maybeSubject == null) {
- throw internalServerError(
- "The parser let in a transaction without subject" +
- ", acctSvcrRef: ${this.getSingletonAcctSvcrRef()}."
- )
- }
+ fun getSingletonSubject(): String? {
+ val maybeSubject = getSingletonTxDtls()?.unstructuredRemittanceInformation ?: return null
return maybeSubject
}
- @JsonIgnore
- fun getSingletonAcctSvcrRef(): String? {
- return getSingletonTxDtls().accountServicerRef
- }
}
\ No newline at end of file
diff --git a/util/src/main/kotlin/Config.kt b/util/src/main/kotlin/Config.kt
@@ -3,11 +3,8 @@ package tech.libeufin.util
import ch.qos.logback.classic.Level
import ch.qos.logback.classic.LoggerContext
import ch.qos.logback.core.util.Loader
-import io.ktor.server.application.*
import io.ktor.util.*
import org.slf4j.LoggerFactory
-import printLnErr
-import kotlin.system.exitProcess
/**
* Putting those values into the 'attributes' container because they
@@ -42,39 +39,4 @@ fun setLogLevel(logLevel: String?) {
}
}
}
-}
-
-/**
- * Retun the attribute, or throw 500 Internal server error.
- */
-fun <T : Any>ApplicationCall.ensureAttribute(key: AttributeKey<T>): T {
- if (!this.attributes.contains(key)) {
- println("Error: attribute $key not found along the call.")
- throw internalServerError("Attribute $key not found along the call.")
- }
- return this.attributes[key]
-}
-
-fun getValueFromEnv(varName: String): String? {
- val ret = System.getenv(varName)
- if (ret.isNullOrBlank() or ret.isNullOrEmpty()) {
- println("WARNING, $varName was not found in the environment. Will stay unknown")
- return null
- }
- return ret
-}
-
-// Gets the DB connection string from env, or fail if not found.
-fun getDbConnFromEnv(varName: String): String {
- val dbConnStr = System.getenv(varName)
- if (dbConnStr.isNullOrBlank() or dbConnStr.isNullOrEmpty()) {
- printLnErr("\nError: DB connection string undefined in the env variable $varName.")
- printLnErr("\nThe following two examples are valid connection strings:")
- printLnErr("\npostgres:///libeufindb")
- printLnErr("postgresql://localhost:5432/libeufindb?user=Foo&password=secret\n")
- exitProcess(1)
- }
- return dbConnStr
-}
-
-fun getCurrentUser(): String = System.getProperty("user.name")
-\ No newline at end of file
+}
+\ No newline at end of file
diff --git a/util/src/main/kotlin/CryptoUtil.kt b/util/src/main/kotlin/CryptoUtil.kt
@@ -19,8 +19,6 @@
package tech.libeufin.util
-import UtilError
-import io.ktor.http.*
import net.taler.wallet.crypto.Base32Crockford
import org.bouncycastle.jce.provider.BouncyCastleProvider
import java.io.ByteArrayOutputStream
@@ -310,16 +308,6 @@ object CryptoUtil {
return "sha256-salted\$$salt\$$pwh"
}
- // Throws error when credentials don't match. Only returns in case of success.
- fun checkPwOrThrow(pw: String, storedPwHash: String): Boolean {
- if(!this.checkpw(pw, storedPwHash)) throw UtilError(
- HttpStatusCode.Unauthorized,
- "Credentials did not match",
- LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
- )
- return true
- }
-
fun checkpw(pw: String, storedPwHash: String): Boolean {
val components = storedPwHash.split('$')
if (components.size < 2) {
diff --git a/util/src/main/kotlin/DB.kt b/util/src/main/kotlin/DB.kt
@@ -31,12 +31,10 @@ import org.jetbrains.exposed.sql.transactions.transaction
import org.postgresql.jdbc.PgConnection
import java.net.URI
-fun Transaction.isPostgres(): Boolean {
- return this.db.vendor == "postgresql"
-}
+fun getCurrentUser(): String = System.getProperty("user.name")
fun isPostgres(): Boolean {
- val db = TransactionManager.defaultDatabase ?: throw internalServerError(
+ val db = TransactionManager.defaultDatabase ?: throw Exception(
"Could not find the default database, can't check if that's Postgres."
)
return db.vendor == "postgresql"
@@ -93,7 +91,7 @@ fun Transaction.postgresNotify(
if (payload != null) {
val argEnc = Base32Crockford.encode(payload.toByteArray())
if (payload.toByteArray().size > 8000)
- throw internalServerError(
+ throw Exception(
"DB notification on channel $channel used >8000 bytes payload '$payload'"
)
this.exec("NOTIFY $channel, '$argEnc'")
@@ -118,7 +116,7 @@ fun Transaction.postgresNotify(
* delivery more reliable.
*/
class PostgresListenHandle(val channelName: String) {
- private val db = TransactionManager.defaultDatabase ?: throw internalServerError(
+ private val db = TransactionManager.defaultDatabase ?: throw Exception(
"Could not find the default database, won't get Postgres notifications."
)
private val conn = db.connector().connection as PgConnection
@@ -165,7 +163,7 @@ class PostgresListenHandle(val channelName: String) {
for (n in maybeNotifications) {
if (n.name.lowercase() != channelName.lowercase()) {
conn.close() // always close on error, without the optional check.
- throw internalServerError("Channel $channelName got notified from ${n.name}!")
+ throw Exception("Channel $channelName got notified from ${n.name}!")
}
}
logger.debug("Found DB notifications on channel $channelName")
@@ -231,7 +229,7 @@ fun getDatabaseName(): String {
maybe_db_name = oneLineRes.getString("database_name")
}
}
- return maybe_db_name ?: throw internalServerError("Could not find current DB name")
+ return maybe_db_name ?: throw Exception("Could not find current DB name")
}
/**
@@ -250,7 +248,7 @@ fun connectWithSchema(jdbcConn: String, schemaName: String? = null) {
try { transaction { this.db.name } }
catch (e: Throwable) {
logger.error("Test query failed: ${e.message}")
- throw internalServerError("Failed connection to: $jdbcConn")
+ throw Exception("Failed connection to: $jdbcConn")
}
}
@@ -266,7 +264,7 @@ fun getJdbcConnectionFromPg(pgConn: String): String {
fun _getJdbcConnectionFromPg(pgConn: String): String {
if (!pgConn.startsWith("postgresql://") && !pgConn.startsWith("postgres://")) {
logger.info("Not a Postgres connection string: $pgConn")
- throw internalServerError("Not a Postgres connection string: $pgConn")
+ throw Exception("Not a Postgres connection string: $pgConn")
}
var maybeUnixSocket = false
val parsed = URI(pgConn)
@@ -293,10 +291,10 @@ fun _getJdbcConnectionFromPg(pgConn: String): String {
// Check whether the Unix domain socket location was given non-standard.
val socketLocation = hostAsParam ?: "/var/run/postgresql/.s.PGSQL.5432"
if (!socketLocation.startsWith('/')) {
- throw internalServerError("PG connection wants Unix domain socket, but non-null host doesn't start with slash")
+ throw Exception("PG connection wants Unix domain socket, but non-null host doesn't start with slash")
}
return "jdbc:postgresql://localhost${parsed.path}?user=$pgUser&socketFactory=org.newsclub.net.unix." +
"AFUNIXSocketFactory\$FactoryArg&socketFactoryArg=$socketLocation"
}
return "jdbc:$pgConn"
-}
-\ No newline at end of file
+}
diff --git a/util/src/main/kotlin/Errors.kt b/util/src/main/kotlin/Errors.kt
@@ -25,14 +25,6 @@ import org.slf4j.LoggerFactory
*/
val logger: Logger = LoggerFactory.getLogger("tech.libeufin.util")
-
-open class UtilError(
- val statusCode: HttpStatusCode,
- val reason: String,
- val ec: LibeufinErrorCode? = null
-) :
- Exception("$reason (HTTP status $statusCode)")
-
/**
* Helper function that wraps throwable code and
* (1) prints the error message and (2) terminates
@@ -53,5 +45,4 @@ fun execThrowableOrTerminate(func: () -> Unit) {
fun printLnErr(errorMessage: String) {
System.err.println(errorMessage)
-
}
\ No newline at end of file
diff --git a/util/src/main/kotlin/HTTP.kt b/util/src/main/kotlin/HTTP.kt
@@ -1,6 +1,5 @@
package tech.libeufin.util
-import UtilError
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.request.*
@@ -8,113 +7,31 @@ import io.ktor.server.response.*
import io.ktor.server.util.*
import io.ktor.util.*
import logger
-import java.net.URLDecoder
-fun unauthorized(msg: String): UtilError {
- return UtilError(
- HttpStatusCode.Unauthorized,
- msg,
- LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
- )
-}
-
-fun notFound(msg: String): UtilError {
- return UtilError(
- HttpStatusCode.NotFound,
- msg,
- LibeufinErrorCode.LIBEUFIN_EC_NONE
- )
-}
-
-fun badGateway(msg: String): UtilError {
- return UtilError(
- HttpStatusCode.BadGateway,
- msg,
- LibeufinErrorCode.LIBEUFIN_EC_NONE
- )
-}
-
-/**
- * Returns the token (including the 'secret-token:' prefix)
- * from an Authorization header. Throws exception on malformations
- * Note, the token gets URL-decoded before being returned.
- */
-fun extractToken(authHeader: String): String {
- val headerSplit = authHeader.split(" ", limit = 2)
- if (headerSplit.elementAtOrNull(0) != "Bearer") throw unauthorized("Authorization header does not start with 'Bearer'")
- val token = headerSplit.elementAtOrNull(1)
- if (token == null) throw unauthorized("Authorization header did not have the token")
- val tokenSplit = token.split(":", limit = 2)
- if (tokenSplit.elementAtOrNull(0) != "secret-token")
- throw unauthorized("Token lacks the 'secret-token:' prefix, see RFC 8959")
- val maybeToken = tokenSplit.elementAtOrNull(1)
- if(maybeToken == null || maybeToken == "")
- throw unauthorized("Actual token missing after the 'secret-token:' prefix")
- return "${tokenSplit[0]}:${URLDecoder.decode(tokenSplit[1], Charsets.UTF_8)}"
-}
-
-fun forbidden(msg: String): UtilError {
- return UtilError(
- HttpStatusCode.Forbidden,
- msg,
- ec = LibeufinErrorCode.LIBEUFIN_EC_NONE
- )
-}
-
-fun nullConfigValueError(
- configKey: String,
- demobankName: String = "default"
-): Throwable {
- return internalServerError("Configuration value for '$configKey' at demobank '$demobankName' is null.")
-}
-
-fun internalServerError(
- reason: String,
- libeufinErrorCode: LibeufinErrorCode? = LibeufinErrorCode.LIBEUFIN_EC_NONE
-): UtilError {
- return UtilError(
- HttpStatusCode.InternalServerError,
- reason,
- ec = libeufinErrorCode
- )
-}
-
-fun badRequest(msg: String): UtilError {
- return UtilError(
- HttpStatusCode.BadRequest,
- msg,
- ec = LibeufinErrorCode.LIBEUFIN_EC_NONE
- )
-}
-
-fun conflict(msg: String): UtilError {
- return UtilError(
- HttpStatusCode.Conflict,
- msg,
- ec = LibeufinErrorCode.LIBEUFIN_EC_NONE
- )
-}
-
-/**
- * Get the base URL of a request; handles proxied case.
- */
-fun ApplicationRequest.getBaseUrl(): String {
+// Get the base URL of a request; handles proxied case.
+fun ApplicationRequest.getBaseUrl(): String? {
return if (this.headers.contains("X-Forwarded-Host")) {
logger.info("Building X-Forwarded- base URL")
-
// FIXME: should tolerate a missing X-Forwarded-Prefix.
var prefix: String = this.headers["X-Forwarded-Prefix"]
- ?: throw internalServerError("Reverse proxy did not define X-Forwarded-Prefix")
+ ?: run {
+ logger.error("Reverse proxy did not define X-Forwarded-Prefix")
+ return null
+ }
if (!prefix.endsWith("/"))
prefix += "/"
URLBuilder(
protocol = URLProtocol(
- name = this.headers.get("X-Forwarded-Proto") ?: throw internalServerError("Reverse proxy did not define X-Forwarded-Proto"),
+ name = this.headers.get("X-Forwarded-Proto") ?: run {
+ logger.error("Reverse proxy did not define X-Forwarded-Proto")
+ return null
+ },
defaultPort = -1 // Port must be specified with X-Forwarded-Host.
),
- host = this.headers.get("X-Forwarded-Host") ?: throw internalServerError(
- "Reverse proxy did not define X-Forwarded-Host"
- ),
+ host = this.headers.get("X-Forwarded-Host") ?: run {
+ logger.error("Reverse proxy did not define X-Forwarded-Host")
+ return null
+ }
).apply {
encodedPath = prefix
// Gets dropped otherwise.
@@ -133,43 +50,22 @@ fun ApplicationRequest.getBaseUrl(): String {
* Get the URI (path's) component or throw Internal server error.
* @param component the name of the URI component to return.
*/
-fun ApplicationCall.expectUriComponent(name: String): String {
+fun ApplicationCall.expectUriComponent(name: String): String? {
val ret: String? = this.parameters[name]
- if (ret == null) throw badRequest("Component $name not found in URI")
- return ret
-}
-
-/**
- * Throw "unauthorized" if the request is not
- * authenticated by "admin", silently return otherwise.
- *
- * @param username who made the request.
- */
-fun expectAdmin(username: String?) {
- if (username == null) {
- logger.info("Skipping 'admin' authentication for tests.")
- return
+ if (ret == null) {
+ logger.error("Component $name not found in URI")
+ return null
}
- if (username != "admin") throw unauthorized("Only admin allowed: $username is not.")
-}
-
-fun getHTTPBasicAuthCredentials(request: io.ktor.server.request.ApplicationRequest): Pair<String, String> {
- val authHeader = getAuthorizationRawHeader(request)
- return extractUserAndPassword(authHeader)
+ return ret
}
-// Extracts the Authorization:-header line and throws error if not found.
-fun getAuthorizationRawHeader(request: ApplicationRequest): String {
+// Extracts the Authorization:-header line, or returns null if not found.
+fun getAuthorizationRawHeader(request: ApplicationRequest): String? {
val authorization = request.headers["Authorization"]
- return authorization ?: throw badRequest("Authorization header not found")
-}
-
-// Builds the Authorization:-header value, given the credentials.
-fun buildBasicAuthLine(username: String, password: String): String {
- val ret = "Basic "
- val cred = "$username:$password"
- val enc = bytesToBase64(cred.toByteArray(Charsets.UTF_8))
- return ret+enc
+ return authorization ?: run {
+ logger.error("Authorization header not found")
+ return null
+ }
}
/**
@@ -181,64 +77,28 @@ data class AuthorizationDetails(
val scheme: String,
val content: String
)
-// Returns the authorization scheme mentioned in the Auth header.
-fun getAuthorizationDetails(authorizationHeader: String): AuthorizationDetails {
+// Returns the authorization scheme mentioned in the Auth header,
+// or null if that could not be found.
+fun getAuthorizationDetails(authorizationHeader: String): AuthorizationDetails? {
val split = authorizationHeader.split(" ")
- if (split.isEmpty()) throw badRequest("malformed Authorization header: contains no space")
- if (split.size != 2) throw badRequest("malformed Authorization header: contains more than one space")
- return AuthorizationDetails(scheme = split[0], content = split[1])
-}
-
-/**
- * This helper function parses a Authorization:-header line, decode the credentials
- * and returns a pair made of username and hashed (sha256) password. The hashed value
- * will then be compared with the one kept into the database.
- */
-fun extractUserAndPassword(authorizationHeader: String): Pair<String, String> {
- val (username, password) = try {
- // FIXME/note: line below doesn't check for "Basic" presence.
- val split = authorizationHeader.split(" ")
- val plainUserAndPass = String(base64ToBytes(split[1]), Charsets.UTF_8)
- val ret = plainUserAndPass.split(":", limit = 2)
- if (ret.size < 2) throw java.lang.Exception(
- "HTTP Basic auth line does not contain username and password"
- )
- ret
- } catch (e: Exception) {
- throw UtilError(
- HttpStatusCode.BadRequest,
- "invalid Authorization header received: ${e.message}",
- LibeufinErrorCode.LIBEUFIN_EC_AUTHENTICATION_FAILED
- )
- }
- return Pair(username, password)
-}
-
-fun expectInt(uriParam: String): Int {
- return try { Integer.decode(uriParam) }
- catch (e: Exception) {
- logger.error(e.message)
- throw badRequest("'$uriParam' is not Int")
+ if (split.isEmpty()) {
+ logger.error("malformed Authorization header: contains no space")
+ return null
}
-}
-fun expectLong(uriParam: String): Long {
- return try { uriParam.toLong() }
- catch (e: Exception) {
- logger.error(e.message)
- throw badRequest("'$uriParam' is not Long")
+ if (split.size != 2) {
+ logger.error("malformed Authorization header: contains more than one space")
+ return null
}
+ return AuthorizationDetails(scheme = split[0], content = split[1])
}
-// Returns null, or tries to convert the parameter to type T.
-// Throws Bad Request, if the conversion could not be done.
+// Gets a long from the URI param named 'uriParamName',
+// or null if that is not found.
fun ApplicationCall.maybeLong(uriParamName: String): Long? {
val maybeParam = this.parameters[uriParamName] ?: return null
return try { maybeParam.toLong() }
catch (e: Exception) {
- throw badRequest("Could not convert '$uriParamName' to Long")
+ logger.error("Could not convert '$uriParamName' to Long")
+ return null
}
-}
-
-// Join base URL and path ensuring one (and only one) slash in between.
-fun joinUrl(baseUrl: String, path: String): String =
- baseUrl.dropLastWhile { it == '/' } + '/' + path.dropWhile { it == '/' }
-\ No newline at end of file
+}
+\ No newline at end of file
diff --git a/util/src/main/kotlin/JSON.kt b/util/src/main/kotlin/JSON.kt
@@ -22,35 +22,6 @@ package tech.libeufin.util
enum class XLibeufinBankDirection(val direction: String) {
DEBIT("debit"),
CREDIT("credit");
- companion object {
- fun parseXLibeufinDirection(direction: String): XLibeufinBankDirection {
- return when(direction) {
- "credit" -> CREDIT
- "debit" -> DEBIT
- else -> throw internalServerError(
- "Cannot extract ${this::class.java.typeName}' instance from value: $direction'"
- )
- }
- }
-
- /**
- * Sandbox uses _some_ CaMt terminology even for its internal
- * data model. This function helps to bridge such CaMt terminology
- * to the Sandbox simplified JSON format (XLibeufinBankTransaction).
- *
- * Ideally, the terminology should be made more abstract to serve
- * both (and probably more) data formats.
- */
- fun convertCamtDirectionToXLibeufin(camtDirection: String): XLibeufinBankDirection {
- return when(camtDirection) {
- "CRDT" -> CREDIT
- "DBIT" -> DEBIT
- else -> throw internalServerError(
- "Cannot extract ${this::class.java.typeName}' instance from value: $camtDirection'"
- )
- }
- }
- }
fun exportAsCamtDirection(): String =
when(this) {
CREDIT -> "CRDT"
diff --git a/util/src/main/kotlin/Payto.kt b/util/src/main/kotlin/Payto.kt
@@ -1,11 +1,10 @@
package tech.libeufin.util
-import UtilError
import io.ktor.http.*
+import logger
import java.net.URI
import java.net.URLDecoder
import java.net.URLEncoder
-import javax.security.auth.Subject
// Payto information.
data class Payto(
@@ -17,10 +16,9 @@ data class Payto(
val message: String?,
val amount: String?
)
-class InvalidPaytoError(msg: String) : UtilError(HttpStatusCode.BadRequest, msg)
// Return the value of query string parameter 'name', or null if not found.
-// 'params' is the a list of key-value elements of all the query parameters found in the URI.
+// 'params' is the list of key-value elements of all the query parameters found in the URI.
private fun getQueryParamOrNull(name: String, params: List<Pair<String, String>>?): String? {
if (params == null) return null
return params.firstNotNullOfOrNull { pair ->
@@ -28,30 +26,37 @@ private fun getQueryParamOrNull(name: String, params: List<Pair<String, String>>
}
}
-fun parsePayto(payto: String): Payto {
+// Parses a Payto URI, returning null if the input is invalid.
+fun parsePayto(payto: String): Payto? {
/**
* This check is due because URIs having a "payto:" prefix without
* slashes are correctly parsed by the Java 'URI' class. 'mailto'
* for example lacks the double-slash part.
*/
- if (!payto.startsWith("payto://"))
- throw InvalidPaytoError("Invalid payto URI: $payto")
+ if (!payto.startsWith("payto://")) {
+ logger.error("Invalid payto URI: $payto")
+ return null
+ }
val javaParsedUri = try {
URI(payto)
} catch (e: java.lang.Exception) {
- throw InvalidPaytoError("'${payto}' is not a valid URI")
+ logger.error("'${payto}' is not a valid URI")
+ return null
}
if (javaParsedUri.scheme != "payto") {
- throw InvalidPaytoError("'${payto}' is not payto")
+ logger.error("'${payto}' is not payto")
+ return null
}
val wireMethod = javaParsedUri.host
if (wireMethod != "iban") {
- throw InvalidPaytoError("Only 'iban' is supported, not '$wireMethod'")
+ logger.error("Only 'iban' is supported, not '$wireMethod'")
+ return null
}
val splitPath = javaParsedUri.path.split("/").filter { it.isNotEmpty() }
if (splitPath.size > 2) {
- throw InvalidPaytoError("too many path segments in iban payto URI: $payto")
+ logger.error("too many path segments in iban payto URI: $payto")
+ return null
}
val (iban, bic) = if (splitPath.size == 1) {
Pair(splitPath[0], null)
@@ -61,7 +66,10 @@ fun parsePayto(payto: String): Payto {
val queryString: List<String> = javaParsedUri.query.split("&")
queryString.map {
val split = it.split("=");
- if (split.size != 2) throw InvalidPaytoError("parameter '$it' was malformed")
+ if (split.size != 2) {
+ logger.error("parameter '$it' was malformed")
+ return null
+ }
Pair(split[0], split[1])
}
} else null
diff --git a/util/src/main/kotlin/amounts.kt b/util/src/main/kotlin/amounts.kt
@@ -1,7 +1,5 @@
package tech.libeufin.util
-import UtilError
-import io.ktor.http.*
import java.math.BigDecimal
import java.math.RoundingMode
@@ -28,31 +26,8 @@ const val plainAmountRe = "^([0-9]+(\\.[0-9][0-9]?)?)$"
const val plainAmountReWithSign = "^-?([0-9]+(\\.[0-9][0-9]?)?)$"
const val amountWithCurrencyRe = "^([A-Z]+):([0-9]+(\\.[0-9][0-9]?)?)$"
-// Ensures that the number part of one amount matches the allowed format.
-// Currently, at most two fractional digits are allowed. It returns true
-// in the matching case, false otherwise.
-fun validatePlainAmount(plainAmount: String, withSign: Boolean = false): Boolean {
- if (withSign) return Regex(plainAmountReWithSign).matches(plainAmount)
- return Regex(plainAmountRe).matches(plainAmount)
-}
-
-fun parseAmount(amount: String): AmountWithCurrency {
- val match = Regex(amountWithCurrencyRe).find(amount) ?:
- throw UtilError(HttpStatusCode.BadRequest, "invalid amount: $amount")
- val (currency, number) = match.destructured
- return AmountWithCurrency(currency = currency, amount = number)
-}
-
-fun isAmountZero(a: BigDecimal): Boolean {
- a.abs().toPlainString().forEach {
- if (it != '0' && it != '.')
- return false
- }
- return true
-}
-
fun BigDecimal.roundToTwoDigits(): BigDecimal {
// val twoDigitsRounding = MathContext(2)
// return this.round(twoDigitsRounding)
return this.setScale(2, RoundingMode.HALF_UP)
-}
+}
+\ No newline at end of file
diff --git a/util/src/main/kotlin/exec.kt b/util/src/main/kotlin/exec.kt
@@ -31,6 +31,6 @@ fun execCommand(cmd: List<String>, throwIfFails: Boolean = true): Int {
.start()
.waitFor()
if (result != 0 && throwIfFails)
- throw internalServerError("Command '$cmd' failed.")
+ throw Exception("Command '$cmd' failed.")
return result
}
\ No newline at end of file
diff --git a/util/src/main/kotlin/strings.kt b/util/src/main/kotlin/strings.kt
@@ -19,10 +19,8 @@
package tech.libeufin.util
-import UtilError
-import io.ktor.http.HttpStatusCode
+import logger
import java.math.BigInteger
-import java.math.BigDecimal
import java.util.*
fun ByteArray.toHexString(): String {
@@ -103,24 +101,6 @@ data class AmountWithCurrency(
val amount: String
)
-fun parseDecimal(decimalStr: String): BigDecimal {
- if(!validatePlainAmount(decimalStr, withSign = true))
- throw UtilError(
- HttpStatusCode.BadRequest,
- "Bad string amount given: $decimalStr",
- LibeufinErrorCode.LIBEUFIN_EC_GENERIC_PARAMETER_MALFORMED
- )
- return try {
- BigDecimal(decimalStr)
- } catch (e: NumberFormatException) {
- throw UtilError(
- HttpStatusCode.BadRequest,
- "Bad string amount given: $decimalStr",
- LibeufinErrorCode.LIBEUFIN_EC_GENERIC_PARAMETER_MALFORMED
- )
- }
-}
-
fun getRandomString(length: Int): String {
val allowedChars = ('A' .. 'Z') + ('0' .. '9')
return (1 .. length)
@@ -146,31 +126,7 @@ fun isValidResourceName(name: String): Boolean {
return name.matches(Regex("[a-z]([-a-z0-9]*[a-z0-9])?"))
}
-fun requireValidResourceName(name: String): String {
- if (!isValidResourceName(name)) {
- throw UtilError(
- HttpStatusCode.BadRequest,
- "Invalid resource name. The first character must be a lowercase letter, " +
- "and all following characters (except for the last character) must be a dash, " +
- "lowercase letter, or digit. The last character must be a lowercase letter or digit.",
- LibeufinErrorCode.LIBEUFIN_EC_GENERIC_PARAMETER_MALFORMED
- )
- }
- return name
-}
-
-
-fun sanityCheckOrThrow(credentials: Pair<String, String>) {
- if (!sanityCheckCredentials(credentials)) throw UtilError(
- HttpStatusCode.BadRequest,
- "Please only use alphanumeric credentials.",
- LibeufinErrorCode.LIBEUFIN_EC_GENERIC_PARAMETER_MALFORMED
- )
-}
-
-/**
- * Sanity-check user's credentials.
- */
+// Sanity-check user's credentials.
fun sanityCheckCredentials(credentials: Pair<String, String>): Boolean {
val allowedChars = Regex("^[a-zA-Z0-9]+$")
if (!allowedChars.matches(credentials.first)) return false
@@ -182,11 +138,12 @@ fun sanityCheckCredentials(credentials: Pair<String, String>): Boolean {
* Parses string into java.util.UUID format or throws 400 Bad Request.
* The output is usually consumed in database queries.
*/
-fun parseUuid(maybeUuid: String): UUID {
+fun parseUuid(maybeUuid: String): UUID? {
val uuid = try {
UUID.fromString(maybeUuid)
} catch (e: Exception) {
- throw badRequest("'$maybeUuid' is an invalid UUID.")
+ logger.error("'$maybeUuid' is an invalid UUID.")
+ return null
}
return uuid
}
@@ -198,6 +155,7 @@ fun hasWopidPlaceholder(captchaUrl: String): Boolean {
}
// Tries to extract a valid reserve public key from the raw subject line
+// or returns null if the input is invalid.
fun extractReservePubFromSubject(rawSubject: String): String? {
val re = "\\b[a-z0-9A-Z]{52}\\b".toRegex()
val result = re.find(rawSubject.replace("[\n]+".toRegex(), "")) ?: return null
diff --git a/util/src/main/kotlin/time.kt b/util/src/main/kotlin/time.kt
@@ -22,54 +22,8 @@ package tech.libeufin.util
import java.time.*
import java.time.format.DateTimeFormatter
-private var LIBEUFIN_CLOCK = Clock.system(ZoneId.systemDefault())
-
-fun setClock(rel: Duration) {
- LIBEUFIN_CLOCK = Clock.offset(LIBEUFIN_CLOCK, rel)
-}
fun getNow(): ZonedDateTime {
return ZonedDateTime.now(ZoneId.systemDefault())
}
-fun ZonedDateTime.toMicro(): Long = this.nano / 1000L
-fun getNowMillis(): Long = getNow().toInstant().toEpochMilli()
-
-fun getSystemTimeNow(): ZonedDateTime {
- // return ZonedDateTime.now(ZoneOffset.UTC)
- return ZonedDateTime.now(ZoneId.systemDefault())
-}
-
-fun ZonedDateTime.toZonedString(): String {
- return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(this)
-}
-
-fun ZonedDateTime.toDashedDate(): String {
- return DateTimeFormatter.ISO_DATE.format(this)
-}
-
-fun importDateFromMillis(millis: Long): ZonedDateTime {
- return ZonedDateTime.ofInstant(
- Instant.ofEpochMilli(millis),
- ZoneOffset.UTC
- )
-}
-
-fun LocalDateTime.millis(): Long {
- val instant = Instant.from(this.atZone(ZoneOffset.UTC))
- return instant.toEpochMilli()
-}
-
-fun LocalDate.millis(): Long {
- val instant = Instant.from(this.atStartOfDay().atZone(ZoneId.systemDefault()))
- return instant.toEpochMilli()
-}
-
-fun parseDashedDate(maybeDashedDate: String?): LocalDate {
- if (maybeDashedDate == null)
- throw badRequest("dashed date found as null")
- return try {
- LocalDate.parse(maybeDashedDate)
- } catch (e: Exception) {
- throw badRequest("bad dashed date: $maybeDashedDate. ${e.message}")
- }
-}
-\ No newline at end of file
+fun ZonedDateTime.toMicro(): Long = this.nano / 1000L
+\ No newline at end of file
diff --git a/util/src/test/kotlin/AuthTokenTest.kt b/util/src/test/kotlin/AuthTokenTest.kt
@@ -1,34 +0,0 @@
-import org.junit.Test
-import tech.libeufin.util.extractToken
-import java.lang.Exception
-
-class AuthTokenTest {
- @Test
- fun test() {
- val tok = extractToken("Bearer secret-token:XXX")
- assert(tok == "secret-token:XXX")
- val tok_0 = extractToken("Bearer secret-token:XXX%20YYY")
- assert(tok_0 == "secret-token:XXX YYY")
- val tok_1 = extractToken("Bearer secret-token:XXX YYY")
- assert(tok_1 == "secret-token:XXX YYY")
- val tok_2 = extractToken("Bearer secret-token:XXX ")
- assert(tok_2 == "secret-token:XXX ")
-
- val malformedAuths = listOf(
- "", "XXX", "Bearer", "Bearer ", "Bearer XXX",
- "BearerXXX", "XXXBearer", "Bearer secret-token",
- "Bearer secret-token:", " Bearer", " Bearer secret-token:XXX",
- ":: ::"
- )
- for (token in malformedAuths) {
- try {
- extractToken(token)
- } catch (e: Exception) {
- assert(e is UtilError)
- continue
- }
- println("Error: '$token' made it through")
- assert(false) // should never arrive here.
- }
- }
-}
-\ No newline at end of file
diff --git a/util/src/test/kotlin/PaytoTest.kt b/util/src/test/kotlin/PaytoTest.kt
@@ -1,58 +1,29 @@
import org.junit.Test
-import tech.libeufin.util.InvalidPaytoError
+import tech.libeufin.util.Payto
import tech.libeufin.util.parsePayto
class PaytoTest {
@Test
fun wrongCases() {
- try {
- parsePayto("payto://iban/IBAN/BIC")
- } catch (e: InvalidPaytoError) {
- println(e)
- println("must give IBAN _and_ BIC")
- }
- try {
- parsePayto("http://iban/BIC123/IBAN123?receiver-name=The%20Name")
- } catch (e: InvalidPaytoError) {
- println(e)
- println("wrong scheme was caught")
- }
- try {
- parsePayto(
- "payto:iban/BIC123/IBAN123?receiver-name=The%20Name&address=house"
- )
- } catch (e: InvalidPaytoError) {
- println(e)
- println("'://' missing, invalid Payto")
- }
- try {
- parsePayto("payto://iban/BIC123/IBAN123?sender-name=Foo&receiver-name=Foo")
- } catch (e: InvalidPaytoError) {
- println(e)
- }
- try {
- parsePayto("payto://wrong/BIC123/IBAN123?sender-name=Foo&receiver-name=Foo")
- } catch (e: InvalidPaytoError) {
- println(e)
- }
+ assert(parsePayto("http://iban/BIC123/IBAN123?receiver-name=The%20Name") == null)
+ assert(parsePayto("payto:iban/BIC123/IBAN123?receiver-name=The%20Name&address=house") == null)
+ assert(parsePayto("payto://wrong/BIC123/IBAN123?sender-name=Foo&receiver-name=Foo") == null)
}
@Test
fun parsePaytoTest() {
- val withBic = parsePayto("payto://iban/BIC123/IBAN123?receiver-name=The%20Name")
+ val withBic: Payto = parsePayto("payto://iban/BIC123/IBAN123?receiver-name=The%20Name")!!
assert(withBic.iban == "IBAN123")
assert(withBic.bic == "BIC123")
assert(withBic.receiverName == "The Name")
- val complete = parsePayto("payto://iban/BIC123/IBAN123?sender-name=The%20Name&amount=EUR:1&message=donation")
+ val complete = parsePayto("payto://iban/BIC123/IBAN123?sender-name=The%20Name&amount=EUR:1&message=donation")!!
assert(withBic.iban == "IBAN123")
assert(withBic.bic == "BIC123")
assert(withBic.receiverName == "The Name")
assert(complete.message == "donation")
assert(complete.amount == "EUR:1")
- val withoutOptionals = parsePayto(
- "payto://iban/IBAN123"
- )
+ val withoutOptionals = parsePayto("payto://iban/IBAN123")!!
assert(withoutOptionals.bic == null)
assert(withoutOptionals.message == null)
assert(withoutOptionals.receiverName == null)
diff --git a/util/src/test/kotlin/TimeTest.kt b/util/src/test/kotlin/TimeTest.kt
@@ -1,66 +0,0 @@
-import org.junit.Ignore
-import org.junit.Test
-import tech.libeufin.util.getNow
-import tech.libeufin.util.millis
-import tech.libeufin.util.setClock
-import java.time.*
-import java.time.format.DateTimeFormatter
-
-// https://stackoverflow.com/questions/32437550/whats-the-difference-between-instant-and-localdatetime
-
-// Ignoring because no assert takes place here.
-@Ignore
-class TimeTest {
- @Test
- fun mock() {
- println(getNow())
- setClock(Duration.ofHours(2))
- println(getNow())
- }
-
- @Test
- fun importMillis() {
- fun fromLong(millis: Long): LocalDateTime {
- return LocalDateTime.ofInstant(
- Instant.ofEpochMilli(millis),
- ZoneId.systemDefault()
- )
- }
- val ret = fromLong(0)
- println(ret.toString())
- }
-
- @Test
- fun printLong() {
- val l = 1111111L
- println(l.javaClass)
- println(l.toString())
- }
-
- @Test
- fun formatDateTime() {
- fun formatDashed(dateTime: LocalDateTime): String {
- val dtf = DateTimeFormatter.ISO_LOCAL_DATE
- return dtf.format(dateTime)
- }
- fun formatZonedWithOffset(dateTime: ZonedDateTime): String {
- val dtf = DateTimeFormatter.ISO_OFFSET_DATE_TIME
- return dtf.format(dateTime)
- }
- val str = formatDashed(LocalDateTime.now())
- println(str)
- val str0 = formatZonedWithOffset(LocalDateTime.now().atZone(ZoneId.systemDefault()))
- println(str0)
- }
-
- @Test
- fun parseDashedDate() {
- fun parse(dashedDate: String): LocalDate {
- val dtf = DateTimeFormatter.ISO_LOCAL_DATE
- return LocalDate.parse(dashedDate, dtf)
- }
- val ret: LocalDate = parse("1970-01-01")
- println(ret.toString())
- ret.millis() // Just testing it doesn't raise Exception.
- }
-}
-\ No newline at end of file