How an audit can make you less secure

First a disclaimer: I know excellent auditors, starting with my friend Dave from the Payments Systems Blog, but I also know really retarded ones, and here is a little story of a system I built some 8 or 10 years ago that would have been resilient to the HeartBleed bug, but of course, the auditor couldn’t understand it, and it had the word MD5 which puts them to cry like tiny little girls, so we have to “improve” it to make it less secure.

HeartBleed

I’ve been into amateur packet radio and BBS systems in the 80s where monitoring the air, or a serial line was easy, so things like one time passwords and two way authentication have been always within my area of interest. When it came to provide an internal user interface to jPOS, and I had to design a login form, I wanted to protect the user’s passwords against an operator with access to the system; I wanted people to be able to use a password like TheBossIsAnIdiot if they wanted, making it difficult for the programmer/operator on the server side to see it.

So the solution was easy. The server would generate a nonce and send it to the client, the client would use that nonce, some other data (like the session id) and the password, and send an MD5 of all that to the server.

I wasn’t and I’m not a JS expert, and we didn’t have things like jQuery or Angular those days, but I wrote this little piece of code that implemented the login form:

Login Form

function doHash(frm) {
    var username = frm.username.value;
    var password = frm.password.value;

    if (username.length < 3 || password.length < 3) {
        alert ("Invalid Username and/or Password.");
        return false;
    }
    var hash     = frm.hash.value;
    var seed     = readCookie ("JSESSIONID") + hash;
    var pass     = hex_md5 (username + password);

    frm.password.readOnly = true;
    frm.password.value = hex_md5 (seed + pass);
}

The server would do the same computation in order to verify the login.

I wasn’t comfortable with the solution, because somehow the initial password was either entered by the operator, or sent via email, so I forced a password change in the first login, and the password change would just send an XOR of the existing password hash so that the server could apply the same XOR and upgrade the password to the latest version.

But here comes the auditor, with a bucket in his head, and reports the process as insecure (with strong copy/pasted wording to scare management) for the following reasons:

  1. On password change, the complexity of the password (you know, password length, use lower and upper and all that crap) is validated in client side, not in the server side. And there goes a rant that says “Passwords should be at least XXX characters, and have lower case and upper case letters in order to be secure, yada yada yada”, so the manager would look at me like “We trusted in you… look what you did to us, our passwords can be less than XXX characters if the user hacks the client side code!”).

  2. It has the word MD5 and we just heard that MD5 is broken (remember this was 2005~2006), and there goes the rant about how MD5 was recently cracked, and again, the manager would give you that look, like saying, you’re a lost case, I’ve been always scared of open source and the freetards around it.

I think the tradeoff between having one user hacking the JS to force himself a weak password, compared to protecting all users from easy eavesdropping is a good one. I also think that sending an MD5 over the wire is better than sending a clear password (although there’s of course SSL involved, I’m talking about ‘clear’ from an application perspective). It has the side benefit of staying secure while in memory on the server side.

Auditors are obsessed with the things they were told to look after, SQL injection, XSS, or things they test with automated tools. That’s fine and welcome (you don’t need an auditor for that, BTW, but it’s good to have more eyes on the problem). But I’ve never seen an auditor testing to SQL inject say a field 35/45 in an ISO8583 message, something anyone can do forging the track2 of a card and going to a shop around the corner. Take what they say with a grain of salt, and remember not all, but most of them, are just talkers.

I’m playing with the idea of making the client perform a really large number of iterations on the hash, to slow it down (kind of a client side bcrypt) without requiring too much CPU on the server, and then have the server do some more rounds, perhaps with a bcrypt final pass. I’m planning to send some timing information to the server too, in order to alert on client hardware/software changes (how long did it take you to run 100K hashes?). We’ll have to figure out how to explain this in our next audit..

Context trace

In jPOS 1.9.7 cce6a27 we’ve added a new transient trace flag to the Context that can be very useful during development.

Those of you using the TransactionManager with a large number of participants know that sometimes it becomes difficult to know who placed what in the Context.

You get to see a Context with many entries (REQUEST, RESPONSE, IRC, SOURCE, TRANLOG, TIMESTAMP, AMOUNT, PAN, ADDITIONAL_AMOUNT, etc.) but pin pointing where a given value is place gets difficult.

