/*
  Copyright 2021 Grégory Soutadé

  This file is part of libgourou.

  libgourou is free software: you can redistribute it and/or modify
  it under the terms of the GNU Lesser General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  libgourou 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 Lesser General Public License for more details.

  You should have received a copy of the GNU Lesser General Public License
  along with libgourou. If not, see <http://www.gnu.org/licenses/>.
*/

#include <sys/stat.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <pwd.h>
#include <locale.h>

#include <libgourou.h>
#include <libgourou_common.h>
#include <libgourou_log.h>
#include <device.h>

#include <string.h>
#if defined(__linux__) || defined(linux) || defined(__linux)
#include <sys/ioctl.h>
#include <net/if.h>
#include <unistd.h>
#include <netinet/in.h>
#elif (defined(__FreeBSD__) || defined(__NetBSD__) || defined(__OpenBSD__) \
       || defined(__bsdi__) || defined(__DragonFly__) || defined(__APPLE__))
#include <ifaddrs.h>
#include <sys/socket.h>
#include <net/if_dl.h>

#define BSD_HEADERS 1
#endif

#if defined(__linux__) || defined(linux) || defined(__linux)
// From https://stackoverflow.com/questions/1779715/how-to-get-mac-address-of-your-machine-using-a-c-program/35242525
int get_mac_address(unsigned char* mac_address)
{
    struct ifreq ifr;
    struct ifconf ifc;
    char buf[1024];
    int success = 0;

    int sock = socket(AF_INET, SOCK_DGRAM, IPPROTO_IP);
    if (sock == -1) { EXCEPTION(gourou::DEV_MAC_ERROR, "Unable to create socket"); };

    ifc.ifc_len = sizeof(buf);
    ifc.ifc_buf = buf;
    if (ioctl(sock, SIOCGIFCONF, &ifc) == -1) { EXCEPTION(gourou::DEV_MAC_ERROR, "SIOCGIFCONF ioctl failed"); }

    struct ifreq* it = ifc.ifc_req;
    const struct ifreq* const end = it + (ifc.ifc_len / sizeof(struct ifreq));

    for (; it != end; ++it) {
        strcpy(ifr.ifr_name, it->ifr_name);
        if (ioctl(sock, SIOCGIFFLAGS, &ifr) == 0) {
            if (! (ifr.ifr_flags & IFF_LOOPBACK)) { // don't count loopback
                if (ioctl(sock, SIOCGIFHWADDR, &ifr) == 0) {
                    success = 1;
                    break;
                }
            }
        }
        else { EXCEPTION(gourou::DEV_MAC_ERROR, "SIOCGIFFLAGS ioctl failed"); }
    }

    if (success)
    {
        memcpy(mac_address, ifr.ifr_hwaddr.sa_data, 6);
        return 0;
    }

    return 1;
}
#elif BSD_HEADERS
// https://stackoverflow.com/a/3978293
int get_mac_address(unsigned char* mac_address, const char* if_name = "en0")
{
    ifaddrs* iflist;
    int found = 0;
    if (getifaddrs(&iflist) == 0) {
        for (ifaddrs* cur = iflist; cur; cur = cur->ifa_next) {
            if ((cur->ifa_addr->sa_family == AF_LINK) &&
                (strcmp(cur->ifa_name, if_name) == 0) &&
                cur->ifa_addr) {
                sockaddr_dl* sdl = (sockaddr_dl*)cur->ifa_addr;
                memcpy(mac_address, LLADDR(sdl), sdl->sdl_alen);
                found = 1;
                break;
            }
        }

        freeifaddrs(iflist);
    }
    return found;
}
#else
int get_mac_address(unsigned char* mac_address)
{
    GOUROU_LOG(INFO, "get_mac_address() not implemented for your platform, using a static address");

    mac_address[0] = 0x8D;
    mac_address[1] = 0x70;
    mac_address[2] = 0x13;
    mac_address[3] = 0x8D;
    mac_address[4] = 0x43;
    mac_address[5] = 0x27;

    return 1;
}
#endif /* defined(__linux__) || defined(linux) || defined(__linux) */


