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); diff --git a/test/gmt_remote/dataserver_alias.sh b/test/gmt_remote/dataserver_alias.sh new file mode 100755 index 00000000000..2b144cd7aea --- /dev/null +++ b/test/gmt_remote/dataserver_alias.sh @@ -0,0 +1,34 @@ +#!/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. + +# Remove any cached copy to force a real download from the server +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 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 + +diff expected.txt got.txt > fail