If the Context new trace boolean is set to true (something you can do via a configuration property in one of the initial participants such as PrepareContext or even closer to the incoming message, in the ISORequestListener when you create the Context), the Debug output would look like this:

   <profiler>
        REQUEST='<-- 2100 000000000162 29110001        ' [org.jpos.jcard.IncomingSupport.process(IncomingSupport.java:52)] [0.1/0.1]
        SS='JCARD' [org.jpos.jcard.IncomingSupport.process(IncomingSupport.java:53)] [0.0/0.2]
        TXNNAME='100.00' [org.jpos.jcard.IncomingSupport.process(IncomingSupport.java:68)] [0.0/0.2]
        SOURCE='org.jpos.iso.channel.CSChannel@2c42dc17' [org.jpos.jcard.IncomingSupport.process(IncomingSupport.java:69)] [0.0/0.2]
        WATCHDOG='org.jpos.jcard.IncomingSupport$1@1aad5bb2' [org.jpos.jcard.IncomingSupport.process(IncomingSupport.java:76)] [0.0/0.3]
     prepare-context [4.6/4.9]
        TIMESTAMP='Tue Apr 08 12:29:20 UYT 2014' [org.jpos.jcard.PrepareContext.prepare(PrepareContext.java:33)] [0.0/5.0]
        TXNMGR='txnmgr' [org.jpos.jcard.PrepareContext.prepare(PrepareContext.java:38)] [0.0/5.0]
        DB='org.jpos.ee.DB@3f3fd620' [org.jpos.transaction.TxnSupport.getDB(TxnSupport.java:157)] [0.1/5.1]
        TX='org.hibernate.engine.transaction.internal.jdbc.JdbcTransaction@4d0ee1de' [org.jpos.transaction.Open.prepare(Open.java:38)] [37.4/42.5]
     open [0.0/42.5]
        SWITCH='100.00 (authorization prepareresponse logit close sendresponse)' [org.jpos.jcard.Switch.select(Switch.java:39)] [0.0/42.6]
        PCODE='000000' [org.jpos.jcard.CheckFields.putPCode(CheckFields.java:163)] [0.0/42.7]
        PCODE_TXN_TYPE='00' [org.jpos.jcard.CheckFields.putPCode(CheckFields.java:164)] [2.6/45.3]
        PCODE_ACCOUNT_TYPE='00' [org.jpos.jcard.CheckFields.putPCode(CheckFields.java:165)] [0.0/45.4]
        PCODE_ACCOUNT2_TYPE='00' [org.jpos.jcard.CheckFields.putPCode(CheckFields.java:166)] [0.0/45.4]
        TRANSMISSION_TIMESTAMP='Tue Apr 08 12:29:20 UYT 2014' [org.jpos.jcard.CheckFields.putTransmissionTimestamp(CheckFields.java:301)] [0.0/45.5]
        LOCAL_TRANSACTION_TIMESTAMP='Tue Apr 08 12:29:20 UYT 2014' [org.jpos.jcard.CheckFields.putLocalTransactionTimestamp(CheckFields.java:297)] [0.0/45.5]
        AMOUNT='100.01' [org.jpos.jcard.CheckFields.putAmount(CheckFields.java:231)] [0.0/45.6]
        CURRENCY='840' [org.jpos.jcard.CheckFields.putAmount(CheckFields.java:232)] [0.0/45.6]
        PAN='6009330000000033' [org.jpos.jcard.CheckFields.putPAN(CheckFields.java:180)] [0.0/45.7]
        EXP='4912' [org.jpos.jcard.CheckFields.putPAN(CheckFields.java:181)] [0.0/45.7]
        TID='29110001        ' [org.jpos.jcard.CheckFields.assertFields(CheckFields.java:127)] [0.0/45.7]
        NETWORK_CAPTURE_DATE='Tue Apr 08 12:00:00 UYT 2014' [org.jpos.jcard.CheckFields.putCaptureDate(CheckFields.java:275)] [0.0/45.8]
        MID='001001' [org.jpos.jcard.CheckFields.assertFields(CheckFields.java:130)] [0.0/45.8]
        TRANLOG='org.jpos.ee.TranLog@7dcadb39[id=166]' [org.jpos.jcard.CreateTranLog.doPrepare(CreateTranLog.java:99)] [2.6/48.5]
        CAPTURE_DATE='Tue Apr 08 00:00:00 UYT 2014' [org.jpos.jcard.CreateTranLog.doPrepare(CreateTranLog.java:100)] [0.0/48.5]
     create-tranlog [0.0/48.6]
        CARD='org.jpos.ee.Card@42c613bd[id=5,pan=600933...0033]' [org.jpos.jcard.CheckCard.prepare(CheckCard.java:65)] [10.0/58.6]
        ISSUER='org.jpos.ee.Issuer@61188c80[id=1,name=1]' [org.jpos.jcard.CheckCard.prepare(CheckCard.java:97)] [2.8/61.5]
        CARDPRODUCT='org.jpos.ee.CardProduct@60ea1534[id=3,name=3]' [org.jpos.jcard.CheckCard.prepare(CheckCard.java:98)] [0.0/61.5]
     check-card [0.0/61.5]
     check-terminal [6.3/67.9]
        ACQUIRER='org.jpos.ee.Acquirer@2b001c59[id=1,name=1]' [org.jpos.jcard.CheckAcquirer.prepare(CheckAcquirer.java:51)] [6.6/74.5]
     check-acquirer [0.0/74.5]
        ACCOUNT='org.jpos.gl.FinalAccount@7f7a1bec[id=28,code=22.0000000002]' [org.jpos.jcard.SelectAccount.prepare(SelectAccount.java:49)] [1.0/75.6]
     select-account [0.0/75.6]
     check-previous-reverse [3.2/79.1]
     check-velocity [18.2/97.3]
     authorization-start [0.0/97.4]
        GLSESSION='org.jpos.gl.GLSession@5976dbd8[DB=org.jpos.ee.DB@3f3fd620]' [org.jpos.jcard.JCardTxnSupport.getGLSession(JCardTxnSupport.java:146)] [1.7/99.2]
     authorization-pre-lock-journal [0.0/99.2]
     authorization-post-lock-journal [1.7/101.0]
     authorization-compute-balance [7.0/108.0]
        ACCOUNT='org.jpos.gl.FinalAccount@7f7a1bec[id=28,code=22.0000000002]' [org.jpos.jcard.Authorization.prepare(Authorization.java:110)] [0.1/108.2]
     authorization-get-credit-line [8.1/116.3]
        RC='not.sufficient.funds' [org.jpos.jcard.Authorization.prepare(Authorization.java:195)] [0.8/117.1]
        EXTRC='Credit line is 0.00, issuer fee=6.75' [org.jpos.jcard.Authorization.prepare(Authorization.java:197)] [0.0/117.1]
     authorization [0.0/117.2]
     create-cache-ledger [6.3/123.5]
     create-cache-pending-and-credit [8.4/132.0]
     create-cache-pending [47.5/179.5]
        LEDGER_BALANCE='100.00' [org.jpos.jcard.ComputeBalances.prepare(ComputeBalances.java:84)] [0.1/179.6]
        AVAILABLE_BALANCE='100.00' [org.jpos.jcard.ComputeBalances.prepare(ComputeBalances.java:85)] [0.0/179.7]
     compute-balances [0.0/179.7]
        IRC='1016' [org.jpos.jcard.PrepareResponse.setRetCode(PrepareResponse.java:142)] [2.9/182.6]
        RESPONSE='<-- 2110 000000000162 29110001        ' [org.jpos.jcard.PrepareResponse.prepareForAbort(PrepareResponse.java:56)] [19.9/202.6]
     close [9.5/212.1]
        REQUEST='<-- 2100 000000000162 29110001        ' [org.jpos.jcard.ProtectDebugInfo.protect(ProtectDebugInfo.java:43)] [647.9/860.1]
     end [0.2/860.3]
   </profiler>