namespace gourou
{
    Device::Device(DRMProcessor* processor):
        processor(processor)
    {}

    Device::Device(DRMProcessor* processor, const std::string& deviceFile, const std::string& deviceKeyFile):
        processor(processor), deviceFile(deviceFile), deviceKeyFile(deviceKeyFile)
    {
        parseDeviceKeyFile();
        parseDeviceFile();
    }

    /* SHA1(uid ":" username ":" macaddress ":" */
    std::string Device::makeSerial(bool random)
    {
        unsigned char sha_out[SHA1_LEN];
        DRMProcessorClient* client = processor->getClient();

        if (!random)
        {
            uid_t uid = getuid();
            struct passwd * passwd = getpwuid(uid);
            // Default mac address in case of failure
            unsigned char mac_address[6] = {0x01, 0x02, 0x03, 0x04, 0x05};

            get_mac_address(mac_address);

            int dataToHashLen = 10 /* UID */ + strlen(passwd->pw_name) + sizeof(mac_address)*2 /*mac address*/ + 1 /* \0 */;
            dataToHashLen += 8; /* Separators */
            unsigned char* dataToHash = new unsigned char[dataToHashLen];
            dataToHashLen = snprintf((char*)dataToHash, dataToHashLen, "%d:%s:%02x:%02x:%02x:%02x:%02x:%02x:",
                                     uid, passwd->pw_name,
                                     mac_address[0], mac_address[1], mac_address[2],
                                     mac_address[3], mac_address[4], mac_address[5]);

            client->digest("SHA1", dataToHash, dataToHashLen+1, sha_out);

            delete[] dataToHash;
        }
        else
        {
            client->randBytes(sha_out, sizeof(sha_out));
        }

        std::string res = ByteArray((const char*)sha_out, DEVICE_SERIAL_LEN).toHex();
        GOUROU_LOG(DEBUG, "Serial : " << res);
        return res;
    }

    /* base64(SHA1 (serial + privateKey)) */
    std::string Device::makeFingerprint(const std::string& serial)
    {
        DRMProcessorClient* client = processor->getClient();
        unsigned char sha_out[SHA1_LEN];

        void* handler = client->createDigest("SHA1");
        client->digestUpdate(handler, (unsigned char*) serial.c_str(), serial.length());
        client->digestUpdate(handler, deviceKey, sizeof(deviceKey));
        client->digestFinalize(handler, sha_out);

        std::string res = ByteArray(sha_out, sizeof(sha_out)).toBase64();
        GOUROU_LOG(DEBUG, "Fingerprint : " << res);
        return res;
    }

    void Device::createDeviceFile(const std::string& hobbes, bool randomSerial)
    {
        struct utsname sysname;
        uname(&sysname);

        std::string serial = makeSerial(randomSerial);
        std::string fingerprint = makeFingerprint(serial);

        pugi::xml_document deviceDoc;
        pugi::xml_node decl = deviceDoc.append_child(pugi::node_declaration);
        decl.append_attribute("version") = "1.0";

        pugi::xml_node root = deviceDoc.append_child("adept:deviceInfo");
        root.append_attribute("xmlns:adept") = ADOBE_ADEPT_NS;

        appendTextElem(root, "adept:deviceClass",  "Desktop");
        appendTextElem(root, "adept:deviceSerial", serial);
        appendTextElem(root, "adept:deviceName",   sysname.nodename);
        appendTextElem(root, "adept:deviceType",   "standalone");

        pugi::xml_node version = root.append_child("adept:version");
        version.append_attribute("name") = "hobbes";
        version.append_attribute("value") = hobbes.c_str();

        version = root.append_child("adept:version");
        version.append_attribute("name") = "clientOS";
        std::string os = std::string(sysname.sysname) + " " + std::string(sysname.release);
        version.append_attribute("value") = os.c_str();

        version = root.append_child("adept:version");
        version.append_attribute("name") = "clientLocale";
        version.append_attribute("value") = setlocale(LC_ALL, NULL);

        appendTextElem(root, "adept:fingerprint", fingerprint);

        StringXMLWriter xmlWriter;
        deviceDoc.save(xmlWriter, "  ");

        GOUROU_LOG(DEBUG, "Create device file " << deviceFile);

        writeFile(deviceFile, xmlWriter.getResult());
    }

