/*
 *
 *  Visual Voicemail Daemon
 *
 *  Copyright (C) 2022, akrosi8
 *                2022, Chris Talbot <chris@talbothome.com>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License version 2 as
 *  published by the Free Software Foundation.
 *
 *  This program 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 this program; if not, write to the Free Software
 *  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 *
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <gio/gio.h>
#include <curl/curl.h>

#include "vvm3-activation.h"
#include "vvm.h"

// Code on AOSP that activates vvm3
// https://cs.android.com/android/platform/superproject/+/master:packages/apps/Dialer/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java

size_t
vvm3_curl_writeback (void    *ptr,
                     size_t   size,
                     size_t   nmemb,
                     GString *s)
{
  if (g_string_append_len (s, ptr, size * nmemb) == NULL)
    {
      g_warning ("Curl's writeback function wrote no data");
      return 0;
    }
  return size * nmemb;
}

void
vvm3_gmarkup_parsetext (GMarkupParseContext *context,
                        const char          *text,
                        gsize                text_len,
                        gpointer             user_data,
                        GError             **error)
{
  if (!strcmp (g_markup_parse_context_get_element (context), "spgurl"))
    {
      *((char **)user_data) = g_strndup (text, text_len);
      g_debug ("URL grabbed: %s", text);
    }
}

void
vvm3_gmarkup_parseelement (GMarkupParseContext *context,
                           const char          *element_name,
                           const char         **attribute_names,
                           const char         **attribute_values,
                           gpointer             user_data,
                           GError             **error)
{
  const char **name_cursor = attribute_names;
  const char **value_cursor = attribute_values;

  while (*name_cursor)
    {
      if ((strcmp (*name_cursor, "href") == 0) &&
          (!strcmp (g_markup_parse_context_get_element (context), "a")))
        {
          *((char **)user_data) = g_strdup (*value_cursor);
          g_debug ("Grabbed url %s", *value_cursor);
        }
      name_cursor++;
      value_cursor++;
    }
}

static GMarkupParser vvm3_xml_parser = {
  NULL,
  NULL,
  vvm3_gmarkup_parsetext,
  NULL,
  NULL
};

static GMarkupParser vvm3_htmlline_parser = {
  vvm3_gmarkup_parseelement,
  NULL,
  NULL,
  NULL,
  NULL
};

char *
vvm3_find_button (char *input)
{
  g_autofree char **inputLines = NULL;

  inputLines = g_strsplit_set (input, "\r\n", -1);
  if (inputLines == NULL)
    {
      g_debug ("Failed to split the HTML!");
      return NULL;
    }
  for (unsigned int i = 0; i < g_strv_length (inputLines); i++)
    {
      /*
       * https://cs.android.com/android/platform/superproject/+/master:packages/apps/Dialer/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java;drc=ba93c4fe5016341d06be1ce47410885522e09a58;l=126
       * Look for `Subscribe to Basic Visual Voice Mail` or `Subscribe to Basic Visual Voicemail`
       */
      char *cmpResult;
      cmpResult = strstr (inputLines[i], "Subscribe to Basic Visual Voice Mail");
      if (cmpResult == NULL)
        cmpResult = strstr (inputLines[i], "Subscribe to Basic Visual Voicemail");

      if (cmpResult != NULL)
        {
          // Return the beginning of the `<a href` link
          cmpResult = strstr (inputLines[i], "<a");
          g_debug ("Found the subscribe button: %s", cmpResult);
          return g_strdup (cmpResult);
        }
    }
  g_warning ("Button search fell through!");
  return NULL;
}

char *
vvm3_xml_find_spgUrl (GString *data)
{
  GMarkupParseContext *context;
  g_autofree char *spgUrl = NULL;
  if (data->str == NULL)
    return NULL;

  context = g_markup_parse_context_new (&vvm3_xml_parser, 0, &spgUrl, NULL);
  g_markup_parse_context_parse (context, data->str, data->len, NULL);
  g_markup_parse_context_free (context);
  g_debug ("URL to pass back: %s", spgUrl);

  return g_strdup (spgUrl);
}