Although it may look verbose, this could be very useful while coding, it helps you spot problems and assist on debugging.

I just found one issue in the jCard system while writing this blog post, look at this, we set the ACCOUNT in SelectAccount

        ACCOUNT='org.jpos.gl.FinalAccount@7f7a1bec[id=28,code=22.0000000002]' [org.jpos.jcard.SelectAccount.prepare(SelectAccount.java:49)] [1.0/75.6]

then we set it again in Authorization.

        ACCOUNT='org.jpos.gl.FinalAccount@7f7a1bec[id=28,code=22.0000000002]' [org.jpos.jcard.Authorization.prepare(Authorization.java:110)] [0.1/108.2]

Not a big deal, it’s the same account, but worth checking why we are doing that.

QMUX internal space

The jPOS QMUX service uses the Space (usually the default global space) in order to communicate with other components such as the ChannelAdaptor or QServer using its in and out queues. But in addition, it implements the MUX interface by storing selected parts of a request message (known as the QMUX key), as shown in the picture below:

QMUX Space Dance

In high traffic systems, with many QMUXes, every thread waiting for a response would wake up, albeit for a tiny little while, when something happens in the space. This small patch done in 1.9.7 keep using the global space for the QMUX in and out queues, but uses an internal Space (currently a TSpace) to perform the key-matching dance.

