Return error messages and write a journal.
authorWerner Koch <wk@gnupg.org>
Tue, 15 Apr 2014 14:40:48 +0000 (16:40 +0200)
committerWerner Koch <wk@gnupg.org>
Thu, 17 Apr 2014 08:28:43 +0000 (10:28 +0200)
README
configure.ac
src/Makefile.am
src/connection.c
src/journal.c [new file with mode: 0644]
src/journal.h [new file with mode: 0644]
src/payprocd.c
src/stripe.c
src/stripe.h
src/util.c
src/util.h

diff --git a/README b/README
index c71e0bf..ac13d6b 100644 (file)
--- a/README
+++ b/README
@@ -1,2 +1,76 @@
             Payproc - A local payment processor
            -------------------------------------
+
+To run the daemon:
+
+   payprocd --stripe-key /etc/payproc/stripe.testkey \
+            --verbose --log-file /var/log/payproc/payprocd.log \
+            --journal /var/log/payproc/journal
+
+Payprocd creates the socket /var/run/payproc/daemon and expect the CA
+root certificate in /etc/payproc/tls-ca.pem .  The given journal file
+name will be suffixed with the current data and ".log".  The time
+stamp in the journal is given in UTC.
+
+
+Example use:
+
+$ socat - unix-client:/var/run/payproc/daemon
+CARDTOKEN
+Number: 4242424242424242
+Exp-month: 8
+exp-year: 2014
+Cvc: 666
+Name: Juscelino Kubitschek
+
+OK
+Token: tok_103rEw23ctCHxH4kTpC9BDTm
+Last4: 4242
+Live: f
+
+Note that a request starts off with a command (here CARDTOKEN) and is
+terminated by an empty line.  The response is either the "OK" or "ERR"
+optionally followed by words on the line.  The response may then
+consists of header lines and is terminated by a blank line.  Lines can
+be continued on the next line by prefixing a continuation line with a
+space.
+
+The use of CARDTOKEN is not suggested - better use the Stripe's
+checkout Javascript to avoid handling sensitive card data on your
+machine.  Having access to an unused card token, it is possible to
+charge the card:
+
+$ socat - unix-client:/var/run/payproc/daemon
+CHARGECARD
+Card-Token: tok_103rEw23ctCHxH4kTpC9BDTm
+Currency: USD
+Amount: 17.50
+Desc: OpenPGP card for use with GnuPG
+Stmt-Desc: Openpgp card
+Meta[name]: Juscelino Kubitschek
+Meta[ship-to]: Pal├ício da Alvorada
+ 70000 Brasilia
+ Brazil
+
+OK
+_amount: 1750
+Currency: usd
+Live: f
+Charge-Id: ch_103rEw23ctCHxH4ktmJ5na8N
+
+An arbitrary number of Meta header lines may be used in the request,
+they will all be written to the journal as key-value pairs.  An
+example for an error return is:
+
+ERR 1 (General error)
+failure: incorrect_number
+failure-mesg: Your card number is incorrect.
+Name: Juscelino Kubitschek
+
+The "failure" data line contains a short description of the error.  It
+may be returned to the user.  If a "failure-mesg" line is returned,
+that may be returned verbatim to the user.  There is no guarantee that
+a "failure" line will be preset.  However, the number after ERR is a
+gpg-error error code and may be show to the user.  The description
+after the number is usually the gpg_strerror of the error code but may
+also be a more specific human readable string.
index 47a04ca..b0a9b76 100644 (file)
@@ -196,7 +196,7 @@ AC_DECL_SYS_SIGLIST
 # Checks for library functions.
 #
 AC_MSG_NOTICE([checking for library functions])
-AC_CHECK_FUNCS([strerror strlwr])
+AC_CHECK_FUNCS([strerror strlwr gmtime_r])
 
 # For http.c
 AC_CHECK_FUNCS([strtoull])
index 4fe27bd..4b5386a 100644 (file)
@@ -39,6 +39,7 @@ payprocd_SOURCES = \
        stripe.c stripe.h \
        tlssupport.c tlssupport.h \
        cred.c cred.h \
+       journal.c journal.h \
        $(utility_sources)
 
 noinst_PROGRAMS = $(module_tests) t-http
@@ -58,6 +59,6 @@ t_http_SOURCES = t-http.c $(t_common_sources)
 t_http_CFLAGS  = $(t_common_cflags)
 t_http_LDADD   = $(t_common_ldadd)
 
-t_connection_SOURCES = t-connection.c stripe.c $(t_common_sources)
+t_connection_SOURCES = t-connection.c stripe.c journal.c $(t_common_sources)
 t_connection_CFLAGS  = $(t_common_cflags)
 t_connection_LDADD   = $(t_common_ldadd)
index 64b2fca..9e90062 100644 (file)
@@ -29,6 +29,7 @@
 #include "estream.h"
 #include "payprocd.h"
 #include "stripe.h"
+#include "journal.h"
 #include "connection.h"
 
 /* Maximum length of an input line.  */
@@ -177,7 +178,7 @@ store_data_line (conn_t conn, char *line)
       /* Continuation.  */
       if (!conn->dataitems)
         return gpg_error (GPG_ERR_PROTOCOL_VIOLATION);
