# SPDX-License-Identifier: GPL-2.0-or-later
# This file is part of Scapy
# See https://scapy.net/ for more information
# Copyright (C) Michael Farrell <micolous+git@gmail.com>
#
# scapy.contrib.description = Eddystone BLE proximity beacon
# scapy.contrib.status = loads
"""
scapy.contrib.eddystone - Google Eddystone Bluetooth LE proximity beacons.

The Eddystone specification can be found at:
https://github.com/google/eddystone/blob/master/protocol-specification.md

These beacons are used as building blocks for other systems:

* Google's Physical Web <https://google.github.io/physical-web/>
* RuuviTag <https://github.com/ruuvi/ruuvi-sensor-protocols>
* Waze Beacons <https://www.waze.com/beacons>

"""

from scapy.compat import orb
from scapy.fields import IntField, SignedByteField, StrField, BitField, \
    StrFixedLenField, ShortField, FixedPointField, ByteEnumField
from scapy.layers.bluetooth import EIR_Hdr, EIR_ServiceData16BitUUID, \
    EIR_CompleteList16BitServiceUUIDs, LowEnergyBeaconHelper
from scapy.packet import bind_layers, Packet

EDDYSTONE_UUID = 0xfeaa

EDDYSTONE_URL_SCHEMES = {
    0: b"http://www.",
    1: b"https://www.",
    2: b"http://",
    3: b"https://",
}

EDDYSTONE_URL_TABLE = {
    0: b".com/",
    1: b".org/",
    2: b".edu/",
    3: b".net/",
    4: b".info/",
    5: b".biz/",
    6: b".gov/",
    7: b".com",
    8: b".org",
    9: b".edu",
    10: b".net",
    11: b".info",
    12: b".biz",
    13: b".gov",
}


class EddystoneURLField(StrField):
    # https://github.com/google/eddystone/tree/master/eddystone-url#eddystone-url-http-url-encoding
    def i2m(self, pkt, x):
        if x is None:
            return b""

        o = bytearray()
        p = 0
        while p < len(x):
            c = orb(x[p])
            if c == 46:  # "."
                for k, v in EDDYSTONE_URL_TABLE.items():
                    if x.startswith(v, p):
                        o.append(k)
                        p += len(v) - 1
                        break
                else:
                    o.append(c)
            else:
                o.append(c)
            p += 1

        # Make the output immutable.
        return bytes(o)

    def m2i(self, pkt, x):
        if not x:
            return None

        o = bytearray()
        for c in x:
            i = orb(c)
            r = EDDYSTONE_URL_TABLE.get(i)
            if r is None:
                o.append(i)
            else:
                o.extend(r)
        return bytes(o)

    def any2i(self, pkt, x):
        if isinstance(x, str):
            x = x.encode("ascii")
        return x


class Eddystone_Frame(Packet, LowEnergyBeaconHelper):
    """
    The base Eddystone frame on which all Eddystone messages are built.

    https://github.com/google/eddystone/blob/master/protocol-specification.md
    """
    name = "Eddystone Frame"
    fields_desc = [
        BitField("type", None, 4),
        BitField("reserved", 0, 4),
    ]

    def build_eir(self):
        """Builds a list of EIR messages to wrap this frame."""

        return LowEnergyBeaconHelper.base_eir + [
            EIR_Hdr() / EIR_CompleteList16BitServiceUUIDs(svc_uuids=[
                EDDYSTONE_UUID]),
            EIR_Hdr() / EIR_ServiceData16BitUUID() / self
        ]


class Eddystone_UID(Packet):
    """
    An Eddystone type for transmitting a unique identifier.

    https://github.com/google/eddystone/tree/master/eddystone-uid
    """
    name = "Eddystone UID"
    fields_desc = [
        SignedByteField("tx_power", 0),
        StrFixedLenField("namespace", None, 10),
        StrFixedLenField("instance", None, 6),
        StrFixedLenField("reserved", None, 2),
    ]


class Eddystone_URL(Packet):
    """
    An Eddystone type for transmitting a URL (to a web page).

    https://github.com/google/eddystone/tree/master/eddystone-url
    """
    name = "Eddystone URL"
    fields_desc = [
        SignedByteField("tx_power", 0),
        ByteEnumField("url_scheme", 0, EDDYSTONE_URL_SCHEMES),
        EddystoneURLField("url", None),
    ]

    def to_url(self):
        return EDDYSTONE_URL_SCHEMES[self.url_scheme] + self.url

    @staticmethod
    def from_url(url):
        """Creates an Eddystone_Frame with a Eddystone_URL for a given URL."""
        url = url.encode('ascii')
        scheme = None
        for k, v in EDDYSTONE_URL_SCHEMES.items():
            if url.startswith(v):
                scheme = k
                url = url[len(v):]
                break
        else:
            raise Exception("URLs must start with EDDYSTONE_URL_SCHEMES")

        return Eddystone_Frame() / Eddystone_URL(
            url_scheme=scheme,
            url=url)


class Eddystone_TLM(Packet):
    """
    An Eddystone type for transmitting beacon telemetry information.

    https://github.com/google/eddystone/tree/master/eddystone-tlm
    """
    name = "Eddystone TLM"
    fields_desc = [
        ByteEnumField("version", None, {
            0: "unencrypted",
            1: "encrypted",
        }),
    ]


class Eddystone_TLM_Unencrypted(Packet):
    """
    A subtype of Eddystone-TLM for transmitting telemetry in unencrypted form.

    https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-plain.md
    """
    name = "Eddystone TLM (Unencrypted)"
    fields_desc = [
        ShortField("batt_mv", 0),
        FixedPointField("temperature", -128, 16, 8),
        IntField("adv_cnt", None),
        IntField("sec_cnt", None),
    ]


class Eddystone_TLM_Encrypted(Packet):
    """
    A subtype of Eddystone-TLM for transmitting telemetry in encrypted form.

    This implementation does not support decrypting this data.

    https://github.com/google/eddystone/blob/master/eddystone-tlm/tlm-encrypted.md
    """
    name = "Eddystone TLM (Encrypted)"
    fields_desc = [
        StrFixedLenField("etlm", None, 12),
        StrFixedLenField("salt", None, 2),
        StrFixedLenField("mic", None, 2),
    ]


class Eddystone_EID(Packet):
    """
    An Eddystone type for transmitting encrypted, ephemeral identifiers.

    This implementation does not support decrypting this data.

    https://github.com/google/eddystone/tree/master/eddystone-eid
    """
    name = "Eddystone EID"
    fields_desc = [
        SignedByteField("tx_power", 0),
        StrFixedLenField("eid", None, 8),
    ]


bind_layers(Eddystone_TLM, Eddystone_TLM_Unencrypted, version=0)
bind_layers(Eddystone_TLM, Eddystone_TLM_Encrypted, version=1)

bind_layers(Eddystone_Frame, Eddystone_UID, type=0)
bind_layers(Eddystone_Frame, Eddystone_URL, type=1)
bind_layers(Eddystone_Frame, Eddystone_TLM, type=2)
bind_layers(Eddystone_Frame, Eddystone_EID, type=3)

bind_layers(EIR_ServiceData16BitUUID, Eddystone_Frame, svc_uuid=EDDYSTONE_UUID)