The change should be transparent for most users, but we’ve seen some implementations out there that dangerously peek and poke our entries in the Space, and actually this patch, in addition to improve performance, intend to discourage such use in the future (by not exposing the internal space to other components). But anyway, for backward compatibility, we honor a new property reuse-space that if set to true, would revert to the old implementation, using the global space.

Pull configurations

You might have heard a thousand times, push is good, IoC is good. pull is bad, and I have to agree.

jPOS components get their configurations pushed by the Q2 container when they implement the Configurable interface.

But if you’re used to jPOS configurations, which can be filtered at build time by the Gradle build based on the desired target profile, or can be decorated by means of @vsalaman‘s contributed decorator, you may find yourself reinventing the wheel and figuring out how to get some Configuration object into a non jPOS component (such as a servlet or any other non jPOSsy code).

To solve that in a standard way, we’ve created QConfig. QConfig is a minimalistic QBean that just register its own Configuration object into the NameRegistrar (with a “config.” prefix). So for example, you can deploy something like this:

<config>
    <property name="test" value="ABC" />
    <property name="test1" value="123" />
    <property file="cfg/myprops.cfg" />
</config>

The word config has been registered in QFactory.properties so the <config> element above is equivalent to:

<config name='config' class='org.jpos.q2.qbean.QConfig'>
    ...
    ...
</config>

So non jPOS running inside Q2 can get a reference to ‘config’ configuration by calling:

Configuration cfg = QConfig.getConfiguration("myconfigname");

While we were at it, we added the ability to merge configuration objects in other QBeans; There many ways to achieve the same without using this technique, for example, you can use <property file="xxx" /> in different QBeans to pull the same config, or you can use XML entities for that, but, because we can, we just offer this additional way to do it, which is quite simple.

Any QBean descriptor now accepts an optional attribute called merge-configuration that accepts a list of QConfig configurations and merges them on-the-fly at QBean configuration time. Here is a simple example:

deploy/00_config.xml

<config>
    <property name="test" value="ABC" />
    <property name="test1" value="123" />
</config>

deploy/01_config.xml

<config name='config1'>
    <property name="test2" value="XYZ" />
</config>

deploy/90_script.xml

<script merge-configuration='config, config1'>
    print ("TEST: " + cfg.get("test"));
    print ("TEST2: " + cfg.get("test2"));
</script>

Because this merge-configuration handling is honored by QFactory, used by other components such as the TransactionManager to instantiate its participants, you can use it in TM participants as well (i.e. to pull reused configuration, such as result codes and the like).

jPOS 1.9.4 released

jPOS 1.9.4 has been released and it includes the following changes, most notably OSGi support.

  • Added len,description constructor to IF_NOP
  • Added IFB_LLHEX (can be used to deal with encrypted track2s)
  • Added HexNibblesPrefixer (required by IFB_LLHEX)
  • Added OSGi support
  • Added ‘qnode’ (OSGi testbed)
  • DirPoll now supports file compression
  • Profiler can be reenabled
  • TransactionManager PAUSED transactions reuse profiler to provide combined elapsed times
  • Added org.jpos.iso.GenericSSLSocketFactory
  • jPOS-105 QServer should unregister as a space listener
  • jPOS-106 ChannelAdaptor reconnect flag uses non serializable Object
  • jPOS-108 FSDMsg consuming input stream
  • DirPoll.scan is now protected
  • MUX interface now extends ISOSource
  • QMUX.send now checks isConnected()
  • DirPoll now accepts a priority.regex boolean property (73c2f84)
  • jPOS-110 QMUX major start-up issue (1526dab)
  • DirPoll Retry when archive.timestamp is set to true (pull/33)
  • Generate optional app specific version info 02f739a

See full ChangeLog.

1.9.4 is available in Maven Central.

W55Y

What API designers could learn from the payments industry


We all know the Fallacies of Distributed Computing:

  1. The network is reliable.
  2. Latency is zero.
  3. Bandwidth is infinite.
  4. The network is secure.
  5. Topology doesn’t change.
  6. There is one administrator.
  7. Transport cost is zero.
  8. The network is homogeneous.

I think there’s a 9th: REST API designers know the meaning of the word Fallacy.

So to clarify and as a public service, we need to start talking about distributed computing facts instead:

  1. The network IS NOT reliable.
  2. Latency IS NOT zero.
  3. Bandwidth is limited.
  4. The network is not secure (you should know that already)
  5. Topology does change, and at the worst possible time.
  6. The administrator is dead.
  7. Moving bits around the net has a cost.
  8. The network heterogeneous.

