diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b0674253..123c516b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,8 +1,8 @@ { - "image": "mcr.microsoft.com/devcontainers/universal:2", + "image": "mcr.microsoft.com/devcontainers/universal:3", "features": { "ghcr.io/akhildevelops/devcontainer-features/apt:0": { - "PACKAGES": "gtk-doc-tools docbook-xsl yelp-tools libpng-dev libgtk-3-dev libicu-dev libjson-glib-dev intltool autopoint xxd libcurl4-gnutls-dev libglib2.0-dev-bin libsqlite3-dev nettle-dev libmapnik-dev libgeoclue-2-dev libgexiv2-dev libgps-dev libmagic-dev libbz2-dev libzip-dev liboauth-dev libnova-dev yaru-theme-icon" + "PACKAGES": "gtk-doc-tools docbook-xsl yelp-tools libpng-dev libgtk-3-dev libicu-dev libjson-glib-dev intltool autopoint xxd libcurl4-gnutls-dev libglib2.0-dev-bin libsqlite3-dev nettle-dev libmapnik-dev libgeoclue-2-dev libgexiv2-dev libgps-dev libmagic-dev libbz2-dev libzip-dev liboauth-dev libnova-dev yaru-theme-icon librest-dev" } }, "mounts": [ diff --git a/.github/workflows/build-variants.yml b/.github/workflows/build-variants.yml index d6e3c5d4..d44d4e70 100644 --- a/.github/workflows/build-variants.yml +++ b/.github/workflows/build-variants.yml @@ -11,12 +11,12 @@ jobs: strategy: fail-fast: false matrix: - # v----------------------- Optional library components -----------------------------------------v v----------------code features -----------------v - no-variants: [bzip2, geoclue, geotag, magic, mapnik, mbtiles, nova, nettle, oauth, realtime-gps-tracking, zip, bing, google, bluemarble, geonames, openstreetmap] + # v----------------------- Optional library components ----------------------------------v v----------------code features -----------------v + no-variants: [bzip2, geoclue, geotag, magic, mapnik, mbtiles, nova, nettle, realtime-gps-tracking, zip, bing, google, bluemarble, geonames, openstreetmap] # NB specifically limited to single OS variant and default GTK, in order to keep number of runs within reason # Thus much more likely to keep within overall compute quota limits for a given time period - runs-on: ubuntu-22.04 + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v3 @@ -25,7 +25,7 @@ jobs: # regarding ubuntu default image vs server packages mismatch # Hence must always *update* before applying any install - name: "Install requirements" - run: sudo apt-get update -y && sudo apt-get install gtk-doc-tools docbook-xsl yelp-tools libpng-dev libgtk-3-dev libicu-dev libjson-glib-dev intltool autopoint xxd libcurl4-gnutls-dev libglib2.0-dev-bin libsqlite3-dev nettle-dev libmapnik-dev libgeoclue-2-dev libgexiv2-dev libgps-dev libmagic-dev libbz2-dev libzip-dev liboauth-dev libnova-dev + run: sudo apt-get update -y && sudo apt-get install gtk-doc-tools docbook-xsl yelp-tools libpng-dev libgtk-3-dev libicu-dev libjson-glib-dev intltool autopoint xxd libcurl4-gnutls-dev libglib2.0-dev-bin libsqlite3-dev nettle-dev libmapnik-dev libgeoclue-2-dev libgexiv2-dev libgps-dev libmagic-dev libbz2-dev libzip-dev liboauth-dev librest-dev libnova-dev shell: bash # Should work without the optional library component being installed - but ATM not going bother (de)configuring the OS diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9cc3105b..03010597 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -11,7 +11,7 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-22.04, ubuntu-24.04] + os: [ubuntu-24.04] libgtk: [libgtk-3-dev, libgtk2.0-dev] runs-on: ${{ matrix.os }} @@ -24,7 +24,7 @@ jobs: # Hence must always *update* before applying any install # xvfb is only needed for headless CI tests - name: "Install requirements" - run: sudo apt-get update -y && sudo apt-get install gtk-doc-tools docbook-xsl yelp-tools libpng-dev ${{ matrix.libgtk }} libicu-dev libjson-glib-dev intltool autopoint xxd libcurl4-gnutls-dev libglib2.0-dev-bin libsqlite3-dev nettle-dev libmapnik-dev libgeoclue-2-dev libgexiv2-dev libgps-dev libmagic-dev libbz2-dev libzip-dev liboauth-dev libnova-dev xvfb + run: sudo apt-get update -y && sudo apt-get install gtk-doc-tools docbook-xsl yelp-tools libpng-dev ${{ matrix.libgtk }} libicu-dev libjson-glib-dev intltool autopoint xxd libcurl4-gnutls-dev libglib2.0-dev-bin libsqlite3-dev nettle-dev libmapnik-dev libgeoclue-2-dev libgexiv2-dev libgps-dev libmagic-dev libbz2-dev libzip-dev liboauth-dev libnova-dev librest-dev xvfb shell: bash # Fake DISPLAY for detection as used in configure - which decides which tests to run for the main check diff --git a/.gitignore b/.gitignore index 3892d18d..c0e93174 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ help/C/*html /src/vikenumtypes.h /src/vikenumtypes.c /src/documenters.h +/src/osm-oauth2-example # /src/libjpeg /src/libjpeg/*.o diff --git a/README.md b/README.md index f90e16b2..e511d5f2 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ On Debian Sid, the following packages must be installed before building: The following packages are needed (they are included by default in Debian Sid, but not in other distributions). They must be installed too: - $ sudo apt-get install libcurl4-gnutls-dev libglib2.0-dev-bin + $ sudo apt-get install libcurl4-gnutls-dev libglib2.0-dev-bin librest-dev The following packages are also used, but they can each be disabled with configure options, if desired: diff --git a/configure.ac b/configure.ac index 08402d79..b70d1199 100644 --- a/configure.ac +++ b/configure.ac @@ -108,6 +108,7 @@ PKG_CHECK_MODULES(PACKAGE, [ gio-2.0 >= 2.32 gdk-pixbuf-2.0 >= 2.26 json-glib-1.0 >= 0.16 + rest-1.0 >= 0.9 ]) AC_SUBST(PACKAGE_CFLAGS) diff --git a/src/Makefile.am b/src/Makefile.am index 7234ef0e..58ded87f 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -3,7 +3,7 @@ SUBDIRS = icons -bin_PROGRAMS = viking +bin_PROGRAMS = viking osm-oauth2-example noinst_LIBRARIES = \ libviking.a @@ -199,6 +199,7 @@ endif if OPENSTREETMAP libviking_a_SOURCES += \ osm.c osm.h \ + osm_api_client.c osm_api_client.h \ osm-traces.c osm-traces.h \ datasource_osm_my_traces.c \ datasource_osm.c diff --git a/src/osm-oauth2-example.c b/src/osm-oauth2-example.c new file mode 100644 index 00000000..8d77e803 --- /dev/null +++ b/src/osm-oauth2-example.c @@ -0,0 +1,157 @@ +/* gitlab-oauth2-example.c + * + * Copyright 2021 Günther Wagner + * + * This program is free software; you can redistribute it and/or modify it + * under the terms and conditions of the GNU Lesser General Public License, + * version 2.1, as published by the Free Software Foundation. + * + * This program is distributed in the hope 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 this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St - Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/** + * This example shows the recommended PKCE authorization flow for Gitlab. + */ + +#include +#include +#include +#include + + + +GMainLoop *loop; + +static void +fetch_logged_in_user(RestProxy *proxy) +{ + g_autoptr(GError) error = NULL; + RestProxyCall *call; + + call = rest_proxy_new_call (proxy); + rest_proxy_call_set_method (call, "GET"); + rest_proxy_call_set_function (call, "/user/details"); + rest_proxy_call_sync (call, &error); + + g_print ("%s\n", rest_proxy_call_get_payload (call)); + g_main_loop_quit (loop); +} + +static void +osm_oauth2_example_refreshed_access_token (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + RestOAuth2Proxy *oauth2_proxy = (RestOAuth2Proxy *)object; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_OBJECT (object)); + g_assert (G_IS_ASYNC_RESULT (result)); + + rest_oauth2_proxy_refresh_access_token_finish (oauth2_proxy, result, &error); + if (error) + g_error ("%s", error->message); + + g_print ("Access Token: %s\n", rest_oauth2_proxy_get_access_token (oauth2_proxy)); + g_print ("Refresh Token: %s\n", rest_oauth2_proxy_get_refresh_token (oauth2_proxy)); + GDateTime *expiration_date = rest_oauth2_proxy_get_expiration_date (oauth2_proxy); + if (expiration_date) + g_print ("Expires in: %s\n", g_date_time_format (expiration_date, "%X %x")); + + fetch_logged_in_user (REST_PROXY (oauth2_proxy)); +} + +static void +osm_oauth2_example_fetched_access_token (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + RestOAuth2Proxy *oauth2_proxy = (RestOAuth2Proxy *)object; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_OBJECT (object)); + g_assert (G_IS_ASYNC_RESULT (result)); + + rest_oauth2_proxy_fetch_access_token_finish (oauth2_proxy, result, &error); + if (error) + g_error ("%s", error->message); + + g_print ("Access Token: %s\n", rest_oauth2_proxy_get_access_token (oauth2_proxy)); + g_print ("Refresh Token: %s\n", rest_oauth2_proxy_get_refresh_token (oauth2_proxy)); + GDateTime *expiration_date = rest_oauth2_proxy_get_expiration_date (oauth2_proxy); + if (expiration_date) + g_print ("Expires in: %s\n", g_date_time_format (expiration_date, "%X %x")); + + fetch_logged_in_user (REST_PROXY (oauth2_proxy)); + + /* refresh token */ + rest_oauth2_proxy_refresh_access_token_async (oauth2_proxy, NULL, osm_oauth2_example_refreshed_access_token, user_data); +} + +gint +main (gint argc, + gchar *argv[]) +{ + g_autofree gchar *line = NULL; + size_t len = 0; + + gchar **environment = g_get_environ (); + gchar *authurl = "https://www.openstreetmap.org/oauth2/authorize"; + gchar *tokenurl = "https://www.openstreetmap.org/oauth2/token"; + gchar *redirecturl = "urn:ietf:wg:oauth:2.0:oob"; + gchar *baseurl = "https://api.openstreetmap.org/api/0.6/"; + + + // ID 2ovOGzjSEA1W7ebG7D8aqyVvFPfs6RJW_TLweVcsPJk + // x1_mDnLQbSgOvw2fFSde5WIKrTJH1v4tV4FFwCJMQc4 + const gchar *clientid = g_environ_getenv (environment, "REST_OAUTH2_CLIENT_ID"); + if (!clientid) + { + g_print ("You have to define your OpenStreetMap Client ID as REST_OAUTH2_CLIENT_ID environment variable\n"); + return EXIT_SUCCESS; + } +// Secret annkUVUxacqmfqzOYMG3Bs8mNtjUH7SddoO4Nt7GRas +// DKop7jo1FbXBW9J9B-wk85pOyQCQxrt8hQ7s9Srgz0A + const gchar *clientsecret = g_environ_getenv (environment, "REST_OAUTH2_CLIENT_SECRET"); + if (!clientsecret) + { + g_print ("You have to define your OpenStreetMap Client Secret as REST_OAUTH2_CLIENT_SECRET environment variable\n"); + return EXIT_SUCCESS; + } + RestPkceCodeChallenge *pkce = rest_pkce_code_challenge_new_random (); + gchar *state = NULL; + +#ifdef WITH_SOUP_2 + SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_HEADERS, -1); +#else + SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_HEADERS); +#endif + + RestOAuth2Proxy *oauth2_proxy = rest_oauth2_proxy_new (authurl, tokenurl, redirecturl, clientid, clientsecret, baseurl); + rest_proxy_add_soup_feature (REST_PROXY (oauth2_proxy), SOUP_SESSION_FEATURE (logger)); + const gchar *authorize_url = rest_oauth2_proxy_build_authorization_url (oauth2_proxy, rest_pkce_code_challenge_get_challenge (pkce), "openid", &state); + + g_print ("URL to authorize: %s\n", authorize_url); + + ssize_t chars = getline (&line, &len, stdin); + if (line[chars - 1] == '\n') { + line[chars - 1] = '\0'; + } + + g_print ("Got Authorization Grant: %s\n", line); + + /* fetch access token */ + rest_oauth2_proxy_fetch_access_token_async (oauth2_proxy, line, rest_pkce_code_challenge_get_verifier (pkce), NULL, osm_oauth2_example_fetched_access_token, NULL); + + loop = g_main_loop_new (NULL, FALSE); + g_main_loop_run (loop); + + return 0; +} diff --git a/src/osm_api_client.c b/src/osm_api_client.c new file mode 100644 index 00000000..756a957f --- /dev/null +++ b/src/osm_api_client.c @@ -0,0 +1,187 @@ +/* + * viking -- GPS Data and Topo Analyzer, Explorer, and Manager + * + * Copyright (C) 2025, Guilhem Bonnefille + * + * 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 2 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, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include "viking.h" + +#include "osm_api_client.h" + +#include +#include +#include +#include + +#define OSM_API_AUTH_URL "https://www.openstreetmap.org/oauth2/authorize" +#define OSM_API_TOKEN_URL "https://www.openstreetmap.org/oauth2/token" +#define OSM_API_REDIRECT_URL "urn:ietf:wg:oauth:2.0:oob" +#define OSM_API_BASE_URL "https://api.openstreetmap.org/api/0.6/" + +#define OSM_API_OAUTH2_CLIENT_ID "x1_mDnLQbSgOvw2fFSde5WIKrTJH1v4tV4FFwCJMQc4" +#define OSM_API_OAUTH2_CLIENT_SECRET "DKop7jo1FbXBW9J9B-wk85pOyQCQxrt8hQ7s9Srgz0A" + +typedef struct _OsmApiClientPrivate OsmApiClientPrivate; +struct _OsmApiClientPrivate +{ + gchar *token; // Property to hold the token + RestOAuth2Proxy *oauth2_proxy; + RestPkceCodeChallenge *pkce; // For challenge +}; + +G_DEFINE_TYPE_WITH_PRIVATE(OsmApiClient, osm_api_client, G_TYPE_OBJECT) +#define OSM_API_CLIENT_GET_PRIVATE(o) (osm_api_client_get_instance_private (OSM_API_CLIENT(o))) + +/* properties */ +enum +{ + PROP_0, + + PROP_TOKEN, +}; + +static void osm_api_client_set_property(GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { + OsmApiClient *self = (OsmApiClient *)object; + OsmApiClientPrivate *priv = osm_api_client_get_instance_private (self); + + switch (prop_id) { + case PROP_TOKEN: // Assuming 1 is the ID for the token property + g_free(priv->token); + priv->token = g_value_dup_string(value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void osm_api_client_get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { + OsmApiClient *self = (OsmApiClient *)object; + OsmApiClientPrivate *priv = osm_api_client_get_instance_private (self); + + switch (prop_id) { + case PROP_TOKEN: // Assuming 1 is the ID for the token property + g_value_set_string(value, priv->token); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec); + break; + } +} + +static void osm_api_client_class_init(OsmApiClientClass *klass) { + GObjectClass *object_class = G_OBJECT_CLASS(klass); + + object_class->set_property = osm_api_client_set_property; + object_class->get_property = osm_api_client_get_property; + + klass->challenge_start = osm_api_client_challenge_start; + klass->challenge_finish = osm_api_client_challenge_finish; + klass->create_request = osm_api_client_create_request; + + g_object_class_install_property(object_class, PROP_TOKEN, + g_param_spec_string("token", "Token", "Authentication token", NULL, + G_PARAM_READWRITE)); +} + +static void +osm_api_client_init(OsmApiClient *self) { + /* initialize the object here */ + OsmApiClientPrivate *priv = osm_api_client_get_instance_private (self); + + priv->token = NULL; + priv->oauth2_proxy = NULL; + priv->pkce = NULL; +} + +static void +osm_api_client_finalize (GObject *object) +{ + OsmApiClientPrivate *priv = OSM_API_CLIENT_GET_PRIVATE (object); + + g_unref(priv->oauth2_proxy); + priv->oauth2_proxy = NULL; + rest_pkce_code_challenge_free(priv->pkce); + priv->pkce = NULL; + g_free (priv->token); + priv->token = NULL; + + G_OBJECT_CLASS (osm_api_client_parent_class)->finalize (object); +} + +gchar *osm_api_client_challenge_start(OsmApiClient *self) { + OsmApiClientPrivate *priv = osm_api_client_get_instance_private (self); + + priv->pkce = rest_pkce_code_challenge_new_random (); + gchar *state = NULL; + + #ifdef WITH_SOUP_2 + SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_HEADERS, -1); + #else + SoupLogger *logger = soup_logger_new (SOUP_LOGGER_LOG_HEADERS); + #endif + + priv->oauth2_proxy = rest_oauth2_proxy_new (OSM_API_AUTH_URL, OSM_API_TOKEN_URL, OSM_API_REDIRECT_URL, OSM_API_OAUTH2_CLIENT_ID, OSM_API_OAUTH2_CLIENT_SECRET, OSM_API_BASE_URL); + rest_proxy_add_soup_feature (REST_PROXY (priv->oauth2_proxy), SOUP_SESSION_FEATURE (logger)); + const gchar *authorize_url = rest_oauth2_proxy_build_authorization_url (priv->oauth2_proxy, rest_pkce_code_challenge_get_challenge (priv->pkce), "openid", &state); + + return authorize_url; +} + + +static void +osm_api_client_fetched_access_token (GObject *object, + GAsyncResult *result, + gpointer user_data) +{ + RestOAuth2Proxy *oauth2_proxy = (RestOAuth2Proxy *)object; + g_autoptr(GError) error = NULL; + + g_assert (G_IS_OBJECT (object)); + g_assert (G_IS_ASYNC_RESULT (result)); + + rest_oauth2_proxy_fetch_access_token_finish (oauth2_proxy, result, &error); + if (error) + g_error ("%s", error->message); + + OsmApiClientPrivate *priv = OSM_API_CLIENT_GET_PRIVATE (object); + + // FIXME duplicate? + priv->token = rest_oauth2_proxy_get_access_token (oauth2_proxy); +} + +void +osm_api_client_challenge_finish(OsmApiClient *self, gchar *code) { + OsmApiClientPrivate *priv = osm_api_client_get_instance_private (self); + + rest_oauth2_proxy_fetch_access_token_async (priv->oauth2_proxy, code, rest_pkce_code_challenge_get_verifier (priv->pkce), NULL, osm_api_client_fetched_access_token, self); +} + +gchar *osm_api_client_create_request(OsmApiClient *self, const gchar *function) { + OsmApiClientPrivate *priv = osm_api_client_get_instance_private (self); + RestProxy *proxy = REST_PROXY (priv->oauth2_proxy); + RestProxyCall *call; + g_autoptr(GError) error = NULL; + + call = rest_proxy_new_call (proxy); + rest_proxy_call_set_method (call, "GET"); + rest_proxy_call_set_function (call, function); + rest_proxy_call_sync (call, &error); + + return rest_proxy_call_get_payload (call); +} diff --git a/src/osm_api_client.h b/src/osm_api_client.h new file mode 100644 index 00000000..684dda35 --- /dev/null +++ b/src/osm_api_client.h @@ -0,0 +1,59 @@ +/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*- */ +/* + * viking + * Copyright (C) 2025, Guilhem Bonnefille + * + * viking 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 2 of the License, or + * (at your option) any later version. + * + * viking 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 . + */ + +#ifndef _OSM_API_CLIENT_H +#define _OSM_API_CLIENT_H + +#include + +G_BEGIN_DECLS + +#define OSM_TYPE_API_CLIENT (osm_api_client_get_type()) +#define OSM_API_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj), OSM_TYPE_API_CLIENT, OsmApiClient)) +#define OSM_IS_API_CLIENT(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), OSM_TYPE_API_CLIENT)) + +typedef struct _OsmApiClientClass OsmApiClientClass; +typedef struct _OsmApiClient OsmApiClient; + +struct _OsmApiClient { + GObject parent_instance; +}; + +struct _OsmApiClientClass { + GObjectClass parent_class; + gchar *(*challenge_start)(OsmApiClient *self); + gchar *(*challenge_finish)(OsmApiClient *self, gchar *code); + gchar *(*create_request)(OsmApiClient *self, const gchar *url); +}; + + +// Function prototypes +GType osm_api_client_get_type(void); +OsmApiClient *osm_api_client_new(void); +gchar *osm_api_client_challenge_start(OsmApiClient *self); +void osm_api_client_challenge_finish(OsmApiClient *self, gchar *code); +gchar *osm_api_client_create_request(OsmApiClient *self, const gchar *function); + +// Property accessors +void osm_api_client_set_token(OsmApiClient *self, const gchar *token); +const gchar *osm_api_client_get_token(OsmApiClient *self); + +G_END_DECLS + +#endif /* OSM_API_CLIENT_H */