🏡 index : quine-global-labs/bazaar.git

/* global-net.c
 *
 * Copyright 2025 Adam Masciola
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * 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, see <https://www.gnu.org/licenses/>.
 *
 * SPDX-License-Identifier: GPL-3.0-or-later
 */

#define G_LOG_DOMAIN "BAZAAR::GLOBAL-NET"

#include "config.h"

#include <json-glib/json-glib.h>
#include <libproxy/proxy.h>

#include "env.h"
#include "global-net.h"
#include "util.h"

BZ_DEFINE_DATA (
    http_request,
    HttpRequest,
    {
      SoupMessage   *message;
      GOutputStream *splice_into;
      gboolean       close_output;
    },
    BZ_RELEASE_DATA (message, g_object_unref);
    BZ_RELEASE_DATA (splice_into, g_object_unref));

static DexFuture *
http_send_fiber (HttpRequestData *data);

static void
http_send_and_splice_finish (GObject      *object,
                             GAsyncResult *result,
                             gpointer      user_data);

static DexFuture *
query_json_source_then (DexFuture     *future,
                        GOutputStream *output_stream);

static DexFuture *
send (SoupMessage   *message,
      GOutputStream *splice_into,
      gboolean       close_output);

static DexFuture *
query_uri_json_with_method (const char *uri,
                            const char *method,
                            const char *token);

GProxyResolver *
bz_get_default_proxy_resolver (void)
{
  static GProxyResolver *resolver = NULL;

  if (g_once_init_enter_pointer (&resolver))
    {
      pxProxyFactory *factory                      = NULL;
      g_auto (GStrv) proxies                       = NULL;
      g_autoptr (GProxyResolver) resolver_instance = NULL;

      factory = px_proxy_factory_new ();
      /* blocking */
      proxies = px_proxy_factory_get_proxies (factory, DONATE_LINK);
      g_clear_pointer (&factory, px_proxy_factory_free);

      resolver_instance = g_simple_proxy_resolver_new (
          proxies != NULL && *proxies != NULL
              ? proxies[0]
              : NULL,
          NULL);

      g_once_init_leave_pointer (&resolver, g_steal_pointer (&resolver_instance));
    }

  return resolver;
}

DexFuture *
bz_send_with_global_http_session (SoupMessage *message)
{
  dex_return_error_if_fail (SOUP_IS_MESSAGE (message));
  return send (message, NULL, FALSE);
}

DexFuture *
bz_send_with_global_http_session_then_splice_into (SoupMessage   *message,
                                                   GOutputStream *output)
{
  dex_return_error_if_fail (SOUP_IS_MESSAGE (message));
  dex_return_error_if_fail (G_IS_OUTPUT_STREAM (output));
  return send (message, output, TRUE);
}

DexFuture *
bz_https_query_json (const char *uri)
{
  g_autoptr (GError) local_error   = NULL;
  g_autoptr (SoupMessage) message  = NULL;
  SoupMessageHeaders *headers      = NULL;
  g_autoptr (GOutputStream) output = NULL;
  g_autoptr (DexFuture) future     = NULL;

  dex_return_error_if_fail (uri != NULL);

  message = soup_message_new (SOUP_METHOD_GET, uri);
  headers = soup_message_get_request_headers (message);
  soup_message_headers_append (headers, "User-Agent", "Bazaar");

  output = g_memory_output_stream_new_resizable ();

  future = send (message, output, TRUE);
  future = dex_future_then (
      future,
      (DexFutureCallback) query_json_source_then,
      g_object_ref (output), g_object_unref);
  return g_steal_pointer (&future);
}

DexFuture *
bz_query_flathub_v2_json (const char *request)
{
  g_autofree char *uri = NULL;

  dex_return_error_if_fail (request != NULL);

  uri = g_strdup_printf ("https://flathub.org/api/v2%s", request);
  return query_uri_json_with_method (uri, SOUP_METHOD_GET, NULL);
}

DexFuture *
bz_query_flathub_v2_json_take (char *request)
{
  DexFuture *future = NULL;

  dex_return_error_if_fail (request != NULL);

  future = bz_query_flathub_v2_json (request);
  g_free (request);

  return future;
}