Interesting enough, a very old protocol, ISO-8583, designed in the 80s to support slow 300 and 1200 bps dialup links is extremely aware of these facts and work around these problems with a very simple and asynchronous message flow.

Take a look any popular REST payments API, you usually call a method to authorize a transaction and you pass parameters like this:

  • Card and Expiration (or a token)
  • Amount, perhaps currency
  • A description

Lovely, simple, and wrong!

Using that popular design, you POST the transaction and pray to get a response from the server. If you get a response, either a 200, 201, or even a 500 from the server, everything is alright, but if the request times out, or you go down, or the servers goes down, or the ISP is reconfiguring a router, you can’t really tell what happened. If you’re lucky and the server didn’t receive your request, then that’s fine, you can retry it, but if the server did receive your request, and authorized against its upper level acquirer, then you’ll have an angry cardholder with a hold in its account, and perhaps even a debit (because I don’t see many payment gateways accepting reconciliation messages or settlement files).

In ISO-8583, when you send an authorization request or a financial request and you don’t get a response, you queue a reversal in a persistent store and forward (SAF) queue. So the next time you contact the server, before sending a new transaction, you send that reversal. If you receive a response from the server, but the response comes late and you have already timed out, you also send a ‘late-response’ reversal to the server.

In the same way, when you post a transaction that already took place (i.e. adjusting a previously approved transaction for a different amount, something that happens all the time at restaurants that support tips, or gas pump transactions where you approve for $100 but then complete for $20), and you don’t get a response, you send a retransmission of the same transaction, as many times as necessary in order to deal with transient network problems.

In order to support reversals and retransmissions, you need a transaction unique identifier. Different networks use different transaction identifiers, either the Terminal ID + a Serial Trace Audit Number, or a Retrieval Reference Number, or in ISO-8583 v2003 the new Transaction life cycle identification data, the client generates a unique identifier so that when you send a follow-up transaction, you can send the server a reference to the original one.

I believe all payment APIs out there (including those super very cool ones) should consider adding three things:

A new parameter, RRN

Client code could generate a UUID and use it as the RRN (Retrieval Reference Number)

Support reversals (DELETE)

Could be as simple as adding a new verb, DELETE, where the only parameter can be — along with authentication data — the RRN

Support for retransmissions (PUT)

If your initial transaction was a POST, I propose that you also accept a PUT, with the same parameters. On the server side the difference between the POST and the PUT operations would be just an extra step to make sure that you didn’t process the original POST, and return the original auth code if that was the case.

Of course, if you’re designing a new super cool minimalistic REST API you probably don’t listen to people with grey hair, but just in case, my 2c :)

@apr

jPOS Programmer’s Guide

For those not regularly checking the [jPOS main site], please note there’s a new Learn tab in the main site with a direct link to download the new and free jPOS Programmer’s guide draft.

While it’s still work in progress, it provides useful information related to jPOS 1.9.x series that complement the standard for-sale guide.

Feedback is of course very Welcome!

Eating our own dogfood

I really like the TransactionManager, it allows me to clearly define a transaction as if it was processed in an assembly line, with each small reusable participant doing its own little part, and the TransactionManager giving useful profiling information.

But now we live in a RESTful world, we need to implement RESTful based services here and there.

A typical REST call implementation looks like this:

@Path("/customers/{customer_id}/wallets/{wallet_id}/credit")
public class WalletCredit extends WalletCreditDebitSupport {
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response credit (
            @Context UriInfo uriInfo,
            @PathParam("customer_id") String customerId,
            @PathParam("wallet_id") String walletId,
            @FormParam("rrn") String rrn,
            @FormParam("detail") String detail,
            @FormParam("amount") BigDecimal amount,
            @FormParam("currency") String currency)
            throws IOException, URISyntaxException, BLException, ISOException
    {
        …
        …
        …

    }
}

so the first reaction is to just start coding the business logic as part of the body of that method. You need to:

  • sanity check the parameters
  • open a database connection
  • create a TranLog record
  • Validate the Customer, Account, etc.
  • create a GL Transaction
  • do some audit logging
  • commit the JDBC transaction

This of course work fine, but hey, we already have code that does that:

  • We have a standard transaction Open participant
  • We usually have a CreateTranLog participant
  • We have a CheckCustomer participant and CheckAccount
  • We have participants that generate GLTransactions
  • And of course, the Close and Debug participants

Problem with a traditional implementation inside that body is that we need to reinvent the wheel, repeat code, add custom profiling or otherwise we wouldn’t know which operations are fast and which ones are slow.

The solution was to eat our own dog food and use the TransactionManager.

