diff options
Diffstat (limited to 'src/taler-mdb.c')
-rw-r--r-- | src/taler-mdb.c | 3522 |
1 files changed, 3522 insertions, 0 deletions
diff --git a/src/taler-mdb.c b/src/taler-mdb.c new file mode 100644 index 0000000..f0b1529 --- /dev/null +++ b/src/taler-mdb.c @@ -0,0 +1,3522 @@ +/* + This file is part of TALER + Copyright (C) 2019, 2020, 2022 Taler Systems SA + + TALER 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, or (at your option) any later version. + + TALER 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 + TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> +*/ +/** +* @file main.c +* @brief runs the payment logic for a Taler-enabled snack machine +* @author Marco Boss +* @author Christian Grothoff +* @author Dominik Hofer +* +*/ +#include "config.h" +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/socket.h> +#if HAVE_SYS_UN_H +#include <sys/un.h> +#endif +#if HAVE_NETINET_IN_H +#include <netinet/in.h> +#endif +#if HAVE_NETINET_IP_H +#include <netinet/ip.h> /* superset of previous */ +#endif +#include <sys/stat.h> +#include <sys/types.h> +#include <errno.h> +#include <termios.h> +#include <nfc/nfc.h> +#include <microhttpd.h> +#include <gnunet/gnunet_util_lib.h> +#include <gnunet/gnunet_json_lib.h> +#include <taler/taler_json_lib.h> +#include <taler/taler_merchant_service.h> +#if HAVE_QRENCODE_H +#include <qrencode.h> +#endif +#include <sys/mman.h> +#include <sys/ioctl.h> +#include <fcntl.h> +/* for adafruit pitft display */ +#include <linux/fb.h> + +#ifndef EXIT_NOTCONFIGURED +#define EXIT_NOTCONFIGURED 6 +#endif + + +/* Constants */ +#define MAX_SIZE_RX_BUFFER 256 + +/* Constants */ +#define MAX_SIZE_TX_BUFFER 256 + +/** + * Disable i18n support. + */ +#define _(s) (s) + +#define BACKEND_POLL_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_SECONDS, 30) + +/** + * Set payment deadline below what will work with the snack machine. + */ +#define PAY_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 2) + +/** + * How long could it take at most for us to notify the Taler merchant + * backend to grant a refund to a user if dispensing the product + * failed? (Very conservative value here, for vending machines brewing + * coffee or something complex that could fail.) + */ +#define MAX_REFUND_DELAY GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MINUTES, 5) + + +#define NFC_FAILURE_RETRY_FREQ GNUNET_TIME_UNIT_SECONDS + +/** + * How long do we wait at most for an ACK from MDB? + */ +#define MAX_ACK_LATENCY GNUNET_TIME_UNIT_SECONDS + +/** + * Timeout in milliseconds for libnfc operations. + */ +#define NFC_TIMEOUT 500 + +#define MAX_HTTP_RETRY_FREQ GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MILLISECONDS, 500) + +#define MAX_HTTP_RETRY_FREQ GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MILLISECONDS, 500) + +/** + * Code returned by libnfc in case of success. + */ +#define APDU_SUCCESS "\x90\x00" + +/** + * Code returned by libnfc in case Taler wallet is not installed. + */ +#define APDU_NOT_FOUND "\x6a\x82" + +/* upper and lower bounds for mifare targets uid length */ +/** + * Upper length of the uid for a valid MIFARE target + */ +#define UID_LEN_UPPER 7 + +/** + * Lower length of the uid for a valid MIFARE target + */ +#define UID_LEN_LOWER 4 + +/* Commands for communication via MDB/ICP */ +/* VMC commands */ +#define VMC_CMD_START 0x02 +#define VMC_CMD_END 0x03 +#define VMC_CMD_RESET 0x10 + +/** + * Acknowledgement + */ +#define VMC_ACKN 0x00 + +/** + * Request for configuration. + */ +#define VMC_CONF 0x11 +#define VMC_READER_CONF 0x00 +#define VMC_SETUP_MAX_MIN_PRICES 0x01 + +/** + * Machine is polling for something. + */ +#define VMC_POLL 0x12 + +/** + * Vending, with sub-command. + */ +#define VMC_VEND 0x13 +#define VMC_VEND_REQUEST 0x00 +#define VMC_VEND_CANCEL 0x01 +#define VMC_VEND_SUCCESS 0x02 +#define VMC_VEND_FAILURE 0x03 +#define VMC_VEND_SESSION_COMPLETE 0x04 + +/** + * VMC Revalue Request + */ +#define VMC_REVALUE 0x15 +#define VMC_REVALUE_REQUEST 0x00 +#define VMC_REVALUE_LIMIT_REQUEST 0x01 +/** + * Commands for the reader (our device). + */ +#define VMC_READER 0x14 +#define VMC_READER_DISABLE 0x00 +#define VMC_READER_ENABLE 0x01 +#define VMC_READER_CANCEL 0x02 + +#define VMC_REQUEST_ID 0x17 + +/** + * Out of sequence. + */ +#define VMC_OOSQ 0xB0 + +/** + * Request to retransmit last command. + */ +#define VMC_RETR 0xAA + +/* Reader commands */ + +/* Reader Not Acknowledge */ +#define READER_NACK "FF" + +/* Config Data */ +/* Refer to the mdb interface specifications v4.2 p.288 */ +#define READER_CONFIG "01" +#define READER_FEATURE_LEVEL "01" +#define READER_COUNTRYCODE "0972" +#define READER_SCALE_FACTOR "0A" +#define READER_DECIMAL_PLACES "02" +#define READER_MAX_RESPONSE_TIME "07" +#define READER_MISC_OPTIONS "0D" + +/* Session Commands */ +/* Refer to the mdb interface specifications v4.2 p.131 */ +#define READER_BEGIN_SESSION "03" +#define READER_FUNDS_AVAILABLE "000A" +#define READER_END_SESSION "07" + +/* Vend Commands */ +/* Refer to the mdb interface specifications v4.2 p.134 */ +#define READER_VEND_APPROVE "05" +#define READER_VEND_AMOUNT "FFFE" +#define READER_VEND_DENIED "06" + +/* Revalue */ +#define READER_REVALUE_APPROVED "0D" +#define READER_REVALUE_APPROVED "0D" +#define READER_REVALUE_LIMIT "0F" +#define READER_REVALUE_LIMIT_AMOUNT "FFFE" + +/* Cancelled Command */ +#define READER_CANCELLED "08" + +/* Display Request for Sold Out product */ +#define READER_DISPLAY_REQUEST "02" +#define READER_DISPLAY_REQUEST_TIME "32" +#define READER_DISPLAY_SOLD_OUT \ + "202020202020202050726f6475637420736f6c64206f75742020202020202020" +#define READER_DISPLAY_INTERNAL_ERROR \ + "202020496e7465726e616c204572726f72202d2054727920416761696e202020i" +#define READER_DISPLAY_BACKEND_NOT_REACHABLE \ + "20202020204261636b656e64206e6f7420726561636861626c65202020202020" + +/* Unused reader commands */ +#define READER_SESSION_CANCEL_REQUEST "04" +#define READER_REVALUE_DENIED "0E" + +/** + * How long are we willing to wait for MDB during + * shutdown? + */ +#define SHUTDOWN_MDB_TIMEOUT GNUNET_TIME_relative_multiply ( \ + GNUNET_TIME_UNIT_MILLISECONDS, 100) + +/** + * Datatype for mdb subcommands and data + */ +struct MdbBlock +{ + /** + * Data containing a mdb command or the data of an mdb command + */ + uint8_t *bin; + + /** + * Size of the data referenced by *bin + */ + size_t bin_size; +}; + + +/** + * Datatype for mdb command + */ +struct MdbCommand +{ + /** + * Name of the command for the logging + */ + const char *name; + + /** + * Data block containing the information about the mdb command + */ + struct MdbBlock cmd; + + /** + * Data block containing the information about the mdb command data + */ + struct MdbBlock data; +}; + + +/** + * Struct holding the information for a product to sell + */ +struct Product +{ + /** + * The price for the product + */ + struct TALER_Amount price; + + /** + * Description (or name) of the product + */ + char *description; + + /** + * Authorization to header to use for this @e instance. + */ + char *auth_header; + + /** + * Which instance should be used for billing? Full URL, replaces + * the default base URL for orders involving this product. NULL if + * we should use the #backend_base_url. + */ + char *instance; + + /** + * Preview image to embed in the contract, NULL for + * no preview. Already base64 encoded. + */ + char *preview; + + /** + * Number of the product in the vending machine + */ + unsigned long long number; + + /** + * Set to #GNUNET_YES if this product was found + * to have been sold out (VEND failure). + */ + bool sold_out; + + /** + * Key for the product (optional, needed to test the application without vending machine) + */ + char key; + +}; + + +/** + * Handle for a payment + */ +struct PaymentActivity +{ + + /** + * Curl context for communication with taler backend + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule(). + */ + struct GNUNET_CURL_RescheduleContext *rc; + + /** + * Handle to a POST /orders operation + */ + struct TALER_MERCHANT_PostOrdersHandle *po; + + /** + * Handle for a GET /private/orders/$ID operation. + */ + struct TALER_MERCHANT_OrderMerchantGetHandle *ogh; + + /** + * The product being sold. + */ + struct Product *product; + + /** + * Base URL for merchant interactions for this pa. + */ + const char *base_url; + + /** + * Order ID for pending order + */ + char *order_id; + + /** + * URI needed to pay the pending order + */ + char *taler_pay_uri; + + /** + * NFC device + */ + nfc_device *pnd; + + /** + * Target to send the data via NFC + */ + nfc_target nt; + + /** + * Current task running + */ + struct GNUNET_SCHEDULER_Task *task; + + /** + * Tasks delayed + */ + struct GNUNET_SCHEDULER_Task *delay_task; + + /** + * Payment checking delayed task + */ + struct GNUNET_SCHEDULER_Task *delay_pay_task; + + /** + * What is the price the user is paying? + */ + struct TALER_Amount amount; + + /** + * Handle for our attempt to delete an ongoing order. + */ + struct TALER_MERCHANT_OrderDeleteHandle *odh; + + /** + * Member to see if the wallet already received a uri + * If true, tunneling can be offered to the wallet. + */ + bool wallet_has_uri; + + /** + * Set to true once the product has been paid + * (and we are in the process of yielding the product). + */ + bool paid; +}; + + +/** + * Data structures associated with the MDB. + */ +struct MdbHandle +{ + + /** + * Buffer to save the received data from UART + */ + uint8_t rxBuffer[MAX_SIZE_RX_BUFFER]; + + /** + * Buffer to save the data to send via UART + */ + uint8_t txBuffer[MAX_SIZE_TX_BUFFER]; + + /** + * Reference to scheduler task to read from UART + */ + struct GNUNET_SCHEDULER_Task *rtask; + + /** + * Reference to scheduler task to write to UART + */ + struct GNUNET_SCHEDULER_Task *wtask; + + /** + * Reference to the mdb cmd which will be sent next + */ + const struct MdbCommand *cmd; + + /** + * Reference to the mdb cmd which was sent last + */ + const struct MdbCommand *last_cmd; + + /** + * Current read offset in @e rxBuffer. + */ + size_t rx_off; + + /** + * Current write offset in @e txBuffer. + */ + size_t tx_off; + + /** + * Number of bytes in @e txBuffer with the serialized data of the + * @e last_cmd. + */ + size_t tx_len; + + /** + * Time out to wait for an acknowledge received via the mdb bus + */ + struct GNUNET_TIME_Absolute ack_timeout; + + /** + * Backup of the config data to restore the configuration of the UART before closing it + */ + struct termios uart_opts_backup; + + /** + * Indicates if a vend session is running or not + */ + bool session_running; + + /** + * File descriptor to the UART device file + */ + int uartfd; + +}; + + +/** + * Handle for the Framebuffer device + */ +struct Display +{ + /** + * File descriptor for the screen + */ + int devicefd; + + /** + * File descriptor to set backlight information + */ + int backlightfd; + + /** + * The display memory to set the pixel information + */ + uint16_t *memory; + + /** + * Original screen information + */ + struct fb_var_screeninfo orig_vinfo; + + /** + * Variable screen information (color depth ...) + */ + struct fb_var_screeninfo var_info; + + /** + * Fixed screen informtaion + */ + struct fb_fix_screeninfo fix_info; +}; + +/** + * Handle for the Cancel Button + */ +struct CancelButton +{ + /** + * File descriptor to read the state of the cancel button gpio pin + */ + int cancelbuttonfd; +}; + + +/** + * DLL of pending refund operations. + */ +struct Refund +{ + /** + * DLL next pointer. + */ + struct Refund *next; + + /** + * DLL prev pointer. + */ + struct Refund *prev; + + /** + * Curl context for communication with taler backend + */ + struct GNUNET_CURL_Context *ctx; + + /** + * Closure for #GNUNET_CURL_gnunet_scheduler_reschedule(). + */ + struct GNUNET_CURL_RescheduleContext *rc; + + /** + * Handle to the ongoing operation. + */ + struct TALER_MERCHANT_OrderRefundHandle *orh; + +}; + + +/** + * DLL head of refunds. + */ +static struct Refund *refund_head; + +/** + * DLL tail of refunds. + */ +static struct Refund *refund_tail; + +/** + * NFC context used by the NFC reader + */ +static nfc_context *context; + +/** + * Global return value + */ +static int global_ret; + +/** + * Flag set to remember that we are in shutdown. + */ +static int in_shutdown; + +/** + * Flag set to remember that MDB needs shutdown + * (because we were actually able to talk to MDB). + */ +static bool mdb_active; + +/** + * Reference to the keyboard task + */ +static struct GNUNET_SCHEDULER_Task *keyboard_task; + +/** + * Reference to the cancel button task + */ +static struct GNUNET_SCHEDULER_Task *cancelbutton_task; + +/** + * Taler Backend url read from configuration file + */ +static char *backend_base_url; + +/** + * Fulfillment message to display after successful payment, read from configuration file + */ +static char *fulfillment_msg; + +/** + * ESSID of a WLAN network offered by the snack system, or NULL. + */ +static char *essid; + +/** + * Handle for the payment + */ +static struct PaymentActivity *payment_activity; + +/** + * Products read from configuration file + */ +static struct Product *products; + +/** + * Amount of products + */ +static unsigned int products_length; + +/** + * Data associated with the MDB session. + */ +static struct MdbHandle mdb; + +/** + * MDB response to the request for configuration. + */ +static struct MdbCommand cmd_reader_config_data; + +/** + * Ask MDB to begin session (with "infinite" money) + */ +static struct MdbCommand cmd_begin_session; + +/** + * Refuse vending request (payment failed) + */ +static struct MdbCommand cmd_deny_vend; + +/** + * Approve vending request (payment succeeded) + */ +static struct MdbCommand cmd_approve_vend; + +/** + * Confirm cancellation by machine. + */ +static struct MdbCommand cmd_reader_cancelled; + +/** + * Approve Revalue + */ +static struct MdbCommand cmd_revalue_approved; + +/** + * Send Revalue Limit Amount + */ +static struct MdbCommand cmd_revalue_amount; + +/** + * Send NACK + */ +static struct MdbCommand cmd_reader_NACK; + +/** + * Display Request for Sold Out + */ +static struct MdbCommand cmd_reader_display_sold_out; + +/** + * Display Request for Error Message + */ +static struct MdbCommand cmd_reader_display_internal_error; + +/** + * Display Request for Error Message + */ +static struct MdbCommand cmd_reader_display_backend_not_reachable; + +/** + * Terminate session. + */ +static struct MdbCommand endSession; + +/** + * Name of the framebuffer device (i.e. /dev/fb1). + */ +static char *framebuffer_device_filename; + +/** + * Name of the backlight file of @e framebuffer_device_filename (i.e. /sys/class/backlight/soc:backlight/brightness). + */ +static char *framebuffer_backlight_filename; + +/** + * Global option '-i' to invert backlight on/off values + */ +static int backlight_invert; + +/** + * Standard backlight on value + */ +static char backlight_on = '1'; + +/** + * Standard backlight off value + */ +static char backlight_off = '0'; + +/** + * State for the implementation of the 'cancel' button. + */ +static struct CancelButton cancel_button; + +/** + * Name of the UART device with the MDB (i.e. /dev/ttyAMA0). + */ +static char *uart_device_filename; + +/** + * Global option '-d' to disable MDB set. + */ +static int disable_mdb; + +/** + * Global option '-t' to disable stdin / terminal. + */ +static int disable_tty; + +/** + * Global option '-s' to enable sold-out detection. + */ +static int sold_out_enabled; + +/** + * Taler wallet application identifier + */ +static const uint8_t taler_aid[] = { 0xF0, 0x00, 0x54, 0x41, 0x4c, 0x45, 0x52 }; + +/** + * NFC select file command to select wallet aid + */ +static const uint8_t select_file[] = { 0x00, 0xA4, 0x04, 0x00, 0x07 }; + +/** + * NFC put command to send data to the wallet + */ +static const uint8_t put_data[] = { 0x00, 0xDA, 0x01, 0x00, 0x7c, 0x01 }; + +#if FUTURE_FEATURES +/* tunneling */ +static const uint8_t get_data[] = { 0x00, 0xCA, 0x01, 0x00, 0x00, 0x00 }; +#endif + +/** + * Handle for the framebuffer device + */ +static struct Display qrDisplay; + + +#if HAVE_QRENCODE_H +#include <qrencode.h> + +/** + * @brief Create the QR code to pay and display it on screen + * + * @param uri what text to show in the QR code + */ +static void +show_qrcode (const char *uri) +{ + QRinput *qri; + QRcode *qrc; + unsigned int size; + char *upper; + char *base; + size_t xOff; + size_t yOff; + const char *dddash; + + if (0 > qrDisplay.devicefd) + return; /* no display, no dice */ + /* find the fourth '/' in the payto://pay/hostname/-uri */ + dddash = strchr (uri, '/'); + if (NULL != dddash) + dddash = strchr (dddash + 1, '/'); + if (NULL != dddash) + dddash = strchr (dddash + 1, '/'); + if (NULL != dddash) + dddash = strchr (dddash + 1, '/'); + if (NULL == dddash) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "taler://pay/-URI malformed: `%s'\n", + uri); + return; + } + + qri = QRinput_new2 (0, QR_ECLEVEL_L); + if (NULL == qri) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, + "QRinput_new2"); + return; + } + /* convert all characters until the fourth '/' to upper + case. The rest _should_ be upper case in a NICE setup, + but we can't warrant it and must not touch those. */ + base = GNUNET_strndup (uri, + dddash - uri); + + GNUNET_STRINGS_utf8_toupper (base, base); + GNUNET_asprintf (&upper, + "%s%s", + base, + dddash); + GNUNET_free (base); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Showing QR code for `%s'\n", + upper); + /* first try encoding as uppercase-only alpha-numerical + QR code (much smaller encoding); if that fails, also + try using binary encoding (in case nick contains + special characters). */ + if ( (0 != + QRinput_append (qri, + QR_MODE_AN, + strlen (upper), + (unsigned char *) upper)) && + (0 != + QRinput_append (qri, + QR_MODE_8, + strlen (upper), + (unsigned char *) upper))) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, + "QRinput_append"); + GNUNET_free (upper); + return; + } + GNUNET_free (upper); + qrc = QRcode_encodeInput (qri); + if (NULL == qrc) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, + "QRcode_encodeInput"); + QRinput_free (qri); + return; + } + + /* set QR-code border */ + size = GNUNET_MIN (qrDisplay.var_info.xres, + qrDisplay.var_info.yres); + unsigned int nwidth = qrc->width + 8; /* +8 for 4 pixel border */ + xOff = 4 * size / nwidth; + yOff = 4 * size / nwidth; + /* calculate offset to show the code centered */ + if (qrDisplay.var_info.xres < qrDisplay.var_info.yres) + yOff += (qrDisplay.var_info.yres - qrDisplay.var_info.xres) / 2; + else + xOff += (qrDisplay.var_info.xres - qrDisplay.var_info.yres) / 2; + for (unsigned int x = 0; x < qrDisplay.var_info.xres - 2 * xOff; x++) + for (unsigned int y = 0; y < qrDisplay.var_info.yres - 2 * yOff; y++) + { + unsigned int xoff = x * nwidth / size; + unsigned int yoff = y * nwidth / size; + unsigned int off = xoff + yoff * qrc->width; + if ( (xoff >= (unsigned) qrc->width) || + (yoff >= (unsigned) qrc->width) ) + continue; + /* set the pixels in the display memory */ + qrDisplay.memory[(y + yOff) * qrDisplay.var_info.xres + (x + xOff)] = + (0 == (qrc->data[off] & 1)) ? 0xFFFF : 0x0000; + } + + QRcode_free (qrc); + QRinput_free (qri); + + /* turn on backlight if supported */ + if (0 < qrDisplay.backlightfd) + (void) ! write (qrDisplay.backlightfd, + &backlight_on, + 1); +} + + +#endif + + +static void +run_mdb_event_loop (void); + + +/** + * Runs asynchronous cleanup part for freeing a + * payment activity. + * + * @param[in] cls a `struct PaymentActivity` to clean up + */ +static void +async_pa_cleanup_job (void *cls) +{ + struct PaymentActivity *pa = cls; + + if (NULL != pa->ctx) + GNUNET_CURL_fini (pa->ctx); + if (NULL != pa->rc) + GNUNET_CURL_gnunet_rc_destroy (pa->rc); + GNUNET_free (pa); +} + + +/** + * @brief Cleanup all the data when a order has succeeded or got cancelled + * + * @param pa the payment activity to clean up + */ +static void +cleanup_payment (struct PaymentActivity *pa); + + +/** + * Function called with the result of the DELETE /orders/$ID operation. + * + * @param cls closure with the `struct PaymentActivity *` + * @param hr HTTP response details + */ +static void +order_delete_cb ( + void *cls, + const struct TALER_MERCHANT_HttpResponse *hr) +{ + struct PaymentActivity *pa = cls; + + pa->odh = NULL; + if ( (MHD_HTTP_OK != hr->http_status) && + (MHD_HTTP_NO_CONTENT != hr->http_status) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to delete incomplete order from backend: %d/%u\n", + (int) hr->http_status, + (unsigned int) hr->ec); + } + cleanup_payment (pa); +} + + +static void +cleanup_payment (struct PaymentActivity *pa) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cleaning up payment\n"); + if ( (! pa->paid) && + (NULL != pa->order_id) ) + { + char *oid; + + oid = pa->order_id; + pa->order_id = NULL; + pa->odh = TALER_MERCHANT_order_delete ( + pa->ctx, + pa->base_url, + oid, + true, /* delete even claimed orders */ + &order_delete_cb, + pa); + GNUNET_free (oid); + return; + } + if (NULL != pa->odh) + TALER_MERCHANT_order_delete_cancel (pa->odh); + if (NULL != pa->pnd) + { + nfc_abort_command (pa->pnd); + nfc_close (pa->pnd); + } + if (NULL != cancelbutton_task) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Stopping watching of the cancel button\n"); + GNUNET_SCHEDULER_cancel (cancelbutton_task); + cancelbutton_task = NULL; + } + if (NULL != pa->po) + TALER_MERCHANT_orders_post_cancel (pa->po); + if (NULL != pa->ogh) + TALER_MERCHANT_merchant_order_get_cancel (pa->ogh); + GNUNET_CURL_gnunet_scheduler_reschedule (&pa->rc); + if (NULL != pa->task) + GNUNET_SCHEDULER_cancel (pa->task); + if (NULL != pa->delay_task) + GNUNET_SCHEDULER_cancel (pa->delay_task); + if (NULL != pa->delay_pay_task) + GNUNET_SCHEDULER_cancel (pa->delay_pay_task); + if (NULL != pa->taler_pay_uri) + { +#if HAVE_QRENCODE_H + if (NULL != qrDisplay.memory) + memset (qrDisplay.memory, + 0xFF, + qrDisplay.var_info.xres * qrDisplay.var_info.yres + * sizeof (uint16_t)); + if (0 < qrDisplay.backlightfd) + (void) ! write (qrDisplay.backlightfd, + &backlight_off, + 1); +#endif + GNUNET_free (pa->taler_pay_uri); + } + GNUNET_free (pa->order_id); + GNUNET_SCHEDULER_add_now (&async_pa_cleanup_job, + pa); +} + + +/** + * @brief Shutdown the mdb communication tasks + */ +static void +mdb_shutdown (void) +{ + if (NULL != mdb.rtask) + { + GNUNET_SCHEDULER_cancel (mdb.rtask); + mdb.rtask = NULL; + } + if (NULL != mdb.wtask) + { + GNUNET_SCHEDULER_cancel (mdb.wtask); + mdb.wtask = NULL; + } + if (disable_mdb) + return; + /* restore UART */ + if (0 != tcsetattr (mdb.uartfd, + TCSAFLUSH, + &mdb.uart_opts_backup)) + { + printf ("Failed to restore uart discipline\n"); + global_ret = EXIT_FAILURE; + } + if (-1 != mdb.uartfd) + { + GNUNET_break (0 == close (mdb.uartfd)); + mdb.uartfd = -1; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Shutdown complete (including MDB)\n"); +} + + +/** + * @brief Shutdown the application. + * + * @param cls closure + */ +static void +shutdown_task (void *cls) +{ + struct Refund *r; + + (void) cls; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Shutdown initiated\n"); + while (NULL != (r = refund_head)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Pending refund operation aborted due to shutdown\n"); + GNUNET_CONTAINER_DLL_remove (refund_head, + refund_tail, + r); + TALER_MERCHANT_post_order_refund_cancel (r->orh); + GNUNET_free (r); + } + if (NULL != context) + { + nfc_exit (context); + context = NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "NFC down\n"); + if (NULL != payment_activity) + { + cleanup_payment (payment_activity); + payment_activity = NULL; + } + if (NULL != cancelbutton_task) + { + GNUNET_SCHEDULER_cancel (cancelbutton_task); + cancelbutton_task = NULL; + } + if (NULL != keyboard_task) + { + GNUNET_SCHEDULER_cancel (keyboard_task); + keyboard_task = NULL; + } + /* last ditch saying nicely goodbye to MDB */ + in_shutdown = GNUNET_YES; + mdb.cmd = &endSession; + if (-1 != mdb.uartfd) + run_mdb_event_loop (); + if ( (MAP_FAILED != qrDisplay.memory) && + (NULL != qrDisplay.memory) ) + { + /* free the display data */ + munmap (qrDisplay.memory, + qrDisplay.fix_info.smem_len); + qrDisplay.memory = NULL; + /* reset original state */ + if (0 > ioctl (qrDisplay.devicefd, + FBIOPUT_VSCREENINFO, + &qrDisplay.orig_vinfo)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "failed to reset originial display state\n"); + } + /* close the device */ + GNUNET_break (0 == close (qrDisplay.devicefd)); + qrDisplay.devicefd = -1; + if (0 < qrDisplay.backlightfd) + GNUNET_break (0 == close (qrDisplay.backlightfd)); + qrDisplay.backlightfd = -1; + } + if (-1 != cancel_button.cancelbuttonfd) + { + GNUNET_break (0 == + close (cancel_button.cancelbuttonfd)); + cancel_button.cancelbuttonfd = -1; + } + { + int efd; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Unexporting GPIO pin 23\n"); + efd = open ("/sys/class/gpio/unexport", + O_WRONLY); + if (-1 == efd) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unable to open /gpio/unexport for cancel button\n"); + } + else + { + if (2 != write (efd, + "23", + 2)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "write", + "/sys/class/gpio/unexport"); + + } + GNUNET_break (0 == close (efd)); + } + } + /* free the allocated productes read from config file */ + if (NULL != products) + { + for (unsigned int i = 0; i < products_length; i++) + { + GNUNET_free (products[i].description); + GNUNET_free (products[i].auth_header); + GNUNET_free (products[i].instance); + GNUNET_free (products[i].preview); + } + GNUNET_array_grow (products, + products_length, + 0); + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Shutdown complete (except for MDB)\n"); +} + + +static void +check_payment_again (void *cls); + + +static void +connect_target (void *cls); + + +static void +wallet_select_aid (void *cls); + + +/** + * @brief Transmit the pay uri from taler to the wallet application via NFC + * + * @param cls closure + */ +static void +wallet_transmit_uri (void *cls) +{ + struct PaymentActivity *pa = cls; + /* response array for APDU response status word */ + uint8_t response[] = { 0x00, 0x00 }; + size_t slen = strlen (pa->taler_pay_uri); + uint8_t message[sizeof (put_data) + slen]; + + pa->delay_task = NULL; + /* append the pay uri to the put data command */ + memcpy (message, put_data, sizeof (put_data)); + memcpy (&message[sizeof (put_data)], pa->taler_pay_uri, slen); + /* send the put data command via nfc */ + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Sending 'PUT DATA' command for `%s' to wallet\n", + pa->taler_pay_uri); + if (0 > nfc_initiator_transceive_bytes (pa->pnd, + message, + sizeof (message), + response, + sizeof(response), + NFC_TIMEOUT)) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Failed to send command via NFC\n"); + pa->task = GNUNET_SCHEDULER_add_now (&connect_target, + pa); + return; + } + /* check if the transmission succeeded */ + if (0 != memcmp (response, + APDU_SUCCESS, + sizeof (response))) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "'PUT DATA' command transmission failed, return code: %x%x\n", + response[0], + response[1]); + pa->task = GNUNET_SCHEDULER_add_now (&connect_target, + pa); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "'PUT DATA' command sent successfully via NFC\n"); + pa->wallet_has_uri = true; + /* FIXME: or just offer Internet service here? */ + + /* transmit the uri again later, there can be many external failures, + for example the taler wallet app was not opened and thus did not receive + the data */ + pa->delay_task = GNUNET_SCHEDULER_add_delayed (MAX_HTTP_RETRY_FREQ, + &wallet_transmit_uri, + pa); +} + + +/** + * @brief Select the taler wallet app via NFC on the target selected with + * @e connect_target() + * (check if it is installed on the smartphone) + * + * @param cls closure + */ +static void +wallet_select_aid (void *cls) +{ + struct PaymentActivity *pa = cls; + /* response array for APDU response status word */ + uint8_t response[] = { 0x00, 0x00 }; + uint8_t message[sizeof(select_file) + sizeof(taler_aid)]; + + pa->task = NULL; + /* append the taler wallet aid to the select file command */ + memcpy (message, select_file, sizeof (select_file)); + memcpy (&message[sizeof (select_file)], taler_aid, sizeof (taler_aid)); + + /* send the select file command via nfc */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Trying to find Taler wallet on NFC\n"); + if (0 > nfc_initiator_transceive_bytes (pa->pnd, + message, + sizeof (message), + response, + sizeof (response), + NFC_TIMEOUT)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Failed to transceive with NFC app, trying to find another NFC client\n"); + pa->task = GNUNET_SCHEDULER_add_now (&connect_target, + pa); + return; + } + /* check if the transmission succeeded */ + if (0 == memcmp (response, + APDU_SUCCESS, + sizeof (response))) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Taler wallet found over NFC\n"); + pa->delay_task = GNUNET_SCHEDULER_add_now (&wallet_transmit_uri, + pa); + return; + } + /* if the transmission was not successful chack if the app is available at all */ + if (0 == memcmp (response, + APDU_NOT_FOUND, + sizeof (response))) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Taler wallet NOT found on this device\n"); + pa->task = GNUNET_SCHEDULER_add_now (&connect_target, + pa); + return; + } + /* If the upper cases did not match, there was an unknown APDU status returned */ + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "AID selection failure, return code: %x%x, trying to find another NFC client\n", + response[0], + response[1]); + /* start the selection again */ + pa->task = GNUNET_SCHEDULER_add_delayed (NFC_FAILURE_RETRY_FREQ, + &connect_target, + pa); +} + + +/** + * @brief Connect the NFC reader with a compatible NFC target + * + * @param cls closure + */ +static void +connect_target (void *cls) +{ + struct PaymentActivity *pa = cls; + /* nfc modulation used */ + const nfc_modulation nmMifare = { + .nmt = NMT_ISO14443A, + .nbr = NBR_212, + }; + + pa->task = NULL; + /* set the uid len to zero (maybe it is still set from earlier selections) */ + pa->nt.nti.nai.szUidLen = 0; + /* poll for a fitting nfc target (we use the shortest time possible to not block the scheduler) */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Trying to find NFC client\n"); + if (0 > nfc_initiator_poll_target (pa->pnd, + &nmMifare, + 1, + 0x01, + 0x01, + &pa->nt)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Failed to connect to NFC target\n"); + } + /* if the uid length are out of bound abort */ + else if ( (pa->nt.nti.nai.szUidLen > UID_LEN_UPPER) || + (pa->nt.nti.nai.szUidLen < UID_LEN_LOWER) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to connect, wrong NFC modulation\n"); + } + else + { + /* the target was successfully selected, + now we have to check if the taler wallet is installed on it */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Found NFC client\n"); + pa->task = GNUNET_SCHEDULER_add_now (&wallet_select_aid, + pa); + return; + } + /* if no target was found try again */ + pa->task = GNUNET_SCHEDULER_add_delayed (NFC_FAILURE_RETRY_FREQ, + &connect_target, + pa); +} + + +/** + * @brief Open the NFC reader. + * + * @param cls closure + */ +static void +open_nfc_reader (void *cls) +{ + struct PaymentActivity *pa = cls; + + pa->task = NULL; + /* open the nfc reader via libnfc's open */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Trying to open NFC device\n"); + pa->pnd = nfc_open (context, NULL); + if (NULL == pa->pnd) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Payment inititation: Unable to open NFC device\n"); + pa->task = GNUNET_SCHEDULER_add_delayed (NFC_FAILURE_RETRY_FREQ, + &open_nfc_reader, + pa); + return; + } + /* initialize the reader as initiator */ + if (0 > nfc_initiator_init (pa->pnd)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to initialize NFC device: %s\n", + nfc_strerror (pa->pnd)); + cleanup_payment (pa); + GNUNET_assert (payment_activity == pa); + payment_activity = NULL; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "NFC in operation %s / %s\n", + nfc_device_get_name (pa->pnd), + nfc_device_get_connstring (pa->pnd)); + /* the nfc reader was opened successfully, now try to find a mobile device as a target */ + pa->task = GNUNET_SCHEDULER_add_now (&connect_target, + pa); +} + + +static void +start_read_keyboard (void); + + +/** + * @brief Callback to process a GET /check-payment request + * + * @param cls closure + * @param osr order status response details (on success) + */ +static void +check_payment_cb (void *cls, + const struct TALER_MERCHANT_OrderStatusResponse *osr) +{ + struct PaymentActivity *pa = cls; + const struct TALER_MERCHANT_HttpResponse *hr = &osr->hr; + char *uri; + + GNUNET_assert (payment_activity == pa); + pa->ogh = NULL; + if ( (MHD_HTTP_OK != hr->http_status) && + (MHD_HTTP_GATEWAY_TIMEOUT != hr->http_status) && + (MHD_HTTP_REQUEST_TIMEOUT != hr->http_status) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Backend request to /check-payment failed: %u/%d\n", + hr->http_status, + (int) hr->ec); + mdb.cmd = &cmd_reader_display_backend_not_reachable; + run_mdb_event_loop (); + cleanup_payment (pa); + GNUNET_assert (payment_activity == pa); + payment_activity = NULL; + return; + } + + if ( (MHD_HTTP_OK != hr->http_status) && + (TALER_MERCHANT_OSC_PAID == osr->details.success.status) ) + { + mdb.cmd = &cmd_approve_vend; + payment_activity->paid = true; + run_mdb_event_loop (); + if ((disable_mdb) && (! disable_tty)) + { + GNUNET_SCHEDULER_cancel (keyboard_task); + keyboard_task = NULL; + start_read_keyboard (); + } + return; + } + /* Start to check for payment. Note that we do this even before + we talked successfully to the wallet via NFC because we MAY show the + QR code in the future and in that case the payment may happen + anytime even before the NFC communication succeeds. */ + if ( (NULL == pa->ogh) && + (NULL == pa->delay_pay_task) ) + { + pa->delay_pay_task = GNUNET_SCHEDULER_add_delayed (MAX_HTTP_RETRY_FREQ, + &check_payment_again, + pa); + } + if ( (NULL == pa->taler_pay_uri) && + (MHD_HTTP_OK == hr->http_status) && + (TALER_MERCHANT_OSC_UNPAID == osr->details.success.status) ) + { + if (NULL == essid) + uri = GNUNET_strdup (osr->details.success.details.unpaid.taler_pay_uri); + else + GNUNET_asprintf (&uri, + "%s#%s", + osr->details.success.details.unpaid.taler_pay_uri, + essid); + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Trying to talk to wallet to give it pay URI `%s'\n", + uri); + GNUNET_assert (NULL == pa->pnd); + pa->taler_pay_uri = uri; +#if HAVE_QRENCODE_H + show_qrcode (uri); +#endif + pa->task = GNUNET_SCHEDULER_add_now (&open_nfc_reader, + pa); + } +} + + +/** + * @brief Check the payment status again + * + * @param cls closure + */ +static void +check_payment_again (void *cls) +{ + struct PaymentActivity *pa = cls; + + pa->delay_pay_task = NULL; + GNUNET_assert (NULL == pa->ogh); + pa->ogh = TALER_MERCHANT_merchant_order_get (pa->ctx, + pa->base_url, + pa->order_id, + NULL /* snack machine, no Web crap */, + false, + BACKEND_POLL_TIMEOUT, + &check_payment_cb, + pa); +} + + +/** + * @brief Callback for a POST /orders request + * + * @param cls closure + * @param por response for this request + */ +static void +proposal_cb (void *cls, + const struct TALER_MERCHANT_PostOrdersReply *por) +{ + struct PaymentActivity *pa = cls; + + pa->po = NULL; + if (MHD_HTTP_OK != por->hr.http_status) + { + /* FIXME: In the future, we may want to support MHD_HTTP_GONE + explicitly and show 'product out of stock' here! */ + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to setup order with backend: %u/%d\n", + por->hr.http_status, + (int) por->hr.ec); + json_dumpf (por->hr.reply, + stderr, + 0); + mdb.cmd = &cmd_reader_display_backend_not_reachable; + run_mdb_event_loop (); + cleanup_payment (pa); + GNUNET_assert (payment_activity == pa); + payment_activity = NULL; + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Backend successfully created order `%s'\n", + por->details.ok.order_id); + pa->order_id = GNUNET_strdup (por->details.ok.order_id); + pa->ogh = TALER_MERCHANT_merchant_order_get (pa->ctx, + pa->base_url, + pa->order_id, + NULL /* snack machine, no Web crap */, + false, + GNUNET_TIME_UNIT_ZERO, + &check_payment_cb, + pa); +} + + +static void +start_read_cancel_button (void); + + +/** + * @brief Launch a new order + * + * @param product information for product to sell + * @return payment activity for the order, NULL on failure + */ +static struct PaymentActivity * +launch_payment (struct Product *product) +{ + struct PaymentActivity *pa; + json_t *orderReq; + char *msg; + const char *pos; + + pos = strstr (fulfillment_msg, + "${PRODUCT_DESCRIPTION}"); + if (NULL != pos) + { + /* replace ${PRODUCT_DESCRIPTION} with the real one */ + GNUNET_asprintf (&msg, + "%.*s%s%s", + /* first output URL until ${PRODUCT_DESCRIPTION} */ + (int) (pos - fulfillment_msg), + fulfillment_msg, + /* replace ${PRODUCT_DESCRIPTION} with the right description */ + product->description, + /* append rest of original URL */ + pos + strlen ("${PRODUCT_DESCRIPTION}")); + } + else + { + msg = GNUNET_strdup (fulfillment_msg); + } + + /* create the json object for the order request */ + if (NULL != product->preview) + { + json_t *products; + + products = json_array (); + GNUNET_assert (NULL != products); + GNUNET_assert ( + 0 == + json_array_append_new (products, + GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("description", + product->description), + GNUNET_JSON_pack_string ("image", + product->preview), + TALER_JSON_pack_amount ("price", + &product->price), + GNUNET_JSON_pack_uint64 ("quantity", + 1)))); + orderReq = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("summary", + product->description), +#if BUG + GNUNET_JSON_pack_timestamp ("pay_deadline", + GNUNET_TIME_relative_to_timestamp ( + PAY_TIMEOUT)), +#endif + GNUNET_JSON_pack_array_steal ( + "products", + products), + TALER_JSON_pack_amount ("amount", + &product->price), + GNUNET_JSON_pack_string ("fulfillment_message", + msg), + GNUNET_JSON_pack_time_rel ("auto_refund", + MAX_REFUND_DELAY)); + } + else + { + orderReq = GNUNET_JSON_PACK ( + GNUNET_JSON_pack_string ("summary", + product->description), +#if BUG + GNUNET_JSON_pack_timestamp ("pay_deadline", + GNUNET_TIME_relative_to_timestamp ( + PAY_TIMEOUT)), +#endif + TALER_JSON_pack_amount ("amount", + &product->price), + GNUNET_JSON_pack_string ("fulfillment_message", + msg), + GNUNET_JSON_pack_time_rel ("auto_refund", + MAX_REFUND_DELAY)); + } + GNUNET_free (msg); + if (NULL == orderReq) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "json_pack failed (out of memory?)\n"); + return NULL; + } + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Creating order for `%s' at backend `%s'\n", + product->description, + (NULL == product->instance) + ? backend_base_url + : product->instance); + pa = GNUNET_new (struct PaymentActivity); + pa->ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &pa->rc); + pa->rc = GNUNET_CURL_gnunet_rc_create (pa->ctx); + GNUNET_assert (GNUNET_OK == + GNUNET_CURL_append_header (pa->ctx, + product->auth_header)); + pa->product = product; + pa->amount = product->price; + /* put the order on the merchant's backend */ + pa->base_url = (NULL == product->instance) + ? backend_base_url + : product->instance; + pa->po = TALER_MERCHANT_orders_post2 (pa->ctx, + pa->base_url, + orderReq, + MAX_REFUND_DELAY, + NULL, /* no payment target preference */ + 0, + NULL, /* no inventory */ + 0, + NULL, /* no locks */ + false, /* no claim token */ + &proposal_cb, + pa); + json_decref (orderReq); + if (NULL == pa->po) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "TALER_MERCHANT_order_put failed (out of memory?)\n"); + cleanup_payment (pa); + return NULL; + } + /* Start to read the button on the VM to cancel this payment */ + if (-1 != cancel_button.cancelbuttonfd) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Payment started, watching for cancel button\n"); + start_read_cancel_button (); + } + return pa; +} + + +/** + * @brief Vending successful, conclude payment activity. + */ +static void +vend_success (void) +{ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "MDB vend success received\n"); + GNUNET_break (NULL != payment_activity); + if (NULL != payment_activity) + { + cleanup_payment (payment_activity); + payment_activity = NULL; + } +} + + +/** + * Runs asynchronous cleanup part for freeing a + * refund activity. + * + * @param[in] cls a `struct Refund` to clean up + */ +static void +async_refund_cleanup_job (void *cls) +{ + struct Refund *r = cls; + + if (NULL != r->ctx) + GNUNET_CURL_fini (r->ctx); + if (NULL != r->rc) + GNUNET_CURL_gnunet_rc_destroy (r->rc); + GNUNET_free (r); +} + + +/** + * @brief Callback to process a POST /refund request + * + * @param cls closure + * @param hr HTTP response details + * @param refund_uri the refund uri offered to the wallet + * @param h_contract hash of the contract a Browser may need to authorize + * obtaining the HTTP response. + */ +static void +refund_complete_cb (void *cls, + const struct TALER_MERCHANT_HttpResponse *hr, + const char *refund_uri, + const struct TALER_PrivateContractHashP *h_contract) +{ + struct Refund *r = cls; + + (void) refund_uri; + r->orh = NULL; + if (MHD_HTTP_OK != hr->http_status) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to grant consumer refund: %u/%d\n", + hr->http_status, + (int) hr->ec); + } + GNUNET_CONTAINER_DLL_remove (refund_head, + refund_tail, + r); + GNUNET_SCHEDULER_add_now (&async_refund_cleanup_job, + r); +} + + +/** + * @brief Vending failed, provide refund. + */ +static void +vend_failure (void) +{ + struct Product *p; + struct Refund *r; + + if (NULL == payment_activity) + { + GNUNET_break (0); + return; + } + p = payment_activity->product; + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received MDB vend failure for `%s', refunding customer\n", + p->description); + p->sold_out = true; + mdb.cmd = &endSession; + mdb.session_running = false; + r = GNUNET_new (struct Refund); + r->ctx = GNUNET_CURL_init (&GNUNET_CURL_gnunet_scheduler_reschedule, + &r->rc); + r->rc = GNUNET_CURL_gnunet_rc_create (r->ctx); + GNUNET_assert (GNUNET_OK == + GNUNET_CURL_append_header (r->ctx, + p->auth_header)); + r->orh = TALER_MERCHANT_post_order_refund (r->ctx, + (NULL == p->instance) + ? backend_base_url + : p->instance, + payment_activity->order_id, + &payment_activity->amount, + "failed to dispense product", + &refund_complete_cb, + r); + if (NULL == r->orh) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Failed to launch refund interaction with the merchant backend!\n"); + GNUNET_free (r); + } + else + { + GNUNET_CONTAINER_DLL_insert (refund_head, + refund_tail, + r); + } + if (NULL != payment_activity) + { + cleanup_payment (payment_activity); + payment_activity = NULL; + } +} + + +/** + * @brief Read the character from stdin and activate the selected task + * + * @param cls closure + */ +static void +read_keyboard_command (void *cls) +{ + (void) cls; + int input; + + keyboard_task = NULL; + input = getchar (); + if ( (EOF == input) || + ('x' == (char) input) ) + { + GNUNET_SCHEDULER_shutdown (); + return; + } + if (NULL != payment_activity) + { + switch ((char) input) + { + case 'c': + if (GNUNET_NO == payment_activity->paid) + { + mdb.cmd = &cmd_deny_vend; + } + else + { + mdb.cmd = &endSession; + mdb.session_running = false; + } + run_mdb_event_loop (); + cleanup_payment (payment_activity); + payment_activity = NULL; + break; + case 'a': + payment_activity->paid = true; + mdb.cmd = &cmd_approve_vend; + run_mdb_event_loop (); + break; + case 'n': + if (disable_mdb) + { + vend_failure (); + } + else + { + fprintf (stderr, + "Cannot fail to vend at this time, waiting for payment\n"); + } + break; + case 'y': + if (disable_mdb) + { + vend_success (); + } + else + { + fprintf (stderr, + "Cannot succeed to vend at this time, waiting for payment\n"); + } + break; + default: + fprintf (stderr, + "Unknown command `%c'\n", + input); + break; + } + start_read_keyboard (); + return; + } + if (NULL != payment_activity) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Purchase activity already pending\n"); + start_read_keyboard (); + return; + } + for (unsigned int i = 0; i < products_length; i++) + if (((char) input) == products[i].key) + { + if ( (sold_out_enabled) && + (products[i].sold_out) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Product %s sold out, denying vend\n", + products[i].description); + mdb.cmd = &cmd_reader_display_sold_out; + run_mdb_event_loop (); + start_read_keyboard (); + return; + } + payment_activity = launch_payment (&products[i]); + start_read_keyboard (); + return; + } + fprintf (stderr, + "Unknown command '%c'\n", + (char) input); + start_read_keyboard (); +} + + +/** + * @brief Read the state of the cancel button GPIO pin + * + * @param cls closure + */ +static void +cancel_button_pressed (void *cls) +{ + (void) cls; + char value; + + cancelbutton_task = NULL; + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Cancel button event detected\n"); + if (1 != + read (cancel_button.cancelbuttonfd, + &value, + 1)) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "read"); + start_read_cancel_button (); + return; + } + + GNUNET_break (0 == lseek (cancel_button.cancelbuttonfd, + 0, + SEEK_SET)); + /* This point should only be reached when a order is pending, because + * the scheduler read file gets added in the function "launch_payment". + * But anyway safe check, else do nothing */ + if (NULL != payment_activity) + { + if ('1' == value) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cancel button pressed, canceling current order\n"); + if (GNUNET_NO == payment_activity->paid) + { + /* The current payment was not paid already, deny it */ + mdb.cmd = &cmd_deny_vend; + } + else + { + /* The order was paid and if we know this, then it is also yielded, + * just end the current session */ + mdb.cmd = &endSession; + mdb.session_running = false; + } + run_mdb_event_loop (); + cleanup_payment (payment_activity); + payment_activity = NULL; + } + else + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Cancel button spurious event (%d), looking for more\n", + (int) value); + start_read_cancel_button (); + } + } +} + + +/** + * @brief Wait for a keyboard input + */ +static void +start_read_keyboard (void) +{ + static struct GNUNET_DISK_FileHandle fh = { STDIN_FILENO }; + + GNUNET_assert (NULL == keyboard_task); + if (NULL == payment_activity) + { + for (unsigned int i = 0; i < products_length; i++) + printf ("'%c' to buy %s\n", + products[i].key, + products[i].description); + } + else + { + if (GNUNET_NO == payment_activity->paid) + { + printf ("'a' to fake payment for the last purchase\n" + "'c' to cancel last purchase\n"); + } + else + { + if (disable_mdb) + printf ("'y' to simulate product successfully dispensed\n" + "'n' to simulate product failed to be dispensed\n"); + } + } + printf ("'x' to quit\n"); + printf ("Waiting for keyboard input\n"); + keyboard_task = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, + &fh, + &read_keyboard_command, + NULL); +} + + +/** + * @brief Wait for cancel button during payment activity + */ +static void +start_read_cancel_button (void) +{ + struct GNUNET_DISK_FileHandle fh = { + cancel_button.cancelbuttonfd + }; + + if (NULL != cancelbutton_task) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Cancel task still active (!?), not starting another one\n"); + return; + } + cancelbutton_task = GNUNET_SCHEDULER_add_read_file ( + GNUNET_TIME_UNIT_FOREVER_REL, + &fh, + &cancel_button_pressed, + NULL); +} + + +/** + * @brief Send data to the vmc via the uart bus + * + * @param cls closure + */ +static void +write_mdb_command (void *cls) +{ + int did_write = 0; + + (void) cls; + mdb.wtask = NULL; + + if (disable_mdb) + { + /* check if there is a cmd to send and overwrite last command */ + if (NULL == mdb.cmd) + return; + mdb.last_cmd = mdb.cmd; + mdb.cmd = NULL; + run_mdb_event_loop (); + return; + } + /* if command was sent partially, send the rest */ + if (mdb.tx_off < mdb.tx_len) + { + ssize_t ret = write (mdb.uartfd, + &mdb.txBuffer[mdb.tx_off], + mdb.tx_len - mdb.tx_off); + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Wrote %d bytes to MDB\n", + (int) ret); + did_write = 1; + if (-1 == ret) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "write", + uart_device_filename); + if (in_shutdown) + mdb_shutdown (); + else + GNUNET_SCHEDULER_shutdown (); + return; + } + mdb.tx_off += ret; + /* if command was sent sucessfully start the timer for ACK timeout */ + if ( (ret > 0) && + (mdb.tx_off == mdb.tx_len) ) + mdb.ack_timeout = GNUNET_TIME_relative_to_absolute (MAX_ACK_LATENCY); + } + /* ongoing write incomplete, continue later */ + if (mdb.tx_off < mdb.tx_len) + { + run_mdb_event_loop (); + return; + } + if (NULL != mdb.last_cmd) + { + struct GNUNET_TIME_Relative del; + + /* Still waiting for ACK! -> delay write task */ + del = GNUNET_TIME_absolute_get_remaining (mdb.ack_timeout); + if (0 != del.rel_value_us) + { + if (did_write) + run_mdb_event_loop (); + else + mdb.wtask = GNUNET_SCHEDULER_add_delayed (del, + &write_mdb_command, + NULL); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "MDB failed to acknowledge command `%s' within timeout\n", + mdb.last_cmd->name); + mdb.last_cmd = NULL; + if (in_shutdown) + { + mdb_shutdown (); + return; + } + } + if (NULL == mdb.cmd) + return; + /* if there is a command to send calculate length and checksum */ + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Sending command `%s'\n", + mdb.cmd->name); + mdb.tx_off = 0; + mdb.tx_len = mdb.cmd->cmd.bin_size + mdb.cmd->data.bin_size + 1; + GNUNET_assert (mdb.tx_len <= sizeof (mdb.txBuffer)); + { + /* calculate checksum: sum up command and data, take just the LSB of the result */ + uint32_t chkSum = 0; + + for (size_t idx = 0; idx < mdb.cmd->cmd.bin_size; idx++) + chkSum += mdb.txBuffer[idx] = mdb.cmd->cmd.bin[idx]; + for (size_t idx = 0; idx < mdb.cmd->data.bin_size; idx++) + chkSum += mdb.txBuffer[idx + mdb.cmd->cmd.bin_size] = + mdb.cmd->data.bin[idx]; + mdb.txBuffer[mdb.cmd->cmd.bin_size + mdb.cmd->data.bin_size] = + (uint8_t) (chkSum & 0xFF); + } + mdb.last_cmd = mdb.cmd; + mdb.cmd = NULL; + run_mdb_event_loop (); +} + + +/** + * @brief MDB acknowledged the last command, proceed. + */ +static void +handle_ack () +{ + if (&cmd_begin_session == mdb.last_cmd) + mdb.session_running = true; + if (&cmd_deny_vend == mdb.last_cmd) + { + mdb.session_running = false; + mdb.cmd = &endSession; + } + if (&cmd_reader_display_sold_out == mdb.last_cmd) + mdb.cmd = &cmd_deny_vend; + if (&cmd_reader_display_internal_error == mdb.last_cmd) + mdb.cmd = &cmd_deny_vend; + if (&cmd_reader_display_backend_not_reachable == mdb.last_cmd) + mdb.cmd = &cmd_deny_vend; + mdb.last_cmd = NULL; + /* Cause the write-task to be re-scheduled now */ + if (NULL != mdb.wtask) + { + GNUNET_SCHEDULER_cancel (mdb.wtask); + mdb.wtask = NULL; + } +} + + +/** + * @brief Parse received command from the VMC + * + * @param hex received command from VMC + * @param hex_len number of characters in @a hex + */ +static void +handle_command (const char *hex, + size_t hex_len) +{ + unsigned int cmd; + unsigned int tmp = 0; + uint32_t chkSum; + + /* if the received command is 0 or not a multiple of 2 we cannot parse it */ + if (0 == hex_len) + return; + if (0 != (hex_len % 2)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Received unexpected input `%.*s'\n", + (int) hex_len, + hex); + GNUNET_break_op (0); + return; + } + /* convert the received 2 bytes from ASCII to hex */ + if (1 != sscanf (hex, + "%2X", + &cmd)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Received non-HEX input `%.*s'\n", + (int) hex_len, + hex); + GNUNET_break_op (0); + return; + } + + /* parse the first byte (cmd) and the second byte (subcmd) */ + switch (cmd) + { + case VMC_VEND: + { + unsigned int subcmd; + + if (4 > hex_len) + { + GNUNET_break_op (0); + return; + } + if (1 != sscanf (&hex[2], + "%2X", + &subcmd)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Received non-HEX input `%.*s'\n", + (int) hex_len - 2, + &hex[2]); + GNUNET_break_op (0); + return; + } + switch (subcmd) + { + case VMC_VEND_REQUEST: + { + + /* Calculate the checksum and check it */ + chkSum = cmd; + + for (size_t offset = 1; offset < ((hex_len / 2)); offset++) + { + chkSum += tmp; + if (1 != sscanf (hex + (2 * offset), + "%2X", + &tmp)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Received non-HEX input `%.*s'\n", + (int) hex_len, + hex); + GNUNET_break_op (0); + return; + } + } + if ( ((uint8_t) (chkSum & 0xFF)) != tmp) + { + mdb.cmd = &cmd_reader_display_internal_error; + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received command with wrong checksum `%.*s'\n", + (int) hex_len, + hex); + break; + + } + unsigned int product; + + GNUNET_break (mdb.session_running); + /* NOTE: hex[4..7] contain the price */ + if (12 > hex_len) + { + GNUNET_break_op (0); + return; + } + if (1 != sscanf (&hex[8], + "%4X", + &product)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Received non-HEX input `%.*s'\n", + (int) hex_len - 8, + &hex[8]); + GNUNET_break_op (0); + return; + } + /* compare the received product number with the defined product numbers */ + for (unsigned int i = 0; i < products_length; i++) + if (product == products[i].number) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Product %u selected on MDB\n", + product); + if ( (sold_out_enabled) && + (products[i].sold_out) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Product %s sold out, denying vend\n", + products[i].description); + mdb.cmd = &cmd_reader_display_sold_out; + return; + } + payment_activity = launch_payment (&products[i]); + if (NULL == payment_activity) + vend_failure (); + return; + } + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unknown product %u selected on MDB, denying vend\n", + product); + mdb.cmd = &cmd_deny_vend; + break; + } + case VMC_VEND_SUCCESS: + GNUNET_break (mdb.session_running); + vend_success (); + break; + case VMC_VEND_FAILURE: + { + GNUNET_break (mdb.session_running); + vend_failure (); + break; + } + case VMC_VEND_SESSION_COMPLETE: + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received MDB session complete\n"); + mdb.session_running = false; + mdb.cmd = &endSession; + if (NULL != payment_activity) + { + cleanup_payment (payment_activity); + payment_activity = NULL; + } + } + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unknown MDB sub-command %X of command %X\n", + subcmd, + cmd); + break; + } + break; + } + case VMC_REVALUE: + { + unsigned int subcmd; + + if (4 > hex_len) + { + GNUNET_break_op (0); + return; + } + if (1 != sscanf (&hex[2], + "%2X", + &subcmd)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Received non-HEX input `%.*s'\n", + (int) hex_len - 2, + &hex[2]); + GNUNET_break_op (0); + return; + } + switch (subcmd) + { + case VMC_REVALUE_REQUEST: + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received request for revalue via MDB\n"); + mdb.cmd = &cmd_revalue_approved; + break; + } + case VMC_REVALUE_LIMIT_REQUEST: + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received request for revalue limit amount via MDB\n"); + mdb.cmd = &cmd_revalue_amount; + break; + } + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unknown MDB sub-command %X of command %X\n", + subcmd, + cmd); + break; + } + break; + } + case VMC_CONF: + { + unsigned int subcmd; + + if (4 > hex_len) + { + GNUNET_break_op (0); + return; + } + if (1 != sscanf (&hex[2], + "%2X", + &subcmd)) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Received non-HEX input `%.*s'\n", + (int) hex_len - 2, + &hex[2]); + GNUNET_break_op (0); + return; + } + switch (subcmd) + { + case VMC_READER_CONF: + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received request for configuration via MDB\n"); + mdb.cmd = &cmd_reader_config_data; + break; + + } + case VMC_SETUP_MAX_MIN_PRICES: + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received max and min prices via MDB\n"); + break; + } + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unknown MDB sub-command %X of command %X\n", + subcmd, + cmd); + break; + } + break; + } + case VMC_POLL: + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Received POLL from MDB (ignored)\n"); + break; + case VMC_CMD_RESET: + { + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Received RESET from MDB (ignored)\n"); + break; + } + case VMC_READER: + { + unsigned int subcmd; + + if (4 > hex_len) + { + GNUNET_break_op (0); + return; + } + if (1 != sscanf (&hex[2], + "%2X", + &subcmd)) + { + GNUNET_break_op (0); + return; + } + + switch (subcmd) + { + case VMC_READER_DISABLE: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received Reader Disable via MDB\n"); + mdb.session_running = false; + if (NULL != payment_activity) + { + cleanup_payment (payment_activity); + payment_activity = NULL; + } + for (unsigned int i = 0; i < products_length; i++) + { + if ( (sold_out_enabled) && + (0 != strcmp (products[i].description, + "empty")) && + (products[i].sold_out) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Resetting sold out state of product %s\n", + products[i].description); + products[i].sold_out = false; + } + } + break; + case VMC_READER_ENABLE: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received Reader Enable via MDB\n"); + mdb.session_running = false; + break; + case VMC_READER_CANCEL: + mdb.cmd = &cmd_reader_cancelled; + mdb.session_running = false; + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unknown MDB sub-command %X of command %X\n", + subcmd, + cmd); + break; + } + break; + } + case VMC_REQUEST_ID: + GNUNET_log (GNUNET_ERROR_TYPE_DEBUG, + "Received VMC request ID, no need to handle (done by HW)\n"); + break; + case VMC_ACKN: + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Received acknowledgement (for command `%s') from MDB\n", + (NULL != mdb.last_cmd) ? mdb.last_cmd->name : "?"); + handle_ack (); + break; + case VMC_OOSQ: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received command out of sequence from MDB (for command `%s')\n", + (NULL != mdb.last_cmd) ? mdb.last_cmd->name : "?"); + mdb.session_running = false; + if (mdb.last_cmd != &endSession) + mdb.cmd = &endSession; + else + mdb.last_cmd = NULL; + break; + case VMC_RETR: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received request to resend previous data from MDB (previous command was `%s')\n", + (NULL != mdb.last_cmd) ? mdb.last_cmd->name : "?"); + GNUNET_break (NULL == mdb.cmd); + GNUNET_break (NULL != mdb.last_cmd); + mdb.cmd = mdb.last_cmd; + mdb.last_cmd = NULL; + break; + default: + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Received unknown MDB command %X\n", + cmd); + break; + } +} + + +/** + * @brief Read in the sent commands of the VMC controller + * + * @param cls closure + */ +static void +read_mdb_command (void *cls) +{ + ssize_t ret; + size_t cmdStartIdx; + size_t cmdEndIdx; + + (void) cls; + /* don't read if the mdb bus is disabled (only for testing) */ + GNUNET_assert (! disable_mdb); + mdb.rtask = NULL; + ret = read (mdb.uartfd, + &mdb.rxBuffer[mdb.rx_off], + sizeof (mdb.rxBuffer) - mdb.rx_off); + if (-1 == ret) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "read", + uart_device_filename); + GNUNET_SCHEDULER_shutdown (); + return; + } + mdb.rx_off += ret; + + while (mdb.rx_off > 0) + { + mdb_active = true; + /* find beginning of command */ + for (cmdStartIdx = 0; cmdStartIdx < mdb.rx_off; cmdStartIdx++) + if (VMC_CMD_START == mdb.rxBuffer[cmdStartIdx]) + break; + if (cmdStartIdx == mdb.rx_off) + { + mdb.rx_off = 0; + break; + } + /* find end of command */ + for (cmdEndIdx = cmdStartIdx; cmdEndIdx < mdb.rx_off; cmdEndIdx++) + if (VMC_CMD_END == mdb.rxBuffer[cmdEndIdx]) + break; + if (cmdEndIdx == mdb.rx_off) + { + /* check to make sure rxBuffer was big enough in principle */ + if ( (cmdStartIdx == 0) && + (mdb.rx_off == sizeof (mdb.rxBuffer)) ) + { + /* Developer: if this happens, try increasing rxBuffer! */ + GNUNET_break (0); + GNUNET_SCHEDULER_shutdown (); + return; + } + /* move cmd in buffer to the beginning of the buffer */ + memmove (mdb.rxBuffer, + &mdb.rxBuffer[cmdStartIdx], + mdb.rx_off - cmdStartIdx); + mdb.rx_off -= cmdStartIdx; + break; + } + /* if the full command was received parse it */ + handle_command ((const char *) &mdb.rxBuffer[cmdStartIdx + 1], + cmdEndIdx - cmdStartIdx - 1); + /* move the data after the processed command to the left */ + memmove (mdb.rxBuffer, + &mdb.rxBuffer[cmdEndIdx + 1], + mdb.rx_off - cmdEndIdx + 1); + mdb.rx_off -= (cmdEndIdx + 1); + } + if (in_shutdown) + { + mdb_shutdown (); + return; + } + run_mdb_event_loop (); +} + + +/** + * @brief Mdb event loop to start read and write tasks + */ +static void +run_mdb_event_loop (void) +{ + struct GNUNET_DISK_FileHandle fh = { mdb.uartfd }; + + /* begin session if no cmd waits for sending and no cmd is received from the VMC */ + if ( (! mdb.session_running) && + (NULL == mdb.cmd) && + (NULL == mdb.last_cmd) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Beginning MDB session\n"); + mdb.cmd = &cmd_begin_session; + } + /* start write task if he doesn't exist and if there is a cmd waiting to get sent */ + if ( (NULL == mdb.wtask) && + ( (NULL != mdb.cmd) || + (in_shutdown) || + (mdb.tx_len > mdb.tx_off) ) ) + { + if (disable_mdb || ! mdb_active) + mdb.wtask = GNUNET_SCHEDULER_add_now (&write_mdb_command, + NULL); + else + mdb.wtask = GNUNET_SCHEDULER_add_write_file ((in_shutdown) + ? SHUTDOWN_MDB_TIMEOUT + : + GNUNET_TIME_UNIT_FOREVER_REL, + &fh, + &write_mdb_command, + NULL); + } + if ( (disable_mdb) && + (NULL != mdb.last_cmd) ) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Faking acknowledgement (for command `%s') from MDB\n", + mdb.last_cmd->name); + handle_ack (); + } + /* start read task if he doesn't exist and the mdb communication is not disabled (only for testing) */ + if ( (NULL == mdb.rtask) && + (! disable_mdb) ) + mdb.rtask = GNUNET_SCHEDULER_add_read_file (GNUNET_TIME_UNIT_FOREVER_REL, + &fh, + &read_mdb_command, + NULL); +} + + +/** + * @brief Read the products from the configuration file + * + * @param cls closure in this case the name of the configuration file to read from + * @param section section of the config file to read from + */ +static void +read_products (void *cls, + const char *section) +{ + const struct GNUNET_CONFIGURATION_Handle *cfg = cls; + struct Product tmpProduct; + char *tmpKey; + char *thumbnail_fn; + + /* if the current section is not a product skip it */ + if (0 != strncmp (section, + "product-", + strlen ("product-"))) + return; + /* the current section is a product, parse its specifications and store it in a temporary product */ + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "description", + &tmpProduct.description)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "description"); + return; + } + if (sold_out_enabled) + { + if (0 == strcmp (tmpProduct.description, + "empty")) + { + tmpProduct.sold_out = true; + } + else + { + tmpProduct.sold_out = false; + } + } + if (GNUNET_OK != + TALER_config_get_amount (cfg, + section, + "price", + &tmpProduct.price)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "price"); + GNUNET_free (tmpProduct.description); + return; + } + if (GNUNET_OK == + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "key", + &tmpKey)) + { + tmpProduct.key = tmpKey[0]; + GNUNET_free (tmpKey); + } + else + { + /* no key */ + tmpProduct.key = '\0'; + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "instance", + &tmpProduct.instance)) + tmpProduct.instance = NULL; + tmpProduct.preview = NULL; + if (GNUNET_OK == + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "thumbnail", + &thumbnail_fn)) + { + struct GNUNET_DISK_FileHandle *fh; + + fh = GNUNET_DISK_file_open (thumbnail_fn, + GNUNET_DISK_OPEN_READ, + GNUNET_DISK_PERM_NONE); + if (NULL == fh) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Could not open thumbnail `%s'\n", + thumbnail_fn); + } + else + { + off_t flen; + + if (GNUNET_OK != + GNUNET_DISK_file_handle_size (fh, + &flen)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "stat", + thumbnail_fn); + } + else + { + void *thumb; + size_t len; + ssize_t ret; + + len = (size_t) flen; + thumb = GNUNET_malloc (len); + ret = GNUNET_DISK_file_read (fh, + thumb, + len); + if ( (ret < 0) || + (len != ((size_t) ret)) ) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "read", + thumbnail_fn); + } + else + { + const char *mime_type = ""; + const char *ext; + char *base64; + + ext = strrchr (thumbnail_fn, '.'); + if (NULL != ext) + { + static const struct + { + const char *ext; + const char *mime; + } mimes[] = { + { ".png", "image/png" }, + { ".jpg", "image/jpeg" }, + { ".jpeg", "image/jpeg" }, + { ".svg", "image/svg" }, + { NULL, NULL } + }; + + for (unsigned int i = 0; NULL != mimes[i].ext; i++) + if (0 == strcasecmp (mimes[i].ext, + ext)) + { + mime_type = mimes[i].mime; + break; + } + } + (void) GNUNET_STRINGS_base64_encode (thumb, + len, + &base64); + GNUNET_asprintf (&tmpProduct.preview, + "data:%s;base64,%s", + mime_type, + base64); + GNUNET_free (base64); + } + GNUNET_free (thumb); + } + GNUNET_break (GNUNET_OK == + GNUNET_DISK_file_close (fh)); + } + GNUNET_free (thumbnail_fn); + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_number (cfg, + section, + "number", + &tmpProduct.number)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "number"); + GNUNET_free (tmpProduct.description); + GNUNET_free (tmpProduct.instance); + GNUNET_free (tmpProduct.preview); + return; + } + + { + char *auth; + + if ( (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + section, + "BACKEND-AUTHORIZATION", + &auth)) && + (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "taler-mdb", + "BACKEND-AUTHORIZATION", + &auth)) ) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + section, + "BACKEND-AUTHORIZATION"); + GNUNET_free (tmpProduct.description); + GNUNET_free (tmpProduct.instance); + GNUNET_free (tmpProduct.preview); + return; + } + GNUNET_asprintf (&tmpProduct.auth_header, + "%s: %s", + MHD_HTTP_HEADER_AUTHORIZATION, + auth); + GNUNET_free (auth); + } + /* append the temporary product to the existing products */ + GNUNET_array_append (products, + products_length, + tmpProduct); +} + + +/** + * @brief Initialise the uart device to send mdb commands + * + * @return #GNUNET_OK on success + */ +static enum GNUNET_GenericReturnValue +mdb_init (void) +{ + struct termios uart_opts_raw; + + if (disable_mdb) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Running with MDB disabled!\n"); + run_mdb_event_loop (); + return GNUNET_OK; + } + /* open uart connection */ + if (0 > (mdb.uartfd = open (uart_device_filename, + O_RDWR | O_NOCTTY | O_NDELAY))) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_ERROR, + "open", + uart_device_filename); + return GNUNET_SYSERR; + } + + if (0 != tcgetattr (mdb.uartfd, + &mdb.uart_opts_backup)) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "tcgetattr"); + return GNUNET_SYSERR; + } + uart_opts_raw = mdb.uart_opts_backup; + + /* reset current uart discipline */ + memset (&uart_opts_raw, + 0, + sizeof(uart_opts_raw)); + + /* set baudrate */ + cfsetispeed (&uart_opts_raw, B9600); + cfsetospeed (&uart_opts_raw, B9600); + + /* set options */ + uart_opts_raw.c_cflag &= ~PARENB; + uart_opts_raw.c_cflag &= ~CSTOPB; + uart_opts_raw.c_cflag &= ~CSIZE; + uart_opts_raw.c_cflag |= CS8; + + /* 19200 bps, 8 databits, ignore cd-signal, allow reading */ + uart_opts_raw.c_cflag |= (CLOCAL | CREAD); + uart_opts_raw.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG); + uart_opts_raw.c_iflag = IGNPAR; + uart_opts_raw.c_oflag &= ~OPOST; + uart_opts_raw.c_cc[VMIN] = 0; + uart_opts_raw.c_cc[VTIME] = 50; + tcflush (mdb.uartfd, TCIOFLUSH); + if (0 != tcsetattr (mdb.uartfd, + TCSAFLUSH, + &uart_opts_raw)) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_ERROR, + "tcsetattr"); + return GNUNET_SYSERR; + } + /* if the configuration of the uart was successful start the mdb write and read tasks */ + run_mdb_event_loop (); + return GNUNET_OK; +} + + +/** + * @brief Start the application + * + * @param cls closure + * @param args arguments left + * @param cfgfile config file name + * @param cfg handle for the configuration file + */ +static void +run (void *cls, + char *const *args, + const char *cfgfile, + const struct GNUNET_CONFIGURATION_Handle *cfg) +{ + (void) cls; + (void) args; + (void) cfgfile; + + /* parse the devices, if no config entry is found, a standard is used */ + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_filename (cfg, + "taler-mdb", + "UART_DEVICE", + &uart_device_filename)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "taler-mdb", + "UART_DEVICE"); + uart_device_filename = GNUNET_strdup ("/dev/ttyAMA0"); + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_filename (cfg, + "taler-mdb", + "FRAMEBUFFER_DEVICE", + &framebuffer_device_filename)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "taler-mdb", + "FRAMEBUFFER_DEVICE"); + framebuffer_device_filename = GNUNET_strdup ("/dev/fb1"); + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_filename (cfg, + "taler-mdb", + "FRAMEBUFFER_BACKLIGHT", + &framebuffer_backlight_filename)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "taler-mdb", + "FRAMEBUFFER_BACKLIGHT"); + framebuffer_backlight_filename = GNUNET_strdup ( + "/sys/class/backlight/soc:backlight/brightness"); + } + /* parse the taler configurations */ + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "taler-mdb", + "backend-base-url", + &backend_base_url)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "taler-mdb", + "backend-base-url"); + global_ret = EXIT_FAILURE; + return; + } + { + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "taler-mdb", + "essid", + &essid)) + { + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "No ESSID specified, will not advertise WLAN\n"); + } + } + if (GNUNET_OK != + GNUNET_CONFIGURATION_get_value_string (cfg, + "taler-mdb", + "fulfillment-msg", + &fulfillment_msg)) + { + GNUNET_log_config_missing (GNUNET_ERROR_TYPE_ERROR, + "taler-mdb", + "fulfillment-msg"); + global_ret = EXIT_FAILURE; + return; + } + + /* parse the products */ + GNUNET_CONFIGURATION_iterate_sections (cfg, + &read_products, + (void *) cfg); + if (NULL == products) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "No valid products configured\n"); + global_ret = EXIT_NOTCONFIGURED; + return; + } + + GNUNET_SCHEDULER_add_shutdown (&shutdown_task, + NULL); + + /* initialize mdb */ + if (GNUNET_OK != mdb_init ()) + { + GNUNET_log (GNUNET_ERROR_TYPE_ERROR, + "Unable to initialize MDB (mdb_init() failed)\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + + /* initialize nfc */ + nfc_init (&context); + if (NULL == context) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unable to initialize NFC (nfc_init() failed)\n"); + global_ret = EXIT_FAILURE; + GNUNET_SCHEDULER_shutdown (); + return; + } + + /* open gpio pin for cancel button */ + { + int efd; + + GNUNET_log (GNUNET_ERROR_TYPE_INFO, + "Exporting GPIO pin 23\n"); + efd = open ("/sys/class/gpio/export", + O_WRONLY); + if (-1 == efd) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unable to open /gpio/export for cancel button\n"); + } + else + { + if (2 != write (efd, + "23", + 2)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "write", + "/sys/class/gpio/export"); + } + GNUNET_assert (0 == close (efd)); + } + } + + /* set direction: input */ + { + int dfd; + + dfd = open ("/sys/class/gpio/gpio23/direction", + O_WRONLY); + if (-1 == dfd) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unable to open /gpio/gpio23/direction for cancel button\n"); + } + else + { + if (2 != write (dfd, + "in", + 2)) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "write", + "/sys/class/gpio/gpio23/direction"); + } + } + GNUNET_assert (0 == close (dfd)); + } + + { + /* actually open fd for reading the state */ + cancel_button.cancelbuttonfd = open ("/sys/class/gpio/gpio23/value", + O_RDONLY); + if (-1 == cancel_button.cancelbuttonfd) + { + GNUNET_log (GNUNET_ERROR_TYPE_WARNING, + "Unable to open /gpio/gpio23/value for cancel button\n"); + } + } + + +#if HAVE_QRENCODE_H + /* open the framebuffer device */ + qrDisplay.devicefd = open (framebuffer_device_filename, + O_RDWR); + if (-1 != qrDisplay.devicefd) + { + /* read information about the screen */ + ioctl (qrDisplay.devicefd, + FBIOGET_VSCREENINFO, + &qrDisplay.var_info); + + /* store current screeninfo for reset */ + qrDisplay.orig_vinfo = qrDisplay.var_info; + + if (16 != qrDisplay.var_info.bits_per_pixel) + { + /* Change variable info to 16 bit per pixel */ + qrDisplay.var_info.bits_per_pixel = 16; + if (0 > ioctl (qrDisplay.devicefd, + FBIOPUT_VSCREENINFO, + &qrDisplay.var_info)) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, + "ioctl(FBIOPUT_VSCREENINFO)"); + return; + } + } + + /* Get fixed screen information */ + if (0 > ioctl (qrDisplay.devicefd, + FBIOGET_FSCREENINFO, + &qrDisplay.fix_info)) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, + "ioctl(FBIOGET_FSCREENINFO)"); + return; + } + + /* get pointer onto frame buffer */ + qrDisplay.memory = mmap (NULL, + qrDisplay.fix_info.smem_len, + PROT_READ | PROT_WRITE, MAP_SHARED, + qrDisplay.devicefd, + 0); + if (MAP_FAILED == qrDisplay.memory) + { + GNUNET_log_strerror (GNUNET_ERROR_TYPE_WARNING, + "mmap"); + return; + } + + /* set the screen to white */ + memset (qrDisplay.memory, + 0xFF, + qrDisplay.var_info.xres * qrDisplay.var_info.yres + * sizeof (uint16_t)); + + /* open backlight file to turn display backlight on and off */ + qrDisplay.backlightfd = open ( + framebuffer_backlight_filename, O_WRONLY); + if (0 > qrDisplay.backlightfd) + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "open", + framebuffer_backlight_filename); + } + else + { + /* if '-i' flag was set, invert the on and off values */ + if (backlight_invert) + { + backlight_on = '0'; + backlight_off = '1'; + } + /* turn off the backlight */ + (void) ! write (qrDisplay.backlightfd, &backlight_off, 1); + } + } + else + { + GNUNET_log_strerror_file (GNUNET_ERROR_TYPE_WARNING, + "open", + framebuffer_device_filename); + } +#endif + if (! disable_tty) + start_read_keyboard (); +} + + +/** + * @brief Convert the ASCII cmd in @hex to hex and store it in the mdb data struct @blk + * + * @param *blk pointer with reference to the mdb datablock + * @param *hex pointer to the string containing the cmd data + */ +static void +parse_block (struct MdbBlock *blk, + const char *hex) +{ + if (NULL == hex) + { + blk->bin_size = 0; + blk->bin = NULL; + return; + } + blk->bin_size = strlen (hex) / 2; + blk->bin = GNUNET_malloc (blk->bin_size); + for (size_t idx = 0; idx < blk->bin_size; idx++) + { + unsigned int val; + + GNUNET_assert (1 == + sscanf (&hex[idx * 2], + "%2X", + &val)); + blk->bin[idx] = (uint8_t) val; + } +} + + +/** + * @brief Create a new mdb command + * + * @param *name pointer to the string containing the command name + * @param *cmd pointer to the string containing the command + * @param *data pointer to the string containing the command data + * @return structure of type MdbCommand holding the given information by the parameters + */ +static struct MdbCommand +setup_mdb_cmd (const char *name, + const char *cmd, + const char *data) +{ + struct MdbCommand cmdNew; + + cmdNew.name = (NULL == name) + ? "No Cmd Name Set" + : name; + parse_block (&cmdNew.cmd, cmd); + parse_block (&cmdNew.data, data); + return cmdNew; +} + + +int +main (int argc, + char*const*argv) +{ + struct termios tty_opts_backup, tty_opts_raw; + int have_tty; + int ret; + + /* the available command line options */ + struct GNUNET_GETOPT_CommandLineOption options[] = { + GNUNET_GETOPT_option_flag ('d', + "disable-mdb", + "disable all interactions with the MDB (for testing without machine)", + &disable_mdb), + GNUNET_GETOPT_option_flag ('s', + "enable-soldout", + "enable detection of sold-out products, preventing vend operations of the respective product until the process is restarted", + &sold_out_enabled), + GNUNET_GETOPT_option_flag ('t', + "disable-tty", + "disable all keyboard interactions (for running from systemd)", + &disable_tty), + GNUNET_GETOPT_option_flag ('i', + "backlight-invert", + "invert the backlight on/off values (standard on = 1)", + &backlight_invert), + GNUNET_GETOPT_OPTION_END + }; + + if (! disable_tty) + { + have_tty = isatty (STDIN_FILENO); + if (have_tty) + { + if (0 != tcgetattr (STDIN_FILENO, &tty_opts_backup)) + fprintf (stderr, + "Failed to get terminal discipline\n"); + tty_opts_raw = tty_opts_backup; + tty_opts_raw.c_lflag &= ~(ECHO | ECHONL | ICANON); + if (0 != tcsetattr (STDIN_FILENO, TCSANOW, &tty_opts_raw)) + fprintf (stderr, + "Failed to set terminal discipline\n"); + } + } + /* make the needed commands for the communication with the vending machine controller */ + cmd_reader_config_data = setup_mdb_cmd ("Reader Config", + READER_CONFIG, + READER_FEATURE_LEVEL + READER_COUNTRYCODE + READER_SCALE_FACTOR + READER_DECIMAL_PLACES + READER_MAX_RESPONSE_TIME + READER_MISC_OPTIONS); + cmd_begin_session = setup_mdb_cmd ("Begin Session", + READER_BEGIN_SESSION, + READER_FUNDS_AVAILABLE); + cmd_approve_vend = setup_mdb_cmd ("Approve Vend", + READER_VEND_APPROVE, + READER_VEND_AMOUNT); + cmd_reader_cancelled = setup_mdb_cmd ("Confirm cancellation", + READER_CANCELLED, + NULL); + cmd_deny_vend = setup_mdb_cmd ("Deny Vend", + READER_VEND_DENIED, + NULL); + endSession = setup_mdb_cmd ("End Session", + READER_END_SESSION, + NULL); + cmd_revalue_approved = setup_mdb_cmd ("Reader Approve Revalue", + READER_REVALUE_APPROVED, + NULL); + + cmd_revalue_amount = setup_mdb_cmd ("Send Revalue Limit Amount", + READER_REVALUE_LIMIT, + READER_REVALUE_LIMIT_AMOUNT); + + cmd_reader_NACK = setup_mdb_cmd ("Reader NACK", + READER_NACK, + NULL); + + cmd_reader_display_sold_out = setup_mdb_cmd ("Display Sold Out", + READER_DISPLAY_REQUEST, + READER_DISPLAY_REQUEST_TIME + READER_DISPLAY_SOLD_OUT); + + cmd_reader_display_internal_error = setup_mdb_cmd ( + "Display Communication Error", + READER_DISPLAY_REQUEST, + READER_DISPLAY_REQUEST_TIME + READER_DISPLAY_INTERNAL_ERROR); + + cmd_reader_display_backend_not_reachable = setup_mdb_cmd ( + "Display Backend not reachable", + READER_DISPLAY_REQUEST, + READER_DISPLAY_REQUEST_TIME + READER_DISPLAY_BACKEND_NOT_REACHABLE); + if (GNUNET_OK != + GNUNET_STRINGS_get_utf8_args (argc, argv, + &argc, &argv)) + return 4; + ret = GNUNET_PROGRAM_run (argc, + argv, + "taler-mdb", + "This is an application for snack machines to pay with GNU Taler via NFC.\n", + options, + &run, + NULL); + GNUNET_free_nz ((void *) argv); + if (! disable_tty) + { + if (have_tty) + { + /* Restore previous TTY settings */ + if (0 != tcsetattr (STDIN_FILENO, + TCSANOW, + &tty_opts_backup)) + fprintf (stderr, + "Failed to restore terminal discipline\n"); + } + } + if (GNUNET_NO == ret) + return 0; + if (GNUNET_OK != ret) + return 1; + return global_ret; +} |