char *
vvm3_html_find_buttonUrl (GString *data)
{
  g_autofree char *buttonElement = NULL;
  g_autofree char *buttonUrl = NULL;
  GMarkupParseContext *context;

  if (data->str == NULL)
    return NULL;

  //Third stage, "clicking" a button on a returned webpage
  buttonElement = vvm3_find_button (data->str);
  if (buttonElement == NULL)
    {
      g_warning ("Could not find the subscribe button!");
      return NULL;
    }
  context = g_markup_parse_context_new (&vvm3_htmlline_parser, 0, &buttonUrl, NULL);
  if (g_markup_parse_context_parse (context, buttonElement, strlen (buttonElement), NULL) == FALSE)
    g_warning ("Parsing of the response HTML failed! You may want to try again, or file a bug if this happens repeatedly.");

  g_markup_parse_context_free (context);
  return g_strdup (buttonUrl);
}

/*
 * https://datatracker.ietf.org/doc/html/rfc1738#section-3.3
 * An HTTP URL takes the form: http://<host>:<port>/<path>?<searchpart>
 *
 */

int
validate_vvm3_host (const char *spgUrl)
{
  g_autofree char **known_hosts = NULL;
  g_autofree char **host_and_port = NULL;
  g_autofree char **host = NULL;
  const char *stripped_url = NULL;

  if (!spgUrl || !*spgUrl)
    return FALSE;

  g_debug ("URL to Check: %s", spgUrl);
  if (g_str_has_prefix (spgUrl, "http://"))
    stripped_url = spgUrl + strlen ("http://");
  else if (g_str_has_prefix (spgUrl, "https://"))
    stripped_url = spgUrl + strlen ("https://");
  else {
      g_debug ("URL does not begin with http:// or https://");
      return FALSE;
    }

  host_and_port = g_strsplit_set (stripped_url, "/", 2);
  host = g_strsplit_set (host_and_port[0], ":", 2);
  g_debug ("Hostname to Check: %s", host[0]);

  known_hosts = g_strsplit_set (KNOWN_VVM3_HOSTS, ",", -1);
  for (unsigned int i = 0; i < g_strv_length (known_hosts); i++)
    if (g_str_has_suffix (host[0], known_hosts[i]))
      return TRUE;

  g_debug ("URL: %s is not on the list of trusted hosts: %s.", spgUrl, KNOWN_VVM3_HOSTS);
  return FALSE;
}