-      return keyvalue_append_to_last (conn->dataitems, line);
+      return keyvalue_append_with_nl (conn->dataitems, line+1);
     }
 
   /* A name must start with a letter.  Note that for items used only
@@ -253,7 +254,6 @@ read_request (conn_t conn)
         buffer[--n] = 0;
     }
 
-  log_debug ("recvd cmd: '%s'\n", buffer);
   conn->command = xtrystrdup (buffer);
   if (!conn->command)
     {
@@ -297,7 +297,6 @@ read_request (conn_t conn)
             buffer[--n] = 0;
         }
 
-      log_debug ("recvd dat: '%s'\n", buffer);
       if (*buffer)
         {
           err = store_data_line (conn, buffer);
@@ -315,6 +314,32 @@ read_request (conn_t conn)
 }
 
 
+static void
+write_data_line (keyvalue_t kv, estream_t fp)
+{
+  const char *value;
+
+  if (!kv)
+    return;
+  value = kv->value;
+  if (!value)
+    return;
+  es_fputs (kv->name, fp);
+  es_fputs (": ", fp);
+  for ( ; *value; value++)
+    {
+      if (*value == '\n')
+        {
+          if (value[1])
+            es_fputs ("\n ", fp);
+        }
+      else
+        es_putc (*value, fp);
+    }
+  es_putc ('\n', fp);
+}
+
+
 \f
 /*
  * Helper functions.
@@ -386,6 +411,26 @@ convert_amount (const char *string, int decdigits)
 }
 
 
+/* Retrun a string with the amount computed from CENTS.  DECDIGITS
+   gives the number of post decimal positions in CENTS.  Return NULL
+   on error.  */
+static char *
+reconvert_amount (int cents, int decdigits)
+{
+  unsigned int tens;
+  int i;
+
+  if (decdigits <= 0)
+    return es_asprintf ("%d", cents);
+  else
+    {
+      for (tens=1, i=0; i < decdigits; i++)
+        tens *= 10;
+      return es_asprintf ("%d.%0*d", cents / tens, decdigits, cents % tens);
+    }
+}
+
+
 
 \f
 /* The CARDTOKEN command creates a token for a card.  The following
@@ -409,7 +454,6 @@ cmd_cardtoken (conn_t conn, char *args)
 {
   gpg_error_t err;
   keyvalue_t dict = conn->dataitems;
-  keyvalue_t result = NULL;
   keyvalue_t kv;
   const char *s;
   int aint;
@@ -444,18 +488,23 @@ cmd_cardtoken (conn_t conn, char *args)
       goto leave;
     }
 
-
-  err = stripe_create_card_token (conn->dataitems, &result);
+  err = stripe_create_card_token (&conn->dataitems);
 
  leave:
   if (err)
-    es_fprintf (conn->stream, "ERR %d (%s)\n", err,
-                conn->errdesc? conn->errdesc : gpg_strerror (err));
+    {
+      es_fprintf (conn->stream, "ERR %d (%s)\n", err,
+                  conn->errdesc? conn->errdesc : gpg_strerror (err));
+      write_data_line (keyvalue_find (conn->dataitems, "failure"),
+                       conn->stream);
+      write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
+                       conn->stream);
+    }
   else
     es_fprintf (conn->stream, "OK\n");
-  for (kv = result; kv; kv = kv->next)
-    es_fprintf (conn->stream, "%s: %s\n", kv->name, kv->value);
-  keyvalue_release (result);
+  for (kv = conn->dataitems; kv; kv = kv->next)
+    if (kv->name[0] >= 'A' && kv->name[0] < 'Z')
+      write_data_line (kv, conn->stream);
 
   return err;
 }
@@ -476,7 +525,7 @@ cmd_cardtoken (conn_t conn, char *args)
    Stmt-Desc:  Optional string to be displayed on the credit
                card statement.  Will be truncated at about 15 characters.
    Email:      Optional contact mail address of the customer
-   Meta[NAME]: Meta data further described by NAME.  This is used convey
+   Meta[NAME]: Meta data further described by NAME.  This is used ro convey
                application specific data to the log file.
 
    On success these items are returned:
@@ -492,11 +541,11 @@ cmd_chargecard (conn_t conn, char *args)
 {
   gpg_error_t err;
   keyvalue_t dict = conn->dataitems;
-  keyvalue_t result = NULL;
   keyvalue_t kv;
   const char *s;
   unsigned int cents;
   int decdigs;
+  char *buf = NULL;
 
   (void)args;
 
@@ -530,29 +579,47 @@ cmd_chargecard (conn_t conn, char *args)
     }
 
   /* Let's ask Stripe to process it.  */
-  err = stripe_charge_card (conn->dataitems, &result);
+  err = stripe_charge_card (&conn->dataitems);
+  if (err)
+    goto leave;
+
+  buf = reconvert_amount (keyvalue_get_int (conn->dataitems, "_amount"),
+                          decdigs);
+  if (!buf)
+    {
+      err = gpg_error_from_syserror ();
+      conn->errdesc = "error converting _amount";
+      goto leave;
+    }
+  err = keyvalue_put (&conn->dataitems, "Amount", buf);
+  if (err)
+    goto leave;
+  jrnl_store_charge_record (conn->dataitems);
 
  leave:
   if (err)
-    es_fprintf (conn->stream, "ERR %d (%s)\n", err,
-                conn->errdesc? conn->errdesc : gpg_strerror (err));
+    {
+      es_fprintf (conn->stream, "ERR %d (%s)\n", err,
+                  conn->errdesc? conn->errdesc : gpg_strerror (err));
+      write_data_line (keyvalue_find (conn->dataitems, "failure"),
+                       conn->stream);
+      write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
+                       conn->stream);
+    }
   else
     es_fprintf (conn->stream, "OK\n");