So in this jCard case I’ve created a rest_txnmgr.xml that looks like this:

<!DOCTYPE txnmgr [
    <!ENTITY PAN_PATTERN   "^[\d]{10}$">
    <!ENTITY AMOUNT_PATTERN  "[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?">
    <!ENTITY CURRENCY_PATTERN "^\d{1,4}">
    <!ENTITY RRN_PATTERN "^\d{1,12}">
    <!ENTITY DETAIL_PATTERN "^.{0,255}$">
    <!ENTITY WALLET_PATTERN "^[\d]{1,64}$">
    <!ENTITY TEXT50_PATTERN "^[\w\s.\-\']{0,50}$">
    ...
    ...
]>

<txnmgr name='rest-txnmgr' class="org.jpos.transaction.TransactionManager" logger="Q2" realm='rest-txnmgr'>
  <property name="queue" value="JCARD.RESTAPI.TXN" />
  <property name="sessions" value="2" />
  <property name="max-sessions" value="64" />
  <property name="debug" value="true" />

  <participant class="org.jpos.jcard.PrepareContext" logger="Q2" realm="prepare-context" />
  <participant class="org.jpos.jcard.Switch" logger="Q2" realm="Switch">
    <property name="WalletCreate" value="walletcreate close trigger-response" />
    <property name="WalletCredit" value="walletcredit close trigger-response" />
    <property name="WalletDebit" value="walletdebit close trigger-response" />
    <property name="BalanceInquiry" value="balance-inquiry close trigger-response" />
    <property name="MiniStatement" value="mini-statement close trigger-response" />
    …
    ..

</participant>
…
…
     <group name="walletcreate">
   <participant class="org.jpos.jcard.rest.ValidateParams" logger="Q2">
     <mandatory>
       <param name="PAN">&PAN_PATTERN;</param>
       <param name="WALLET_NUMBER">&WALLET_PATTERN;</param>
     </mandatory>
     <optional>
       <!-- no optional fields -->
     </optional>
   </participant>
   <participant class="org.jpos.transaction.Open" logger="Q2" realm="open">
     <property name="checkpoint" value="open" />
     <property name="timeout" value="300" />
   </participant>
   <participant class="org.jpos.jcard.CreateTranLog" logger="Q2"
                realm="create-tranlog">
     <property name="capture-date" value="capture-date" />
     <property name="node"         value="01" />
   </participant>
   <participant class="org.jpos.jcard.CheckCustomer"
                logger="Q2" realm="checkcustomer">
   </participant>
   <participant class="org.jpos.jcard.rest.WalletCreateParticipant" logger="Q2" realm="create-wallet">
     <property name="chart"        value="jcard" />
     <property name="customers-account" value="21" />
     <property name="kid" value="&KID;" />
   </participant>
 </group>
 ...
 ...

</txnmgr>

Now the code looks like this:

@Path("/customers/{customer_id}/wallets/{wallet_id}/credit")
public class WalletCredit extends WalletCreditDebitSupport {
    @POST
    @Produces(MediaType.APPLICATION_JSON)
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public Response credit (
            @Context UriInfo uriInfo,
            @PathParam("customer_id") String customerId,
            @PathParam("wallet_id") String walletId,
            @FormParam("rrn") String rrn,
            @FormParam("detail") String detail,
            @FormParam("amount") BigDecimal amount,
            @FormParam("currency") String currency)
            throws IOException, URISyntaxException, BLException, ISOException
    {
        return process(uriInfo, customerId, walletId, rrn, detail, amount, currency);
    }
}
public abstract class WalletCreditDebitSupport extends RestSupport {
    @SuppressWarnings("unchecked")
    public Response process (
            UriInfo uriInfo,
            String customerId,
            String walletId,
            String rrn,
            String detail,
            BigDecimal amount,
            String currency)
            throws IOException, URISyntaxException, BLException, ISOException
    {
        org.jpos.transaction.Context ctx = new org.jpos.transaction.Context();
        ctx.put(TXNNAME, getClass().getSimpleName());
        ctx.put(PAN, customerId);
        ctx.put(WALLET_NUMBER, walletId);
        ctx.put(CURRENCY, currency);
        ctx.put(RRN, rrn);
        ctx.put(AMOUNT, amount);
        ctx.put(DETAIL, detail);

        int result = queryTxnMgr(ctx);
        Map<String,Object> resp = createResponseMap();
        if (result == TransactionManager.PREPARED) {
            String irc = (String) ctx.get (IRC);
            resp.put("success", TRAN_APPROVED.equals(irc));
            resp.put("balance", ctx.get (LEDGER_BALANCE));
            return Response.ok(toJson(resp), MediaType.APPLICATION_JSON)
                .status(Response.Status.CREATED)
                .location(URI.create(uriInfo.getAbsolutePath().toString())).build();
        } else {
            resp.put("success", false);
            resp.put("message", ctx.getString(TxnConstants.RC));
            return Response.status(Response.Status.BAD_REQUEST).entity(toJson(resp)).build();
        }
    }
}

