Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ test/**/*.ps
.vscode
cmake/ConfigUser.cmake.orig
cmake/ConfigUserAdvanced.cmake.orig
gmt.conf
69 changes: 66 additions & 3 deletions src/gmt_remote.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
34 changes: 34 additions & 0 deletions test/gmt_remote/dataserver_alias.sh
Original file line number Diff line number Diff line change
@@ -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
Loading