DexFuture *
bz_query_flathub_v2_json_authenticated (const char *request,
                                        const char *token)
{
  g_autofree char *uri = NULL;

  dex_return_error_if_fail (request != NULL);

  uri = g_strdup_printf ("https://flathub.org/api/v2%s", request);
  return query_uri_json_with_method (uri, SOUP_METHOD_GET, token);
}

DexFuture *
bz_query_flathub_v2_json_authenticated_post (const char *request,
                                             const char *token)
{
  g_autofree char *uri = NULL;

  dex_return_error_if_fail (request != NULL);

  uri = g_strdup_printf ("https://flathub.org/api/v2%s", request);
  return query_uri_json_with_method (uri, SOUP_METHOD_POST, token);
}

DexFuture *
bz_query_flathub_v2_json_authenticated_delete (const char *request,
                                               const char *token)
{
  g_autofree char *uri = NULL;

  dex_return_error_if_fail (request != NULL);

  uri = g_strdup_printf ("https://flathub.org/api/v2%s", request);
  return query_uri_json_with_method (uri, SOUP_METHOD_DELETE, token);
}

DexFuture *
bz_query_bazaar_json (const char *request)
{
  g_autofree char *uri = NULL;

  dex_return_error_if_fail (request != NULL);

  uri = g_strdup_printf ("https://usebazaar.org%s", request);
  return query_uri_json_with_method (uri, SOUP_METHOD_GET, NULL);
}

static DexFuture *
query_uri_json_with_method (const char *uri,
                            const char *method,
                            const char *token)
{
  g_autoptr (SoupMessage) message  = NULL;
  SoupMessageHeaders *headers      = NULL;
  g_autoptr (GOutputStream) output = NULL;
  g_autoptr (DexFuture) future     = NULL;

  message = soup_message_new (method, uri);
  headers = soup_message_get_request_headers (message);

  soup_message_headers_append (headers, "User-Agent", "Bazaar");

  if (token != NULL && token[0] != '\0')
    {
      g_autofree char *cookie_value = NULL;

      cookie_value = g_strdup_printf ("session=%s", token);
      soup_message_headers_append (headers, "Cookie", cookie_value);
    }

  output = g_memory_output_stream_new_resizable ();

  future = send (message, output, TRUE);
  future = dex_future_then (
      future,
      (DexFutureCallback) query_json_source_then,
      g_object_ref (output), g_object_unref);
  return g_steal_pointer (&future);
}

static DexFuture *
http_send_fiber (HttpRequestData *data)
{
  static SoupSession      *session      = NULL;
  SoupMessage             *message      = data->message;
  GOutputStream           *splice_into  = data->splice_into;
  gboolean                 close_output = data->close_output;
  GOutputStreamSpliceFlags splice_flags = G_OUTPUT_STREAM_SPLICE_NONE;
  g_autoptr (DexPromise) promise        = NULL;

  if (g_once_init_enter_pointer (&session))
    {
      g_autoptr (SoupSession) session_instance = NULL;

      session_instance = soup_session_new ();
      soup_session_set_proxy_resolver (session_instance, bz_get_default_proxy_resolver ());

      g_once_init_leave_pointer (&session, g_steal_pointer (&session_instance));
    }

  splice_flags = G_OUTPUT_STREAM_SPLICE_CLOSE_SOURCE;
  if (close_output)
    splice_flags |= G_OUTPUT_STREAM_SPLICE_CLOSE_TARGET;

  promise = dex_promise_new_cancellable ();
  soup_session_send_and_splice_async (
      session,
      message,
      splice_into,
      splice_flags,
      G_PRIORITY_DEFAULT_IDLE,
      dex_promise_get_cancellable (promise),
      http_send_and_splice_finish,
      dex_ref (promise));

  return DEX_FUTURE (g_steal_pointer (&promise));
}