-  for (kv = result; kv; kv = kv->next)
-    es_fprintf (conn->stream, "%s: %s\n", kv->name, kv->value);
-  keyvalue_release (result);
-
+  for (kv = conn->dataitems; kv; kv = kv->next)
+    if (kv->name[0] >= 'A' && kv->name[0] < 'Z')
+      write_data_line (kv, conn->stream);
+  es_free (buf);
   return err;
 }
 
 
 \f
 /* GETINFO is a multipurpose command to return certain config data. It
-   requires a subcommand:
-
-     list-currencies
-
-
+   requires a subcommand.  See the online help for a list of
+   subcommands.
  */
 static gpg_error_t
 cmd_getinfo (conn_t conn, char *args)
@@ -566,11 +633,21 @@ cmd_getinfo (conn_t conn, char *args)
         es_fprintf (conn->stream, "# %s - %s\n",
                     currency_table[i].name, currency_table[i].desc);
     }
+  else if (has_leading_keyword (args, "version"))
+    {
+      es_fputs ("OK " PACKAGE_VERSION "\n", conn->stream);
+    }
+  else if (has_leading_keyword (args, "pid"))
+    {
+      es_fprintf (conn->stream, "OK %u\n", (unsigned int)getpid());
+    }
   else
     {
       es_fputs ("ERR 1 (Unknown sub-command)\n"
                 "# Supported sub-commands are:\n"
-                "#   list-currencies  - List supported currencies\n"
+                "#   list-currencies    List supported currencies\n"
+                "#   version            Show the version of this daemon\n"
+                "#   pid                Show the pid of this process\n"
                 , conn->stream);
     }
 
