/* This file is part of TALER Copyright (C) 2019, 2020, 2022, 2024 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 */ /** * @file taler-mdb.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 #include #include #include #include #include #if HAVE_SYS_UN_H #include #endif #if HAVE_NETINET_IN_H #include #endif #if HAVE_NETINET_IP_H #include /* superset of previous */ #endif #include #include #include #include #include #include #include #include #include #include #if HAVE_QRENCODE_H #include #endif #include #include #include /* for adafruit pitft display */ #include #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 to show a transient error. */ #define ERR_DELAY GNUNET_TIME_relative_multiply ( \ GNUNET_TIME_UNIT_SECONDS, 30) /** * 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_MINUTES #define NFC_NOT_FOUND_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 an 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; /** * Task to stop showing transient errors. */ static struct GNUNET_SCHEDULER_Task *err_stop_task; /** * Handle to the process showing messages/advertisements * while we are inactive. */ static struct GNUNET_OS_Process *adv_child; /** * Handle to the process showing error messages * while we have one. */ static struct GNUNET_OS_Process *err_child; /** * Command to run when advertising is enabled. * Can be NULL. */ static char *adv_process_command; /** * Command to run when advertising is enabled. * Can be NULL. */ static char *err_process_command; /** * 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; /** * Should we hide a transient error when MDB is ready? */ static bool clear_error_on_start; /** * 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; /** * Start process using the @a command command-line. * * @param command command to run * @param ... extra arguments to pass * @return process handle, NULL on failure */ static struct GNUNET_OS_Process * start_command (const char *command, ...) { char **argv = NULL; unsigned int argc = 0; char *cpy = GNUNET_strdup (command); struct GNUNET_OS_Process *ret; va_list ap; const char *arg; for (const char *tok = strtok (cpy, " "); NULL != tok; tok = strtok (NULL, " ")) { GNUNET_array_append (argv, argc, GNUNET_strdup (tok)); } va_start (ap, command); while (NULL != (arg = va_arg (ap, const char *))) { GNUNET_array_append (argv, argc, GNUNET_strdup (arg)); } va_end (ap); GNUNET_array_append (argv, argc, NULL); ret = GNUNET_OS_start_process_vap (GNUNET_OS_INHERIT_STD_ERR, NULL, NULL, NULL, argv[0], argv); if (NULL == ret) GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to launch %s\n", argv[0]); for (unsigned int i = 0; i /** * @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; stop_advertising (); hide_error (); 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 */ memset (qrDisplay.memory, 0xFF, qrDisplay.var_info.xres * qrDisplay.var_info.yres * sizeof (uint16_t)); 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); start_advertising (); } /** * @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); } /** * Clear the screen showing the QR code for the order. * * @param[in,out] pa payment activity to clear screen for */ static void clear_screen (struct PaymentActivity *pa) { if (NULL == pa->taler_pay_uri) return; #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); } 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); pa->odh = NULL; } if (NULL != pa->pnd) { nfc_abort_command (pa->pnd); nfc_close (pa->pnd); pa->pnd = NULL; } 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); pa->po = NULL; } if (NULL != pa->ogh) { TALER_MERCHANT_merchant_order_get_cancel (pa->ogh); pa->ogh = NULL; } GNUNET_CURL_gnunet_scheduler_reschedule (&pa->rc); if (NULL != pa->task) { GNUNET_SCHEDULER_cancel (pa->task); pa->task = NULL; } if (NULL != pa->delay_task) { GNUNET_SCHEDULER_cancel (pa->delay_task); pa->delay_task = NULL; } if (NULL != pa->delay_pay_task) { GNUNET_SCHEDULER_cancel (pa->delay_pay_task); pa->delay_pay_task = NULL; } clear_screen (pa); 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; } hide_error (); stop_advertising (); 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"); stop_advertising (); 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_NOT_FOUND_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_NOT_FOUND_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 GET /orders/$ID failed: %u/%d\n", hr->http_status, (int) hr->ec); mdb.cmd = &cmd_reader_display_backend_not_reachable; temporary_error ("backend-unexpected-failure"); run_mdb_event_loop (); cleanup_payment (pa); GNUNET_assert (payment_activity == pa); payment_activity = NULL; return; } GNUNET_log (GNUNET_ERROR_TYPE_INFO, "Backend request to GET /orders/$ID returned: %u\n", hr->http_status); if ( (MHD_HTTP_OK == hr->http_status) && (TALER_MERCHANT_OSC_PAID == osr->details.ok.status) ) { clear_screen (pa); clear_error_on_start = true; temporary_error ("dispensing"); 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.ok.status) ) { if (NULL == essid) uri = GNUNET_strdup (osr->details.ok.details.unpaid.taler_pay_uri); else GNUNET_asprintf (&uri, "%s#%s", osr->details.ok.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 */, 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); temporary_error ("backend-temporary-failure"); 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 */, 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) { static const char *uuids[1]; 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; GNUNET_assert (NULL == pa->po); 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, uuids, /* 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"); temporary_error ("internal-failure"); 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; } if (clear_error_on_start) { hide_error (); start_advertising (); } } /** * 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 rr response details */ static void refund_complete_cb (void *cls, const struct TALER_MERCHANT_RefundResponse *rr) { struct Refund *r = cls; r->orh = NULL; if (MHD_HTTP_OK != rr->hr.http_status) { GNUNET_log (GNUNET_ERROR_TYPE_ERROR, "Failed to grant consumer refund: %u/%d\n", rr->hr.http_status, (int) rr->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); temporary_error ("err-num-read-fail"); 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; temporary_error ("err-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); temporary_error ("unknown-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 = { .sold_out = false }; 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 */ 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_WARNING, "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; } (void) GNUNET_CONFIGURATION_get_value_string (cfg, "taler-mdb", "ADVERTISEMENT_COMMAND", &adv_process_command); (void) GNUNET_CONFIGURATION_get_value_string (cfg, "taler-mdb", "FAIL_COMMAND", &err_process_command); 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 start_advertising (); 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; struct termios tty_opts_raw; int have_tty; int ret; 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 ('i', "backlight-invert", "invert the backlight on/off values (standard on = 1)", &backlight_invert), 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_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; }