static void
http_send_and_splice_finish (GObject      *object,
                             GAsyncResult *result,
                             gpointer      user_data)
{
  DexPromise *promise            = user_data;
  g_autoptr (GError) local_error = NULL;
  gssize bytes_written           = 0;

  g_assert (SOUP_IS_SESSION (object));
  g_assert (G_IS_ASYNC_RESULT (result));
  g_assert (DEX_IS_PROMISE (promise));

  bytes_written = soup_session_send_and_splice_finish (SOUP_SESSION (object), result, &local_error);
  if (bytes_written >= 0)
    {
      g_debug ("Spliced %zu bytes from http reply into output stream", bytes_written);
      dex_promise_resolve_uint64 (promise, bytes_written);
    }
  else
    {
      g_debug ("Could not splice http reply into output stream: %s", local_error->message);
      dex_promise_reject (promise, g_steal_pointer (&local_error));
    }

  dex_unref (promise);
}

static DexFuture *
query_json_source_then (DexFuture     *future,
                        GOutputStream *output_stream)
{
  g_autoptr (GError) local_error = NULL;
  g_autoptr (GBytes) bytes       = NULL;
  gsize         bytes_size       = 0;
  gconstpointer bytes_data       = NULL;
  g_autoptr (JsonParser) parser  = NULL;
  gboolean  result               = FALSE;
  JsonNode *node                 = NULL;

  bytes = g_memory_output_stream_steal_as_bytes (
      G_MEMORY_OUTPUT_STREAM (output_stream));
  bytes_data = g_bytes_get_data (bytes, &bytes_size);

  if (bytes_size == 0)
    return dex_future_new_take_boxed (JSON_TYPE_NODE, json_node_new (JSON_NODE_NULL));

  parser = json_parser_new_immutable ();
  result = json_parser_load_from_data (parser, bytes_data, bytes_size, &local_error);
  if (!result)
    return dex_future_new_for_error (g_steal_pointer (&local_error));

  node = json_parser_get_root (parser);
  return dex_future_new_take_boxed (JSON_TYPE_NODE, json_node_ref (node));
}

static DexFuture *
send (SoupMessage   *message,
      GOutputStream *splice_into,
      gboolean       close_output)
{
  g_autoptr (HttpRequestData) data = NULL;
  g_autoptr (DexFuture) future     = NULL;

  data               = http_request_data_new ();
  data->message      = g_object_ref (message);
  data->splice_into  = bz_object_maybe_ref (splice_into);
  data->close_output = close_output;

  future = dex_scheduler_spawn (
      dex_scheduler_get_default (),
      bz_get_dex_stack_size (),
      (DexFiberFunc) http_send_fiber,
      http_request_data_ref (data),
      http_request_data_unref);
  return g_steal_pointer (&future);
}

static DexFuture *
fetch_uri_contents_then (DexFuture     *future,
                         GOutputStream *output)
{
  return dex_future_new_take_boxed (
      G_TYPE_BYTES,
      g_memory_output_stream_steal_as_bytes (
          G_MEMORY_OUTPUT_STREAM (output)));
}

DexFuture *
bz_fetch_uri_contents (const char *uri)
{
  g_autoptr (SoupMessage) message  = NULL;
  SoupMessageHeaders *headers      = NULL;
  g_autoptr (GOutputStream) output = NULL;
  g_autoptr (DexFuture) future     = NULL;

  dex_return_error_if_fail (uri != NULL);

  if (g_str_has_prefix (uri, "http://") ||
      g_str_has_prefix (uri, "https://"))
    {
      message = soup_message_new (SOUP_METHOD_GET, uri);
      if (message == NULL)
        return dex_future_new_reject (
            G_IO_ERROR,
            G_IO_ERROR_INVALID_ARGUMENT,
            "could not construct a request for uri %s",
            uri);

      headers = soup_message_get_request_headers (message);
      soup_message_headers_append (headers, "User-Agent", "Bazaar");

      output = g_memory_output_stream_new_resizable ();
      future = bz_send_with_global_http_session_then_splice_into (message, output);
      future = dex_future_then (
          future,
          (DexFutureCallback) fetch_uri_contents_then,
          g_object_ref (output), g_object_unref);

      return g_steal_pointer (&future);
    }
  else
    {
      g_autoptr (GFile) file = NULL;

      file = g_file_new_for_uri (uri);
      return dex_file_load_contents_bytes (file);
    }
}