diff options
Diffstat (limited to 'src/mm-sms-utils.c')
-rw-r--r-- | src/mm-sms-utils.c | 515 |
1 files changed, 460 insertions, 55 deletions
diff --git a/src/mm-sms-utils.c b/src/mm-sms-utils.c index 3f56a64..4e52ec4 100644 --- a/src/mm-sms-utils.c +++ b/src/mm-sms-utils.c @@ -13,12 +13,17 @@ * Copyright (C) 2011 Red Hat, Inc. */ +#include <ctype.h> +#include <string.h> + #include <glib.h> #include "mm-charsets.h" #include "mm-errors.h" #include "mm-utils.h" #include "mm-sms-utils.h" +#include "mm-log.h" +#include "dbus/dbus-glib.h" #define SMS_TP_MTI_MASK 0x03 #define SMS_TP_MTI_SMS_DELIVER 0x00 @@ -70,6 +75,93 @@ sms_semi_octets_to_bcd_string (char *dest, const guint8 *octets, int num_octets) *dest++ = '\0'; } +static gboolean +char_to_bcd (char in, guint8 *out) +{ + guint32 z; + + if (isdigit (in)) { + *out = in - 0x30; + return TRUE; + } + + for (z = 10; z < 16; z++) { + if (in == sms_bcd_chars[z]) { + *out = z; + return TRUE; + } + } + return FALSE; +} + +static gsize +sms_string_to_bcd_semi_octets (guint8 *buf, gsize buflen, const char *string) +{ + guint i; + guint8 bcd; + gsize addrlen, slen; + + addrlen = slen = strlen (string); + if (addrlen % 2) + addrlen++; + g_return_val_if_fail (buflen >= addrlen, 0); + + for (i = 0; i < addrlen; i += 2) { + if (!char_to_bcd (string[i], &bcd)) + return 0; + buf[i / 2] = bcd & 0xF; + + if (i >= slen - 1) { + /* PDU address gets padded with 0xF if string is odd length */ + bcd = 0xF; + } else if (!char_to_bcd (string[i + 1], &bcd)) + return 0; + buf[i / 2] |= bcd << 4; + } + return addrlen / 2; +} + +/** + * sms_encode_address: + * + * @address: the phone number to encode + * @buf: the buffer to encode @address in + * @buflen: the size of @buf + * @is_smsc: if %TRUE encode size as number of octets of address infromation, + * otherwise if %FALSE encode size as number of digits of @address + * + * Returns: the size in bytes of the data added to @buf + **/ +guint +sms_encode_address (const char *address, + guint8 *buf, + size_t buflen, + gboolean is_smsc) +{ + gsize len; + + g_return_val_if_fail (address != NULL, 0); + g_return_val_if_fail (buf != NULL, 0); + g_return_val_if_fail (buflen >= 2, 0); + + /* Handle number type & plan */ + buf[1] = 0x80; /* Bit 7 always 1 */ + if (address[0] == '+') { + buf[1] |= SMS_NUMBER_TYPE_INTL; + address++; + } + buf[1] |= SMS_NUMBER_PLAN_TELEPHONE; + + len = sms_string_to_bcd_semi_octets (&buf[2], buflen, address); + + if (is_smsc) + buf[0] = len + 1; /* addr length + size byte */ + else + buf[0] = strlen (address); /* number of digits in address */ + + return len ? len + 2 : 0; /* addr length + size byte + number type/plan */ +} + /* len is in semi-octets */ static char * sms_decode_address (const guint8 *address, int len) @@ -200,10 +292,10 @@ sms_decode_text (const guint8 *text, int len, SmsEncoding encoding, int bit_offs g_free (unpacked); } else if (encoding == MM_SMS_ENCODING_UCS2) utf8 = g_convert ((char *) text, len, "UTF8", "UCS-2BE", NULL, NULL, NULL); - else if (encoding == MM_SMS_ENCODING_8BIT) - utf8 = g_strndup ((const char *)text, len); - else + else { + g_warn_if_reached (); utf8 = g_strdup (""); + } return utf8; } @@ -230,48 +322,83 @@ simple_uint_value (guint32 i) } static GValue * -simple_boolean_value (gboolean b) +simple_string_value (const char *str) { GValue *val; val = g_slice_new0 (GValue); - g_value_init (val, G_TYPE_BOOLEAN); - g_value_set_boolean (val, b); + g_value_init (val, G_TYPE_STRING); + g_value_set_string (val, str); return val; } static GValue * -simple_string_value (const char *str) +byte_array_value (const GByteArray *array) { GValue *val; val = g_slice_new0 (GValue); - g_value_init (val, G_TYPE_STRING); - g_value_set_string (val, str); + g_value_init (val, DBUS_TYPE_G_UCHAR_ARRAY); + g_value_set_boxed (val, array); return val; } GHashTable * +sms_properties_hash_new (const char *smsc, + const char *number, + const char *timestamp, + const char *text, + const GByteArray *data, + guint data_coding_scheme, + guint *class) +{ + GHashTable *properties; + + g_return_val_if_fail (number != NULL, NULL); + g_return_val_if_fail (text != NULL, NULL); + g_return_val_if_fail (data != NULL, NULL); + + properties = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, simple_free_gvalue); + g_hash_table_insert (properties, "number", simple_string_value (number)); + g_hash_table_insert (properties, "data", byte_array_value (data)); + g_hash_table_insert (properties, "data-coding-scheme", simple_uint_value (data_coding_scheme)); + g_hash_table_insert (properties, "text", simple_string_value (text)); + + if (smsc) + g_hash_table_insert (properties, "smsc", simple_string_value (smsc)); + + if (timestamp) + g_hash_table_insert (properties, "timestamp", simple_string_value (timestamp)); + + if (class) + g_hash_table_insert (properties, "class", simple_uint_value (*class)); + + return properties; +} + +GHashTable * sms_parse_pdu (const char *hexpdu, GError **error) { GHashTable *properties; gsize pdu_len; guint8 *pdu; - int smsc_addr_num_octets, variable_length_items, msg_start_offset, + guint smsc_addr_num_octets, variable_length_items, msg_start_offset, sender_addr_num_digits, sender_addr_num_octets, tp_pid_offset, tp_dcs_offset, user_data_offset, user_data_len, user_data_len_offset, bit_offset; char *smsc_addr, *sender_addr, *sc_timestamp, *msg_text; SmsEncoding user_data_encoding; + GByteArray *pdu_data; + guint concat_ref = 0, concat_max = 0, concat_seq = 0, msg_class = 0; + gboolean multipart = FALSE, class_valid = FALSE; /* Convert PDU from hex to binary */ pdu = (guint8 *) utils_hexstr2bin (hexpdu, &pdu_len); if (!pdu) { - *error = g_error_new_literal (MM_MODEM_ERROR, - MM_MODEM_ERROR_GENERAL, - "Couldn't parse PDU of SMS GET response from hex"); + g_set_error_literal (error, MM_MODEM_ERROR, MM_MODEM_ERROR_GENERAL, + "Couldn't parse PDU of SMS GET response from hex"); return NULL; } @@ -279,10 +406,10 @@ sms_parse_pdu (const char *hexpdu, GError **error) smsc_addr_num_octets = pdu[0]; variable_length_items = smsc_addr_num_octets; if (pdu_len < variable_length_items + SMS_MIN_PDU_LEN) { - *error = g_error_new (MM_MODEM_ERROR, - MM_MODEM_ERROR_GENERAL, - "PDU too short (1): %zd vs %d", pdu_len, - variable_length_items + SMS_MIN_PDU_LEN); + g_set_error (error, MM_MODEM_ERROR, MM_MODEM_ERROR_GENERAL, + "PDU too short (1): %zd vs %d", + pdu_len, + variable_length_items + SMS_MIN_PDU_LEN); g_free (pdu); return NULL; } @@ -297,10 +424,10 @@ sms_parse_pdu (const char *hexpdu, GError **error) sender_addr_num_octets = (sender_addr_num_digits + 1) >> 1; variable_length_items += sender_addr_num_octets; if (pdu_len < variable_length_items + SMS_MIN_PDU_LEN) { - *error = g_error_new (MM_MODEM_ERROR, - MM_MODEM_ERROR_GENERAL, - "PDU too short (2): %zd vs %d", pdu_len, - variable_length_items + SMS_MIN_PDU_LEN); + g_set_error (error, MM_MODEM_ERROR, MM_MODEM_ERROR_GENERAL, + "PDU too short (2): %zd vs %d", + pdu_len, + variable_length_items + SMS_MIN_PDU_LEN); g_free (pdu); return NULL; } @@ -317,36 +444,71 @@ sms_parse_pdu (const char *hexpdu, GError **error) else variable_length_items += user_data_len; if (pdu_len < variable_length_items + SMS_MIN_PDU_LEN) { - *error = g_error_new (MM_MODEM_ERROR, - MM_MODEM_ERROR_GENERAL, - "PDU too short (3): %zd vs %d", pdu_len, - variable_length_items + SMS_MIN_PDU_LEN); + g_set_error (error, MM_MODEM_ERROR, MM_MODEM_ERROR_GENERAL, + "PDU too short (3): %zd vs %d", + pdu_len, + variable_length_items + SMS_MIN_PDU_LEN); g_free (pdu); return NULL; } /* Only handle SMS-DELIVER */ if ((pdu[msg_start_offset] & SMS_TP_MTI_MASK) != SMS_TP_MTI_SMS_DELIVER) { - *error = g_error_new (MM_MODEM_ERROR, - MM_MODEM_ERROR_GENERAL, - "Unhandled message type: 0x%02x", - pdu[msg_start_offset]); + g_set_error (error, MM_MODEM_ERROR, MM_MODEM_ERROR_GENERAL, + "Unhandled message type: 0x%02x", + pdu[msg_start_offset]); g_free (pdu); return NULL; } - smsc_addr = sms_decode_address (&pdu[1], 2 * (pdu[0] - 1)); - sender_addr = sms_decode_address (&pdu[msg_start_offset + 2], - pdu[msg_start_offset + 1]); - sc_timestamp = sms_decode_timestamp (&pdu[tp_dcs_offset + 1]); bit_offset = 0; if (pdu[msg_start_offset] & SMS_TP_UDHI) { + int udhl, end, offset; + udhl = pdu[user_data_offset] + 1; + end = user_data_offset + udhl; + + for (offset = user_data_offset + 1; offset < end;) { + guint8 ie_id, ie_len; + + ie_id = pdu[offset++]; + ie_len = pdu[offset++]; + + switch (ie_id) { + case 0x00: + /* + * Ignore the IE if one of the following is true: + * - it claims to be part 0 of M + * - it claims to be part N of M, N > M + */ + if (pdu[offset + 2] == 0 || + pdu[offset + 2] > pdu[offset + 1]) + break; + + concat_ref = pdu[offset]; + concat_max = pdu[offset + 1]; + concat_seq = pdu[offset + 2]; + multipart = TRUE; + break; + case 0x08: + /* Concatenated short message, 16-bit reference */ + if (pdu[offset + 3] == 0 || + pdu[offset + 3] > pdu[offset + 2]) + break; + + concat_ref = (pdu[offset] << 8) | pdu[offset + 1]; + concat_max = pdu[offset + 2]; + concat_seq = pdu[offset + 3]; + multipart = TRUE; + break; + } + + offset += ie_len; + } + /* - * Skip over the user data headers to prevent it from being + * Move past the user data headers to prevent it from being * decoded into garbage text. */ - int udhl; - udhl = pdu[user_data_offset] + 1; user_data_offset += udhl; if (user_data_encoding == MM_SMS_ENCODING_GSM7) { /* @@ -359,31 +521,274 @@ sms_parse_pdu (const char *hexpdu, GError **error) user_data_len -= udhl; } - msg_text = sms_decode_text (&pdu[user_data_offset], user_data_len, - user_data_encoding, bit_offset); - - properties = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, - simple_free_gvalue); - g_hash_table_insert (properties, "number", - simple_string_value (sender_addr)); - g_hash_table_insert (properties, "text", - simple_string_value (msg_text)); - g_hash_table_insert (properties, "smsc", - simple_string_value (smsc_addr)); - g_hash_table_insert (properties, "timestamp", - simple_string_value (sc_timestamp)); - if (pdu[tp_dcs_offset] & SMS_DCS_CLASS_VALID) - g_hash_table_insert (properties, "class", - simple_uint_value (pdu[tp_dcs_offset] & - SMS_DCS_CLASS_MASK)); - g_hash_table_insert (properties, "completed", simple_boolean_value (TRUE)); + if ( user_data_encoding == MM_SMS_ENCODING_8BIT + || user_data_encoding == MM_SMS_ENCODING_UNKNOWN) { + /* 8-bit encoding is usually binary data, and we have no idea what + * actual encoding the data is in so we can't convert it. + */ + msg_text = g_strdup (""); + } else { + /* Otherwise if it's 7-bit or UCS2 we can decode it */ + msg_text = sms_decode_text (&pdu[user_data_offset], user_data_len, + user_data_encoding, bit_offset); + g_warn_if_fail (msg_text != NULL); + } + + /* Raw PDU data */ + pdu_data = g_byte_array_sized_new (user_data_len); + g_byte_array_append (pdu_data, &pdu[user_data_offset], user_data_len); + + if (pdu[tp_dcs_offset] & SMS_DCS_CLASS_VALID) { + msg_class = pdu[tp_dcs_offset] & SMS_DCS_CLASS_MASK; + class_valid = TRUE; + } + + smsc_addr = sms_decode_address (&pdu[1], 2 * (pdu[0] - 1)); + sender_addr = sms_decode_address (&pdu[msg_start_offset + 2], pdu[msg_start_offset + 1]); + sc_timestamp = sms_decode_timestamp (&pdu[tp_dcs_offset + 1]); + + properties = sms_properties_hash_new (smsc_addr, + sender_addr, + sc_timestamp, + msg_text, + pdu_data, + pdu[tp_dcs_offset] & 0xFF, + class_valid ? &msg_class : NULL); + g_assert (properties); + if (multipart) { + g_hash_table_insert (properties, "concat-reference", simple_uint_value (concat_ref)); + g_hash_table_insert (properties, "concat-max", simple_uint_value (concat_max)); + g_hash_table_insert (properties, "concat-sequence", simple_uint_value (concat_seq)); + } g_free (smsc_addr); g_free (sender_addr); g_free (sc_timestamp); g_free (msg_text); + g_byte_array_free (pdu_data, TRUE); g_free (pdu); - return properties; } + +static guint8 +validity_to_relative (guint validity) +{ + if (validity == 0) + return 167; /* 24 hours */ + + if (validity <= 720) { + /* 5 minute units up to 12 hours */ + if (validity % 5) + validity += 5; + return (validity / 5) - 1; + } + + if (validity > 720 && validity <= 1440) { + /* 12 hours + 30 minute units up to 1 day */ + if (validity % 30) + validity += 30; /* round up to next 30 minutes */ + validity = MIN (validity, 1440); + return 143 + ((validity - 720) / 30); + } + + if (validity > 1440 && validity <= 43200) { + /* 2 days up to 1 month */ + if (validity % 1440) + validity += 1440; /* round up to next day */ + validity = MIN (validity, 43200); + return 167 + ((validity - 1440) / 1440); + } + + /* 43200 = 30 days in minutes + * 10080 = 7 days in minutes + * 635040 = 63 weeks in minutes + * 40320 = 4 weeks in minutes + */ + if (validity > 43200 && validity <= 635040) { + /* 5 weeks up to 63 weeks */ + if (validity % 10080) + validity += 10080; /* round up to next week */ + validity = MIN (validity, 635040); + return 196 + ((validity - 40320) / 10080); + } + + return 255; /* 63 weeks */ +} + +#define PDU_SIZE 200 + +/** + * sms_create_submit_pdu: + * + * @number: the subscriber number to send this message to + * @text: the body of this SMS + * @smsc: if given, the SMSC address + * @validity: minutes until the SMS should expire in the SMSC, or 0 for a + * suitable default + * @class: unused + * @out_pdulen: on success, the size of the returned PDU in bytes + * @out_msgstart: on success, the byte index in the returned PDU where the + * message starts (ie, skipping the SMSC length byte and address, if present) + * @error: on error, filled with the error that occurred + * + * Constructs a single-part SMS message with the given details, preferring to + * use the UCS2 character set when the message will fit, otherwise falling back + * to the GSM character set. + * + * Returns: the constructed PDU data on success, or %NULL on error + **/ +guint8 * +sms_create_submit_pdu (const char *number, + const char *text, + const char *smsc, + guint validity, + guint class, + guint *out_pdulen, + guint *out_msgstart, + GError **error) +{ + guint8 *pdu; + guint len, offset = 0; + MMModemCharset best_cs = MM_MODEM_CHARSET_GSM; + guint ucs2len = 0, gsm_unsupported = 0; + guint textlen = 0; + + g_return_val_if_fail (number != NULL, NULL); + g_return_val_if_fail (text != NULL, NULL); + + /* FIXME: support multiple fragments */ + + textlen = mm_charset_get_encoded_len (text, MM_MODEM_CHARSET_GSM, &gsm_unsupported); + if (textlen > 160) { + g_set_error_literal (error, + MM_MODEM_ERROR, + MM_MODEM_ERROR_OPERATION_NOT_SUPPORTED, + "Cannot encode message to fit into an SMS."); + return NULL; + } + + /* If there are characters that are unsupported in the GSM charset, try + * UCS2. If the UCS2 encoded string is too long to fit in an SMS, then + * just use GSM and suck up the unconverted chars. + */ + if (gsm_unsupported > 0) { + ucs2len = mm_charset_get_encoded_len (text, MM_MODEM_CHARSET_UCS2, NULL); + if (ucs2len <= 140) { + best_cs = MM_MODEM_CHARSET_UCS2; + textlen = ucs2len; + } + } + + /* Build up the PDU */ + pdu = g_malloc0 (PDU_SIZE); + g_return_val_if_fail (pdu != NULL, NULL); + + if (smsc) { + len = sms_encode_address (smsc, pdu, PDU_SIZE, TRUE); + if (len == 0) { + g_set_error (error, + MM_MSG_ERROR, + MM_MSG_ERROR_INVALID_PDU_PARAMETER, + "Invalid SMSC address '%s'", smsc); + goto error; + } + offset += len; + } else { + /* No SMSC, use default */ + pdu[offset++] = 0x00; + } + + if (out_msgstart) + *out_msgstart = offset; + + if (validity > 0) + pdu[offset] = 1 << 4; /* TP-VP present; format RELATIVE */ + else + pdu[offset] = 0; /* TP-VP not present */ + pdu[offset++] |= 0x01; /* TP-MTI = SMS-SUBMIT */ + + pdu[offset++] = 0x00; /* TP-Message-Reference: filled by device */ + + len = sms_encode_address (number, &pdu[offset], PDU_SIZE - offset, FALSE); + if (len == 0) { + g_set_error (error, + MM_MSG_ERROR, + MM_MSG_ERROR_INVALID_PDU_PARAMETER, + "Invalid send-to number '%s'", number); + goto error; + } + offset += len; + + /* TP-PID */ + pdu[offset++] = 0x00; + + /* TP-DCS */ + if (best_cs == MM_MODEM_CHARSET_UCS2) + pdu[offset++] = 0x08; + else + pdu[offset++] = 0x00; /* GSM */ + + /* TP-Validity-Period: 4 days */ + if (validity > 0) + pdu[offset++] = validity_to_relative (validity); + + /* TP-User-Data-Length */ + pdu[offset++] = textlen; + + if (best_cs == MM_MODEM_CHARSET_GSM) { + guint8 *unpacked, *packed; + guint32 unlen = 0, packlen = 0; + + unpacked = mm_charset_utf8_to_unpacked_gsm (text, &unlen); + if (!unpacked || unlen == 0) { + g_free (unpacked); + g_set_error_literal (error, + MM_MSG_ERROR, + MM_MSG_ERROR_INVALID_PDU_PARAMETER, + "Failed to convert message text to GSM."); + goto error; + } + + packed = gsm_pack (unpacked, unlen, 0, &packlen); + g_free (unpacked); + if (!packed || packlen == 0) { + g_free (packed); + g_set_error_literal (error, + MM_MSG_ERROR, + MM_MSG_ERROR_INVALID_PDU_PARAMETER, + "Failed to pack message text to GSM."); + goto error; + } + + memcpy (&pdu[offset], packed, packlen); + g_free (packed); + offset += packlen; + } else if (best_cs == MM_MODEM_CHARSET_UCS2) { + GByteArray *array; + + array = g_byte_array_sized_new (textlen / 2); + if (!mm_modem_charset_byte_array_append (array, text, FALSE, best_cs)) { + g_byte_array_free (array, TRUE); + g_set_error_literal (error, + MM_MSG_ERROR, + MM_MSG_ERROR_INVALID_PDU_PARAMETER, + "Failed to convert message text to UCS2."); + goto error; + } + + memcpy (&pdu[offset], array->data, array->len); + offset += array->len; + g_byte_array_free (array, TRUE); + } else + g_assert_not_reached (); + + if (out_pdulen) + *out_pdulen = offset; + return pdu; + +error: + g_free (pdu); + return NULL; +} + |