And the best part of this, is that we get our familiar ‘Debug’ and ‘Trace’ events:

<log realm="rest-txnmgr" at="Wed Oct 09 20:07:01 UYST 2013.489" lifespan="925ms">
<debug>
    rest-txnmgr-0:idle:1
            prepare: org.jpos.jcard.PrepareContext NO_JOIN
            prepare: org.jpos.jcard.Switch READONLY NO_JOIN
        selector: walletcreate close trigger-response
            prepare: org.jpos.jcard.rest.ValidateParams READONLY NO_JOIN
            prepare: org.jpos.transaction.Open READONLY NO_JOIN
            prepare: org.jpos.jcard.CreateTranLog NO_JOIN
            prepare: org.jpos.jcard.CheckCustomer NO_JOIN
            prepare: org.jpos.jcard.rest.WalletCreateParticipant READONLY NO_JOIN
            prepare: org.jpos.transaction.Close READONLY
            prepare: org.jpos.jcard.rest.TriggerResponse READONLY
            prepare: org.jpos.transaction.Debug READONLY
            commit: org.jpos.transaction.Close
            commit: org.jpos.jcard.rest.TriggerResponse
            commit: org.jpos.transaction.Debug
    head=2, tail=2, outstanding=0, active-sessions=2/64, tps=0, peak=0, avg=0.00, elapsed=925ms
    <profiler>
    prepare: org.jpos.jcard.PrepareContext [0.1/0.1]
    prepare: org.jpos.jcard.Switch [0.0/0.2]
    prepare: org.jpos.jcard.rest.ValidateParams [0.2/0.5]
    prepare: org.jpos.transaction.Open [0.7/1.2]
    prepare: org.jpos.jcard.CreateTranLog [2.3/3.6]
    prepare: org.jpos.jcard.CheckCustomer [8.5/12.2]
    prepare: org.jpos.jcard.rest.WalletCreateParticipant [556.5/568.7]
    prepare: org.jpos.transaction.Close [0.1/568.9]
    prepare: org.jpos.jcard.rest.TriggerResponse [0.1/569.1]
    prepare: org.jpos.transaction.Debug [0.1/569.2]
    commit: org.jpos.transaction.Close [344.9/914.2]
    commit: org.jpos.jcard.rest.TriggerResponse [0.6/914.8]
    commit: org.jpos.transaction.Debug [9.8/924.7]
    end [0.9/925.7]
    </profiler>
</debug>
</log>
<log realm="debug" at="Wed Oct 09 20:07:03 UYST 2013.845">
<commit>
    <id>2</id>
    <context>
    <entry key='PAN'>0000000005</entry>
    <entry key='RRN'>000000000001</entry>
    <entry key='LOGEVT'><log realm="" at="Wed Oct 09 20:07:03 UYST 2013.846" lifespan="38ms">
<log>

    <![CDATA[
<transaction id="97" date="20131009200703" post-date="20131009" journal="jcard">
<detail>WalletCredit 2</detail>
<entry account="11.001.00" type="debit" layer="840">
    <amount>100.00</amount>
</entry>
<entry account="21.0000000005.1" type="credit" layer="840">
    <amount>100.00</amount>
</entry>
</transaction>
    ]]>
</log>
</log>
</entry>
    <entry key='WALLET_NUMBER'>0000000005</entry>
    <entry key='CARDHOLDER'>org.jpos.ee.CardHolder@64a5efa3[id=5]</entry>
    <entry key='DB'>org.jpos.ee.DB@63130f0d</entry>
    <entry key='LEDGER_BALANCE'>100.00</entry>
    <entry key='TRANLOG'>org.jpos.ee.TranLog@76250246[id=172]</entry>
    <entry key='AMOUNT'>100.00</entry>
    <entry key='SWITCH'>WalletCredit (walletcredit close trigger-response)</entry>
    <entry key='ACCOUNT'>org.jpos.gl.FinalAccount@20c0fe7c[id=49,code=21.0000000005.1]</entry>
    <entry key='TXNRESULT'>1</entry>
    <entry key='TXNMGR'>rest-txnmgr</entry>
    <entry key='IRC'>0000</entry>
    <entry key='CURRENCY'>840</entry>
    <entry key='DETAIL'>Test wallet credit</entry>
    <entry key='WALLET'>org.jpos.ee.Wallet@71df1c8a[id=1,number=0000000005]</entry>
    <entry key='CAPTURE_DATE'>Wed Oct 09 00:00:00 UYST 2013</entry>
    <entry key='TXNNAME'>WalletCredit</entry>
    <entry key='TIMESTAMP'>Wed Oct 09 20:07:03 UYST 2013</entry>
    <entry key='PROFILER'>
    <profiler>
        prepare-context [0.0/0.0]
        open [1.1/1.2]
        close [533.2/534.4]
        end [10.9/545.4]
    </profiler>
    </entry>
    <entry key='ISSUER'>org.jpos.ee.Issuer@6f59db4f[id=1,name=jcard]</entry>
    </context>
