From 4a6ebcef4c03cff21ec6e52deacacf59eed6c934 Mon Sep 17 00:00:00 2001 From: Solar Smith Date: Tue, 16 Jun 2026 16:55:01 -0400 Subject: [PATCH 1/4] explicitly resolve DNS url then build URL download path --- .gitignore | 1 + src/gmt_remote.c | 69 +++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 67 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index fcdb05ae2f3..e3a79202683 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ test/**/*.ps .vscode cmake/ConfigUser.cmake.orig cmake/ConfigUserAdvanced.cmake.orig +gmt.conf diff --git a/src/gmt_remote.c b/src/gmt_remote.c index 70c34ac2d77..bc8dad8ba20 100644 --- a/src/gmt_remote.c +++ b/src/gmt_remote.c @@ -646,6 +646,61 @@ GMT_LOCAL char *gmtremote_lockfile (struct GMT_CTRL *GMT, char *file) { return (strdup (Lfile)); } +GMT_LOCAL size_t gmtremote_discard_body (void *ptr, size_t size, size_t nmemb, void *userdata) { + /* Write callback that discards all data and aborts the transfer immediately. + * Returning 0 causes libcurl to stop with CURLE_WRITE_ERROR, which is expected. */ + (void)ptr; (void)size; (void)nmemb; (void)userdata; + return 0; +} + +GMT_LOCAL char *gmtremote_resolve_redirect (struct GMTAPI_CTRL *API, const char *url) { + /* Follow any HTTP redirect and return the effective base URL (no trailing slash). + * This resolves vanity alias domains like china.generic-mapping-tools.org that + * issue a 302 redirect to the actual mirror (e.g. https://mirrors.ustc.edu.cn/gmtdata). + * Uses a GET request (not HEAD) because some redirect servers return 404 for HEAD. + * The body is discarded immediately via a write callback to avoid downloading content. + * Returns NULL on connection failure, leaving the caller to use the original URL. */ + CURL *curl; + CURLcode res; + static char resolved[GMT_LEN256] = {""}; + char *effective = NULL; + size_t len; + + GMT_Report (API, GMT_MSG_INFORMATION, "Resolving redirect for %s ...\n", url); + if ((curl = curl_easy_init ()) == NULL) return NULL; + curl_easy_setopt (curl, CURLOPT_URL, url); + curl_easy_setopt (curl, CURLOPT_FOLLOWLOCATION, 1L); /* Follow 30x redirects */ + curl_easy_setopt (curl, CURLOPT_SSL_VERIFYPEER, 0L); + curl_easy_setopt (curl, CURLOPT_CONNECTTIMEOUT, 3L); /* Short timeout: this is best-effort */ + curl_easy_setopt (curl, CURLOPT_TIMEOUT, 5L); /* Overall cap including redirect chain */ + curl_easy_setopt (curl, CURLOPT_WRITEFUNCTION, gmtremote_discard_body); /* Abort on first data */ + res = curl_easy_perform (curl); + GMT_Report (API, GMT_MSG_DEBUG, "Resolver curl result: %d (%s)\n", (int)res, curl_easy_strerror (res)); + /* Read effective URL regardless of error code: when the discard callback aborts the + * transfer, platform SSL backends may report CURLE_WRITE_ERROR, CURLE_RECV_ERROR, or + * CURLE_OK — all are acceptable if a redirect was followed. */ + if (curl_easy_getinfo (curl, CURLINFO_EFFECTIVE_URL, &effective) == CURLE_OK && effective) { + strncpy (resolved, effective, GMT_LEN256-1); + resolved[GMT_LEN256-1] = '\0'; + len = strlen (resolved); + if (len > 0 && resolved[len-1] == '/') /* Strip trailing slash */ + resolved[len-1] = '\0'; + } + curl_easy_cleanup (curl); + if (effective == NULL) return NULL; + /* Only return the resolved URL if a redirect actually occurred */ + { + char url_trimmed[GMT_LEN256]; + strncpy (url_trimmed, url, GMT_LEN256-1); + url_trimmed[GMT_LEN256-1] = '\0'; + len = strlen (url_trimmed); + if (len > 0 && url_trimmed[len-1] == '/') url_trimmed[len-1] = '\0'; + if (strcmp (resolved, url_trimmed) == 0) return NULL; /* No redirect occurred */ + } + GMT_Report (API, GMT_MSG_INFORMATION, "Resolved %s -> %s\n", url, resolved); + return resolved; +} + GMT_LOCAL size_t gmtremote_skip_large_files (struct GMT_CTRL *GMT, char * URL, size_t limit) { /* Get the remote file's size and if too large we refuse to download */ CURL *curl = NULL; @@ -1910,15 +1965,23 @@ int gmt_download_tiles (struct GMTAPI_CTRL *API, char *list, unsigned int mode) char *gmt_dataserver_url (struct GMTAPI_CTRL *API) { /* Build the full URL to the currently selected data server */ static char URL[GMT_LEN256] = {""}, *link = URL; + static char last_candidate[GMT_LEN256] = {""}; /* Cache: avoid re-resolving same server */ if (strncmp (API->GMT->session.DATASERVER, "http", 4U)) { /* Not an URL so must assume it is the country/unit name, e.g., oceania */ /* We make this part case insensitive since all official GMT servers are lower-case */ - char name[GMT_LEN64] = {""}; + char name[GMT_LEN64] = {""}, candidate[GMT_LEN256] = {""}; + char *resolved = NULL; strncpy (name, API->GMT->session.DATASERVER, GMT_LEN64-1); gmt_str_tolower (name); if (strchr (name, '.')) /* Must assume it is stuff like mybox.somedomain.type so prepend http */ - snprintf (URL, GMT_LEN256-1, "http://%s", name); + snprintf (candidate, GMT_LEN256-1, "http://%s", name); else /* Expand server name to full URL */ - snprintf (URL, GMT_LEN256-1, "http://%s.generic-mapping-tools.org", name); + snprintf (candidate, GMT_LEN256-1, "http://%s.generic-mapping-tools.org", name); + if (strcmp (candidate, last_candidate)) { /* New or changed server — resolve redirect */ + resolved = gmtremote_resolve_redirect (API, candidate); + strncpy (URL, resolved ? resolved : candidate, GMT_LEN256-1); + strncpy (last_candidate, candidate, GMT_LEN256-1); + } + /* else URL already holds the resolved base from the previous call */ } else /* Must use the URL as is */ snprintf (URL, GMT_LEN256-1, "%s", API->GMT->session.DATASERVER); From 2b997f425047cfac446a746bb3fc52fd2e7b2cdb Mon Sep 17 00:00:00 2001 From: Solar Smith Date: Mon, 22 Jun 2026 14:35:01 -0400 Subject: [PATCH 2/4] Add URL alias test of oceania --- test/gmt_remote/dataserver_alias.sh | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100755 test/gmt_remote/dataserver_alias.sh diff --git a/test/gmt_remote/dataserver_alias.sh b/test/gmt_remote/dataserver_alias.sh new file mode 100755 index 00000000000..85d8d7cf064 --- /dev/null +++ b/test/gmt_remote/dataserver_alias.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Regression test for GMT_DATA_SERVER alias redirect resolution. +# +# Background: Alias server names (e.g., china, australia) expand to Hover DNS +# URL-forwards that redirect to the real mirror but strip the file path. +# Before the fix, GMT constructed download URLs using the alias base (e.g., +# http://china.generic-mapping-tools.org/server/earth/earth_relief/file.grd), +# which Hover would redirect to the mirror root — discarding the file path and +# returning an HTML directory listing instead of the requested file. +# +# The fix (gmtremote_resolve_redirect in gmt_remote.c) follows redirects once +# at session start to discover the real mirror base URL, then constructs all +# file paths against that resolved URL. +# +# With GMT_DATA_SERVER=static (CI default) this test uses a locally cached +# grid and verifies the grdinfo output is sane. With a live alias server it +# also exercises the redirect resolver; a broken resolver would return an HTML +# body that GMT cannot parse as a netCDF grid, causing the test to fail. + +# Force an alias server name so the alias expansion and redirect resolver code +# in gmt_dataserver_url() is always exercised, regardless of the build-time +# GMT_DATA_SERVER default. oceania is a stable direct server (no DNS forward). +export GMT_DATA_SERVER=oceania + +# Download (or use cached) global 15 arc-minute relief grid and get its range +gmt grdinfo @earth_relief_15m_g.grd -I- > got.txt 2>err.txt + +# A valid global grid must report exactly this region +echo "-R-180/180/-90/90" > expected.txt + +diff expected.txt got.txt > fail From c15366dbf1a1a231f40f69e655b690e0ec979106 Mon Sep 17 00:00:00 2001 From: Solar Smith Date: Mon, 22 Jun 2026 14:37:06 -0400 Subject: [PATCH 3/4] clear cache before gmt run --- test/gmt_remote/dataserver_alias.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/gmt_remote/dataserver_alias.sh b/test/gmt_remote/dataserver_alias.sh index 85d8d7cf064..8738d027cdc 100755 --- a/test/gmt_remote/dataserver_alias.sh +++ b/test/gmt_remote/dataserver_alias.sh @@ -17,6 +17,9 @@ # also exercises the redirect resolver; a broken resolver would return an HTML # body that GMT cannot parse as a netCDF grid, causing the test to fail. +# Remove any cached copy to force a real download from the server +rm -f "${HOME}/.gmt/server/earth/earth_relief/earth_relief_15m_g.grd" + # Force an alias server name so the alias expansion and redirect resolver code # in gmt_dataserver_url() is always exercised, regardless of the build-time # GMT_DATA_SERVER default. oceania is a stable direct server (no DNS forward). From 06c4d858ec3a7c95a77328046d967219751b23d8 Mon Sep 17 00:00:00 2001 From: Solar Smith Date: Tue, 23 Jun 2026 10:09:20 -0400 Subject: [PATCH 4/4] use smaller grd in download test --- test/gmt_remote/dataserver_alias.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/gmt_remote/dataserver_alias.sh b/test/gmt_remote/dataserver_alias.sh index 8738d027cdc..2b144cd7aea 100755 --- a/test/gmt_remote/dataserver_alias.sh +++ b/test/gmt_remote/dataserver_alias.sh @@ -18,15 +18,15 @@ # body that GMT cannot parse as a netCDF grid, causing the test to fail. # Remove any cached copy to force a real download from the server -rm -f "${HOME}/.gmt/server/earth/earth_relief/earth_relief_15m_g.grd" +rm -f "${HOME}/.gmt/server/earth/earth_relief/earth_relief_01d_p.grd" # Force an alias server name so the alias expansion and redirect resolver code # in gmt_dataserver_url() is always exercised, regardless of the build-time # GMT_DATA_SERVER default. oceania is a stable direct server (no DNS forward). export GMT_DATA_SERVER=oceania -# Download (or use cached) global 15 arc-minute relief grid and get its range -gmt grdinfo @earth_relief_15m_g.grd -I- > got.txt 2>err.txt +# Download (or use cached) global 1 arc-degree relief grid and get its range +gmt grdinfo @earth_relief_01d_p.grd -I- > got.txt 2>err.txt # A valid global grid must report exactly this region echo "-R-180/180/-90/90" > expected.txt