diff --git a/src/journal.c b/src/journal.c
new file mode 100644 (file)
index 0000000..9be2ac2
--- /dev/null
@@ -0,0 +1,342 @@
+/* journal.c - Write journal file
+ * Copyright (C) 2014 g10 Code GmbH
+ *
+ * This file is part of Payproc.
+ *
+ * Payproc is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Payproc is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* The journal file is written with one line per transaction.  Thus a
+   line may be arbitrary long.  The fields of the records are
+   delimited with colons and percent escaping is used.  Percent
+   escaping has the advantage that unescaping can be done in-place and
+   it is well defined.  Backslash escaping would be more complex to
+   handle and won't allow for easy spitting into fields (e.g. using
+   cut(1)).  This tool is for a Unix system and thus we only use a LF
+   as record (line) terminating character.  To allow for structured
+   fields, the content of such a structured field consist of key-value
+   pairs delimited by an ampersand (like HTTP form data).
+
+   Current definition of the journal:
+
+   | No | Name     | Description                                    |
+   |----+----------+------------------------------------------------|
+   |    | date     | UTC the record was created (yyyymmddThhmmss)   |
+   |    | type     | Record type                                    |
+   |    |          | - := sync mark record                          |
+   |    |          | $ := system record                             |
+   |    |          | C := credit card charge                        |
+   |    |          | R := credit card refund                        |
+   |    | account  | Even numbers are test accounts.                |
+   |    | currency | 3 letter ISO code for the currency (lowercase) |
+   |    | amount   | Amount with decimal point                      |
+   |    | desc     | Description for this transaction               |
+   |    | email    | Email address                                  |
+   |    | meta     | Structured field with additional data          |
+   |    | last4    | The last 4 digits of the card                  |
+   |    | paygw    | Payment gateway (0=n/a, 1=stripe.com)          |
+   |    | chargeid | Charge id
+   |    | blntxid  | Balance transaction id                         |
+   |----+----------+------------------------------------------------|
+
+   Because of the multithreaded operation it may happen that records
+   are not properly sorted by date.  To avoid problems with log file
+   rotating a new log file is created for each day.
+
+   This is a simple log which does not account for potential crashes
+   or disk full conditions.  Thus it is possible that a record for a
+   fully charged transaction was not written to disk.  The remedy for
+   this would be the use of an extra record written right before a
+   Stripe transaction.  However, this is for now too much overhead.
+ */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <time.h>
+#include <npth.h>
+
+#include "util.h"
+#include "logging.h"
+#include "estream.h"
+#include "payprocd.h"
+#include "http.h"
+#include "journal.h"
+
+
+/* Infor about an open log file.  */
+struct logfile_s
+{
+  char *basename;  /* The base name of the file.  */
+  char *fullname;  /* The full name of the file.  */
+  FILE *fp;
+  char suffix[8+1];
+} logfile;
+static npth_mutex_t logfile_lock = NPTH_MUTEX_INITIALIZER;
+
+
+/* A severe error was encountered.  Stop the process as sson as
+   possible but first give other connections a chance to
+   terminate.  */
+static void
+severe_error (void)
+{
+  /* FIXME: stop only this thread and wait for other threads.  */
+  exit (4);
+}
+
+
+/* Write the log to the log file.  */
+static void
+write_log (const char *buffer)
+{
+  int res;
+
+  if (!logfile.basename)
+    return;  /* Journal not enabled.  */
+
+  res = npth_mutex_lock (&logfile_lock);
+  if (res)
+    log_fatal ("failed to acquire journal writing lock: %s\n",
+               gpg_strerror (gpg_error_from_errno (res)));
+
+
+  if (!logfile.fp || strncmp (logfile.suffix, buffer, 8))
+    {
+      if (logfile.fp && fclose (logfile.fp))
+        {
+          log_error ("error closing '%s': %s\n",
+                     logfile.fullname,
+                     gpg_strerror (gpg_error_from_syserror()));
+          npth_mutex_unlock (&logfile_lock);
+          severe_error ();
+        }
+
+      strncpy (logfile.suffix, buffer, 8);
+      logfile.suffix[8] = 0;
+
+      xfree (logfile.fullname);
+      logfile.fullname = strconcat (logfile.basename, "-", logfile.suffix,
+                                    ".log", NULL);
+      if (!logfile.fullname || !(logfile.fp = fopen (logfile.fullname, "a")))
+        {
+          log_error ("error opening '%s': %s\n",
+                     logfile.fullname,
+                     gpg_strerror (gpg_error_from_syserror()));
+          npth_mutex_unlock (&logfile_lock);
+          severe_error ();
+        }
+    }
+
+  if (fputs (buffer, logfile.fp) == EOF || fflush (logfile.fp))
+    {
+      log_error ("error writing to logfile '%s': %s\n",
+                 logfile.fullname, gpg_strerror (gpg_error_from_syserror()));
+      npth_mutex_unlock (&logfile_lock);
+      severe_error ();
+    }
+
+  res = npth_mutex_unlock (&logfile_lock);
+  if (res)
+    log_fatal ("failed to release journal writing lock: %s\n",
+               gpg_strerror (gpg_error_from_errno (res)));
+}
+
+
+
+/* Close the stream FP and put its data into the queue.  */
+static void
+write_and_close_fp (estream_t fp)
+{
+  void *buffer;
+  size_t buflen;
+
+  /* Write a LF and an extra Nul so that we can use snatched memory as
+     a C-string.  */
+  if (es_fwrite ("\n", 2, 1, fp) != 1
+      || es_fclose_snatch (fp, &buffer, &buflen))
+    {
+      log_error ("error closing memory stream for the journal: %s\n",
+                 gpg_strerror (gpg_error_from_syserror()));
+      severe_error ();
+    }
+  if (buflen < 16)
+    {
+      log_error ("internal error: journal record too short (%s)\n",
+                 (char*)buffer);
+      severe_error ();
+    }
+
+  write_log (buffer);
+
+  es_free (buffer);
+}
+
+
+
+static void
+put_current_time (estream_t fp)
+{
+  time_t atime = time (NULL);
+  struct tm *tp;
+
+  if (atime == (time_t)(-1))
+    {
+      log_error ("time() failed: %s\n",
+                 gpg_strerror (gpg_error_from_syserror()));
+      severe_error ();
+    }
+
+#ifdef HAVE_GMTIME_R
+  {
+    struct tm tmbuf;
+
+    tp = gmtime_r (&atime, &tmbuf);
+  }
+#else
+  tp = gmtime (&atime);
+#endif
+
+  es_fprintf (fp, "%04d%02d%02dT%02d%02d%02d",
+              1900 + tp->tm_year, tp->tm_mon+1, tp->tm_mday,
+              tp->tm_hour, tp->tm_min, tp->tm_sec);
+}
+
+
+/* Register the journal file.  */
+void
+jrnl_set_file (const char *fname)
+{
+  logfile.basename = xstrdup (fname);
+}
+
+
+static estream_t
+start_record (char type)
+{
+  estream_t fp;
+
+  fp = es_fopenmem (0, "w+");
+  if (!fp)
+    {
+      log_error ("error creating new memory stream for the journal: %s\n",
+                 gpg_strerror (gpg_error_from_syserror()));
+      severe_error ();
+    }
+
+  put_current_time (fp);
+  es_fprintf (fp, ":%c:", type);
+  return fp;
+}
+
+
+static void
+write_escaped_buf (const void *buf, size_t len, estream_t fp)
+{
+  const unsigned char *s;
+
+  for (s = buf; len; s++, len--)
+    {
+      if (!strchr (":&\n\r", *s))
+        es_putc (*s, fp);
+      else
+        es_fprintf (fp, "%%%02X", *s);
+    }
+}
+
+
+static void
+write_escaped (const char *string, estream_t fp)
+{
+  write_escaped_buf (string, strlen (string), fp);
+  es_putc (':', fp);
+}
+
+
+/* Iterate over all keys named "Meta[FOO]" for all FOO and print the
+   meta data field.  */
+static void
+write_meta (keyvalue_t dict, estream_t fp)
+{
+  keyvalue_t kv;
+  const char *s, *name;
+  int any = 0;
+
+  for (kv=dict; kv; kv = kv->next)
+    {
+      if (!strncmp (kv->name, "Meta[", 5) && kv->value && *kv->value)
+        {
+          name = kv->name + 5;
+          for (s = name; *s; s++)
+            {
+              if (*s == ']')
+                break;
+              else if (strchr ("=& \t", *s))
+                break;
+            }
+          if (*s != ']' || s == name || s[1])
+            continue; /* Not a valid key.  */
+          if (!any)
+            any = 1;
+          else
+            es_putc ('&', fp);
+          write_escaped_buf (name, s - name, fp);
+          es_putc ('=', fp);
+          write_escaped_buf (kv->value, strlen (kv->value), fp);
+        }
+    }
+  es_putc (':', fp);
+}
+
+
+/* Store a system record in the journal. */
+void
+jrnl_store_sys_record (const char *text)
+{
+  estream_t fp;
+
+  fp = start_record ('$');
+  es_fputs (":::", fp);
+  write_escaped (text, fp);
+  es_fputs ("::::::", fp);
+  write_and_close_fp (fp);
+}
+
+/* Create a new record and spool it.  There is no error return because
+   the actual transaction has already happened and we want to make
+   sure to write that to the journal.  If we can't do that, we better
+   stop the process to limit the number of records lost.  I consider
+   it better to have a non-working web form than to have too many non
+   recorded transaction. */
+void
+jrnl_store_charge_record (keyvalue_t dict)
+{
+  estream_t fp;
+
+  fp = start_record ('C');
+  es_fprintf (fp, "%d:", (*keyvalue_get_string (dict, "Live") == 't'));
+  write_escaped (keyvalue_get_string (dict, "Currency"), fp);
+  write_escaped (keyvalue_get_string (dict, "Amount"), fp);
+  write_escaped (keyvalue_get_string (dict, "Desc"), fp);
+  write_escaped (keyvalue_get_string (dict, "Email"), fp);
+  write_meta (dict, fp);
+  write_escaped (keyvalue_get_string (dict, "Last4"), fp);
+  es_fputs ("1:", fp);
+  write_escaped (keyvalue_get_string (dict, "Charge-Id"), fp);
+  write_escaped (keyvalue_get_string (dict, "balance-transaction"), fp);
+
+  write_and_close_fp (fp);
+}
diff --git a/src/journal.h b/src/journal.h
new file mode 100644 (file)
index 0000000..eaa2213
--- /dev/null
@@ -0,0 +1,28 @@
+/* journal.h - Definition for journal realted functions
+ * Copyright (C) 2014 g10 Code GmbH
+ *
+ * This file is part of Payproc.
+ *
+ * Payproc is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Payproc is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef JOURNAL_H
+#define JOURNAL_H
+
+void jrnl_set_file (const char *fname);
+void jrnl_store_sys_record (const char *text);
+void jrnl_store_charge_record (keyvalue_t dict);
+
+
+#endif /*JOURNAL_H*/
index 88ac743..6a03b9d 100644 (file)
@@ -38,6 +38,7 @@
 #include "connection.h"
 #include "tlssupport.h"
 #include "cred.h"