</commit>
</log>
<log realm="rest-txnmgr" at="Wed Oct 09 20:07:03 UYST 2013.864" lifespan="553ms">
<debug>
    rest-txnmgr-0:idle:2
            prepare: org.jpos.jcard.PrepareContext NO_JOIN
            prepare: org.jpos.jcard.Switch READONLY NO_JOIN
        selector: walletcredit close trigger-response
            prepare: org.jpos.jcard.rest.ValidateParams READONLY NO_JOIN
            prepare: org.jpos.transaction.Open READONLY NO_JOIN
            prepare: org.jpos.jcard.CreateTranLog NO_JOIN
            prepare: org.jpos.jcard.CheckCustomer NO_JOIN
            prepare: org.jpos.jcard.CheckWallet READONLY NO_JOIN
            prepare: org.jpos.jcard.rest.WalletTransactionParticipant READONLY NO_JOIN
            prepare: org.jpos.transaction.Close READONLY
            prepare: org.jpos.jcard.rest.TriggerResponse READONLY
            prepare: org.jpos.transaction.Debug READONLY
            commit: org.jpos.transaction.Close
            commit: org.jpos.jcard.rest.TriggerResponse
            commit: org.jpos.transaction.Debug
    head=3, tail=3, outstanding=0, active-sessions=2/64, tps=0, peak=1, avg=0.50, elapsed=553ms
    <profiler>
    prepare: org.jpos.jcard.PrepareContext [0.1/0.1]
    prepare: org.jpos.jcard.Switch [0.1/0.2]
    prepare: org.jpos.jcard.rest.ValidateParams [0.2/0.5]
    prepare: org.jpos.transaction.Open [0.8/1.3]
    prepare: org.jpos.jcard.CreateTranLog [2.4/3.8]
    prepare: org.jpos.jcard.CheckCustomer [8.7/12.5]
    prepare: org.jpos.jcard.CheckWallet [7.3/19.9]
    prepare: org.jpos.jcard.rest.WalletTransactionParticipant [487.2/507.2]
    prepare: org.jpos.transaction.Close [0.1/507.3]
    prepare: org.jpos.jcard.rest.TriggerResponse [0.0/507.4]
    prepare: org.jpos.transaction.Debug [0.0/507.5]
    commit: org.jpos.transaction.Close [27.0/534.5]
    commit: org.jpos.jcard.rest.TriggerResponse [0.1/534.7]
    commit: org.jpos.transaction.Debug [18.3/553.0]
    end [1.0/554.1]
    </profiler>
  </debug>
</log>

Hope you consider using the TransactionManager the next time you need to write a REST API call.

Preliminary OSGi support

As of f19a445d we added support for OSGi.

The jar task produces the jPOS OSGi bundle (in build/libs directory)

The bundleFull task creates a bigger bundle that include jPOS dependencies in its lib directory under the name jpos-1.9.3-SNAPSHOT-bundle-full.jar (precompiled versions can be downloaded from jpos-1.9.3-SNAPSHOT.jar and jpos-1.9.3-SNAPSHOT-bundle-full.jar in the jPOS Bundles repository.

There’s a new qnode module that you can use to test the jPOS bundle (if you don’t have an OSGi container already installed). You can go to qnode directory (sibling with the jpos one), run gradle installApp and then copy your bundle to the ‘bundles’ directory.

Here is a full session:

cd jpos
gradle bundleFull

cd ../qnode
gradle installApp

cp ../jpos/build/libs/jpos-1.9.3-SNAPSHOT-bundle-full.jar build/install/qnode/bundle
build/install/qnode/bin/qnode

This should launch Q2 installed as a Bundle in an OSGi container (in this case, Apache Felix).

@apr