    void Device::createDeviceKeyFile()
    {
        unsigned char key[DEVICE_KEY_SIZE];

        GOUROU_LOG(DEBUG, "Create device key file " << deviceKeyFile);

        processor->getClient()->randBytes(key, sizeof(key));

        writeFile(deviceKeyFile, key, sizeof(key));
    }

    Device* Device::createDevice(DRMProcessor* processor, const std::string& dirName, const std::string& hobbes, bool randomSerial)
    {
        struct stat _stat;

        if (stat(dirName.c_str(), &_stat) != 0)
        {
            if (mkdir_p(dirName.c_str(), S_IRWXU))
                EXCEPTION(DEV_MKPATH, "Unable to create " << dirName)
                    }

        Device* device = new Device(processor);

        device->deviceFile = dirName + "/device.xml";
        device->deviceKeyFile = dirName + "/devicesalt";

        try
        {
            device->parseDeviceKeyFile();
        }
        catch (...)
        {
            device->createDeviceKeyFile();
            device->parseDeviceKeyFile();
        }

        try
        {
            device->parseDeviceFile();
        }
        catch (...)
        {
            device->createDeviceFile(hobbes, randomSerial);
            device->parseDeviceFile();
        }

        return device;
    }

    const unsigned char* Device::getDeviceKey()
    {
        return deviceKey;
    }

    void Device::parseDeviceFile()
    {
        pugi::xml_document doc;

        if (!doc.load_file(deviceFile.c_str()))
            EXCEPTION(DEV_INVALID_DEVICE_FILE, "Invalid device file");

        try
        {
            properties["deviceClass"]  = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:deviceClass");
            properties["deviceSerial"] = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:deviceSerial");
            properties["deviceName"]   = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:deviceName");
            properties["deviceType"]   = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:deviceType");
            properties["fingerprint"]  = gourou::extractTextElem(doc, "/adept:deviceInfo/adept:fingerprint");

            pugi::xpath_node_set nodeSet = doc.select_nodes("/adept:deviceInfo/adept:version");

            for (pugi::xpath_node_set::const_iterator it = nodeSet.begin();
                 it != nodeSet.end(); ++it)
            {
                pugi::xml_node node = it->node();
                pugi::xml_attribute name = node.attribute("name");
                pugi::xml_attribute value = node.attribute("value");

                properties[name.value()] = value.value();
            }
        }
        catch (gourou::Exception& e)
        {
            EXCEPTION(DEV_INVALID_DEVICE_FILE, "Invalid device file");
        }
    }

    void Device::parseDeviceKeyFile()
    {
        struct stat _stat;

        if (stat(deviceKeyFile.c_str(), &_stat) == 0 &&
            _stat.st_size == DEVICE_KEY_SIZE)
        {
            readFile(deviceKeyFile, deviceKey, sizeof(deviceKey));
        }
        else
            EXCEPTION(DEV_INVALID_DEVICE_KEY_FILE, "Invalid device key file");
    }

    std::string Device::getProperty(const std::string& property, const std::string& _default)
    {
        if (properties.find(property) == properties.end())
        {
            if (_default == "")
                EXCEPTION(DEV_INVALID_DEV_PROPERTY, "Invalid property " << property);

            return _default;
        }

        return properties[property];
    }

    std::string Device::operator[](const std::string& property)
    {
        return getProperty(property);
    }
}