+#include "journal.h"
 #include "payprocd.h"
 
 
@@ -67,6 +68,7 @@ enum opt_values
 
     oLogFile   = 500,
     oNoDetach,
+    oJournal,
     oStripeKey,
     oLive,
 
@@ -83,6 +85,7 @@ static ARGPARSE_OPTS opts[] = {
   ARGPARSE_s_s (oLogFile,  "log-file",  "|FILE|write log output to FILE"),
   ARGPARSE_s_s (oAllowUID, "allow-uid", "|N|allow access from uid N"),
   ARGPARSE_s_s (oAllowGID, "allow-gid", "|N|allow access from gid N"),
+  ARGPARSE_s_s (oJournal,  "journal",   "|FILE|write the journal to FILE"),
   ARGPARSE_s_s (oStripeKey,
                 "stripe-key", "|FILE|read key for Stripe account from FILE"),
   ARGPARSE_s_n (oLive, "live",  "enable live mode"),
@@ -199,6 +202,7 @@ main (int argc, char **argv)
         case oVerbose:  opt.verbose++; break;
         case oNoDetach: opt.nodetach = 1; break;
         case oLogFile:  logfile = pargs.r.ret_str; break;
+        case oJournal:  jrnl_set_file (pargs.r.ret_str); break;
         case oAllowUID: /*FIXME*/ break;
         case oAllowGID: /*FIXME*/ break;
         case oStripeKey: set_stripe_key (pargs.r.ret_str); break;
@@ -463,6 +467,7 @@ launch_server (const char *logfile)
   }
 
   log_info ("payprocd %s started\n", PACKAGE_VERSION);
+  jrnl_store_sys_record ("payprocd "PACKAGE_VERSION" started");
   server_loop (fd);
   close (fd);
 }
@@ -592,8 +597,9 @@ server_loop (int listen_fd)
        }
     }
 
-  cleanup ();
+  jrnl_store_sys_record ("payprocd "PACKAGE_VERSION" stopped");
   log_info ("payprocd %s stopped\n", PACKAGE_VERSION);
+  cleanup ();
   npth_attr_destroy (&tattr);
 }
 
