First take on the PPIPNHD command.
authorWerner Koch <wk@gnupg.org>
Fri, 13 Jun 2014 06:51:15 +0000 (08:51 +0200)
committerWerner Koch <wk@gnupg.org>
Fri, 13 Jun 2014 06:51:15 +0000 (08:51 +0200)
* src/ppipnhd.c: New.
* src/paypal.h: New.
* src/paypal-ipn.c: New.
* src/Makefile.am: Add new files.
* src/connection.c (release_connection_obj): Move close to ...
(shutdown_connection_obj): new.
(connection_handler): Add command PPIPNHD.
--

Note that the verification does not yet work.

.gitignore
src/Makefile.am
src/connection.c
src/paypal-ipn.c [new file with mode: 0644]
src/paypal.h [new file with mode: 0644]
src/ppipnhd.c [new file with mode: 0644]

index ab15052..31313b6 100644 (file)
@@ -36,3 +36,4 @@
 /src/payproc-jrnl
 /src/libcommon.a
 /src/libcommonpth.a
+/src/ppipnhd
index 7e47336..146fe17 100644 (file)
@@ -18,7 +18,7 @@
 
 EXTRA_DIST = cJSON.readme tls-ca.pem
 
-bin_PROGRAMS = payprocd payproc-jrnl
+bin_PROGRAMS = payprocd payproc-jrnl ppipnhd
 noinst_PROGRAMS = $(module_tests) t-http
 noinst_LIBRARIES = libcommon.a libcommonpth.a
 
@@ -48,6 +48,7 @@ payprocd_SOURCES = \
        payprocd.c payprocd.h \
        connection.c connection.h \
        stripe.c stripe.h \
+       paypal-ipn.c paypal.h \
        tlssupport.c tlssupport.h \
        cred.c cred.h \
        journal.c journal.h \
@@ -62,6 +63,9 @@ payprocd_LDADD = -lm libcommonpth.a \
 payproc_jrnl_SOURCES = \
         payproc-jrnl.c
 
+ppipnhd_SOURCES = ppipnhd.c
+ppipnhd_CFLAGS =
+ppipnhd_LDADD =
 
 module_tests = t-connection
 
@@ -78,7 +82,7 @@ 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 journal.c session.c \
-                      $(t_common_sources)
+t_connection_SOURCES = t-connection.c stripe.c paypal-ipn.c \
+                       journal.c session.c $(t_common_sources)
 t_connection_CFLAGS  = $(t_common_cflags) $(LIBGCRYPT_CFLAGS)
 t_connection_LDADD   = $(t_common_ldadd) $(LIBGCRYPT_LIBS)
index 1033d3c..7ba6358 100644 (file)
@@ -29,6 +29,7 @@
 #include "estream.h"
 #include "payprocd.h"
 #include "stripe.h"
+#include "paypal.h"
 #include "journal.h"
 #include "session.h"
 #include "connection.h"
@@ -101,6 +102,25 @@ init_connection_obj (conn_t conn, int fd)
 }
 
 
+/* Shutdown a connection.  This is used by asynchronous calls to tell
+   the client that the request has been received and processing will
+   continue.  */
+void
+shutdown_connection_obj (conn_t conn)
+{
+  if (conn->stream)
+    {
+      es_fclose (conn->stream);
+      conn->stream = NULL;
+    }
+  if (conn->fd != -1)
+    {
+      close (conn->fd);
+      conn->fd = -1;
+    }
+}
+
+
 /* Release a connection object.  */
 void
 release_connection_obj (conn_t conn)
@@ -108,9 +128,7 @@ release_connection_obj (conn_t conn)
   if (!conn)
     return;
 
-  es_fclose (conn->stream);
-  if (conn->fd != -1)
-    close (conn->fd);
+  shutdown_connection_obj (conn);
 
   xfree (conn->command);
   keyvalue_release (conn->dataitems);
@@ -898,6 +916,14 @@ connection_handler (conn_t conn)
     err = cmd_chargecard (conn, cmdargs);
   else if ((cmdargs = has_leading_keyword (conn->command, "CHECKAMOUNT")))
     err = cmd_checkamount (conn, cmdargs);