void
vvm3_activation (const char *stripped_number,
                 const char *activate_url)
{
  g_autofree char *gatewayRequestXml = NULL;
  CURL *curl = curl_easy_init ();
  GString *data = g_string_new (NULL);

  /* Make sure that the activate_url is known */
  if (!validate_vvm3_host (activate_url))
    {
      curl_easy_cleanup (curl);
      goto cleanup;
    }

  /*
   * Seed the random number generator with system time. This is necessary
   * because transaction ID in the initial request XML is randomly
   * generated, and without seeding, the generator will return 0.
   */
  srand (time (NULL));

  /*
   * This is the template for the special XML request that needs to be
   * sent to the server obtained via SMS.
   * transactionid is a random number: https://cs.android.com/android/platform/superproject/+/master:packages/apps/Dialer/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java
   * mdn is your number. From: https://cs.android.com/android/platform/superproject/+/master:packages/apps/Dialer/java/com/android/voicemail/impl/protocol/Vvm3Subscriber.java;drc=ba93c4fe5016341d06be1ce47410885522e09a58;l=187
   * Self provisioning gateway expects 10 digit national format
   * Device Model is a Pixel 5
   */
  gatewayRequestXml = g_strdup_printf ("<?xml version=\"1.0\" encoding=\"UTF-8\"?><VMGVVMRequest><MessageHeader><transactionid>%ld</transactionid></MessageHeader><MessageBody><mdn>%s</mdn><operation>retrieveSPGURL</operation><source>Device</source><devicemodel>%s</devicemodel></MessageBody></VMGVVMRequest>", random (), stripped_number, VVM3_DEVICE_MODEL);
  g_debug ("The XML request to transmit is (newline):\n%s", gatewayRequestXml);

  if (curl)
    {
      CURLcode res = CURLE_OK;
      g_autofree char *spgUrl = NULL;

      curl_easy_setopt (curl, CURLOPT_URL, activate_url);
      curl_easy_setopt (curl, CURLOPT_POSTFIELDS, curl_easy_escape (curl, gatewayRequestXml, strlen (gatewayRequestXml)));
      curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, vvm3_curl_writeback);
      curl_easy_setopt (curl, CURLOPT_WRITEDATA, data);
      /* only allow HTTP and HTTPS */
      curl_easy_setopt (curl, CURLOPT_PROTOCOLS_STR, "http,https");
      //curl_easy_setopt (curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
      res = curl_easy_perform (curl);

      if (res != CURLE_OK)
        {
          g_warning ("Error with the first curl request: %s\n", curl_easy_strerror (res));
          curl_easy_cleanup (curl);
          goto cleanup;
        }
      curl_easy_cleanup (curl);
      curl = NULL;
      g_debug ("The response to the request (newline):\n%s\n", data->str);

      spgUrl = vvm3_xml_find_spgUrl (data);
      if (spgUrl == NULL)
        {
          g_warning ("Parsing of the response XML failed! You might want to try again, or file a bug if this happens repeatedly.");
          goto cleanup;
        }
      if (!validate_vvm3_host (spgUrl))
        goto cleanup;
      //Prepare for second stage, sending request to SPG URL grabbed from XML
      g_debug ("Captured URL is %s", spgUrl);
      data = g_string_truncate (data, 0);

      curl = curl_easy_init ();
      if (curl)
        {
          g_autofree char *requestParams = NULL;
          g_autofree char *buttonUrl = NULL;

          curl_easy_setopt (curl, CURLOPT_URL, spgUrl);
          curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, vvm3_curl_writeback);
          curl_easy_setopt (curl, CURLOPT_WRITEDATA, data);
          curl_easy_setopt (curl, CURLOPT_COOKIEFILE, "");
          curl_easy_setopt (curl, CURLOPT_POST, 1);
          /* only allow HTTP and HTTPS */
          curl_easy_setopt (curl, CURLOPT_PROTOCOLS_STR, "http,https");
          //curl_easy_setopt (curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);

          //Form a special request based on constants derived from AOSP
          requestParams = g_strdup_printf ("%s=%s&%s=%s&%s=%s&%s=%s&%s=%s",
                                           SPG_VZW_MDN_PARAM,
                                           stripped_number,
                                           SPG_VZW_SERVICE_PARAM,
                                           SPG_VZW_SERVICE_BASIC,
                                           SPG_DEVICE_MODEL_PARAM,
                                           SPG_DEVICE_MODEL_ANDROID,
                                           SPG_APP_TOKEN_PARAM,
                                           SPG_APP_TOKEN,
                                           SPG_LANGUAGE_PARAM,
                                           SPG_LANGUAGE_EN);

          g_debug ("The second request is being sent with the following parameters:\n%s", requestParams);
          curl_easy_setopt (curl, CURLOPT_POSTFIELDS, requestParams);
          res = curl_easy_perform (curl);
          if (res != CURLE_OK)
            {
              g_warning ("Error with the second curl request: %s\n", curl_easy_strerror (res));
              curl_easy_cleanup (curl);
              goto cleanup;
            }
          g_debug ("The response to the second request was:\n%s", data->str);

          buttonUrl = vvm3_html_find_buttonUrl (data);
          if (buttonUrl == NULL)
            {
              g_warning ("No URL was found during parsing! You may want to try again, or file a bug if this happens repeatedly.");
              curl_easy_cleanup (curl);
              goto cleanup;
            }
          g_debug ("URL obtained is %s", buttonUrl);
          if (!validate_vvm3_host (buttonUrl))
            {
              curl_easy_cleanup (curl);
              goto cleanup;
            }

          data = g_string_truncate (data, 0);
          curl_easy_setopt (curl, CURLOPT_URL, buttonUrl);
          curl_easy_setopt (curl, CURLOPT_HTTPGET, 1);
          res = curl_easy_perform (curl);
          if (res != CURLE_OK)
            {
              g_warning ("Error with the third curl request: %s\n", curl_easy_strerror (res));
              curl_easy_cleanup (curl);
              goto cleanup;
            }
          g_debug ("The response to the third request was:\n%s", data->str);
          curl_easy_cleanup (curl);
        }
      else {
          g_critical ("Failed to set up curl!");
          goto cleanup;
        }
    }
  else {
      g_critical ("Failed to set up curl!");
      goto cleanup;
    }
 cleanup:
  g_string_free (data, TRUE);
  return;
}