@@ -627,6 +633,8 @@ handle_signal (int signo)
       if (shutdown_pending > 2)
         {
           log_info ("shutdown forced\n");
+          jrnl_store_sys_record ("payprocd "PACKAGE_VERSION
+                                 " stopped (forced)");
           log_info ("payprocd %s stopped\n", PACKAGE_VERSION);
           cleanup ();
           exit (0);
@@ -635,6 +643,7 @@ handle_signal (int signo)
 
     case SIGINT:
       log_info ("SIGINT received - immediate shutdown\n");
+      jrnl_store_sys_record ("payprocd "PACKAGE_VERSION" stopped (SIGINT)");
       log_info( "payprocd %s stopped\n", PACKAGE_VERSION);
       cleanup ();
       exit (0);
index 9192ced..0836285 100644 (file)
@@ -135,8 +135,6 @@ call_stripe (const char *keystring, const char *method, const char *data,
       if (err)
         goto leave;
 
-      log_debug ("formdata: '%s'\n", escaped);
-
       es_fprintf (fp,
                   "Content-Type: application/x-www-form-urlencoded\r\n"
                   "Content-Length: %zu\r\n", strlen (escaped));
@@ -154,7 +152,6 @@ call_stripe (const char *keystring, const char *method, const char *data,
     }
 
   status = http_get_status_code (http);
-  log_info ("get '%s' status=%u\n", url, status);
   *r_status = status;
   if ((status / 100) == 2 || (status / 100) == 4)
     {
@@ -190,9 +187,80 @@ call_stripe (const char *keystring, const char *method, const char *data,
 }
 
 
+/* Extract the error information from JSON and put useful stuff into
+   DICT.  */
+static gpg_error_t
+extract_error_from_json (keyvalue_t *dict, cjson_t json)
+{
+  gpg_error_t err;
+  cjson_t j_error, j_obj;
+  const char *type, *mesg, *code;
+
+  j_error = cJSON_GetObjectItem (json, "error");
+  if (!j_error || !cjson_is_object (j_error))
+    {
+      log_error ("stripe: no proper error object returned\n");
+      return 0; /* Ooops. */
+    }
+
+  j_obj = cJSON_GetObjectItem (j_error, "type");
+  if (!j_obj || !cjson_is_string (j_obj))
+    {
+      log_error ("stripe: error object has no 'type'\n");
+      return 0; /* Ooops. */
+    }
+  type = j_obj->valuestring;
+
+  j_obj = cJSON_GetObjectItem (j_error, "message");
+  if (!j_obj || !cjson_is_string (j_obj))
+    {
+      if (j_obj)
+        log_error ("stripe: error object has no proper 'message'\n");
+      mesg = "";
+    }
+  else
+    mesg = j_obj->valuestring;
+
+  j_obj = cJSON_GetObjectItem (j_error, "code");
+  if (!j_obj || !cjson_is_string (j_obj))
+    {
+      if (j_obj)
+        log_error ("stripe: error object has no proper 'code'\n");
+      code = "";
+    }
+  else
+    code = j_obj->valuestring;
+
+  log_info ("stripe: error: type='%s' code='%s' mesg='%.100s'\n",
+            type, code, mesg);
+
+  if (!strcmp (type, "invalid_request_error"))
+    {
+      err = keyvalue_put (dict, "failure", "invalid request to stripe");
+    }
+  else if (!strcmp (type, "api_error"))
+    {
+      err = keyvalue_put (dict, "failure", "bad request to stripe");
+    }
+  else if (!strcmp (type, "card_error"))
+    {
+      err = keyvalue_put (dict, "failure", *code? code : "card error");
+      if (!err && *mesg)
+        err = keyvalue_put (dict, "failure-mesg", mesg);
+    }
+  else
+    {
+      log_error ("stripe: unknown type '%s' in error object\n", type);
+      err = keyvalue_put (dict, "failure", "unknown error");
+    }
+
+  return err;
+}
+
+
 /* The implementation of CARDTOKEN.  */
 gpg_error_t
-stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result)
+stripe_create_card_token (keyvalue_t *dict)
 {
   gpg_error_t err;
   int status;
@@ -202,9 +270,7 @@ stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result)
   int aint;
   cjson_t j_id, j_livemode, j_card, j_last4;
 
-  *r_result = NULL;
-
-  s = keyvalue_get_string (dict, "Number");
+  s = keyvalue_get_string (*dict, "Number");
   if (!*s)
     {
       err = gpg_error (GPG_ERR_MISSING_VALUE);
@@ -213,8 +279,9 @@ stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result)
   err = keyvalue_put (&query, "card[number]", s);
   if (err)
     goto leave;
+  keyvalue_del (*dict, "Number");
 
-  s = keyvalue_get_string (dict, "Exp-Year");
+  s = keyvalue_get_string (*dict, "Exp-Year");
   if (!*s || (aint = atoi (s)) < 2014 || aint > 2199 )
     {
       err = gpg_error (GPG_ERR_INV_VALUE);
@@ -223,8 +290,9 @@ stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result)
   err = keyvalue_putf (&query, "card[exp_year]", "%d", aint);
   if (err)
     goto leave;
+  keyvalue_del (*dict, "Exp-Year");
 
-  s = keyvalue_get_string (dict, "Exp-Month");
+  s = keyvalue_get_string (*dict, "Exp-Month");
   if (!*s || (aint = atoi (s)) < 1 || aint > 12 )
     {
       err = gpg_error (GPG_ERR_INV_VALUE);
@@ -233,8 +301,9 @@ stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result)
   err = keyvalue_putf (&query, "card[exp_month]", "%d", aint);
   if (err)
     goto leave;
+  keyvalue_del (*dict, "Exp-Month");
 
-  s = keyvalue_get_string (dict, "Cvc");
+  s = keyvalue_get_string (*dict, "Cvc");
   if (!*s || (aint = atoi (s)) < 100 || aint > 9999 )
     {
       err = gpg_error (GPG_ERR_INV_VALUE);
@@ -243,8 +312,9 @@ stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result)
   err = keyvalue_putf (&query, "card[cvc]", "%d", aint);
   if (err)
     goto leave;
+  keyvalue_del (*dict, "Cvc");
 
-  s = keyvalue_get_string (dict, "Name");
+  s = keyvalue_get_string (*dict, "Name");
   if (*s)
     {
       err = keyvalue_put (&query, "card[name]", s);
@@ -256,12 +326,14 @@ stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result)
   err = call_stripe (opt.stripe_secret_key,
                      "tokens", NULL, query, &status, &json);
   log_debug ("call_stripe => %s status=%d\n", gpg_strerror (err), status);
-  if (!err)
-    log_debug ("Result:\n%s\n", cJSON_Print(json));
+  if (err)
+    goto leave;
   if (status != 200)
     {
       log_error ("create_card_token: error: status=%u\n", status);
-      err = gpg_error (GPG_ERR_GENERAL);
+      err = extract_error_from_json (dict, json);
+      if (!err)
+        err = gpg_error (GPG_ERR_GENERAL);
       goto leave;
     }
   j_id = cJSON_GetObjectItem (json, "id");
@@ -287,11 +359,11 @@ stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result)
       goto leave;
     }
 
