mirror of
https://github.com/MengMengCode/VoCat.git
synced 2026-08-13 03:13:43 +08:00
945 lines
23 KiB
Go
945 lines
23 KiB
Go
package device
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf16"
|
|
)
|
|
|
|
var gsm7DefaultAlphabet = [128]rune{
|
|
'@', '£', '$', '¥', 'è', 'é', 'ù', 'ì',
|
|
'ò', 'Ç', '\n', 'Ø', 'ø', '\r', 'Å', 'å',
|
|
'Δ', '_', 'Φ', 'Γ', 'Λ', 'Ω', 'Π', 'Ψ',
|
|
'Σ', 'Θ', 'Ξ', '\x1b', 'Æ', 'æ', 'ß', 'É',
|
|
' ', '!', '"', '#', '¤', '%', '&', '\'',
|
|
'(', ')', '*', '+', ',', '-', '.', '/',
|
|
'0', '1', '2', '3', '4', '5', '6', '7',
|
|
'8', '9', ':', ';', '<', '=', '>', '?',
|
|
'¡', 'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
|
'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
|
|
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
|
|
'X', 'Y', 'Z', 'Ä', 'Ö', 'Ñ', 'Ü', '§',
|
|
'¿', 'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
|
'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o',
|
|
'p', 'q', 'r', 's', 't', 'u', 'v', 'w',
|
|
'x', 'y', 'z', 'ä', 'ö', 'ñ', 'ü', 'à',
|
|
}
|
|
|
|
var gsm7ExtensionAlphabet = map[byte]rune{
|
|
0x0a: '\f',
|
|
0x14: '^',
|
|
0x28: '{',
|
|
0x29: '}',
|
|
0x2f: '\\',
|
|
0x3c: '[',
|
|
0x3d: '~',
|
|
0x3e: ']',
|
|
0x40: '|',
|
|
0x65: '€',
|
|
}
|
|
|
|
var gsm7Encoder = func() map[rune][]byte {
|
|
result := make(map[rune][]byte, len(gsm7DefaultAlphabet)+len(gsm7ExtensionAlphabet))
|
|
for code, character := range gsm7DefaultAlphabet {
|
|
if code == 0x1b {
|
|
continue
|
|
}
|
|
result[character] = []byte{byte(code)}
|
|
}
|
|
for code, character := range gsm7ExtensionAlphabet {
|
|
result[character] = []byte{0x1b, code}
|
|
}
|
|
return result
|
|
}()
|
|
|
|
type preparedSMS struct {
|
|
to string
|
|
encoding SMSEncoding
|
|
payload []byte
|
|
prompt string
|
|
setup []string
|
|
tpduLength int
|
|
part int
|
|
total int
|
|
concatReference *int
|
|
}
|
|
|
|
// PrepareSMSSubmitTPDUs applies the same recipient validation, alphabet
|
|
// selection and concatenation rules as SendSMS, but returns raw SMS-SUBMIT
|
|
// TPDUs suitable for a non-AT transport such as 3GPP SMS over IMS.
|
|
func PrepareSMSSubmitTPDUs(recipient, text string) ([]SMSSubmitTPDU, error) {
|
|
parts, err := prepareSMSParts(recipient, text)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result := make([]SMSSubmitTPDU, 0, len(parts))
|
|
for _, part := range parts {
|
|
pduHex := string(part.payload)
|
|
encoding := part.encoding
|
|
if part.encoding == SMSEncodingGSM7Text {
|
|
septets, ok := encodeGSM7(text)
|
|
if !ok {
|
|
return nil, errors.New("SMS text could not be encoded as GSM-7")
|
|
}
|
|
pduHex, _, err = encodeSubmitPDU(part.to, septets, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
encoding = SMSEncodingGSM7PDU
|
|
}
|
|
pdu, err := hex.DecodeString(pduHex)
|
|
if err != nil || len(pdu) < 2 {
|
|
return nil, errors.New("encoded SMS PDU is invalid")
|
|
}
|
|
smscBytes := int(pdu[0])
|
|
if 1+smscBytes >= len(pdu) {
|
|
return nil, errors.New("encoded SMS PDU has no TPDU")
|
|
}
|
|
result = append(result, SMSSubmitTPDU{
|
|
To: part.to,
|
|
Encoding: encoding,
|
|
TPDU: append([]byte(nil), pdu[1+smscBytes:]...),
|
|
Part: part.part,
|
|
Total: part.total,
|
|
ConcatReference: part.concatReference,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// DecodeSMSDeliverTPDU decodes a TPDU carried by RP-DATA. The existing modem
|
|
// decoder expects the AT PDU-mode SMSC prefix, so an empty SMSC is prepended.
|
|
func DecodeSMSDeliverTPDU(tpdu []byte) (SMSMessage, error) {
|
|
if len(tpdu) == 0 {
|
|
return SMSMessage{}, errors.New("SMS TPDU is empty")
|
|
}
|
|
pdu := make([]byte, 1, len(tpdu)+1)
|
|
pdu = append(pdu, tpdu...)
|
|
return decodeSMSPDU(hex.EncodeToString(pdu))
|
|
}
|
|
|
|
func prepareSMS(recipient, text string) (preparedSMS, error) {
|
|
parts, err := prepareSMSPartsWithReference(recipient, text, 0)
|
|
if err != nil {
|
|
return preparedSMS{}, err
|
|
}
|
|
if len(parts) != 1 {
|
|
return preparedSMS{}, fmt.Errorf(
|
|
"%w: message requires %d parts",
|
|
ErrSMSTooLong,
|
|
len(parts),
|
|
)
|
|
}
|
|
return parts[0], nil
|
|
}
|
|
|
|
func prepareSMSParts(recipient, text string) ([]preparedSMS, error) {
|
|
parts, err := prepareSMSPartsWithReference(recipient, text, 0)
|
|
if err != nil || len(parts) <= 1 {
|
|
return parts, err
|
|
}
|
|
var reference [1]byte
|
|
if _, err := rand.Read(reference[:]); err != nil {
|
|
return nil, fmt.Errorf("generate SMS concatenation reference: %w", err)
|
|
}
|
|
return prepareSMSPartsWithReference(recipient, text, reference[0])
|
|
}
|
|
|
|
func prepareSMSPartsWithReference(
|
|
recipient string,
|
|
text string,
|
|
reference byte,
|
|
) ([]preparedSMS, error) {
|
|
recipient, err := normalizeSMSRecipient(recipient)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if text == "" {
|
|
return nil, ErrSMSEmpty
|
|
}
|
|
if strings.IndexByte(text, 0) >= 0 {
|
|
return nil, errors.New("SMS text contains NUL")
|
|
}
|
|
|
|
septets, gsm7 := encodeGSM7(text)
|
|
if gsm7 && len(septets) <= 160 && canSendDirectGSM7(text) {
|
|
return []preparedSMS{{
|
|
to: recipient,
|
|
encoding: SMSEncodingGSM7Text,
|
|
payload: []byte(text),
|
|
prompt: fmt.Sprintf(`AT+CMGS="%s"`, recipient),
|
|
setup: []string{
|
|
"AT+CMGF=1",
|
|
`AT+CSCS="GSM"`,
|
|
// TP-SRR requests a network delivery status report.
|
|
"AT+CSMP=49,167,0,0",
|
|
},
|
|
part: 1,
|
|
total: 1,
|
|
}}, nil
|
|
}
|
|
if gsm7 && len(septets) <= 160 {
|
|
pdu, tpduLength, err := encodeSubmitPDU(recipient, septets, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []preparedSMS{{
|
|
to: recipient,
|
|
encoding: SMSEncodingGSM7PDU,
|
|
payload: []byte(pdu),
|
|
prompt: fmt.Sprintf("AT+CMGS=%d", tpduLength),
|
|
setup: []string{"AT+CMGF=0"},
|
|
tpduLength: tpduLength,
|
|
part: 1,
|
|
total: 1,
|
|
}}, nil
|
|
}
|
|
if gsm7 {
|
|
segments := splitGSM7(septets, 153)
|
|
if len(segments) > 255 {
|
|
return nil, fmt.Errorf(
|
|
"%w: GSM-7 requires %d concatenated parts; maximum is 255",
|
|
ErrSMSTooLong,
|
|
len(segments),
|
|
)
|
|
}
|
|
referenceValue := int(reference)
|
|
result := make([]preparedSMS, 0, len(segments))
|
|
for index, segment := range segments {
|
|
header := concatUDH(reference, len(segments), index+1)
|
|
pdu, tpduLength, encodeErr := encodeSubmitPDUWithHeader(
|
|
recipient,
|
|
segment,
|
|
nil,
|
|
header,
|
|
)
|
|
if encodeErr != nil {
|
|
return nil, encodeErr
|
|
}
|
|
result = append(result, preparedSMS{
|
|
to: recipient,
|
|
encoding: SMSEncodingGSM7PDU,
|
|
payload: []byte(pdu),
|
|
prompt: fmt.Sprintf("AT+CMGS=%d", tpduLength),
|
|
setup: []string{"AT+CMGF=0"},
|
|
tpduLength: tpduLength,
|
|
part: index + 1,
|
|
total: len(segments),
|
|
concatReference: &referenceValue,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
units := utf16.Encode([]rune(text))
|
|
if len(units) <= 70 {
|
|
ucs2 := encodeUCS2Units(units)
|
|
pdu, tpduLength, err := encodeSubmitPDU(recipient, nil, ucs2)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []preparedSMS{{
|
|
to: recipient,
|
|
encoding: SMSEncodingUCS2PDU,
|
|
payload: []byte(pdu),
|
|
prompt: fmt.Sprintf("AT+CMGS=%d", tpduLength),
|
|
setup: []string{"AT+CMGF=0"},
|
|
tpduLength: tpduLength,
|
|
part: 1,
|
|
total: 1,
|
|
}}, nil
|
|
}
|
|
segments := splitUCS2(units, 67)
|
|
if len(segments) > 255 {
|
|
return nil, fmt.Errorf(
|
|
"%w: UCS2 requires %d concatenated parts; maximum is 255",
|
|
ErrSMSTooLong,
|
|
len(segments),
|
|
)
|
|
}
|
|
referenceValue := int(reference)
|
|
result := make([]preparedSMS, 0, len(segments))
|
|
for index, segment := range segments {
|
|
header := concatUDH(reference, len(segments), index+1)
|
|
pdu, tpduLength, encodeErr := encodeSubmitPDUWithHeader(
|
|
recipient,
|
|
nil,
|
|
encodeUCS2Units(segment),
|
|
header,
|
|
)
|
|
if encodeErr != nil {
|
|
return nil, encodeErr
|
|
}
|
|
result = append(result, preparedSMS{
|
|
to: recipient,
|
|
encoding: SMSEncodingUCS2PDU,
|
|
payload: []byte(pdu),
|
|
prompt: fmt.Sprintf("AT+CMGS=%d", tpduLength),
|
|
setup: []string{"AT+CMGF=0"},
|
|
tpduLength: tpduLength,
|
|
part: index + 1,
|
|
total: len(segments),
|
|
concatReference: &referenceValue,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func splitGSM7(septets []byte, maximum int) [][]byte {
|
|
var result [][]byte
|
|
for len(septets) > 0 {
|
|
count := maximum
|
|
if count > len(septets) {
|
|
count = len(septets)
|
|
}
|
|
if count < len(septets) && septets[count-1] == 0x1b {
|
|
count--
|
|
}
|
|
result = append(result, append([]byte(nil), septets[:count]...))
|
|
septets = septets[count:]
|
|
}
|
|
return result
|
|
}
|
|
|
|
func splitUCS2(units []uint16, maximum int) [][]uint16 {
|
|
var result [][]uint16
|
|
for len(units) > 0 {
|
|
count := maximum
|
|
if count > len(units) {
|
|
count = len(units)
|
|
}
|
|
if count < len(units) &&
|
|
utf16.IsSurrogate(rune(units[count-1])) &&
|
|
units[count-1] >= 0xd800 && units[count-1] <= 0xdbff &&
|
|
units[count] >= 0xdc00 && units[count] <= 0xdfff {
|
|
count--
|
|
}
|
|
result = append(result, append([]uint16(nil), units[:count]...))
|
|
units = units[count:]
|
|
}
|
|
return result
|
|
}
|
|
|
|
func encodeUCS2Units(units []uint16) []byte {
|
|
result := make([]byte, 0, len(units)*2)
|
|
for _, unit := range units {
|
|
result = append(result, byte(unit>>8), byte(unit))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func concatUDH(reference byte, total, sequence int) []byte {
|
|
return []byte{0x05, 0x00, 0x03, reference, byte(total), byte(sequence)}
|
|
}
|
|
|
|
func normalizeSMSRecipient(value string) (string, error) {
|
|
var result strings.Builder
|
|
for _, character := range strings.TrimSpace(value) {
|
|
switch {
|
|
case character >= '0' && character <= '9':
|
|
result.WriteRune(character)
|
|
case character == '+' && result.Len() == 0:
|
|
result.WriteRune(character)
|
|
case character == ' ' || character == '-' ||
|
|
character == '(' || character == ')':
|
|
default:
|
|
return "", ErrSMSInvalidRecipient
|
|
}
|
|
}
|
|
normalized := result.String()
|
|
digits := strings.TrimPrefix(normalized, "+")
|
|
if len(digits) < 1 || len(digits) > 20 {
|
|
return "", ErrSMSInvalidRecipient
|
|
}
|
|
if strings.IndexFunc(digits, func(character rune) bool {
|
|
return character < '0' || character > '9'
|
|
}) >= 0 {
|
|
return "", ErrSMSInvalidRecipient
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
func encodeGSM7(text string) ([]byte, bool) {
|
|
result := make([]byte, 0, len(text))
|
|
for _, character := range text {
|
|
encoded, ok := gsm7Encoder[character]
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
result = append(result, encoded...)
|
|
}
|
|
return result, true
|
|
}
|
|
|
|
func canSendDirectGSM7(text string) bool {
|
|
for _, character := range text {
|
|
encoded, ok := gsm7Encoder[character]
|
|
if !ok || len(encoded) != 1 || encoded[0] == 0x1b ||
|
|
character > 0x7f || byte(character) != encoded[0] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
func encodeSubmitPDU(
|
|
recipient string,
|
|
gsm7Septets []byte,
|
|
ucs2 []byte,
|
|
) (string, int, error) {
|
|
return encodeSubmitPDUWithHeader(recipient, gsm7Septets, ucs2, nil)
|
|
}
|
|
|
|
func encodeSubmitPDUWithHeader(
|
|
recipient string,
|
|
gsm7Septets []byte,
|
|
ucs2 []byte,
|
|
userDataHeader []byte,
|
|
) (string, int, error) {
|
|
digits := strings.TrimPrefix(recipient, "+")
|
|
address, err := encodeSemiOctets(digits)
|
|
if err != nil {
|
|
return "", 0, err
|
|
}
|
|
toa := byte(0x81)
|
|
if strings.HasPrefix(recipient, "+") {
|
|
toa = 0x91
|
|
}
|
|
|
|
// SMS-SUBMIT with TP-SRR set so both AT PDU mode and SMS over IMS can
|
|
// receive an SMS-STATUS-REPORT for this submission.
|
|
firstOctet := byte(0x21)
|
|
if len(userDataHeader) > 0 {
|
|
if int(userDataHeader[0])+1 != len(userDataHeader) {
|
|
return "", 0, errors.New("invalid SMS concatenation header")
|
|
}
|
|
firstOctet |= 0x40
|
|
}
|
|
pdu := []byte{
|
|
0x00, // Use the SMSC configured on the SIM.
|
|
firstOctet,
|
|
0x00, // TP-MR allocated by the modem/network.
|
|
byte(len(digits)),
|
|
toa,
|
|
}
|
|
pdu = append(pdu, address...)
|
|
pdu = append(pdu, 0x00) // TP-PID
|
|
switch {
|
|
case gsm7Septets != nil:
|
|
headerSeptets := 0
|
|
startBit := 0
|
|
if len(userDataHeader) > 0 {
|
|
headerSeptets = (len(userDataHeader)*8 + 6) / 7
|
|
startBit = headerSeptets * 7
|
|
}
|
|
userDataLength := headerSeptets + len(gsm7Septets)
|
|
if userDataLength > 160 {
|
|
return "", 0, ErrSMSTooLong
|
|
}
|
|
packed := packSeptets(gsm7Septets, startBit)
|
|
copy(packed, userDataHeader)
|
|
pdu = append(pdu, 0x00, byte(userDataLength))
|
|
pdu = append(pdu, packed...)
|
|
case ucs2 != nil:
|
|
userDataLength := len(userDataHeader) + len(ucs2)
|
|
if userDataLength > 140 {
|
|
return "", 0, ErrSMSTooLong
|
|
}
|
|
pdu = append(pdu, 0x08, byte(userDataLength))
|
|
pdu = append(pdu, userDataHeader...)
|
|
pdu = append(pdu, ucs2...)
|
|
default:
|
|
return "", 0, errors.New("SMS PDU has no user data")
|
|
}
|
|
return strings.ToUpper(hex.EncodeToString(pdu)), len(pdu) - 1, nil
|
|
}
|
|
|
|
func encodeSemiOctets(digits string) ([]byte, error) {
|
|
if digits == "" {
|
|
return nil, ErrSMSInvalidRecipient
|
|
}
|
|
result := make([]byte, (len(digits)+1)/2)
|
|
for index := 0; index < len(digits); index += 2 {
|
|
low := digits[index]
|
|
if low < '0' || low > '9' {
|
|
return nil, ErrSMSInvalidRecipient
|
|
}
|
|
high := byte('F')
|
|
if index+1 < len(digits) {
|
|
high = digits[index+1]
|
|
if high < '0' || high > '9' {
|
|
return nil, ErrSMSInvalidRecipient
|
|
}
|
|
}
|
|
highNibble := byte(0x0f)
|
|
if high != 'F' {
|
|
highNibble = high - '0'
|
|
}
|
|
result[index/2] = (highNibble << 4) | (low - '0')
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func packSeptets(septets []byte, startBit int) []byte {
|
|
if len(septets) == 0 {
|
|
return nil
|
|
}
|
|
bitLength := startBit + len(septets)*7
|
|
result := make([]byte, (bitLength+7)/8)
|
|
for index, septet := range septets {
|
|
bit := startBit + index*7
|
|
byteIndex := bit / 8
|
|
shift := uint(bit % 8)
|
|
result[byteIndex] |= (septet & 0x7f) << shift
|
|
if shift > 1 && byteIndex+1 < len(result) {
|
|
result[byteIndex+1] |= (septet & 0x7f) >> (8 - shift)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func unpackSeptets(data []byte, count, startBit int) ([]byte, error) {
|
|
if count < 0 || startBit < 0 || startBit+count*7 > len(data)*8 {
|
|
return nil, errors.New("GSM-7 user data is truncated")
|
|
}
|
|
result := make([]byte, count)
|
|
for index := 0; index < count; index++ {
|
|
bit := startBit + index*7
|
|
byteIndex := bit / 8
|
|
shift := uint(bit % 8)
|
|
value := data[byteIndex] >> shift
|
|
if shift > 1 && byteIndex+1 < len(data) {
|
|
value |= data[byteIndex+1] << (8 - shift)
|
|
}
|
|
result[index] = value & 0x7f
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func decodeGSM7(septets []byte) (string, error) {
|
|
var result strings.Builder
|
|
for index := 0; index < len(septets); index++ {
|
|
code := septets[index]
|
|
if code != 0x1b {
|
|
if int(code) >= len(gsm7DefaultAlphabet) {
|
|
return result.String(), errors.New("invalid GSM-7 code")
|
|
}
|
|
result.WriteRune(gsm7DefaultAlphabet[code])
|
|
continue
|
|
}
|
|
index++
|
|
if index >= len(septets) {
|
|
return result.String(), errors.New("trailing GSM-7 escape")
|
|
}
|
|
character, ok := gsm7ExtensionAlphabet[septets[index]]
|
|
if !ok {
|
|
return result.String(), fmt.Errorf(
|
|
"unknown GSM-7 extension 0x%02X",
|
|
septets[index],
|
|
)
|
|
}
|
|
result.WriteRune(character)
|
|
}
|
|
return result.String(), nil
|
|
}
|
|
|
|
type pduCursor struct {
|
|
data []byte
|
|
index int
|
|
}
|
|
|
|
func (cursor *pduCursor) byte() (byte, error) {
|
|
if cursor.index >= len(cursor.data) {
|
|
return 0, errors.New("SMS PDU is truncated")
|
|
}
|
|
value := cursor.data[cursor.index]
|
|
cursor.index++
|
|
return value, nil
|
|
}
|
|
|
|
func (cursor *pduCursor) bytes(count int) ([]byte, error) {
|
|
if count < 0 || cursor.index+count > len(cursor.data) {
|
|
return nil, errors.New("SMS PDU is truncated")
|
|
}
|
|
value := cursor.data[cursor.index : cursor.index+count]
|
|
cursor.index += count
|
|
return value, nil
|
|
}
|
|
|
|
func decodeSMSPDU(raw string) (SMSMessage, error) {
|
|
raw = strings.ToUpper(strings.TrimSpace(raw))
|
|
message := SMSMessage{
|
|
Direction: SMSDirectionUnknown,
|
|
Encoding: SMSEncodingUnknown,
|
|
StorageStatus: SMSStatusUnknown,
|
|
RawPDU: raw,
|
|
}
|
|
decoded, err := hex.DecodeString(raw)
|
|
if err != nil || len(decoded) < 2 {
|
|
if err == nil {
|
|
err = errors.New("SMS PDU is too short")
|
|
}
|
|
return message, err
|
|
}
|
|
cursor := &pduCursor{data: decoded}
|
|
smscLength, _ := cursor.byte()
|
|
if int(smscLength) > len(decoded)-1 {
|
|
return message, errors.New("SMS PDU SMSC length exceeds payload")
|
|
}
|
|
if smscLength > 0 {
|
|
smsc, _ := cursor.bytes(int(smscLength))
|
|
if len(smsc) > 1 {
|
|
message.ServiceCenter = decodeNumericAddress(
|
|
smsc[1:],
|
|
(len(smsc)-1)*2,
|
|
smsc[0],
|
|
)
|
|
}
|
|
}
|
|
firstOctet, err := cursor.byte()
|
|
if err != nil {
|
|
return message, err
|
|
}
|
|
switch firstOctet & 0x03 {
|
|
case 0:
|
|
message.Direction = SMSDirectionReceived
|
|
err = decodeDeliverPDU(cursor, firstOctet, &message)
|
|
case 1:
|
|
message.Direction = SMSDirectionSubmitted
|
|
err = decodeSubmitPDU(cursor, firstOctet, &message)
|
|
case 2:
|
|
message.Direction = SMSDirectionStatusReport
|
|
err = decodeStatusReportPDU(cursor, &message)
|
|
default:
|
|
err = errors.New("unsupported SMS PDU message type")
|
|
}
|
|
return message, err
|
|
}
|
|
|
|
func decodeDeliverPDU(
|
|
cursor *pduCursor,
|
|
firstOctet byte,
|
|
message *SMSMessage,
|
|
) error {
|
|
address, err := readTPAddress(cursor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message.From = address
|
|
pid, err := cursor.byte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dcs, err := cursor.byte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message.ProtocolID = int(pid)
|
|
message.DataCodingScheme = int(dcs)
|
|
timestamp, err := cursor.bytes(7)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if parsed, timestampErr := decodeSMSTimestamp(timestamp); timestampErr == nil {
|
|
message.ServiceCenterTimestamp = parsed
|
|
}
|
|
udl, err := cursor.byte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decodeUserData(cursor.data[cursor.index:], firstOctet, dcs, int(udl), message)
|
|
}
|
|
|
|
func decodeSubmitPDU(
|
|
cursor *pduCursor,
|
|
firstOctet byte,
|
|
message *SMSMessage,
|
|
) error {
|
|
reference, err := cursor.byte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message.MessageReference = intPointer(int(reference))
|
|
address, err := readTPAddress(cursor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message.To = address
|
|
pid, err := cursor.byte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dcs, err := cursor.byte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message.ProtocolID = int(pid)
|
|
message.DataCodingScheme = int(dcs)
|
|
switch (firstOctet >> 3) & 0x03 {
|
|
case 2:
|
|
if _, err := cursor.byte(); err != nil {
|
|
return err
|
|
}
|
|
case 1, 3:
|
|
if _, err := cursor.bytes(7); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
udl, err := cursor.byte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return decodeUserData(cursor.data[cursor.index:], firstOctet, dcs, int(udl), message)
|
|
}
|
|
|
|
func decodeStatusReportPDU(
|
|
cursor *pduCursor,
|
|
message *SMSMessage,
|
|
) error {
|
|
reference, err := cursor.byte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message.MessageReference = intPointer(int(reference))
|
|
address, err := readTPAddress(cursor)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message.To = address
|
|
scts, err := cursor.bytes(7)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
discharge, err := cursor.bytes(7)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
status, err := cursor.byte()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
message.StatusCode = intPointer(int(status))
|
|
message.DeliveryStatus = smsDeliveryStatus(status)
|
|
if parsed, parseErr := decodeSMSTimestamp(scts); parseErr == nil {
|
|
message.ServiceCenterTimestamp = parsed
|
|
}
|
|
if parsed, parseErr := decodeSMSTimestamp(discharge); parseErr == nil {
|
|
message.DischargeTimestamp = parsed
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func smsDeliveryStatus(status byte) string {
|
|
switch {
|
|
case status <= 0x1f:
|
|
return "delivered"
|
|
case status <= 0x3f:
|
|
return "temporary_error"
|
|
case status <= 0x5f:
|
|
return "permanent_error"
|
|
case status <= 0x7f:
|
|
return "temporary_error_no_retry"
|
|
default:
|
|
return "reserved"
|
|
}
|
|
}
|
|
|
|
func readTPAddress(cursor *pduCursor) (string, error) {
|
|
length, err := cursor.byte()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
toa, err := cursor.byte()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
byteCount := (int(length) + 1) / 2
|
|
value, err := cursor.bytes(byteCount)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if toa&0x70 == 0x50 {
|
|
septetCount := int(length) * 4 / 7
|
|
septets, unpackErr := unpackSeptets(value, septetCount, 0)
|
|
if unpackErr != nil {
|
|
return "", unpackErr
|
|
}
|
|
return decodeGSM7(septets)
|
|
}
|
|
return decodeNumericAddress(value, int(length), toa), nil
|
|
}
|
|
|
|
func decodeNumericAddress(value []byte, digits int, toa byte) string {
|
|
var result strings.Builder
|
|
if toa&0x70 == 0x10 {
|
|
result.WriteByte('+')
|
|
}
|
|
written := 0
|
|
for _, octet := range value {
|
|
for _, digit := range []byte{octet & 0x0f, octet >> 4} {
|
|
if written >= digits {
|
|
return result.String()
|
|
}
|
|
if digit <= 9 {
|
|
result.WriteByte('0' + digit)
|
|
written++
|
|
}
|
|
}
|
|
}
|
|
return result.String()
|
|
}
|
|
|
|
func decodeUserData(
|
|
data []byte,
|
|
firstOctet byte,
|
|
dcs byte,
|
|
udl int,
|
|
message *SMSMessage,
|
|
) error {
|
|
alphabet := dcs & 0x0c
|
|
expectedBytes := udl
|
|
if alphabet == 0 {
|
|
expectedBytes = (udl*7 + 7) / 8
|
|
}
|
|
if expectedBytes > len(data) {
|
|
return errors.New("SMS user data is truncated")
|
|
}
|
|
data = data[:expectedBytes]
|
|
message.RawUserData = strings.ToUpper(hex.EncodeToString(data))
|
|
|
|
headerBytes := 0
|
|
if firstOctet&0x40 != 0 {
|
|
if len(data) == 0 {
|
|
return errors.New("SMS has UDHI but no user data header")
|
|
}
|
|
headerBytes = int(data[0]) + 1
|
|
if headerBytes > len(data) {
|
|
return errors.New("SMS user data header is truncated")
|
|
}
|
|
message.Concat = parseConcatHeader(data[1:headerBytes])
|
|
}
|
|
|
|
switch alphabet {
|
|
case 0:
|
|
message.Encoding = SMSEncodingGSM7PDU
|
|
headerSeptets := 0
|
|
if headerBytes > 0 {
|
|
headerSeptets = (headerBytes*8 + 6) / 7
|
|
}
|
|
textSeptets := udl - headerSeptets
|
|
septets, err := unpackSeptets(data, textSeptets, headerSeptets*7)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
text, err := decodeGSM7(septets)
|
|
message.Text = text
|
|
return err
|
|
case 8:
|
|
message.Encoding = SMSEncodingUCS2PDU
|
|
payload := data[headerBytes:]
|
|
if len(payload)%2 != 0 {
|
|
return errors.New("UCS2 SMS has an odd byte count")
|
|
}
|
|
units := make([]uint16, 0, len(payload)/2)
|
|
for index := 0; index < len(payload); index += 2 {
|
|
units = append(units, uint16(payload[index])<<8|uint16(payload[index+1]))
|
|
}
|
|
message.Text = string(utf16.Decode(units))
|
|
return nil
|
|
default:
|
|
message.Encoding = SMSEncoding8BitPDU
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func parseConcatHeader(header []byte) *SMSConcatInfo {
|
|
for index := 0; index+1 < len(header); {
|
|
identifier := header[index]
|
|
length := int(header[index+1])
|
|
index += 2
|
|
if index+length > len(header) {
|
|
return nil
|
|
}
|
|
value := header[index : index+length]
|
|
switch {
|
|
case identifier == 0x00 && length == 3:
|
|
return &SMSConcatInfo{
|
|
Reference: int(value[0]),
|
|
Total: int(value[1]),
|
|
Sequence: int(value[2]),
|
|
}
|
|
case identifier == 0x08 && length == 4:
|
|
return &SMSConcatInfo{
|
|
Reference: int(value[0])<<8 | int(value[1]),
|
|
Total: int(value[2]),
|
|
Sequence: int(value[3]),
|
|
}
|
|
}
|
|
index += length
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func decodeSMSTimestamp(value []byte) (*time.Time, error) {
|
|
if len(value) != 7 {
|
|
return nil, errors.New("SMS timestamp must be seven octets")
|
|
}
|
|
component := func(octet byte) (int, error) {
|
|
low, high := int(octet&0x0f), int(octet>>4)
|
|
if low > 9 || high > 9 {
|
|
return 0, errors.New("invalid timestamp semi-octet")
|
|
}
|
|
return low*10 + high, nil
|
|
}
|
|
parts := make([]int, 6)
|
|
for index := range parts {
|
|
parsed, err := component(value[index])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts[index] = parsed
|
|
}
|
|
zoneByte := value[6]
|
|
negative := zoneByte&0x08 != 0
|
|
zoneByte &^= 0x08
|
|
quarters, err := component(zoneByte)
|
|
if err != nil || quarters > 56 {
|
|
return nil, errors.New("invalid SMS timestamp timezone")
|
|
}
|
|
offset := quarters * 15 * 60
|
|
if negative {
|
|
offset = -offset
|
|
}
|
|
year := 2000 + parts[0]
|
|
if parts[0] >= 90 {
|
|
year = 1900 + parts[0]
|
|
}
|
|
if parts[1] < 1 || parts[1] > 12 ||
|
|
parts[2] < 1 || parts[2] > 31 ||
|
|
parts[3] > 23 || parts[4] > 59 || parts[5] > 60 {
|
|
return nil, errors.New("invalid SMS timestamp component")
|
|
}
|
|
zone := time.FixedZone("SMS", offset)
|
|
result := time.Date(
|
|
year,
|
|
time.Month(parts[1]),
|
|
parts[2],
|
|
parts[3],
|
|
parts[4],
|
|
parts[5],
|
|
0,
|
|
zone,
|
|
)
|
|
return &result, nil
|
|
}
|
|
|
|
func parseDecimal(value string) (int, bool) {
|
|
number, err := strconv.Atoi(strings.Trim(strings.TrimSpace(value), `"`))
|
|
return number, err == nil
|
|
}
|