Page MenuHome GnuPG

No OneTemporary

diff --git a/src/commands.c b/src/commands.c
index 36cc886..e0e417e 100644
--- a/src/commands.c
+++ b/src/commands.c
@@ -1,1413 +1,1414 @@
/* commands.c - Handle a client request.
* Copyright (C) 2014, 2015, 2017 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 "payprocd.h"
#include "stripe.h"
#include "paypal.h"
#include "journal.h"
#include "session.h"
#include "currency.h"
#include "preorder.h"
#include "protocol-io.h"
#include "mbox-util.h"
#include "commands.h"
/* Helper macro for the cmd_ handlers. */
#define set_error(a,b) \
do { \
err = gpg_error (GPG_ERR_ ## a); \
conn->errdesc = (b); \
} while (0)
/* Object describing a connection. */
struct conn_s
{
unsigned int idno; /* Connection id for logging. */
int fd; /* File descriptor for this connection. */
estream_t stream; /* The corresponding stream object. */
/* N.B. The stream object may only be used
by the connection thread. */
char *command; /* The command line (malloced). */
keyvalue_t dataitems; /* The data items. */
const char *errdesc; /* Optional description of an error. */
};
/* Allocate a new conenction object and return it. Returns
NULL on error and sets ERRNO. */
conn_t
new_connection_obj (void)
{
static unsigned int counter;
conn_t conn;
conn = xtrycalloc (1, sizeof *conn);
if (conn)
{
conn->idno = ++counter;
conn->fd = -1;
}
return conn;
}
/* Initialize a connection object which has been allocated with
new_connection_obj. FD is the file descriptor for the
connection. */
void
init_connection_obj (conn_t conn, int fd)
{
conn->fd = 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)
{
if (!conn)
return;
shutdown_connection_obj (conn);
xfree (conn->command);
keyvalue_release (conn->dataitems);
xfree (conn);
}
/* Return the file descriptor for the conenction CONN. */
int
fd_from_connection_obj (conn_t conn)
{
return conn->fd;
}
unsigned int
id_from_connection_obj (conn_t conn)
{
return conn->idno;
}
static void
write_data_value (const char *value, estream_t fp)
{
if (!value)
value = "";
for ( ; *value; value++)
{
if (*value == '\n')
{
if (value[1])
es_fputs ("\n ", fp);
}
else
es_putc (*value, fp);
}
es_putc ('\n', fp);
}
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);
write_data_value (value, fp);
if (opt.debug_client)
log_debug ("client-rsp: %s: %s\n", kv->name, kv->value? kv->value:"");
}
static void
write_data_line_direct (const char *name, const char *value, estream_t fp)
{
if (!value)
return;
es_fputs (name, fp);
es_fputs (": ", fp);
write_data_value (value, fp);
if (opt.debug_client)
log_debug ("client-rsp: %s: %s\n", name, value);
}
static void
write_ok_line (estream_t fp)
{
es_fputs ("OK\n", fp);
if (opt.debug_client)
log_debug ("client-rsp: OK\n");
}
static void
write_ok_linef (estream_t fp, const char *format, ...)
{
va_list arg_ptr;
char *buffer;
va_start (arg_ptr, format);
buffer = gpgrt_vbsprintf (format, arg_ptr);
va_end (arg_ptr);
es_fprintf (fp, "OK %s\n", buffer? buffer : "[out of core]");
if (opt.debug_client)
log_debug ("client-rsp: OK %s\n", buffer? buffer : "[out of core]");
es_free (buffer);
}
static void
write_err_line (gpg_error_t err, const char *desc, estream_t fp)
{
es_fprintf (fp, "ERR %d (%s)\n",
err, desc? desc : gpg_strerror (err));
if (opt.debug_client)
log_debug ("client-rsp: ERR %d (%s)\n",
err, desc? desc : gpg_strerror (err));
}
static void
write_rem_line (const char *comment, estream_t fp)
{
es_fprintf (fp, "# %s\n", comment);
if (opt.debug_client)
log_debug ("client-rsp: # %s\n", comment);
}
static void
write_rem_linef (estream_t fp, const char *format, ...)
{
va_list arg_ptr;
char *buffer;
va_start (arg_ptr, format);
buffer = gpgrt_vbsprintf (format, arg_ptr);
va_end (arg_ptr);
es_fprintf (fp, "# %s\n", buffer? buffer : "[out of core]");
if (opt.debug_client)
log_debug ("client-rsp: # %s\n", buffer? buffer : "[out of core]");
+ es_free (buffer);
}
/* SESSION is a multipurpose command to help implement a state-full
service. Note that the state information is intentional not
persistent and thus won't survive a daemon restart.
The following sub-commands are available:
create [TTL]
Create a new session
A new session is created and the provided data dictionary is
stored by payprocd for future requests. The data dictionary is
optional. On success the returned data has an "_SESSID" item
which is to be used for all further requests. If TTL has been
given this is used instead of the defaul TTL value.
destroy SESSID
Destroy a session.
This shall be used to free the internal storage required for the
session and to avoid leaving sensitive information in RAM.
get SESSID
Get data from a session.
Return the data stored in the session identified by SESSID.
put SESSID
Put data into a session.
Store or update the given data in the session. Deleting an item
from the session dictionary is possible by putting an empty
string for it.
alias SESSID
Create an alias for the session.
On success the returned data has an "_ALIASID" item which is to
be used for all further alias related requests.
dealias ALIASID
Destroy the given ALIAS.
This does not destroy the session.
sessid ALIASID
Return the session id for an alias.
On success the returned data has an "_SESSID" item.
*/
static gpg_error_t
cmd_session (conn_t conn, char *args)
{
gpg_error_t err;
keyvalue_t kv;
char *options;
char *sessid = NULL;
char *aliasid = NULL;
char *errdesc;
if ((options = has_leading_keyword (args, "create")))
{
int ttl = atoi (options);
err = session_create (ttl, conn->dataitems, &sessid);
keyvalue_release (conn->dataitems);
conn->dataitems = NULL;
}
else if ((options = has_leading_keyword (args, "get")))
{
keyvalue_release (conn->dataitems);
conn->dataitems = NULL;
err = session_get (options, &conn->dataitems);
}
else if ((options = has_leading_keyword (args, "put")))
{
err = session_put (options, conn->dataitems);
if (gpg_err_code (err) == GPG_ERR_ENOMEM)
{
/* We are tight on memory - better destroy the session so
that the caller can't try over and over again. */
session_destroy (options);
}
keyvalue_release (conn->dataitems);
conn->dataitems = NULL;
}
else if ((options = has_leading_keyword (args, "destroy")))
{
err = session_destroy (options);
keyvalue_release (conn->dataitems);
conn->dataitems = NULL;
}
else if ((options = has_leading_keyword (args, "alias")))
{
err = session_create_alias (options, &aliasid);
keyvalue_release (conn->dataitems);
conn->dataitems = NULL;
}
else if ((options = has_leading_keyword (args, "dealias")))
{
err = session_destroy_alias (options);
keyvalue_release (conn->dataitems);
conn->dataitems = NULL;
}
else if ((options = has_leading_keyword (args, "sessid")))
{
keyvalue_release (conn->dataitems);
conn->dataitems = NULL;
err = session_get_sessid (options, &sessid);
}
else
{
write_err_line (1, "Unknown sub-command", conn->stream);
write_rem_line ("Supported sub-commands are:", conn->stream);
write_rem_line (" create [TTL]", conn->stream);
write_rem_line (" get SESSID", conn->stream);
write_rem_line (" put SESSID", conn->stream);
write_rem_line (" destroy SESSID", conn->stream);
write_rem_line (" alias SESSID", conn->stream);
write_rem_line (" dealias ALIASID", conn->stream);
write_rem_line (" sessid ALIASID", conn->stream);
return 0;
}
switch (gpg_err_code (err))
{
case GPG_ERR_LIMIT_REACHED:
errdesc = "Too many active sessions or too many aliases for a session";
break;
case GPG_ERR_NOT_FOUND:
errdesc = "No such session or alias or session timed out";
break;
case GPG_ERR_INV_NAME:
errdesc = "Invalid session or alias id";
break;
default: errdesc = NULL;
}
if (err)
write_err_line (err, errdesc, conn->stream);
else
{
write_ok_line (conn->stream);
write_data_line_direct ("_SESSID", sessid, conn->stream);
write_data_line_direct ("_ALIASID", aliasid, conn->stream);
for (kv = conn->dataitems; kv; kv = kv->next)
if (kv->name[0] >= 'A' && kv->name[0] < 'Z')
write_data_line (kv, conn->stream);
}
xfree (sessid);
xfree (aliasid);
return err;
}
/* The CARDTOKEN command creates a token for a card. The following
values are expected in the dataitems:
Number: The number of the card
Exp-Year: The expiration year (2014..2199)
Exp-Month: The expiration month (1..12)
Cvc: The CVS number (100..9999)
Name: Name of the card holder (optional)
On success these items are returned:
Token: The one time use token
Last4: The last 4 digits of the card for display
Live: Set to 'f' in test mode or 't' in live mode.
*/
static gpg_error_t
cmd_cardtoken (conn_t conn, char *args)
{
gpg_error_t err;
keyvalue_t dict = conn->dataitems;
keyvalue_t kv;
const char *s;
int aint;
(void)args;
s = keyvalue_get_string (dict, "Number");
if (!*s)
{
set_error (MISSING_VALUE, "Credit card number not given");
goto leave;
}
s = keyvalue_get_string (dict, "Exp-Year");
if (!*s || (aint = atoi (s)) < 2014 || aint > 2199 )
{
set_error (INV_VALUE, "Expiration year out of range");
goto leave;
}
s = keyvalue_get_string (dict, "Exp-Month");
if (!*s || (aint = atoi (s)) < 1 || aint > 12 )
{
set_error (INV_VALUE, "Invalid expiration month");
goto leave;
}
s = keyvalue_get_string (dict, "Cvc");
if (!*s || (aint = atoi (s)) < 100 || aint > 9999 )
{
set_error (INV_VALUE, "The CVC has not 2 or 4 digits");
goto leave;
}
err = stripe_create_card_token (&conn->dataitems);
leave:
if (err)
{
write_err_line (err, conn->errdesc, conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure"),
conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
conn->stream);
}
else
write_ok_line (conn->stream);
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;
}
/* The CHARGECARD command charges the given amount to a card. The
* following values are expected in the dataitems:
*
* Amount: The amount to charge with optional decimal fraction.
* Currency: A 3 letter currency code (EUR, USD, GBP, JPY)
* Recur: An optional recurrence interval: 0 = not recurring,
* 1 = yearly, 4 = quarterly, 12 = monthly.
* Card-Token: The token returned by the CARDTOKEN command.
* Capture: Optional; defaults to true. If set to false
* this command creates only an authorization.
* The command CAPTURECHARGE must then be used
* to actually charge the card. [currently ignored]
* Desc: Optional description of the charge.
* 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.
* For recurring donations this is required.
* Meta[NAME]: Meta data further described by NAME. This is used to convey
* application specific data to the log file.
*
* On success these items are returned:
*
* Charge-Id: The ID describing this charge
* Live: Set to 'f' in test mode or 't' in live mode.
* Currency: The currency of the charge.
* Amount: The charged amount with optional decimal fraction.
* Recur: 0 or the Recur value. To cope with too small amounts,
* this and then also Amount may be changed from the request.
* account-id: Our account id for recurring payments
* _timestamp: The timestamp as written to the journal
*
*/
static gpg_error_t
cmd_chargecard (conn_t conn, char *args)
{
gpg_error_t err;
keyvalue_t dict = conn->dataitems;
keyvalue_t kv;
const char *s;
unsigned int cents;
int decdigs;
char *buf = NULL;
int recur;
(void)args;
/* Get Recurrence value or replace by default. */
s = keyvalue_get_string (dict, "Recur");
if (!valid_recur_p (s, &recur))
{
set_error (MISSING_VALUE, "Invalid value for 'Recur'");
goto leave;
}
err = keyvalue_putf (&conn->dataitems, "Recur", "%d", recur);
dict = conn->dataitems;
if (err)
goto leave;
/* Get currency and amount. */
s = keyvalue_get_string (dict, "Currency");
if (!valid_currency_p (s, &decdigs))
{
set_error (MISSING_VALUE, "Currency missing or not supported");
goto leave;
}
s = keyvalue_get_string (dict, "Amount");
if (!*s || !(cents = convert_amount (s, decdigs)))
{
set_error (MISSING_VALUE, "Amount missing or invalid");
goto leave;
}
err = keyvalue_putf (&conn->dataitems, "_amount", "%u", cents);
dict = conn->dataitems;
if (err)
goto leave;
/* We only support the use of a card token and no direct supply of
card details. This makes it easier to protect or audit the
actual credit card data. The token may only be used once. */
s = keyvalue_get_string (dict, "Card-Token");
if (!*s)
{
set_error (MISSING_VALUE, "Card-Token missing");
goto leave;
}
if (recur)
{
/* Let's ask Stripe to create a subscription. */
s = keyvalue_get_string (dict, "Email");
if (!is_valid_mailbox (s))
{
set_error (MISSING_VALUE,
"Recurring payment but no valid 'Email' given");
goto leave;
}
/* Find or create a plan. */
err = stripe_find_create_plan (&conn->dataitems);
dict = conn->dataitems;
if (err)
{
conn->errdesc = "error creating a Plan";
goto leave;
}
/* Create a Subscription using the just plan from above and the
* Card-Token supplied to this command. */
err = stripe_create_subscription (&conn->dataitems);
dict = conn->dataitems;
if (err)
{
conn->errdesc = "error creating a Subscription";
goto leave;
}
}
else
{
/* Let's ask Stripe to process it. */
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, PAYMENT_SERVICE_STRIPE, recur);
leave:
if (err)
{
write_err_line (err, conn->errdesc, conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure"),
conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
conn->stream);
}
else
write_ok_line (conn->stream);
for (kv = conn->dataitems; kv; kv = kv->next)
if (kv->name[0] >= 'A' && kv->name[0] < 'Z')
write_data_line (kv, conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "account-id"), conn->stream);
if (!err)
write_data_line (keyvalue_find (conn->dataitems, "_timestamp"),
conn->stream);
es_free (buf);
return err;
}
/* The PPCHECKOUT does a PayPal transaction. Depending on the
sub-command different data items are required.
The following sub-commands are available:
prepare
Start a checkout operation. In this mode the data is collected,
and access code fetched from paypal and a redirect URL returned
to the caller. Required data:
Amount: The amount to charge with optional decimal fraction.
Currency: A 3 letter currency code (EUR, USD, GBP, JPY)
Desc: Optional description of the charge.
Meta[NAME]: Meta data further described by NAME. This is used
to convey application specific data to the log file.
Return-Url: URL to which Paypal redirects.
Cancel-Url: URL to which Paypal redirects on cancel.
Session-Id: Id of the session to be used for storing state. If this
is not given a new session will be created.
Paypal-Xp: An optional Paypa Experience Id.
On success these items are returned:
_SESSID: If Session-Id was not supplied the id of a new session
is returned.
Redirect-Url: The caller must be redirected to this URL for further
processing.
execute
Finish a Paypal checkout operation. Required data:
Alias-Id: The alias id used to access the state from the
prepare command. This should be retrieved from the
Return-Url's "aliasid" parameter which has been
appended to the Return-Url by the prepare sub-command.
Paypal-Payer: Returned by Paypal via the
Return-Url's "PayerID" parameter.
On success these items are returned:
Charge-Id: The ID describing this charge
Live: Set to 'f' in test mode or 't' in live mode.
Currency: The currency of the charge.
Amount: The charged amount with optional decimal fraction.
Email: The mail address as told by Paypal.
_timestamp: The timestamp as written to the journal
*/
static gpg_error_t
cmd_ppcheckout (conn_t conn, char *args)
{
gpg_error_t err;
char *options;
int decdigs;
char *newsessid = NULL;
keyvalue_t dict = conn->dataitems;
keyvalue_t kv;
const char *s;
int execmode = 0;
if ((options = has_leading_keyword (args, "prepare")))
{
/* Get currency and amount. */
s = keyvalue_get_string (dict, "Currency");
if (!valid_currency_p (s, &decdigs))
{
set_error (MISSING_VALUE, "Currency missing or not supported");
goto leave;
}
s = keyvalue_get_string (dict, "Amount");
if (!*s || !convert_amount (s, decdigs))
{
set_error (MISSING_VALUE, "Amount missing or invalid");
goto leave;
}
/* Create a session if no session-id has been supplied. */
s = keyvalue_get_string (dict, "Session-Id");
if (!*s)
{
err = session_create (0, NULL, &newsessid);
if (err)
goto leave;
err = keyvalue_put (&conn->dataitems, "Session-Id", newsessid);
if (err)
goto leave;
dict = conn->dataitems;
}
/* Let's ask Paypal to process it. */
err = paypal_checkout_prepare (&conn->dataitems);
if (err)
goto leave;
dict = conn->dataitems;
}
else if ((options = has_leading_keyword (args, "execute")))
{
execmode = 1;
err = paypal_checkout_execute (&conn->dataitems);
if (err)
goto leave;
dict = conn->dataitems;
jrnl_store_charge_record (&conn->dataitems, PAYMENT_SERVICE_PAYPAL, 0);
dict = conn->dataitems;
}
else
{
write_err_line (1, "Unknown sub-command", conn->stream);
write_rem_line ("Supported sub-commands are:", conn->stream);
write_rem_line (" prepare", conn->stream);
write_rem_line (" execute", conn->stream);
return 0;
}
leave:
if (err)
{
write_err_line (err, conn->errdesc, conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure"),
conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
conn->stream);
}
else
{
write_ok_line (conn->stream);
}
for (kv = conn->dataitems; kv; kv = kv->next)
if ((!execmode && !strcmp (kv->name, "Redirect-Url"))
|| (execmode && (!strcmp (kv->name, "Charge-Id")
|| !strcmp (kv->name, "Live")
|| !strcmp (kv->name, "Email")
|| !strcmp (kv->name, "Currency")
|| !strcmp (kv->name, "Amount"))))
write_data_line (kv, conn->stream);
if (!err)
{
write_data_line_direct ("_SESSID", newsessid, conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "_timestamp"),
conn->stream);
}
xfree (newsessid);
return err;
}
/* The SEPAPREORDER command adds a preorder record for a SEPA payment
into the preorder database. The following values are expected in
the dataitems:
Amount: The amount to charge with optional decimal fraction.
Currency: If given its value must be EUR.
Desc: Optional description of the charge.
Email: Optional contact mail address of the customer
Meta[NAME]: Meta data further described by NAME. This is used to convey
application specific data to the log file.
On success these items are returned:
Sepa-Ref: A string to be returned to the caller.
Amount: The reformatted amount
Currency: The value "EUR"
*/
static gpg_error_t
cmd_sepapreorder (conn_t conn, char *args)
{
gpg_error_t err;
keyvalue_t dict = conn->dataitems;
keyvalue_t kv;
const char *s;
unsigned int cents;
char *buf = NULL;
(void)args;
/* Get currency and amount. */
s = keyvalue_get (dict, "Currency");
if (!s)
{
err = keyvalue_put (&conn->dataitems, "Currency", "EUR");
if (err)
goto leave;
dict = conn->dataitems;
}
else if (strcasecmp (s, "EUR"))
{
set_error (INV_VALUE, "Currency must be \"EUR\" if given");
goto leave;
}
s = keyvalue_get_string (dict, "Amount");
if (!*s || !(cents = convert_amount (s, 2)))
{
set_error (MISSING_VALUE, "Amount missing or invalid");
goto leave;
}
err = keyvalue_putf (&conn->dataitems, "_amount", "%u", cents);
dict = conn->dataitems;
if (err)
goto leave;
buf = reconvert_amount (keyvalue_get_int (conn->dataitems, "_amount"), 2);
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;
/* Note that the next function does not only store the record but
also creates the SEPA-Ref value and puts it into dataitems. This
is to make sure SEPA-Ref is a unique key for the preorder db. */
err = preorder_store_record (&conn->dataitems);
leave:
if (err)
{
write_err_line (err, conn->errdesc, conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure"),
conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
conn->stream);
}
else
write_ok_line (conn->stream);
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;
}
/* The COMMITPREORDER command updates a preorder record and logs the data.
Sepa-Ref: The key referencing the preorder
Amount: The actual amount of the payment.
Currency: If given its value must be EUR.
On success these items are returned:
Sepa-Ref: The Sepa-Ref string
XXX: FIXME:
*/
static gpg_error_t
cmd_commitpreorder (conn_t conn, char *args)
{
gpg_error_t err;
keyvalue_t dict = conn->dataitems;
unsigned int cents;
keyvalue_t kv;
const char *s;
char *buf = NULL;
(void)args;
s = keyvalue_get_string (dict, "Sepa-Ref");
if (!*s)
{
set_error (MISSING_VALUE, "Key 'Sepa-Ref' not given");
goto leave;
}
/* Get currency and amount. */
s = keyvalue_get (dict, "Currency");
if (!s)
{
err = keyvalue_put (&conn->dataitems, "Currency", "EUR");
if (err)
goto leave;
dict = conn->dataitems;
}
else if (strcasecmp (s, "EUR"))
{
set_error (INV_VALUE, "Currency must be \"EUR\" if given");
goto leave;
}
s = keyvalue_get_string (dict, "Amount");
if (!*s || !(cents = convert_amount (s, 2)))
{
set_error (MISSING_VALUE, "Amount missing or invalid");
goto leave;
}
err = keyvalue_putf (&conn->dataitems, "_amount", "%u", cents);
dict = conn->dataitems;
if (err)
goto leave;
buf = reconvert_amount (keyvalue_get_int (conn->dataitems, "_amount"), 2);
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;
err = preorder_update_record (conn->dataitems);
leave:
if (err)
{
write_err_line (err, conn->errdesc, conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure"),
conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
conn->stream);
}
else
{
write_ok_line (conn->stream);
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;
}
/* The GETPREORDER command retrieves a record from the preorder table.
Sepa-Ref: The key to lookup the rceord.
On success these items are returned:
Sepa-Ref: The Sepa-Ref string
XXX: FIXME:
*/
static gpg_error_t
cmd_getpreorder (conn_t conn, char *args)
{
gpg_error_t err;
keyvalue_t dict = conn->dataitems;
keyvalue_t kv;
const char *s;
char *buf = NULL;
(void)args;
s = keyvalue_get_string (dict, "Sepa-Ref");
if (!*s)
{
set_error (MISSING_VALUE, "Key 'Sepa-Ref' not given");
goto leave;
}
err = preorder_get_record (&conn->dataitems);
leave:
if (err)
{
write_err_line (err, conn->errdesc, conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure"),
conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
conn->stream);
}
else
{
write_ok_line (conn->stream);
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;
}
/* The LISTPREORDER command retrieves a record from the preorder table.
Refnn: The reference suffix (-NN). If this is not given all
records are listed in reverse chronological order.
On success these items are returned:
Count: Number of records
D[n]: A formatted line with the data. N starts at 0.
*/
static gpg_error_t
cmd_listpreorder (conn_t conn, char *args)
{
gpg_error_t err;
char *buf = NULL;
unsigned int n, count;
char key[30];
(void)args;
err = preorder_list_records (&conn->dataitems, &count);
if (err)
{
write_err_line (err, conn->errdesc, conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure"),
conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "failure-mesg"),
conn->stream);
}
else
{
write_ok_line (conn->stream);
snprintf (key, sizeof key, "%u", count);
write_data_line_direct ("Count", key, conn->stream);
for (n=0; n < count; n++)
{
snprintf (key, sizeof key, "D[%u]", n);
write_data_line_direct (key,
keyvalue_get_string (conn->dataitems, key),
conn->stream);
}
}
es_free (buf);
return err;
}
/* The CHECKAMOUNT command checks whether a given amount is within the
* configured limits for payment. It may eventually provide
* additional options. The following values are expected in the
* dataitems:
*
* Amount: The amount to check with optional decimal fraction.
* Currency: A 3 letter currency code (EUR, USD, GBP, JPY)
* Recur: Optional: A recurrence interval: 0 = not recurring,
* 1 = yearly, 4 = quarterly, 12 = monthly.
*
* On success these items are returned:
*
* _amount: The amount converted to an integer (i.e. 10.42 EUR -> 1042)
* Amount: The amount as above; but see also Recur.
* Recur: 0 or the Recur value. To cope with too small amounts,
* this and then also Amount may be changed from the request.
* Limit: If given, the maximum amount acceptable
* Euro: If returned, Amount converted to Euro.
*/
static gpg_error_t
cmd_checkamount (conn_t conn, char *args)
{
gpg_error_t err;
keyvalue_t dict = conn->dataitems;
keyvalue_t kv;
const char *curr;
const char *s;
unsigned int cents;
int decdigs;
char amountbuf[AMOUNTBUF_SIZE];
int recur;
(void)args;
/* Delete items, we want to set. */
keyvalue_del (conn->dataitems, "Limit");
/* Get Recurrence value or replace by default. */
s = keyvalue_get_string (dict, "Recur");
if (!valid_recur_p (s, &recur))
{
set_error (MISSING_VALUE, "Invalid value for 'Recur'");
goto leave;
}
err = keyvalue_putf (&conn->dataitems, "Recur", "%d", recur);
dict = conn->dataitems;
if (err)
goto leave;
/* Get currency and amount. */
curr = keyvalue_get_string (dict, "Currency");
if (!valid_currency_p (curr, &decdigs))
{
set_error (MISSING_VALUE, "Currency missing or not supported");
goto leave;
}
s = keyvalue_get_string (dict, "Amount");
if (!*s || !(cents = convert_amount (s, decdigs)))
{
set_error (MISSING_VALUE, "Amount missing or invalid");
goto leave;
}
if (*convert_currency (amountbuf, sizeof amountbuf, curr, s))
err = keyvalue_put (&conn->dataitems, "Euro", amountbuf);
else
err = 0;
if (err)
goto leave;
err = keyvalue_putf (&conn->dataitems, "_amount", "%u", cents);
dict = conn->dataitems;
if (err)
goto leave;
leave:
if (err)
{
write_err_line (err, conn->errdesc, conn->stream);
}
else
{
write_ok_line (conn->stream);
write_data_line (keyvalue_find (conn->dataitems, "_amount"),
conn->stream);
}
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;
}
/* PPIPNHD is a handler for PayPal notifications.
Note: This is an asynchronous call: We send okay, *close* the
socket, and only then process the IPN. */
static gpg_error_t
cmd_ppipnhd (conn_t conn, char *args)
{
(void)args;
es_fputs ("OK\n\n", conn->stream);
shutdown_connection_obj (conn);
paypal_proc_ipn (&conn->dataitems);
return 0;
}
/* GETINFO is a multipurpose command to return certain config data. It
requires a subcommand. See the online help for a list of
subcommands.
*/
static gpg_error_t
cmd_getinfo (conn_t conn, char *args)
{
int i;
if (has_leading_keyword (args, "list-currencies"))
{
const char *name, *desc;
double rate;
write_ok_line (conn->stream);
for (i=0; (name = get_currency_info (i, &desc, &rate)); i++)
write_rem_linef (conn->stream, "%s %11.4f - %s",
name, rate, desc);
}
else if (has_leading_keyword (args, "version"))
{
write_ok_linef (conn->stream, "%s", PACKAGE_VERSION);
}
else if (has_leading_keyword (args, "pid"))
{
write_ok_linef (conn->stream, "%u", (unsigned int)getpid());
}
else if (has_leading_keyword (args, "live"))
{
if (opt.livemode)
write_ok_line (conn->stream);
else
write_err_line (179, "running in test mode", conn->stream);
}
else
{
write_err_line (1, "Unknown sub-command", conn->stream);
write_rem_line ("Supported sub-commands are:", conn->stream);
write_rem_line (" list-currencies List supported currencies",
conn->stream);
write_rem_line (" version Show the version of this daemon",
conn->stream);
write_rem_line (" pid Show the pid of this process",
conn->stream);
write_rem_line (" live Returns OK if in live mode",
conn->stream);
}
return 0;
}
/* Process a PING command. */
static gpg_error_t
cmd_ping (conn_t conn, char *args)
{
write_ok_linef (conn->stream, "%s", *args? args : "pong");
return 0;
}
/* Process a SHUTDOWN command. */
static gpg_error_t
cmd_shutdown (conn_t conn, char *args)
{
(void)args;
write_ok_linef (conn->stream, "terminating daemon");
shutdown_server ();
return 0;
}
static gpg_error_t cmd_help (conn_t conn, char *args);
/* The table with all commands. */
static struct
{
const char *name;
gpg_error_t (*handler)(conn_t conn, char *args);
int admin_required;
} cmdtbl[] =
{
{ "SESSION", cmd_session },
{ "CARDTOKEN", cmd_cardtoken },
{ "CHARGECARD", cmd_chargecard },
{ "PPCHECKOUT", cmd_ppcheckout },
{ "SEPAPREORDER", cmd_sepapreorder },
{ "CHECKAMOUNT", cmd_checkamount },
{ "PPIPNHD", cmd_ppipnhd },
{ "GETINFO", cmd_getinfo },
{ "PING", cmd_ping },
{ "COMMITPREORDER", cmd_commitpreorder, 1 },
{ "GETPREORDER", cmd_getpreorder, 1 },
{ "LISTPREORDER", cmd_listpreorder, 1 },
{ "SHUTDOWN", cmd_shutdown, 1 },
{ "HELP", cmd_help },
{ NULL, NULL}
};
/* The HELP command lists all commands. */
static gpg_error_t
cmd_help (conn_t conn, char *args)
{
int cmdidx;
(void)args;
write_ok_line (conn->stream);
for (cmdidx=0; cmdtbl[cmdidx].name; cmdidx++)
write_rem_line (cmdtbl[cmdidx].name, conn->stream);
return 0;
}
/* The handler serving a connection. UID is the UID of the client. */
void
connection_handler (conn_t conn, uid_t uid)
{
gpg_error_t err;
keyvalue_t kv;
int cmdidx;
char *cmdargs;
int i;
conn->stream = es_fdopen_nc (conn->fd, "r+,samethread");
if (!conn->stream)
{
err = gpg_error_from_syserror ();
log_error ("failed to open fd %d as stream: %s\n",
conn->fd, gpg_strerror (err));
return;
}
err = protocol_read_request (conn->stream, &conn->command, &conn->dataitems);
if (err)
{
log_error ("reading request failed: %s\n", gpg_strerror (err));
write_err_line (err, NULL, conn->stream);
return;
}
es_fflush (conn->stream);
err = 0;
if (opt.n_allowed_uids)
{
for (i=0; i < opt.n_allowed_uids; i++)
if (opt.allowed_uids[i] == uid)
break;
if (!(i < opt.n_allowed_uids))
{
err = gpg_error (GPG_ERR_EPERM);
write_err_line (err, "User not allowed", conn->stream);
}
}
if (!err)
{
cmdargs = NULL;
for (cmdidx=0; cmdtbl[cmdidx].name; cmdidx++)
if ((cmdargs=has_leading_keyword (conn->command, cmdtbl[cmdidx].name)))
break;
if (cmdargs)
{
err = 0;
if (cmdtbl[cmdidx].admin_required)
{
for (i=0; i < opt.n_allowed_admin_uids; i++)
if (opt.allowed_admin_uids[i] == uid)
break;
if (!(i < opt.n_allowed_admin_uids))
{
err = gpg_error (GPG_ERR_FORBIDDEN);
write_err_line (err, "User is not an admin", conn->stream);
}
}
if (!err)
{
if (opt.debug_client)
{
log_debug ("client-req: %s\n", conn->command);
for (kv = conn->dataitems; kv; kv = kv->next)
log_debug ("client-req: %s: %s\n", kv->name, kv->value);
log_debug ("client-req: \n");
}
err = cmdtbl[cmdidx].handler (conn, cmdargs);
}
}
else
{
write_err_line (1, "Unknown command", conn->stream);
write_data_line_direct ("_cmd", conn->command? conn->command :"",
conn->stream);
for (kv = conn->dataitems; kv; kv = kv->next)
write_data_line_direct (kv->name, kv->value? kv->value:"",
conn->stream);
}
}
if (conn->stream)
es_fprintf (conn->stream, "\n");
}
diff --git a/src/journal.c b/src/journal.c
index e73458a..04ba918 100644
--- a/src/journal.c
+++ b/src/journal.c
@@ -1,297 +1,298 @@
/* 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 |
|----+----------+------------------------------------------------|
| 1 | date | UTC the record was created (yyyymmddThhmmss) |
| 2 | type | Record type |
| | | - := sync mark record |
| | | $ := system record |
| | | C := credit card charge |
| | | R := credit card refund |
| | | S := new subscription |
| | | M := manual added payment |
| 3 | live | 1 if this is not a test account |
| 4 | currency | 3 letter ISO code for the currency (lowercase) |
| 5 | amount | Amount with decimal point |
| 6 | desc | Description for this transaction |
| 7 | mail | Email address |
| 8 | meta | Structured field with additional data |
| 9 | last4 | The last 4 digits of the card |
| 10 | service | Payment service (0=n/a, 1=stripe.com,2=PayPal, |
| | | 3=SEPA, 255=user (PAYMENT_SERVICE_xxx)) |
| 11 | account | Account number |
| 12 | chargeid | Charge id |
| 13 | txid | Transaction id |
| 14 | rtxid | Reference txid (e.g. for refunds) |
| | | For preorders, this is the Sepa-Ref |
| | | For subscriptions this is our account id |
| 15 | euro | amount converted to Euro |
| 16 | recur | For type=S: Recurrence indicator |
| | | (0=none, 1=yearly, 4=quarterly, 12=monthly) |
|----+----------+------------------------------------------------|
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 "payprocd.h"
#include "http.h"
#include "currency.h"
#include "journal.h"
/* Info 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;
/* 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 = NULL;
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);
}
/* Register the journal file. */
void
jrnl_set_file (const char *fname)
{
logfile.basename = xstrdup (fname);
}
static estream_t
start_record (char type, char *timestamp)
{
estream_t fp;
char timestamp_buffer[TIMESTAMP_SIZE];
if (!timestamp)
timestamp = timestamp_buffer;
fp = es_fopenmem (0, "w+,samethread");
if (!fp)
{
log_error ("error creating new memory stream for the journal: %s\n",
gpg_strerror (gpg_error_from_syserror()));
severe_error ();
}
get_current_time (timestamp);
es_fprintf (fp, "%s:%c:", timestamp, type);
return fp;
}
/* Store a system record in the journal. */
void
jrnl_store_sys_record (const char *text)
{
estream_t fp;
fp = start_record ('$', NULL);
es_fputs (":::", fp);
write_escaped (text, fp);
es_fputs ("::::::::::", fp);
write_and_close_fp (fp);
}
/* Store a currency exchange record in the journal. */
void
jrnl_store_exchange_rate_record (const char *currency, double rate)
{
estream_t fp;
fp = start_record ('$', NULL); /* System record. */
es_fprintf (fp,"1:%s:%f:new exchange rate:", currency, rate);
es_fputs ("::::::::1.0:", fp);
write_and_close_fp (fp);
}
/* Create a new record and spool it. DICTP ist the address of the
* data dictionary. SERVICE is the payment service type. If RECUR is
* non-zero a subscription instead of a charge is file. 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. Adds "_timestamp" record
* into DICT. */
void
jrnl_store_charge_record (keyvalue_t *dictp, int service, int recur)
{
estream_t fp;
char timestamp[TIMESTAMP_SIZE];
keyvalue_t dict;
const char *curr, *amnt;
char amountbuf[AMOUNTBUF_SIZE];
fp = start_record (recur? 'S':'C', timestamp);
keyvalue_put (dictp, "_timestamp", timestamp);
dict = *dictp;
es_fprintf (fp, "%d:", (*keyvalue_get_string (dict, "Live") == 't'));
write_escaped ((curr=keyvalue_get_string (dict, "Currency")), fp);
es_putc (':', fp);
write_escaped ((amnt=keyvalue_get_string (dict, "Amount")), fp);
es_putc (':', fp);
write_escaped (keyvalue_get_string (dict, "Desc"), fp);
es_putc (':', fp);
write_escaped (keyvalue_get_string (dict, "Email"), fp);
es_putc (':', fp);
write_meta_field (dict, fp);
es_putc (':', fp);
write_escaped (keyvalue_get_string (dict, "Last4"), fp);
es_fprintf (fp, ":%d:", service);
es_fputs ("1:", fp); /* account */
write_escaped (keyvalue_get_string (dict, "Charge-Id"), fp);
es_putc (':', fp);
write_escaped (keyvalue_get_string (dict, "balance-transaction"), fp);
es_putc (':', fp);
if (service == PAYMENT_SERVICE_SEPA)
write_escaped (keyvalue_get_string (dict, "Sepa-Ref"), fp);
else if (recur)
write_escaped (keyvalue_get_string (dict, "account-id"), fp);
es_fputs (":", fp); /* rtxid */
es_fputs (convert_currency (amountbuf, sizeof amountbuf, curr, amnt), fp);
es_fputs (":", fp); /* euro */
es_fprintf (fp, "%d:", recur); /* recur */
write_and_close_fp (fp);
}
diff --git a/src/payprocd.c b/src/payprocd.c
index 3af6ea4..7a3b2e1 100644
--- a/src/payprocd.c
+++ b/src/payprocd.c
@@ -1,1094 +1,1099 @@
/* payprocd.c - Payproc daemon
* 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 <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <signal.h>
#include <errno.h>
#include <gpg-error.h>
#include <npth.h>
#include <gcrypt.h>
#include <pwd.h>
#include <locale.h> /*(for gpgme)*/
#include <gpgme.h>
#include "util.h"
#include "logging.h"
#include "argparse.h"
#include "commands.h"
#include "tlssupport.h"
#include "cred.h"
#include "journal.h"
#include "session.h"
#include "currency.h"
#include "encrypt.h"
#include "payprocd.h"
/* The interval in seconds to check whether to do housekeeping. */
#define TIMERTICK_INTERVAL 30
/* The interval in seconds to run the housekeeping thread. */
#define HOUSEKEEPING_INTERVAL (120)
/* Flag indicating that the socket shall shall be removed by
cleanup. */
static int remove_socket_flag;
/* Flag to indicate that a shutdown was requested. */
static int shutdown_pending;
/* Number of active connections. */
static int active_connections;
/* The thread specific data key. */
static npth_key_t my_tsd_key;
/* The log file. */
static const char *logfile;
/* Constants to identify the options. */
enum opt_values
{
aNull = 0,
oVerbose = 'v',
oAllowUID = 'U',
oAllowGID = 'G',
oConfig = 'C',
oNoConfig = 500,
oLogFile,
oNoLogFile,
oNoDetach,
oJournal,
oStripeKey,
oPaypalKey,
oLive,
oTest,
oAdminUID,
oAdminGID,
oDatabaseKey,
oBackofficeKey,
oDebugClient,
oDebugStripe,
oLast
};
/* The list of commands and options. */
static ARGPARSE_OPTS opts[] = {
ARGPARSE_group (301, "@Options:\n "),
ARGPARSE_s_n (oLive, "live", "enable live mode"),
ARGPARSE_s_n (oTest, "test", "enable test mode"),
ARGPARSE_s_n (oVerbose, "verbose", "verbose"),
ARGPARSE_s_s (oConfig, "config", "|FILE|read config from FILE"),
ARGPARSE_s_n (oNoConfig, "no-config", "ignore default config file"),
ARGPARSE_s_n (oNoDetach, "no-detach", "run in foreground"),
ARGPARSE_s_s (oLogFile, "log-file", "|FILE|write log output to FILE"),
ARGPARSE_s_n (oNoLogFile,"no-log-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 (oAdminUID, "admin-uid", "|N|allow admin access from uid N"),
ARGPARSE_s_s (oAdminGID, "admin-gid", "|N|allow admin 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_s (oPaypalKey,
"paypal-key", "|FILE|read key for PayPal account from FILE"),
ARGPARSE_s_s (oDatabaseKey,
"database-key", "|FPR|secret key for the database"),
ARGPARSE_s_s (oBackofficeKey,
"backoffice-key", "|FPR|public key for the backoffice"),
ARGPARSE_s_n (oDebugClient, "debug-client", "debug I/O with the client"),
ARGPARSE_s_n (oDebugStripe, "debug-stripe", "debug the Stripe REST"),
ARGPARSE_end ()
};
/* Local prototypes. */
static void cleanup (void);
static void launch_server (void);
static void server_loop (int fd);
static void handle_tick (void);
static void handle_signal (int signo);
static void *connection_thread (void *arg);
static const char *
my_strusage( int level )
{
const char *p;
switch (level)
{
case 11: p = "payprocd"; break;
case 13: p = PACKAGE_VERSION; break;
case 19: p = "Please report bugs to bugs@g10code.com.\n"; break;
case 1:
case 40: p = "Usage: payprocd [options] (-h for help)"; break;
case 41: p = ("Syntax: payprocd [options]\n"
"Start the payment processing daemon\n"); break;
default: p = NULL; break;
}
return p;
}
/* Set the Stripe secret key from file FNAME. */
static void
set_account_key (const char *fname, int service)
{
FILE *fp;
char buf[128];
fp = fopen (fname, "r");
if (!fp)
log_error ("error opening key file '%s': %s\n", fname, strerror (errno));
else
{
if (!fgets (buf, sizeof buf, fp))
log_error ("error reading key from '%s': %s\n",
fname, strerror (errno));
else if (service == 1)
{
trim_spaces (buf);
if (strncmp (buf, "sk_test_", 8) && strncmp (buf, "sk_live_", 8))
log_error ("file '%s' seems not to carry a Stripe secret key\n",
fname);
else
{
xfree (opt.stripe_secret_key);
opt.stripe_secret_key = xstrdup (buf);
}
}
else if (service == 2)
{
trim_spaces (buf);
if (!strchr (buf, ':') && strlen (buf) != 121)
log_error ("file '%s' seems not to carry a PayPal secret key\n",
fname);
else
{
xfree (opt.paypal_secret_key);
opt.paypal_secret_key = xstrdup (buf);
}
}
fclose (fp);
}
}
/* Add the UID taken from STRING to the list of allowed clients. if
ALSO_ADMIN is set, add that uid also to the list of allowed admin
users. */
static void
add_allowed_uid (const char *string, int also_admin)
{
char *buffer;
const char *s;
struct passwd *pw;
uid_t uid;
buffer = xstrdup (string);
trim_spaces (buffer);
string = buffer;
if (!*buffer)
{
xfree (buffer);
return; /* Ignore empty strings. */
}
for (s=string; digitp (s); s++)
;
if (!*s)
{
uid = strtoul (string, NULL, 10);
pw = getpwuid (uid);
if (pw && pw->pw_uid != uid)
pw = NULL;
}
else
pw = getpwnam (string);
if (!pw)
{
log_error ("no such user '%s'\n", string);
xfree (buffer);
return;
}
uid = pw->pw_uid;
if (opt.n_allowed_uids >= DIM (opt.allowed_uids))
{
log_error ("can't add user '%s': Table full\n", string);
xfree (buffer);
return;
}
if (also_admin && opt.n_allowed_admin_uids >= DIM (opt.allowed_admin_uids))
{
log_error ("can't add admin user '%s': Table full\n", string);
xfree (buffer);
return;
}
opt.allowed_uids[opt.n_allowed_uids++] = uid;
if (also_admin)
opt.allowed_admin_uids[opt.n_allowed_admin_uids++] = uid;
}
/* This callback is used by the log functions to return an identifier
for the current thread. */
static int
pid_suffix_callback (unsigned long *r_suffix)
{
unsigned int *idnop;
idnop = npth_getspecific (my_tsd_key);
if (!idnop)
{
*r_suffix = 0;
return 0; /* No suffix. */
}
*r_suffix = *idnop;
return 2; /* Print the suffix in hex format. */
}
/* The config and command line option parser. */
static void
parse_options (int argc, char **argv)
{
ARGPARSE_ARGS pargs;
int orig_argc;
char **orig_argv;
int default_config = 1;
const char *configname = NULL;
unsigned int configlineno;
FILE *configfp = NULL;
int live_or_test = 0;
/* First check whether we have a config file on the commandline. We
* also check for the --test and --live flag to decide on the
* default config name. */
orig_argc = argc;
orig_argv = argv;
pargs.argc = &argc;
pargs.argv = &argv;
pargs.flags= ARGPARSE_FLAG_KEEP | ARGPARSE_FLAG_NOVERSION;
while (arg_parse (&pargs, opts))
{
switch (pargs.r_opt)
{
case oConfig:
case oNoConfig:
default_config = 0; /* Do not use the default config. */
break;
case oLive: opt.livemode = 1; break;
case oTest: opt.livemode = 0; break;
default: break;
}
}
if (default_config)
configname = (opt.livemode? "/etc/payproc/payprocd.conf"
/**/ : "/etc/payproc-test/payprocd.conf");
opt.livemode = 0;
/* Parse the option file and the command line. */
argc = orig_argc;
argv = orig_argv;
pargs.argc = &argc;
pargs.argv = &argv;
pargs.flags = ARGPARSE_FLAG_KEEP;
next_pass:
if (configname)
{
configlineno = 0;
configfp = fopen (configname, "r");
if (!configfp)
{
if (default_config)
log_info ("note: default config file '%s': %s\n",
configname, strerror (errno));
else
{
log_error ("error opening config file '%s': %s\n",
configname, strerror (errno));
exit (2);
}
configname = NULL;
default_config = 0;
}
}
while (optfile_parse (configfp, configname, &configlineno, &pargs, opts))
{
switch (pargs.r_opt)
{
case oVerbose: opt.verbose++; break;
case oDebugClient: opt.debug_client++; break;
case oDebugStripe: opt.debug_stripe++; break;
case oNoDetach: opt.nodetach = 1; break;
case oLogFile: logfile = pargs.r.ret_str; break;
case oNoLogFile: logfile = NULL; break;
case oJournal: jrnl_set_file (pargs.r.ret_str); break;
case oAllowUID: add_allowed_uid (pargs.r.ret_str, 0); break;
case oAllowGID: /*FIXME*/ break;
case oAdminUID: add_allowed_uid (pargs.r.ret_str, 1); break;
case oAdminGID: /*FIXME*/ break;
case oStripeKey: set_account_key (pargs.r.ret_str, 1); break;
case oPaypalKey: set_account_key (pargs.r.ret_str, 2); break;
case oLive: opt.livemode = 1; live_or_test = 1; break;
case oTest: opt.livemode = 0; live_or_test = 1; break;
case oDatabaseKey:
xfree (opt.database_key_fpr);
opt.database_key_fpr = xstrdup (pargs.r.ret_str);
break;
case oBackofficeKey:
xfree (opt.backoffice_key_fpr);
opt.backoffice_key_fpr = xstrdup (pargs.r.ret_str);
break;
case oConfig:
if (!configfp)
{
configname = pargs.r.ret_str;
goto next_pass;
}
/* Ignore this option in config files (no nesting). */
break;
case oNoConfig: break; /* Already handled. */
default:
pargs.err = configfp? ARGPARSE_PRINT_WARNING : ARGPARSE_PRINT_ERROR;
break;
}
}
if (configfp)
{
fclose (configfp);
configfp = NULL;
configname = NULL;
goto next_pass;
}
if (argc)
usage (1);
if (!live_or_test)
log_info ("implicitly using --test\n");
}
int
main (int argc, char **argv)
{
/* Set program name etc. */
set_strusage (my_strusage);
log_set_prefix ("payprocd", JNLIB_LOG_WITH_PREFIX);
/* Make sure that our subsystems are ready. */
gpgrt_init ();
gpgrt_set_syscall_clamp (npth_unprotect, npth_protect);
/* Access the standard estreams as early as possible. If we don't
do this the original stdio streams may have been closed when
_es_get_std_stream is first use and in turn it would connect to
the bit bucket. */
{
int i;
for (i=0; i < 3; i++)
(void)_gpgrt_get_std_stream (i);
}
npth_init ();
if (atexit (cleanup))
{
log_error ("atexit failed\n");
cleanup ();
exit (1);
}
if (!npth_key_create (&my_tsd_key, NULL))
if (!npth_setspecific (my_tsd_key, NULL))
log_set_pid_suffix_cb (pid_suffix_callback);
/* Check that Libgcrypt is suitable. */
gcry_control (GCRYCTL_DISABLE_SECMEM, 0);
if (!gcry_check_version (NEED_LIBGCRYPT_VERSION) )
{
log_fatal ("%s is too old (need %s, have %s)\n", "libgcrypt",
NEED_LIBGCRYPT_VERSION, gcry_check_version (NULL) );
}
/* Initialze processing subsystems. */
init_tls_subsystem ();
/* Init GPGME. */
setlocale (LC_ALL, "");
if (!gpgme_check_version (NEED_GPGME_VERSION))
log_fatal ("%s is too old (need %s, have %s)\n", "gpgme",
NEED_GPGME_VERSION, gpgme_check_version (NULL));
gpgme_set_locale (NULL, LC_CTYPE, setlocale (LC_CTYPE, NULL));
#ifdef LC_MESSAGES
gpgme_set_locale (NULL, LC_MESSAGES, setlocale (LC_MESSAGES, NULL));
#endif
parse_options (argc, argv);
if (opt.livemode && (!opt.stripe_secret_key
|| strncmp (opt.stripe_secret_key, "sk_live_", 8)))
log_error ("live mode requested but no live key given\n");
else if (!opt.livemode
&& opt.stripe_secret_key
&& !strncmp (opt.stripe_secret_key, "sk_live_", 8))
log_error ("test mode requested but live key given\n");
encrypt_setup_keys ();
if (log_get_errorcount (0))
exit (2);
if (opt.verbose)
{
int i, j, star;
log_info ("Mode .........: %s\n", opt.livemode? "live" : "test");
log_info ("Stripe key ...: %s\n", opt.stripe_secret_key? "yes":"no");
log_info ("Paypal key ...: %s\n", opt.paypal_secret_key? "yes":"no");
encrypt_show_keys ();
log_info ("Allowed users :");
for (i=0; i < opt.n_allowed_uids; i++)
{
for (j=star=0; j < opt.n_allowed_admin_uids && !star; j++)
if (opt.allowed_admin_uids[j] == opt.allowed_uids[i])
star = 1;
log_printf (" %lu%s", (unsigned long)opt.allowed_uids[i],
star? "*":"");
}
log_printf ("\n");
}
/* Start the server. */
launch_server ();
return 0;
}
/* Cleanup handler - usually called via atexit. */
static void
cleanup (void)
{
static int done;
char *p;
if (done)
return;
done = 1;
if (remove_socket_flag)
remove (server_socket_name ());
p = opt.database_key_fpr;
opt.database_key_fpr = NULL;
xfree (p);
p = opt.backoffice_key_fpr;
opt.backoffice_key_fpr = NULL;
xfree (p);
encrypt_release_keys ();
}
/* Check whether a daemon is already running on the socket NAME. */
static int
already_running_p (const char *name)
{
struct sockaddr_un *addr;
socklen_t len;
int rc;
int fd;
estream_t stream;
char buffer[256];
fd = socket (AF_UNIX, SOCK_STREAM, 0);
if (fd == -1)
{
log_error ("error creating socket: %s\n",
gpg_strerror (gpg_error_from_syserror()));
exit (2);
}
addr = xcalloc (1, sizeof *addr);
addr->sun_family = AF_UNIX;
if (strlen (name) + 1 >= sizeof (addr->sun_path))
{
log_error ("socket name '%s' is too long\n", name);
exit (2);
}
strcpy (addr->sun_path, name);
len = SUN_LEN (addr);
rc = connect (fd, (struct sockaddr *)addr, len);
if (rc == -1)
{
close (fd);
return 0; /* Probably not running. Well, as long as the
permissions are suitable. */
}
/* Also do an alive check for diagnositc reasons. */
stream = es_fdopen (fd, "r+b,samethread");
if (!stream)
{
log_error ("failed to fdopen connected socket: %s\n",
gpg_strerror (gpg_error_from_syserror()));
close (fd);
return 1; /* Assume it is running. */
}
es_fputs ("PING\n\n", stream);
es_fflush (stream);
if (!es_fgets (buffer, sizeof buffer, stream))
{
log_error ("failed to read PING response from '%s': %s\n", name,
gpg_strerror (gpg_error_from_syserror()));
}
else if (!has_leading_keyword (buffer, "OK"))
{
log_error ("PING command on '%s' failed *%s)\n", name, buffer);
}
es_fclose (stream);
return 1; /* Assume the server is running. */
}
/* Create a Unix domain socket with NAME. Returns the file descriptor
or terminates the process in case of an error. */
static int
create_socket (const char *name)
{
struct sockaddr_un *serv_addr;
socklen_t len;
int fd;
int rc;
fd = socket (AF_UNIX, SOCK_STREAM, 0);
if (fd == -1)
{
log_error ("error creating socket: %s\n",
gpg_strerror (gpg_error_from_syserror()));
exit (2);
}
serv_addr = xmalloc (sizeof (*serv_addr));
memset (serv_addr, 0, sizeof *serv_addr);
serv_addr->sun_family = AF_UNIX;
if (strlen (name) + 1 >= sizeof (serv_addr->sun_path))
{
log_error ("socket name '%s' is too long\n", name);
exit (2);
}
strcpy (serv_addr->sun_path, name);
len = SUN_LEN (serv_addr);
rc = bind (fd, (struct sockaddr*) serv_addr, len);
if (rc == -1 && errno == EADDRINUSE)
{
if (already_running_p (name))
{
log_error ("a payprocd process is already running - "
"not starting a new one\n");
close (fd);
exit (2);
}
/* Remove a stale socket file and try again. */
remove (name);
rc = bind (fd, (struct sockaddr*) serv_addr, len);
}
if (rc == -1)
{
log_error ("error binding socket to '%s': %s\n",
serv_addr->sun_path,
gpg_strerror (gpg_error_from_syserror()));
-
close (fd);
+ xfree (serv_addr);
exit (2);
}
if (chmod (name, (S_IRUSR | S_IWUSR | S_IXUSR
| S_IRGRP | S_IWGRP | S_IXGRP)))
{
log_error ("can't set permissions of '%s': %s\n",
name, gpg_strerror (gpg_error_from_syserror ()));
close (fd);
remove (name);
+ xfree (serv_addr);
exit (2);
}
if (listen (fd, 5 ) == -1)
{
log_error ("listen call failed: %s\n",
gpg_strerror (gpg_error_from_syserror()));
close (fd);
+ remove (name);
+ xfree (serv_addr);
exit (2);
}
+ xfree (serv_addr);
+
if (opt.verbose)
log_info ("listening on socket '%s'\n", serv_addr->sun_path);
return fd;
}
/* Fire up the server. */
static void
launch_server (void)
{
int fd;
fd = create_socket (server_socket_name ());
fflush (NULL);
if (!opt.nodetach)
{
pid_t pid;
pid = fork ();
if (pid == (pid_t)-1)
{
log_fatal ("fork failed: %s\n", strerror (errno) );
exit (1);
}
else if (pid)
{ /* We are the parent */
remove_socket_flag = 0; /* Now owned by the child. */
close (fd);
exit (0);
} /* End parent */
}
/*
* This is the child (or the main process in case of --no-detach)
*/
remove_socket_flag = 1;
if (logfile)
{
log_set_file (logfile);
log_set_prefix (NULL, (JNLIB_LOG_WITH_PREFIX
|JNLIB_LOG_WITH_TIME
|JNLIB_LOG_WITH_PID));
}
else
log_set_prefix (NULL, (JNLIB_LOG_WITH_PREFIX
|JNLIB_LOG_WITH_PID));
/* Detach from tty and put process into a new session */
if (!opt.nodetach )
{
int i;
unsigned int oldflags;
/* Close stdin, stdout and stderr unless it is the log stream */
for (i=0; i <= 2; i++)
{
if (!log_test_fd (i) && i != fd )
{
if ( ! close (i)
&& open ("/dev/null", i? O_WRONLY : O_RDONLY) == -1)
{
log_error ("failed to open '%s': %s\n",
"/dev/null", strerror (errno));
cleanup ();
exit (1);
}
}
}
if (setsid() == -1)
{
log_error ("setsid() failed: %s\n", strerror(errno) );
cleanup ();
exit (1);
}
log_get_prefix (&oldflags);
log_set_prefix (NULL, oldflags | JNLIB_LOG_RUN_DETACHED);
}
if (chdir("/"))
{
log_error ("chdir to / failed: %s\n", strerror (errno));
exit (1);
}
{
struct sigaction sa;
sa.sa_handler = SIG_IGN;
sigemptyset (&sa.sa_mask);
sa.sa_flags = 0;
sigaction (SIGPIPE, &sa, NULL);
}
log_info ("payprocd %s started\n", PACKAGE_VERSION);
jrnl_store_sys_record ("payprocd "PACKAGE_VERSION" started");
read_exchange_rates ();
server_loop (fd);
close (fd);
}
/* Main loop: The loops waits for connection requests and spawn a
working thread after accepting the connection. */
static void
server_loop (int listen_fd)
{
gpg_error_t err;
npth_attr_t tattr;
struct sockaddr_un paddr;
socklen_t plen;
fd_set fdset, read_fdset;
int ret;
int fd;
int nfd;
struct timespec abstime;
struct timespec curtime;
struct timespec timeout;
ret = npth_attr_init (&tattr);
if (ret)
log_fatal ("error allocating thread attributes: %s\n",
strerror (ret));
npth_attr_setdetachstate (&tattr, NPTH_CREATE_DETACHED);
npth_sigev_init ();
npth_sigev_add (SIGHUP);
npth_sigev_add (SIGUSR1);
npth_sigev_add (SIGUSR2);
npth_sigev_add (SIGINT);
npth_sigev_add (SIGTERM);
npth_sigev_fini ();
FD_ZERO (&fdset);
FD_SET (listen_fd, &fdset);
nfd = listen_fd;
npth_clock_gettime (&abstime);
abstime.tv_sec += TIMERTICK_INTERVAL;
for (;;)
{
/* Shutdown test. */
if (shutdown_pending)
{
if (!active_connections)
break; /* ready */
/* Do not accept new connections but keep on running the
loop to cope with the timer events. */
FD_ZERO (&fdset);
}
read_fdset = fdset;
npth_clock_gettime (&curtime);
if (!(npth_timercmp (&curtime, &abstime, <)))
{
/* Timeout. */
handle_tick ();
npth_clock_gettime (&abstime);
abstime.tv_sec += TIMERTICK_INTERVAL;
}
npth_timersub (&abstime, &curtime, &timeout);
ret = npth_pselect (nfd+1, &read_fdset, NULL, NULL, &timeout,
npth_sigev_sigmask ());
err = (ret == -1)? gpg_error_from_syserror () : 0;
{
int signo;
while (npth_sigev_get_pending (&signo))
handle_signal (signo);
}
if (err && gpg_err_code (err) != GPG_ERR_EINTR)
{
log_error ("npth_pselect failed: %s - waiting 1s\n",
gpg_strerror (err));
npth_sleep (1);
continue;
}
if (ret <= 0)
{
/* Interrupt or timeout. To be handled when computing the
next timeout. */
continue;
}
if (!shutdown_pending && FD_ISSET (listen_fd, &read_fdset))
{
conn_t conn;
plen = sizeof paddr;
fd = npth_accept (listen_fd, (struct sockaddr *)&paddr, &plen);
if (fd == -1)
{
err = gpg_error_from_syserror ();
log_error ("accept failed: %s\n", gpg_strerror (err));
}
else if (!(conn = new_connection_obj ()))
{
err = gpg_error_from_syserror ();
log_error ("error allocating connection object: %s\n",
gpg_strerror (err) );
close (fd);
fd = -1;
}
else
{
npth_t thread;
init_connection_obj (conn, fd);
fd = -1; /* Now owned by CONN. */
ret = npth_create (&thread, &tattr, connection_thread, conn);
if (ret)
{
err = gpg_error_from_errno (ret);
log_error ("error spawning connection handler: %s\n",
gpg_strerror (err));
release_connection_obj (conn);
}
}
}
}
jrnl_store_sys_record ("payprocd "PACKAGE_VERSION" stopped");
log_info ("payprocd %s stopped\n", PACKAGE_VERSION);
cleanup ();
npth_attr_destroy (&tattr);
}
#if JNLIB_GCC_HAVE_PUSH_PRAGMA
# pragma GCC push_options
# pragma GCC optimize ("no-strict-overflow")
#endif
static int
time_for_housekeeping_p (time_t now)
{
static time_t last_housekeeping;
if (!last_housekeeping)
last_housekeeping = now;
if (last_housekeeping + HOUSEKEEPING_INTERVAL <= now
|| last_housekeeping > now /*(be prepared for y2038)*/)
{
last_housekeeping = now;
return 1;
}
return 0;
}
#if JNLIB_GCC_HAVE_PUSH_PRAGMA
# pragma GCC pop_options
#endif
/* Thread to do the housekeeping. */
static void *
housekeeping_thread (void *arg)
{
static int sentinel;
static int count;
(void)arg;
count++;
if (sentinel)
{
log_info ("only one cleaning person at a time please\n");
return NULL;
}
sentinel++;
if (opt.verbose > 1)
log_info ("starting housekeeping\n");
session_housekeeping ();
/* Stuff we do only every hour: */
if (count >= 3600 / HOUSEKEEPING_INTERVAL)
{
count = 0;
read_exchange_rates ();
}
if (opt.verbose > 1)
log_info ("finished with housekeeping\n");
sentinel--;
return NULL;
}
/* This is the worker for the ticker. It is called every few seconds
and may only do fast operations. */
static void
handle_tick (void)
{
if (time_for_housekeeping_p (time (NULL)))
{
npth_t thread;
npth_attr_t tattr;
int rc;
rc = npth_attr_init (&tattr);
if (rc)
log_error ("error preparing housekeeping thread: %s\n", strerror (rc));
else
{
npth_attr_setdetachstate (&tattr, NPTH_CREATE_DETACHED);
rc = npth_create (&thread, &tattr, housekeeping_thread, NULL);
if (rc)
log_error ("error spawning housekeeping thread: %s\n",
strerror (rc));
npth_attr_destroy (&tattr);
}
}
}
/* Return the name of the socket. */
const char *
server_socket_name (void)
{
return opt.livemode? PAYPROCD_SOCKET_NAME : PAYPROCD_TEST_SOCKET_NAME;
}
void
shutdown_server (void)
{
kill (getpid(), SIGTERM);
}
/* The signal handler for payprocd. It is expected to be run in its
own thread and not in the context of a signal handler. */
static void
handle_signal (int signo)
{
switch (signo)
{
case SIGHUP:
break;
case SIGUSR1:
log_info ("SIGUSR1 received - nothing to do right now\n");
break;
case SIGUSR2:
log_info ("SIGUSR2 received - nothing to do right now\n");
break;
case SIGTERM:
if (!shutdown_pending)
log_info ("SIGTERM received - shutting down ...\n");
else
log_info ("SIGTERM received - still %d open connections\n",
active_connections);
shutdown_pending++;
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);
}
break;
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);
break;
default:
log_info ("signal %d received - no action defined\n", signo);
}
}
/* A connection thread's main function. */
static void *
connection_thread (void *arg)
{
conn_t conn = arg;
unsigned int idno;
pid_t pid;
uid_t uid;
gid_t gid;
idno = id_from_connection_obj (conn);
npth_setspecific (my_tsd_key, &idno);
if (credentials_from_socket (fd_from_connection_obj (conn), &pid, &uid, &gid))
{
log_error ("credentials missing - closing\n");
goto leave;
}
active_connections++;
if (opt.verbose)
log_info ("new connection - pid=%u uid=%u gid=%u\n",
(unsigned int)pid, (unsigned int)uid, (unsigned int)gid);
connection_handler (conn, uid);
if (opt.verbose)
log_info ("connection terminated\n");
active_connections--;
leave:
release_connection_obj (conn);
npth_setspecific (my_tsd_key, NULL); /* To be safe. */
return NULL;
}
diff --git a/src/util.c b/src/util.c
index 849fc7f..399784f 100644
--- a/src/util.c
+++ b/src/util.c
@@ -1,1135 +1,1134 @@
/* util.c - Genereal utility functions.
* Copyright (C) 1998, 1999, 2000, 2001, 2003, 2004, 2005, 2006, 2007,
* 2008, 2009, 2010 Free Software Foundation, Inc.
* Copyright (C) 2014, 2015 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 <stdio.h>
#include <unistd.h>
#include <stdarg.h>
#include <errno.h>
#include <ctype.h>
#include <time.h>
#include <assert.h>
#include "util.h"
#include "logging.h"
/* The error source number for Payproc. */
gpg_err_source_t default_errsource;
/* A severe error was encountered. Stop the process as soon as
possible but first give other connections a chance to
terminate. */
void
severe_error (void)
{
/* FIXME: stop only this thread and wait for other threads. */
exit (4);
}
static void
out_of_core(void)
{
fputs ("\nfatal: out of memory\n", stderr);
exit (2);
}
void *
xmalloc( size_t n )
{
void *p = malloc( n );
if (!p)
out_of_core ();
return p;
}
void *
xrealloc (void *a, size_t n)
{
void *p = realloc (a, n);
if (!p)
out_of_core();
return p;
}
void *
xcalloc (size_t n, size_t m)
{
void *p = calloc( n, m );
if (!p)
out_of_core ();
return p;
}
char *
xstrdup (const char *string)
{
void *p = xmalloc (strlen(string)+1);
strcpy (p, string);
return p;
}
static char *
do_strconcat (const char *s1, va_list arg_ptr)
{
const char *argv[48];
size_t argc;
size_t needed;
char *buffer, *p;
argc = 0;
argv[argc++] = s1;
needed = strlen (s1);
while (((argv[argc] = va_arg (arg_ptr, const char *))))
{
needed += strlen (argv[argc]);
if (argc >= DIM (argv)-1)
{
gpg_err_set_errno (EINVAL);
return NULL;
}
argc++;
}
needed++;
buffer = xtrymalloc (needed);
if (buffer)
{
for (p = buffer, argc=0; argv[argc]; argc++)
p = stpcpy (p, argv[argc]);
}
return buffer;
}
/* Concatenate the string S1 with all the following strings up to a
NULL. Returns a malloced buffer with the new string or NULL on a
malloc error or if too many arguments are given. */
char *
strconcat (const char *s1, ...)
{
va_list arg_ptr;
char *result;
if (!s1)
result = xtrystrdup ("");
else
{
va_start (arg_ptr, s1);
result = do_strconcat (s1, arg_ptr);
va_end (arg_ptr);
}
return result;
}
/* Upcase all ASCII characters in S. */
char *
ascii_strupr (char *s)
{
char *p = s;
for (p=s; *p; p++ )
if (!(*p & 0x80) && *p >= 'a' && *p <= 'z')
*p &= ~0x20;
return s;
}
/* Lowercase all ASCII characters in S. */
char *
ascii_strlwr (char *s)
{
char *p = s;
for (p=s; *p; p++ )
if (isascii (*p) && *p >= 'A' && *p <= 'Z')
*p |= 0x20;
return s;
}
/*
* Check whether STRING starts with KEYWORD. The keyword is
* delimited by end of string, a space or a tab. Returns NULL if not
* found or a pointer into STRING to the next non-space character
* after the KEYWORD (which may be end of string).
*/
char *
has_leading_keyword (const char *string, const char *keyword)
{
size_t n = strlen (keyword);
if (!strncmp (string, keyword, n)
&& (!string[n] || string[n] == ' ' || string[n] == '\t'))
{
string += n;
while (*string == ' ' || *string == '\t')
string++;
return (char*)string;
}
return NULL;
}
/* Find string SUB in (BUFFER,BUFLEN). */
const char *
memstr (const void *buffer, size_t buflen, const char *sub)
{
const char *buf = buffer;
const char *t = buf;
const char *s = sub;
size_t n = buflen;
for (; n; t++, n--)
{
if (*t == *s)
{
for (buf = t++, buflen = n--, s++; n && *t == *s; t++, s++, n--)
;
if (!*s)
return buf;
t = buf;
s = sub ;
n = buflen;
}
}
return NULL;
}
/* Find string SUB in (BUFFER,BUFLEN).
* Comparison is case-insensitive. */
const char *
memistr (const void *buffer, size_t buflen, const char *sub)
{
const unsigned char *buf = buffer;
const unsigned char *t = (const unsigned char *)buffer;
const unsigned char *s = (const unsigned char *)sub;
size_t n = buflen;
for ( ; n ; t++, n-- )
{
if ( toupper (*t) == toupper (*s) )
{
for ( buf=t++, buflen = n--, s++;
n && toupper (*t) == toupper (*s); t++, s++, n-- )
;
if (!*s)
return (const char*)buf;
t = buf;
s = (const unsigned char *)sub ;
n = buflen;
}
}
return NULL;
}
int
memicmp (const char *a, const char *b, size_t n)
{
for ( ; n; n--, a++, b++ )
if (*a != *b && (toupper (*(const unsigned char*)a)
!= toupper(*(const unsigned char*)b)))
return *(const unsigned char *)a - *(const unsigned char*)b;
return 0;
}
/*
* Remove leading and trailing white space from STR. Return STR.
*/
char *
trim_spaces (char *str)
{
char *string, *p, *mark;
string = str;
/* Find first non space character. */
for( p=string; *p && isspace (*(unsigned char*)p) ; p++ )
;
/* Move characters. */
for (mark = NULL; (*string = *p); string++, p++ )
{
if (isspace (*(unsigned char*)p))
{
if (!mark)
mark = string;
}
else
mark = NULL;
}
if (mark)
*mark = '\0' ; /* Remove trailing spaces. */
return str;
}
/* Tokenize STRING using the set of delimiters in DELIM. Leading
* spaces and tabs are removed from all tokens. The caller must xfree
* the result.
*
* Returns: A malloced and NULL delimited array with the tokens. On
* memory error NULL is returned and ERRNO is set.
*/
char **
strtokenize (const char *string, const char *delim)
{
const char *s;
size_t fields;
size_t bytes, n;
char *buffer;
char *p, *px, *pend;
char **result;
/* Count the number of fields. */
for (fields = 1, s = strpbrk (string, delim); s; s = strpbrk (s + 1, delim))
fields++;
fields++; /* Add one for the terminating NULL. */
/* Allocate an array for all fields, a terminating NULL, and space
for a copy of the string. */
bytes = fields * sizeof *result;
if (bytes / sizeof *result != fields)
{
gpg_err_set_errno (ENOMEM);
return NULL;
}
n = strlen (string) + 1;
bytes += n;
if (bytes < n)
{
gpg_err_set_errno (ENOMEM);
return NULL;
}
result = xtrymalloc (bytes);
if (!result)
return NULL;
buffer = (char*)(result + fields);
/* Copy and parse the string. */
strcpy (buffer, string);
for (n = 0, p = buffer; (pend = strpbrk (p, delim)); p = pend + 1)
{
*pend = 0;
while (spacep (p))
p++;
for (px = pend - 1; px >= p && spacep (px); px--)
*px = 0;
result[n++] = p;
}
while (spacep (p))
p++;
for (px = p + strlen (p) - 1; px >= p && spacep (px); px--)
*px = 0;
result[n++] = p;
result[n] = NULL;
assert ((char*)(result + n + 1) == buffer);
return result;
}
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;
/* Insert a new data item. */
kv = xtrymalloc (sizeof *kv + strlen (key));
if (!kv)
return NULL;
kv->next = NULL;
strcpy (kv->name, key);
kv->value = xtrystrdup (value);
if (!kv->value)
{
xfree (kv);
return NULL;
}
return kv;
}
/* Append the string VALUE to the current value of KV. */
gpg_error_t
keyvalue_append_with_nl (keyvalue_t kv, const char *value)
{
char *p;
p = strconcat (kv->value, "\n", value, NULL);
if (!p)
return gpg_err_code_from_syserror ();
xfree (kv->value);
kv->value = p;
return 0;
}
/* Remove all newlines from the value of KV. This is done in place
and works always. */
void
keyvalue_remove_nl (keyvalue_t kv)
{
char *s, *d;
if (!kv || !kv->value)
return;
for (s = d = kv->value; *s; s++)
if (*s != '\n')
*d++ = *s;
*d = 0;
}
gpg_error_t
keyvalue_put (keyvalue_t *list, const char *key, const char *value)
{
keyvalue_t kv;
char *buf;
if (!key || !*key)
return gpg_error (GPG_ERR_INV_VALUE);
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;
}
/* This is the same as keyvalue_put but KEY is modified to include
an index. For example when using a value of 7 for IDX we get
"Desc" -> "Desc[7]"
"Meta[Name]" -> "Meta[Name.7]"
If IDX is -1 the function is identical to keyvalue_put.
*/
gpg_error_t
keyvalue_put_idx (keyvalue_t *list, const char *key, int idx, const char *value)
{
char name[65];
size_t n;
if (idx < 0)
return keyvalue_put (list, key, value);
n = strlen (key);
if (n > 2 && key[n-1] == ']')
snprintf (name, sizeof name, "%.*s.%d]", (int)n-1, key, idx);
else
snprintf (name, sizeof name, "%s[%d]", key, idx);
if (strlen (name) >= sizeof name - 1)
return gpg_error (GPG_ERR_INV_LENGTH);
return keyvalue_put (list, name, value);
}
-
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;
if (!key || !*key)
return gpg_error (GPG_ERR_INV_VALUE);
va_start (arg_ptr, format);
value = gpgrt_vbsprintf (format, arg_ptr);
va_end (arg_ptr);
if (!value)
return gpg_error_from_syserror ();
err = keyvalue_put (list, key, value);
- if (err)
- es_free (value);
+ es_free (value);
return err;
}
+
/* Store STRING as "Meta" field at LIST. */
gpg_error_t
keyvalue_put_meta (keyvalue_t *list, const char *string)
{
gpg_error_t err;
char *buffer;
char key[64];
char *next, *p;
int i;
buffer = xtrystrdup (string);
if (!buffer)
return gpg_error_from_syserror ();
next = buffer;
do
{
strcpy (key, "Meta[");
for (i=5, p = next; *p != '='; i++, p++)
{
if (!*p || strchr ("%:&\n\r", *p) || i >= sizeof key - 3)
{
xfree (buffer);
return gpg_error (GPG_ERR_INV_VALUE); /* No or invalid name. */
}
else
key[i] = *p;
}
if (i==5)
{
xfree (buffer);
return gpg_error (GPG_ERR_INV_VALUE); /* Zero length name. */
}
key[i++] = ']';
key[i] = 0;
p++;
next = strchr (p, '&');
if (next)
*next++ = 0;
p[percent_unescape_inplace (p, 0)] = 0;
err = keyvalue_put (list, key, p);
if (err)
{
xfree (buffer);
return err;
}
}
while (next && *next);
xfree (buffer);
return 0;
}
void
keyvalue_release (keyvalue_t kv)
{
while (kv)
{
keyvalue_t nxt = kv->next;
xfree (kv->value);
+ xfree (kv);
kv = nxt;
}
}
const char *
keyvalue_get (keyvalue_t list, const char *key)
{
keyvalue_t kv;
for (kv = list; kv; kv = kv->next)
if (!strcmp (kv->name, key))
return kv->value;
return NULL;
}
/* Same as keyvalue_get but return the value as a modifiable string
and the value in LIST to NULL. The caller must xfree the
result. */
char *
keyvalue_snatch (keyvalue_t list, const char *key)
{
keyvalue_t kv;
for (kv = list; kv; kv = kv->next)
if (!strcmp (kv->name, key))
{
char *p = kv->value;
kv->value = NULL;
return p;
}
return NULL;
}
const char *
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);
}
unsigned int
keyvalue_get_uint (keyvalue_t list, const char *key)
{
const char *s = keyvalue_get (list, key);
if (!s)
return 0;
return strtoul (s, NULL, 10);
}
/* Parse the www-form-urlencoded DATA into a new dictionary and store
that dictionary at R_DICT. On error store NULL at R_DICT and
return an error code. Note that STRING will be modified on
return. */
gpg_error_t
parse_www_form_urlencoded (keyvalue_t *r_dict, const char *data)
{
gpg_error_t err;
char *string, *endp, *name, *value;
size_t n;
char *buffer = NULL;
keyvalue_t dict = NULL;
*r_dict = NULL;
string = buffer = xtrystrdup (data);
if (!string)
{
err = gpg_error_from_syserror ();
goto leave;
}
do
{
endp = strchr (string, '&');
if (endp)
*endp = 0;
name = string;
value = strchr (name, '=');
if (value)
*value++ = 0;
name[(n=percent_plus_unescape_inplace (name, 0))] = 0;
if (!n || strlen (name) != n)
{
err = gpg_error (GPG_ERR_INV_VALUE); /* Nul in name or empty. */
goto leave;
}
if (value)
{
value[(n=percent_plus_unescape_inplace (value, 0))] = 0;
if (strlen (value) != n)
{
err = gpg_error (GPG_ERR_INV_VALUE); /* Nul in value. */
goto leave;
}
}
err = keyvalue_put (&dict, name, value? value:"");
if (err)
goto leave;
if (endp)
string = endp + 1;
}
while (endp);
*r_dict = dict;
dict = NULL;
err = 0;
leave:
keyvalue_release (dict);
xfree (buffer);
return err;
}
/* Conversion table for base64_encode. */
static char const bintoasc[64] = {
'A','B','C','D','E','F','G','H','I','J','K','L','M',
'N','O','P','Q','R','S','T','U','V','W','X','Y','Z',
'a','b','c','d','e','f','g','h','i','j','k','l','m',
'n','o','p','q','r','s','t','u','v','w','x','y','z',
'0','1','2','3','4','5','6','7','8','9','+','/'
};
/* Encode (DATA,DATALEN) in Base64 format and return a malloced
* string. Returns NULL and sets ERRNO on error. */
char *
base64_encode (const void *data, size_t datalen)
{
char *buffer, *p;
const unsigned char *s = data;
size_t n = datalen;
buffer = p = xtrymalloc ((n+2)/3*4 + 1);
if (!buffer)
return NULL;
for (; n >= 3 ; n -= 3, s += 3)
{
*p++ = bintoasc[ ((s[0] >> 2) & 077) ];
*p++ = bintoasc[ ((((s[0] << 4) & 060) | ((s[1] >> 4) & 017)) & 077) ];
*p++ = bintoasc[ ((((s[1] << 2) & 074) | ((s[2] >> 6) & 003)) & 077) ];
*p++ = bintoasc[ (s[2] & 077) ];
}
if (n == 2)
{
*p++ = bintoasc[ ((s[0] >> 2) & 077) ];
*p++ = bintoasc[ ((((s[0] << 4) & 060) | ((s[1] >> 4) & 017)) & 077) ];
*p++ = bintoasc[ ((s[1] << 2) & 074) ];
*p++ = '=';
}
else if (n == 1)
{
*p++ = bintoasc[ ((s[0] >> 2) & 077) ];
*p++ = bintoasc[ ((s[0] << 4) & 060) ];
*p++ = '=';
*p++ = '=';
}
*p = 0;
return buffer;
}
/* Decode plain Base64 encoded data in STRING and return it in at as a
* malloced buffer at (DATA,DATALEN). On error set them to (NULL,0)
* and return an error code. An extra Nul is always added to a
* returned buffer. */
gpg_error_t
base64_decode (const char *string, void **r_data, size_t *r_datalen)
{
gpg_error_t err;
gpgrt_b64state_t state;
char *buffer;
size_t len;
*r_data = NULL;
*r_datalen = 0;
buffer = xtrystrdup (string);
if (!buffer)
return gpg_error_from_syserror ();
state = gpgrt_b64dec_start (NULL);
if (!state)
{
err = gpg_error_from_syserror ();
xfree (buffer);
return err;
}
err = gpgrt_b64dec_proc (state, buffer, strlen (buffer), &len);
if (err)
{
gpgrt_b64dec_finish (state);
xfree (buffer);
return err;
}
err = gpgrt_b64dec_finish (state);
if (err)
{
xfree (buffer);
return err;
}
buffer[len] = 0; /* We know that there is enough space for this. */
*r_data = buffer;
*r_datalen = len;
return 0;
}
/* Mapping table for zb32. */
static char const zb32asc[32] = {'y','b','n','d','r','f','g','8',
'e','j','k','m','c','p','q','x',
'o','t','1','u','w','i','s','z',
'a','3','4','5','h','7','6','9' };
/* If C is a valid ZB32 character return its index (0..31). If it is
not valid return -1. */
int
zb32_index (int c)
{
const char *p;
p = memchr (zb32asc, c, 32);
if (p)
return p - zb32asc;
if (c >= 'A' && c <= 'Z')
{
p = memchr (zb32asc, c - 'A' + 'a', 32);
if (p)
return p - zb32asc;
}
return -1;
}
/* Zooko's base32 variant. See RFC-6189 and
http://philzimmermann.com/docs/human-oriented-base-32-encoding.txt
Caller must xfree the returned string. Returns NULL and sets ERRNO
on error. To avoid integer overflow DATALEN is limited to 2^16
bytes. Note, that DATABITS is measured in bits!. */
char *
zb32_encode (const void *data, unsigned int databits)
{
const unsigned char *s;
char *output, *d;
size_t datalen;
datalen = (databits + 7) / 8;
if (datalen > (1 << 16))
{
errno = EINVAL;
return NULL;
}
d = output = xtrymalloc (8 * (datalen / 5)
+ 2 * (datalen % 5)
- ((datalen%5)>2)
+ 1);
if (!output)
return NULL;
/* I use straightforward code. The compiler should be able to do a
better job on optimization than me and it is easier to read. */
for (s = data; datalen >= 5; s += 5, datalen -= 5)
{
*d++ = zb32asc[((s[0] ) >> 3) ];
*d++ = zb32asc[((s[0] & 7) << 2) | (s[1] >> 6) ];
*d++ = zb32asc[((s[1] & 63) >> 1) ];
*d++ = zb32asc[((s[1] & 1) << 4) | (s[2] >> 4) ];
*d++ = zb32asc[((s[2] & 15) << 1) | (s[3] >> 7) ];
*d++ = zb32asc[((s[3] & 127) >> 2) ];
*d++ = zb32asc[((s[3] & 3) << 3) | (s[4] >> 5) ];
*d++ = zb32asc[((s[4] & 31) ) ];
}
switch (datalen)
{
case 4:
*d++ = zb32asc[((s[0] ) >> 3) ];
*d++ = zb32asc[((s[0] & 7) << 2) | (s[1] >> 6) ];
*d++ = zb32asc[((s[1] & 63) >> 1) ];
*d++ = zb32asc[((s[1] & 1) << 4) | (s[2] >> 4) ];
*d++ = zb32asc[((s[2] & 15) << 1) | (s[3] >> 7) ];
*d++ = zb32asc[((s[3] & 127) >> 2) ];
*d++ = zb32asc[((s[3] & 3) << 3) ];
break;
case 3:
*d++ = zb32asc[((s[0] ) >> 3) ];
*d++ = zb32asc[((s[0] & 7) << 2) | (s[1] >> 6) ];
*d++ = zb32asc[((s[1] & 63) >> 1) ];
*d++ = zb32asc[((s[1] & 1) << 4) | (s[2] >> 4) ];
*d++ = zb32asc[((s[2] & 15) << 1) ];
break;
case 2:
*d++ = zb32asc[((s[0] ) >> 3) ];
*d++ = zb32asc[((s[0] & 7) << 2) | (s[1] >> 6) ];
*d++ = zb32asc[((s[1] & 63) >> 1) ];
*d++ = zb32asc[((s[1] & 1) << 4) ];
break;
case 1:
*d++ = zb32asc[((s[0] ) >> 3) ];
*d++ = zb32asc[((s[0] & 7) << 2) ];
break;
default:
break;
}
*d = 0;
/* Need to strip some bytes if not a multiple of 40. */
output[(databits + 5 - 1) / 5] = 0;
return output;
}
/* Get the current time and put it into TIMESTAMP, which must be a
buffer of at least TIMESTAMP_SIZE bytes. */
char *
get_current_time (char *timestamp)
{
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
snprintf (timestamp, TIMESTAMP_SIZE, "%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);
return timestamp;
}
/* Check the amount given in STRING and convert it to the smallest
currency unit. DECDIGITS gives the number of allowed post decimal
positions. Return 0 on error or the converted amount. */
unsigned int
convert_amount (const char *string, int decdigits)
{
const char *s;
int ndots = 0;
int nfrac = 0;
unsigned int value = 0;
unsigned int v;
if (*string == '+')
string++; /* Skip an optional leading plus sign. */
for (s = string; *s; s++)
{
if (*s == '.')
{
if (!decdigits)
return 0; /* Post decimal digits are not allowed. */
if (++ndots > 1)
return 0; /* Too many decimal points. */
}
else if (!strchr ("0123456789", *s))
return 0;
else if (ndots && ++nfrac > decdigits)
return 0; /* Too many post decimal digits. */
else
{
v = 10*value + (*s - '0');
if (v < value)
return 0; /* Overflow. */
value = v;
}
}
for (; nfrac < decdigits; nfrac++)
{
v = 10*value;
if (v < value)
return 0; /* Overflow. */
value = v;
}
return value;
}
/* Return a string with the amount computed from CENTS. DECDIGITS
gives the number of post decimal positions in CENTS. Return NULL
on error. es_free must be used to release the return value. */
char *
reconvert_amount (int cents, int decdigits)
{
unsigned int tens;
int i;
if (decdigits <= 0)
return es_bsprintf ("%d", cents);
else
{
for (tens=1, i=0; i < decdigits; i++)
tens *= 10;
return es_bsprintf ("%d.%0*d", cents / tens, decdigits, cents % tens);
}
}
/* Write buffer BUF of length LEN to stream FP. Escape all characters
in a way that the stream can be used for a colon delimited line
format including structured URL like fields. */
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);
}
}
/* Write STRING to stream FP. Escape all characters in a way that the
stream can be used for a colon delimited line format including
structured URL like fields. */
void
write_escaped (const char *string, estream_t fp)
{
write_escaped_buf (string, strlen (string), fp);
}
/* Iterate over all keys named "Meta[FOO]" for all FOO and print the
meta data field. */
void
write_meta_field (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);
}
}
}
/* Create a structured string from the "Meta" field. On error NULL is
return. The returned string must be released with es_free. */
char *
meta_field_to_string (keyvalue_t dict)
{
estream_t fp;
void *buffer;
int writefailed;
keyvalue_t kv;
for (kv=dict; kv; kv = kv->next)
if (!strncmp (kv->name, "Meta[", 5) && kv->value && *kv->value)
break;
if (!kv)
return NULL;/* No Meta data field. */
fp = es_fopenmem (0, "w+,samethread");
if (!fp)
{
log_error ("error creating new memory stream for the Meta field: %s\n",
gpg_strerror (gpg_error_from_syserror()));
return NULL;
}
write_meta_field (dict, fp);
/* Write an extra Nul so that we can snatched the memory as C-string. */
if ((writefailed = es_fwrite ("", 1, 1, fp) != 1)
|| es_fclose_snatch (fp, &buffer, NULL))
{
log_error ("error closing memory stream for the Meta field: %s\n",
gpg_strerror (gpg_error_from_syserror()));
if (writefailed)
es_fclose (fp);
return NULL;
}
return buffer;
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Feb 22, 6:50 PM (16 h, 15 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
f1/50/f96867a240f774097473c3240aab

Event Timeline