-  err = keyvalue_put (r_result, "Live", cjson_is_true (j_livemode)?"t":"f");
+  err = keyvalue_put (dict, "Live", cjson_is_true (j_livemode)?"t":"f");
   if (!err)
-    err = keyvalue_put (r_result, "Last4", j_last4->valuestring);
+    err = keyvalue_put (dict, "Last4", j_last4->valuestring);
   if (!err)
-    err = keyvalue_put (r_result, "Token", j_id->valuestring);
+    err = keyvalue_put (dict, "Token", j_id->valuestring);
 
  leave:
   keyvalue_release (query);
@@ -302,18 +374,16 @@ stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result)
 
 /* The implementation of CHARGECARD.  */
 gpg_error_t
-stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
+stripe_charge_card (keyvalue_t *dict)
 {
   gpg_error_t err;
   int status;
   keyvalue_t query = NULL;
   cjson_t json = NULL;
   const char *s;
-  cjson_t j_obj;
-
-  *r_result = NULL;
+  cjson_t j_obj, j_tmp;
 
-  s = keyvalue_get_string (dict, "Currency");
+  s = keyvalue_get_string (*dict, "Currency");
   if (!*s)
     {
       err = gpg_error (GPG_ERR_MISSING_VALUE);
@@ -324,7 +394,7 @@ stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
     goto leave;
 
   /* _amount is the amount in the smallest unit of the currency.  */
-  s = keyvalue_get_string (dict, "_amount");
+  s = keyvalue_get_string (*dict, "_amount");
   if (!*s)
     {
       err = gpg_error (GPG_ERR_MISSING_VALUE);
@@ -334,7 +404,7 @@ stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
   if (err)
     goto leave;
 
-  s = keyvalue_get_string (dict, "Card-Token");
+  s = keyvalue_get_string (*dict, "Card-Token");
   if (!*s)
     {
       err = gpg_error (GPG_ERR_MISSING_VALUE);
@@ -343,8 +413,9 @@ stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
   err = keyvalue_put (&query, "card", s);
   if (err)
     goto leave;
+  keyvalue_del (*dict, "Card-Token");
 
-  s = keyvalue_get_string (dict, "Desc");
+  s = keyvalue_get_string (*dict, "Desc");
   if (*s)
     {
       err = keyvalue_put (&query, "description", s);
@@ -352,7 +423,7 @@ stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
         goto leave;
     }
 
-  s = keyvalue_get_string (dict, "Stmt-Desc");
+  s = keyvalue_get_string (*dict, "Stmt-Desc");
   if (*s)
     {
       err = keyvalue_put (&query, "statement_description", s);
@@ -364,12 +435,15 @@ stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
   err = call_stripe (opt.stripe_secret_key,
                      "charges", NULL, query, &status, &json);
   log_debug ("call_stripe => %s status=%d\n", gpg_strerror (err), status);
-  if (!err)
-    log_debug ("Result:\n%s\n", cJSON_Print(json));
+  if (err)
+    goto leave;
+  /* log_debug ("Result:\n%s\n", cJSON_Print(json)); */
   if (status != 200)
     {
       log_error ("charge_card: error: status=%u\n", status);
-      err = gpg_error (GPG_ERR_GENERAL);
+      err = extract_error_from_json (dict, json);
+      if (!err)
+        err = gpg_error (GPG_ERR_GENERAL);
       goto leave;
     }
 
@@ -380,7 +454,14 @@ stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
       err = gpg_error (GPG_ERR_GENERAL);
       goto leave;
     }
-  err = keyvalue_put (r_result, "Charge-Id", j_obj->valuestring);
+  err = keyvalue_put (dict, "Charge-Id", j_obj->valuestring);
+  if (err)
+    goto leave;
+
+  j_obj = cJSON_GetObjectItem (json, "balance_transaction");
+  err = keyvalue_put (dict, "balance-transaction",
+                      ((j_obj && cjson_is_string (j_obj))?
+                       j_obj->valuestring : NULL));
   if (err)
     goto leave;
 
@@ -391,7 +472,7 @@ stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
       err = gpg_error (GPG_ERR_GENERAL);
       goto leave;
     }
-  err = keyvalue_put (r_result, "Live", cjson_is_true (j_obj)?"t":"f");
+  err = keyvalue_put (dict, "Live", cjson_is_true (j_obj)?"t":"f");
   if (err)
     goto leave;
 
@@ -402,7 +483,7 @@ stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
       err = gpg_error (GPG_ERR_GENERAL);
       goto leave;
     }
-  err = keyvalue_put (r_result, "Currency", j_obj->valuestring);
+  err = keyvalue_put (dict, "Currency", j_obj->valuestring);
   if (err)
     goto leave;
 
@@ -413,10 +494,18 @@ stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result)
       err = gpg_error (GPG_ERR_GENERAL);
       goto leave;
     }
-  err = keyvalue_putf (r_result, "_amount", "%d", j_obj->valueint);
+  err = keyvalue_putf (dict, "_amount", "%d", j_obj->valueint);
+  if (err)
+    goto leave;
+
+  j_tmp = cJSON_GetObjectItem (json, "card");
+  j_obj = j_tmp? cJSON_GetObjectItem (j_tmp, "last4") : NULL;
+  err = keyvalue_put (dict, "Last4", ((j_obj && cjson_is_string (j_obj))?
+                                        j_obj->valuestring : NULL));
   if (err)
     goto leave;
 