+  else if ((cmdargs = has_leading_keyword (conn->command, "PPIPNHD")))
+    {
+      /* This is an asynchronous call.  Thus send okay, close the
+         socket, and only then process the IPN.  */
+      es_fputs ("OK\n\n", conn->stream);
+      shutdown_connection_obj (conn);
+      paypal_proc_ipn (conn->idno, &conn->dataitems);
+    }
   else if ((cmdargs = has_leading_keyword (conn->command, "GETINFO")))
     err = cmd_getinfo (conn, cmdargs);
   else if ((cmdargs = has_leading_keyword (conn->command, "PING")))
@@ -909,6 +935,8 @@ connection_handler (conn_t conn)
       for (kv = conn->dataitems; kv; kv = kv->next)
         es_fprintf (conn->stream, "%s: %s\n", kv->name, kv->value);
     }
-  es_fprintf (conn->stream, "\n");
+
+  if (conn->stream)
+    es_fprintf (conn->stream, "\n");
 
 }
diff --git a/src/paypal-ipn.c b/src/paypal-ipn.c
new file mode 100644 (file)
index 0000000..a1d4262
--- /dev/null
@@ -0,0 +1,191 @@
+/* paypal-ipn.c - Paypal IPN processing.
+ * 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/>.
+ */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#include "util.h"
+#include "logging.h"
+#include "estream.h"
+#include "http.h"
+#include "membuf.h"
+#include "payprocd.h"
+#include "paypal.h"
+
+
+/* Perform a call to paypal.com.  KEYSTRING is the secret key, METHOD
+   is the method without the version (e.g. "tokens") and DATA the
+   individual part to be appended to the URL (e.g. a token-id).  If
+   FORMDATA is not NULL, a POST operaion is used with that data instead
+   of the default GET operation.  On success the function returns 0
+   and a status code at R_STATUS.  The data send with certain status
+   code is stored in parsed format at R_JSON - this might be NULL.  */
+static gpg_error_t
+call_verify (int live, const char *request)
+{
+  gpg_error_t err;
+  const char *url;
+  http_session_t session = NULL;
+  http_t http = NULL;
+  estream_t fp;
+  unsigned int status;
+  const char cmd[] = "cmd=_notify-validate&";
+  char response[20];
+
+  url = (live? "https://www.paypal.com/cgi-bin/webscr"
+         /**/: "https://www.sandbox.paypal.com/cgi-bin/webscr");
+
+
+  err = http_session_new (&session, NULL);
+  if (err)
+    goto leave;
+
+  err = http_open (&http,
+                   HTTP_REQ_POST,
+                   url,
+                   NULL,
+                   0,
+                   NULL,
+                   session,
+                   NULL,
+                   NULL);
+  if (err)
+    {
+      log_error ("error accessing '%s': %s\n", url, gpg_strerror (err));
+      goto leave;
+    }
+
+  fp = http_get_write_ptr (http);
+  es_fprintf (fp,
+              "Content-Type: application/x-www-form-urlencoded\r\n"
+              "Content-Length: %zu\r\n", strlen (cmd) + strlen (request));
+
+  http_start_data (http);
+  if (es_fputs (cmd, fp))
+    err = gpg_error_from_syserror ();
+  else if (es_fputs (request, fp))
+    err = gpg_error_from_syserror ();
+
+  if (err)
+    {
+      log_error ("error sending to '%s': %s\n", url, gpg_strerror (err));
+      goto leave;
+    }
+
+  err = http_wait_response (http);
+  if (err)
+    {
+      log_error ("error reading '%s': %s\n", url, gpg_strerror (err));
+      goto leave;
+    }
+
+  status = http_get_status_code (http);
+  if (status != 200)
+    {
+      log_error ("error reading '%s': status=%03d\n", url, status);
+      err = gpg_error (GPG_ERR_GENERAL);
+      goto leave;
+    }
+
+  if (!es_fgets (response, sizeof response, http_get_read_ptr (http)))
+    {
+      err = gpg_error_from_syserror ();
+      log_error ("error reading '%s': %s\n", url, gpg_strerror (err));
+      goto leave;
+    }
+
+  log_debug ("PayPal verification status '%s'\n", response);
+  err = 0;
+
+ leave:
+  http_close (http, 0);
+  http_session_release (session);
+  return err;
+}
+
+
+
+
+
+/* The connection has already been shutdown but the request has been
+   stored in the dictionary DICT.  We extract the original full
+   request, validate it with Paypal and then do something with the
+   received notification.  IDNO is the original connection id for
+   logging.  */
+void
+paypal_proc_ipn (unsigned int idno, keyvalue_t *dict)
+{
+  gpg_error_t err;
+  keyvalue_t kv;
+  char *request;
+  keyvalue_t form;
+
+  if ((kv = keyvalue_find (*dict, "Request")))
+    keyvalue_remove_nl (kv);
+  request = keyvalue_snatch (*dict, "Request");
+  if (!request || !*request)
+    {
+      log_error ("ppipnhd %u: no request given\n", idno);
+      xfree (request);
+      return;
+    }
+
+  log_info ("ppipnhd %u: length of request=%zu\n", idno, strlen (request));
+
+  /* Parse it into a dictionary.  */
+  err = parse_www_form_urlencoded (&form, request);
+  if (err)
+    {
+      log_error ("ppipnhd %u: error parsing request: %s\n",
+                 idno, gpg_strerror (err));
+      goto leave;
+    }
+
+  for (kv = form; kv; kv = kv->next)
+    log_printkeyval ("  ", kv->name, kv->value);
+
+  /* To avoid useless verification against Paypal we first check the
+     mail address.  */
+  if (strcmp (keyvalue_get_string (form, "receiver_email"),"sales@g10code.com"))
+    {
+      log_error ("ppipnhd %u: wrong receiver_email\n", idno);
+      log_printval ("  mail=", keyvalue_get_string (form, "receiver_email"));
+      goto leave;
+    }
+
+  if (call_verify (!keyvalue_get_int (form, "test_ipn"), request))
+    {
+      log_error ("ppipnhd %u: IPN is not authentic\n", idno);
+      goto leave;
+    }
+
+  log_info ("ppipnhd %u: IPN is okay\n", idno);
+
+  /* Check for duplicates.  */
+
+  /* Check status of transaction.  */
+
+
+ leave:
+  xfree (request);
+  keyvalue_release (form);
+}
diff --git a/src/paypal.h b/src/paypal.h
new file mode 100644 (file)
index 0000000..e41887a
--- /dev/null
@@ -0,0 +1,27 @@
+/* paypal.h - Definitions to access the paypal.com service
+ * 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 PAYPAL_H
+#define PAYPAL_H
+
+/*-- paypal-ipn.c --*/
+void paypal_proc_ipn (unsigned int idno, keyvalue_t *dict);
+
+
+#endif /*PAYPAL_H*/
diff --git a/src/ppipnhd.c b/src/ppipnhd.c
new file mode 100644 (file)
index 0000000..cf58675
--- /dev/null
@@ -0,0 +1,211 @@
+/* ppipnhd.c - PayPal IPN Handler CGI.
+ * 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/>.
+ */
+
+/* This is a CGI acting as a proxy for IPN messages from PayPal.  It
+   merely reads the request, passes it on to payprocd, and sends back
+   a 200 HTTP response.  */
+
+#ifdef HAVE_CONFIG_H
+# include <config.h>
+#endif
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <errno.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <sys/un.h>
+
+#define PGM "ppipnhd"
+#define MAX_REQUEST (64*1024)
+
+
+/* Allow building standalone.  */
+#ifndef PAYPROCD_SOCKET_NAME
+#define PAYPROCD_SOCKET_NAME "/var/run/payproc/dameon"
+#endif
+#ifndef PACKAGE_NAME
+#define PACKAGE_NAME "payproc"
+#endif
+#ifndef PACKAGE_VERSION
+#define PACKAGE_VERSION __DATE__
+#endif
+
+
+static void
+print_status (int n, const char *text)
+{
+  printf ("Status: %d %s\r\n", n, text);
+}
+
+
+static void
+exit_status (int n, const char *text)
+{
+  print_status (n, text);
+  fputs ("Content-Type: text/plain\r\n\r\n", stdout);
+  exit (0);
+}
+
+
+/* Connect to the daemon and return stdio stream for the connected a
+   socket.  On error returns NULL and sets ERRNO.  */
+static FILE *
+connect_daemon (const char *name)
+{
+  int sock;
+  struct sockaddr_un addr_un;
+  struct sockaddr    *addrp;
+  size_t addrlen;
+  FILE *fp;
+
+  if (strlen (name)+1 >= sizeof addr_un.sun_path)
+    {
+      errno = EINVAL;
+      return NULL;
+    }
+
+  memset (&addr_un, 0, sizeof addr_un);
+  addr_un.sun_family = AF_LOCAL;
+  strncpy (addr_un.sun_path, name, sizeof (addr_un.sun_path) - 1);
+  addr_un.sun_path[sizeof (addr_un.sun_path) - 1] = 0;
+  addrlen = SUN_LEN (&addr_un);
+  addrp = (struct sockaddr *)&addr_un;
+
+  sock = socket (AF_LOCAL, SOCK_STREAM, 0);
+  if (sock == -1)
+    return NULL;
+
+  if (connect (sock, addrp, addrlen))
+    {
+      int saveerr = errno;
+      close (sock);
+      errno = saveerr;
+      return NULL;
+    }
+
+  fp = fdopen (sock, "r+b");
+  if (!fp)
+    {
+      int saveerr = errno;
+      close (sock);
+      errno = saveerr;
+      return NULL;
+    }
+
+  return fp;
+}
+
+
+/* Send the payload to the daemon.  Does not return an error.  */
+static void
+send_to_daemon (const char *buffer)
+{
+  FILE *fp;
+  int n, c;
+
+  fp = connect_daemon (PAYPROCD_SOCKET_NAME);
+  if (!fp)
+    exit_status (500, "Error connecting payprocd");
+
+  fputs ("PPIPNHD\nRequest: ", fp);
+  n = 9;
+  while ((c = *buffer++))
+    {
+      if (n==1024)
+        {
+          putc ('\n', fp);
+          putc (' ', fp);
+          n = 0;
+        }
+      putc (c, fp);
+      n++;
+    }
+  putc ('\n', fp);
+  putc ('\n', fp);
+  if (ferror (fp))
+    exit_status (500, "Error writing to payprocd");
+
+  /* Payproc daemon does not return anything real but in case of an
+     error in this code we check whether the response is OK and not ERR.  */
+  c = fgetc (fp);
+  if (c != 'O')
+    exit_status (500, "Error talking to payprocd");
+  /* Eat the response for a clean connection shutdown.  */
+  while (getc (fp) != EOF)
+    ;
+
+  fclose (fp);
+}
+
+
+int
+main (int argc, char **argv)
+{
+  const char *request_method = getenv("REQUEST_METHOD");
+  const char *content_length = getenv("CONTENT_LENGTH");
+  const char *content_type   = getenv("CONTENT_TYPE");
+  unsigned long length, n;
+  char *buffer;
+
+  /* Allow the usual "--version" option only if run outside of the COI
+     environment.  */
+  if (argc > 1 && !strcmp (argv[1], "--version") && !request_method)
+    {
+      fputs (PGM " (" PACKAGE_NAME ") " PACKAGE_VERSION "\n", stdout);
+      return 0;
+    }
+
+  if (!request_method || strcmp (request_method, "POST"))
+    exit_status (501, "Only POST allowed");
+
+  length = content_length? strtoul (content_length, NULL, 10) : 0;
+  if (!length)
+    exit_status (411, "Content-Length missing");
+  if (length >= MAX_REQUEST)
+    exit_status (413, "Payload too large");
+
+  if (!content_type || !*content_type)
+    exit_status (400, "Content-type missing");
+
+  buffer = malloc (length+1);
+  if (!buffer)
+    exit_status (503, "Service currently unavailable");
+
+  if (fread (buffer, length, 1, stdin) != 1)
+    exit_status (400, feof (stdin)? "Payload shorter than indicated"
+                 /*            */ : "Error reading payload");
+  buffer[length] = 0; /* Make it a string.  */
+  for (n=0; n < length; n++)
+    {
+      if (!buffer[n])
+        exit_status (400, "Binary data in payload not allowed");
+      if (strchr (" \t\r\n", buffer[n]))
+        exit_status (400, "Whitespaces in payload not allowed");
+    }
+
+
+  send_to_daemon (buffer);
+  free (buffer);
+
+  print_status (200, "OK");
+  fputs ("Content-Type: text/plain\r\n\r\n", stdout);
+  return 0;
+}