+
  leave:
   keyvalue_release (query);
   cJSON_Delete (json);
index 2c71866..37b48c6 100644 (file)
@@ -20,8 +20,8 @@
 #ifndef STRIPE_H
 #define STRIPE_H
 
-gpg_error_t stripe_create_card_token (keyvalue_t dict, keyvalue_t *r_result);
-gpg_error_t stripe_charge_card (keyvalue_t dict, keyvalue_t *r_result);
+gpg_error_t stripe_create_card_token (keyvalue_t *dict);
+gpg_error_t stripe_charge_card (keyvalue_t *dict);
 
 
 #endif /*STRIPE_H*/
index 13b21ad..093d60a 100644 (file)
@@ -194,6 +194,17 @@ trim_spaces (char *str)
 
 \f
 keyvalue_t
+keyvalue_find (keyvalue_t list, const char *key)
+{
+  keyvalue_t kv;
+
+  for (kv = list; kv; kv = kv->next)
+    if (!strcmp (kv->name, key))
+      return kv;
+  return NULL;
+}
+
+static keyvalue_t
 keyvalue_create (const char *key, const char *value)
 {
   keyvalue_t kv;
@@ -216,16 +227,13 @@ keyvalue_create (const char *key, const char *value)
 
 /* Append the string VALUE to the current value of KV.  */
 gpg_error_t
-keyvalue_append_to_last (keyvalue_t kv, const char *value)
+keyvalue_append_with_nl (keyvalue_t kv, const char *value)
 {
-  size_t n;
   char *p;
 
-  n = strlen (kv->value) + strlen (value);
-  p = xtrymalloc (n+1);
+  p = strconcat (kv->value, "\n", value, NULL);
   if (!p)
     return gpg_err_code_from_syserror ();
-  strcpy (stpcpy (p, kv->value), value);
   xfree (kv->value);
   kv->value = p;
   return 0;
@@ -236,25 +244,52 @@ gpg_error_t
 keyvalue_put (keyvalue_t *list, const char *key, const char *value)
 {
   keyvalue_t kv;
+  char *buf;
 
-  if (!key || !*key || !value)
+  if (!key || !*key)
     return gpg_error (GPG_ERR_INV_VALUE);
 
-  kv = keyvalue_create (key, value);
-  if (!kv)
-    return gpg_error_from_syserror ();
-  kv->next = *list;
-  *list = kv;
+  kv = keyvalue_find (*list, key);
+  if (kv) /* Update.  */
+    {
+      if (value)
+        {
+          buf = xtrystrdup (value);
+          if (!buf)
+            return gpg_error_from_syserror ();
+        }
+      else
+        buf = NULL;
+      xfree (kv->value);
+      kv->value = buf;
+    }
+  else if (value) /* Insert.  */
+    {
+      kv = keyvalue_create (key, value);
+      if (!kv)
+        return gpg_error_from_syserror ();
+      kv->next = *list;
+      *list = kv;
+    }
   return 0;
 }
 
 
 gpg_error_t
+keyvalue_del (keyvalue_t list, const char *key)
+{
+  /* LIST won't change due to the del operation.  */
+  return keyvalue_put (&list, key, NULL);
+}
+
+
+
+gpg_error_t
 keyvalue_putf (keyvalue_t *list, const char *key, const char *format, ...)
 {
+  gpg_error_t err;
   va_list arg_ptr;
   char *value;
-  keyvalue_t kv;
 
   if (!key || !*key)
     return gpg_error (GPG_ERR_INV_VALUE);
@@ -265,12 +300,10 @@ keyvalue_putf (keyvalue_t *list, const char *key, const char *format, ...)
   if (!value)
     return gpg_error_from_syserror ();
 
-  kv = keyvalue_create (key, value);
-  if (!kv)
-    return gpg_error_from_syserror ();
-  kv->next = *list;
-  *list = kv;
-  return 0;
+  err = keyvalue_put (list, key, value);
+  if (err)
+    es_free (value);
+  return err;
 }
 
 
@@ -304,3 +337,13 @@ keyvalue_get_string (keyvalue_t list, const char *key)
   const char *s = keyvalue_get (list, key);
   return s? s: "";
 }
+
+
+int
+keyvalue_get_int (keyvalue_t list, const char *key)
+{
+  const char *s = keyvalue_get (list, key);
+  if (!s)
+    return 0;
+  return atoi (s);
+}
index 68ae341..1d7b183 100644 (file)
@@ -114,15 +114,17 @@ struct keyvalue_s
 
 typedef struct keyvalue_s *keyvalue_t;
 
-keyvalue_t keyvalue_create (const char *key, const char *value);
-gpg_error_t keyvalue_append_to_last (keyvalue_t kv, const char *value);
+gpg_error_t keyvalue_append_with_nl (keyvalue_t kv, const char *value);
 gpg_error_t keyvalue_put (keyvalue_t *list,
                                const char *key, const char *value);
+gpg_error_t keyvalue_del (keyvalue_t list, const char *key);
 gpg_error_t keyvalue_putf (keyvalue_t *list, const char *key,
                            const char *format, ...) JNLIB_GCC_A_PRINTF (3,4);
 void keyvalue_release (keyvalue_t kv);
+keyvalue_t keyvalue_find (keyvalue_t list, const char *key);
 const char *keyvalue_get (keyvalue_t list, const char *key);
 const char *keyvalue_get_string (keyvalue_t list, const char *key);
+int         keyvalue_get_int (keyvalue_t list, const char *key);