diff --git a/.gitignore b/.gitignore
index c6127b3..0b487b8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -50,3 +50,6 @@ modules.order
Module.symvers
Mkfile.old
dkms.conf
+
+ntripcaster.conf
+sourcetable.dat
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index b9f0f17..2b9b872 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,10 +1,11 @@
-FROM ubuntu:18.04 as builder
+FROM alpine:3.18 as builder
COPY ntripcaster /ntripcaster
WORKDIR /ntripcaster
-RUN apt-get update && apt-get install build-essential --assume-yes
+# Install build tools (Alpine uses apk instead of apt-get)
+RUN apk add --no-cache build-base
RUN ./configure
@@ -12,9 +13,9 @@ RUN make install
# The builder image is dumped and a fresh image is used
# just with the built binary, config and logs made from 'make install'
-FROM ubuntu:18.04
+FROM alpine:3.18
COPY --from=builder /usr/local/ntripcaster/ /usr/local/ntripcaster/
EXPOSE 2101
WORKDIR /usr/local/ntripcaster/logs
-CMD /usr/local/ntripcaster/bin/ntripcaster
+CMD /usr/local/ntripcaster/bin/ntripcaster
\ No newline at end of file
diff --git a/ntripcaster/src/client.c b/ntripcaster/src/client.c
index 8629bb2..26c4fad 100644
--- a/ntripcaster/src/client.c
+++ b/ntripcaster/src/client.c
@@ -363,6 +363,327 @@ client_errors (const client_t *client)
return (CHUNKLEN - (client->cid - client->source->cid)) % CHUNKLEN;
}
+/* Check if the user agent indicates a web browser */
+static int is_browser(const char *user_agent) {
+ if (!user_agent || ice_strcmp(user_agent, "(null)") == 0)
+ return 0;
+
+ /* Check for common browser user agent strings */
+ if (ice_strcasestr(user_agent, "Mozilla") ||
+ ice_strcasestr(user_agent, "Chrome") ||
+ ice_strcasestr(user_agent, "Safari") ||
+ ice_strcasestr(user_agent, "Firefox") ||
+ ice_strcasestr(user_agent, "Edge") ||
+ ice_strcasestr(user_agent, "Opera") ||
+ ice_strcasestr(user_agent, "Internet Explorer") ||
+ ice_strcasestr(user_agent, "MSIE")) {
+ return 1;
+ }
+
+ return 0;
+}
+
+/* Send HTML formatted sourcetable for browsers */
+static void send_html_sourcetable(connection_t *con, FILE *ifp) {
+ char szBuffer[2000], c[2];
+ int nBytes = 1, nBufferBytes = 0;
+ char *time;
+
+ time = get_log_time();
+
+ /* Send HTTP header for HTML */
+ sock_write_line (con->sock, "HTTP/1.0 200 OK");
+ sock_write_line (con->sock, "Server: NTRIP NtripCaster %s/%s", info.version, info.ntrip_version);
+ sock_write_line (con->sock, "Content-Type: text/html");
+ sock_write_line (con->sock, "Connection: close\r\n");
+
+ /* Send HTML header */
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "
");
+ sock_write_line (con->sock, "NTRIP Caster - Source Table");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+
+ /* Server information */
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "
NTRIP Caster Source Table
");
+ if (info.server_name)
+ sock_write_line (con->sock, "
Server: %s
", info.server_name);
+ sock_write_line (con->sock, "
Port: %d
", info.port[0]);
+ sock_write_line (con->sock, "
Version: %s/%s
", info.version, info.ntrip_version);
+ sock_write_line (con->sock, "
Time: %s
", time);
+ sock_write_line (con->sock, "
");
+
+ if (ifp) {
+ /* First, display any non-STR/CAS/NET lines */
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "
General Information
");
+ while (nBytes > 0) {
+ nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ while (((unsigned int)c[0] != 10) && (nBytes > 0)) {
+ szBuffer[nBufferBytes] = c[0];
+ nBufferBytes++;
+ nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ }
+ szBuffer[nBufferBytes] = '\0';
+
+ if (nBufferBytes > 0) {
+ /* Display non-STR/CAS/NET lines as preformatted text */
+ if (strncmp(szBuffer, "STR", 3) != 0 && strncmp(szBuffer, "CAS", 3) != 0 && strncmp(szBuffer, "NET", 3) != 0) {
+ sock_write_line (con->sock, "
%s
", szBuffer);
+ }
+ }
+ nBufferBytes = 0;
+ }
+ sock_write_line (con->sock, "
");
+
+ /* CAS Table */
+ rewind(ifp);
+ nBytes = 1;
+ nBufferBytes = 0;
+
+ sock_write_line (con->sock, "Casters (CAS)
");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "| Type | Host | Port | Identifier | Operator | ");
+ sock_write_line (con->sock, "NMEA | Country | Latitude | Longitude | Fallback Host | ");
+ sock_write_line (con->sock, "Fallback Port | Misc | ");
+ sock_write_line (con->sock, "
");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+
+ while (nBytes > 0) {
+ nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ while (((unsigned int)c[0] != 10) && (nBytes > 0)) {
+ szBuffer[nBufferBytes] = c[0];
+ nBufferBytes++;
+ nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ }
+ szBuffer[nBufferBytes] = '\0';
+
+ if (nBufferBytes > 0 && strncmp(szBuffer, "CAS", 3) == 0) {
+ char *tokens[30];
+ int token_count = 0;
+ char *token;
+ char line_copy[2000];
+ int i;
+
+ strncpy(line_copy, szBuffer, sizeof(line_copy) - 1);
+ line_copy[sizeof(line_copy) - 1] = '\0';
+
+ token = strtok(line_copy, ";");
+ while (token && token_count < 30) {
+ tokens[token_count++] = token;
+ token = strtok(NULL, ";");
+ }
+
+ sock_write_line (con->sock, "");
+
+ /* Display CAS fields (11 defined fields) */
+ for (i = 0; i < 11; i++) {
+ if (i < token_count && tokens[i] && strlen(tokens[i]) > 0) {
+ sock_write_line (con->sock, "| %s | ", tokens[i]);
+ } else {
+ sock_write_line (con->sock, "- | ");
+ }
+ }
+
+ /* Display misc data (everything after field 11) */
+ sock_write_line (con->sock, "");
+ if (token_count > 11) {
+ for (i = 11; i < token_count; i++) {
+ if (tokens[i] && strlen(tokens[i]) > 0) {
+ sock_write_line (con->sock, "%s", tokens[i]);
+ if (i < token_count - 1) sock_write_line (con->sock, "; ");
+ }
+ }
+ } else {
+ sock_write_line (con->sock, "-");
+ }
+ sock_write_line (con->sock, " | ");
+
+ sock_write_line (con->sock, "
");
+ }
+ nBufferBytes = 0;
+ }
+
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "
");
+
+ /* NET Table */
+ rewind(ifp);
+ nBytes = 1;
+ nBufferBytes = 0;
+
+ sock_write_line (con->sock, "Networks (NET)
");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "| Type | Identifier | Operator | Authentication | Fee | ");
+ sock_write_line (con->sock, "Web Net | Web Str | Web Reg | Misc | ");
+ sock_write_line (con->sock, "
");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+
+ while (nBytes > 0) {
+ nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ while (((unsigned int)c[0] != 10) && (nBytes > 0)) {
+ szBuffer[nBufferBytes] = c[0];
+ nBufferBytes++;
+ nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ }
+ szBuffer[nBufferBytes] = '\0';
+
+ if (nBufferBytes > 0 && strncmp(szBuffer, "NET", 3) == 0) {
+ char *tokens[30];
+ int token_count = 0;
+ char *token;
+ char line_copy[2000];
+ int i;
+
+ strncpy(line_copy, szBuffer, sizeof(line_copy) - 1);
+ line_copy[sizeof(line_copy) - 1] = '\0';
+
+ token = strtok(line_copy, ";");
+ while (token && token_count < 30) {
+ tokens[token_count++] = token;
+ token = strtok(NULL, ";");
+ }
+
+ sock_write_line (con->sock, "");
+
+ /* Display NET fields (8 defined fields) */
+ for (i = 0; i < 8; i++) {
+ if (i < token_count && tokens[i] && strlen(tokens[i]) > 0) {
+ sock_write_line (con->sock, "| %s | ", tokens[i]);
+ } else {
+ sock_write_line (con->sock, "- | ");
+ }
+ }
+
+ /* Display misc data (everything after field 8) */
+ sock_write_line (con->sock, "");
+ if (token_count > 8) {
+ for (i = 8; i < token_count; i++) {
+ if (tokens[i] && strlen(tokens[i]) > 0) {
+ sock_write_line (con->sock, "%s", tokens[i]);
+ if (i < token_count - 1) sock_write_line (con->sock, "; ");
+ }
+ }
+ } else {
+ sock_write_line (con->sock, "-");
+ }
+ sock_write_line (con->sock, " | ");
+
+ sock_write_line (con->sock, "
");
+ }
+ nBufferBytes = 0;
+ }
+
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "
");
+
+ /* STR Table */
+ rewind(ifp);
+ nBytes = 1;
+ nBufferBytes = 0;
+
+ sock_write_line (con->sock, "Data Streams (STR)
");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "| Type | Mountpoint | Identifier | Format | Format Details | ");
+ sock_write_line (con->sock, "Carrier | Nav System | Network | Country | Latitude | ");
+ sock_write_line (con->sock, "Longitude | NMEA | Solution | Generator | Compr Encryp | ");
+ sock_write_line (con->sock, "Authentication | Fee | Bitrate | Misc | ");
+ sock_write_line (con->sock, "
");
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+
+ while (nBytes > 0) {
+ nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ while (((unsigned int)c[0] != 10) && (nBytes > 0)) {
+ szBuffer[nBufferBytes] = c[0];
+ nBufferBytes++;
+ nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ }
+ szBuffer[nBufferBytes] = '\0';
+
+ if (nBufferBytes > 0 && strncmp(szBuffer, "STR", 3) == 0) {
+ char *tokens[30];
+ int token_count = 0;
+ char *token;
+ char line_copy[2000];
+ int i;
+
+ strncpy(line_copy, szBuffer, sizeof(line_copy) - 1);
+ line_copy[sizeof(line_copy) - 1] = '\0';
+
+ token = strtok(line_copy, ";");
+ while (token && token_count < 30) {
+ tokens[token_count++] = token;
+ token = strtok(NULL, ";");
+ }
+
+ sock_write_line (con->sock, "");
+
+ /* Display STR fields (18 defined fields) */
+ for (i = 0; i < 18; i++) {
+ if (i < token_count && tokens[i] && strlen(tokens[i]) > 0) {
+ sock_write_line (con->sock, "| %s | ", tokens[i]);
+ } else {
+ sock_write_line (con->sock, "- | ");
+ }
+ }
+
+ /* Display misc data (everything after field 18) */
+ sock_write_line (con->sock, "");
+ if (token_count > 18) {
+ for (i = 18; i < token_count; i++) {
+ if (tokens[i] && strlen(tokens[i]) > 0) {
+ sock_write_line (con->sock, "%s", tokens[i]);
+ if (i < token_count - 1) sock_write_line (con->sock, "; ");
+ }
+ }
+ } else {
+ sock_write_line (con->sock, "-");
+ }
+ sock_write_line (con->sock, " | ");
+
+ sock_write_line (con->sock, "
");
+ }
+ nBufferBytes = 0;
+ }
+
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "
");
+ } else {
+ sock_write_line (con->sock, "No sourcetable available
");
+ }
+
+ /* Add informational note at the bottom */
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "
Note: This source table has been returned as an HTML page because you requested it using a web browser rather than an NTRIP client. NTRIP clients would receive this data in plain text format.
");
+ sock_write_line (con->sock, "
");
+
+ sock_write_line (con->sock, "");
+ sock_write_line (con->sock, "");
+
+ free (time);
+}
+
void
send_sourcetable (connection_t *con) {
@@ -372,20 +693,58 @@ send_sourcetable (connection_t *con) {
nBufferBytes = 0,
fsize = 0;
char *time;
+ const char *user_agent;
time = get_log_time();
+ user_agent = get_user_agent(con);
+
+ xa_debug(2, "DEBUG: send_sourcetable() User-Agent: [%s]", user_agent ? user_agent : "(null)");
+ ifp = fopen("../conf/sourcetable.dat","r");
+
+ /* Check if this is a browser request */
+ if (is_browser(user_agent)) {
+ xa_debug(2, "DEBUG: Browser detected, sending HTML sourcetable");
+ send_html_sourcetable(con, ifp);
+ if (ifp) fclose(ifp);
+ free (time);
+ return;
+ }
+
+ /* Original NTRIP client handling */
+ xa_debug(2, "DEBUG: NTRIP client detected, sending plain text sourcetable");
+
sock_write_line (con->sock, "SOURCETABLE 200 OK");
sock_write_line (con->sock, "Server: NTRIP NtripCaster %s/%s", info.version, info.ntrip_version);
// sock_write_line (con->sock, "Date: %s %s", time, info.timezone);
- ifp = fopen("../conf/sourcetable.dat","r");
if (ifp != NULL) {
- fseek(ifp, 0, SEEK_END);
- fsize = (int)ftell(ifp);
+ int filtered_size = 0;
+ char temp_buffer[2000];
+ int temp_nBytes = 1, temp_nBufferBytes = 0;
+
+ /* First pass: calculate filtered content size */
+ while (temp_nBytes > 0) {
+ temp_nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ while (((unsigned int)c[0] != 10) && (temp_nBytes > 0)) {
+ temp_buffer[temp_nBufferBytes] = c[0];
+ temp_nBufferBytes++;
+ temp_nBytes = fread(c,sizeof(char),sizeof(char),ifp);
+ }
+ temp_buffer[temp_nBufferBytes] = '\0';
+ /* Only count lines that start with "STR" */
+ if (temp_nBufferBytes > 0 && strncmp(temp_buffer, "STR", 3) == 0) {
+ filtered_size += temp_nBufferBytes + 2; /* +2 for \r\n */
+ }
+ temp_nBufferBytes = 0;
+ }
+ filtered_size += 13; /* for "ENDSOURCETABLE" + \r\n */
+
+ /* Reset file pointer for second pass */
rewind(ifp);
-
+ nBytes = 1;
+
sock_write_line (con->sock, "Content-Type: text/plain");
- sock_write_line (con->sock, "Content-Length: %d\r\n", fsize);
+ sock_write_line (con->sock, "Content-Length: %d\r\n", filtered_size);
while (nBytes > 0) {
nBytes = fread(c,sizeof(char),sizeof(char),ifp);
@@ -395,7 +754,10 @@ send_sourcetable (connection_t *con) {
nBytes = fread(c,sizeof(char),sizeof(char),ifp);
}
szBuffer[nBufferBytes] = '\0';
- if (nBufferBytes > 0) sock_write_line (con->sock, szBuffer);
+ /* Only send lines that start with "STR" */
+ if (nBufferBytes > 0 && strncmp(szBuffer, "STR", 3) == 0) {
+ sock_write_line (con->sock, szBuffer);
+ }
nBufferBytes = 0;
}
diff --git a/ntripcaster/src/ntrip_string.c b/ntripcaster/src/ntrip_string.c
index 663f7d1..7b8134c 100644
--- a/ntripcaster/src/ntrip_string.c
+++ b/ntripcaster/src/ntrip_string.c
@@ -464,6 +464,38 @@ ice_strcasecmp (const char *s1, const char *s2)
#endif
}
+const char *
+ice_strcasestr (const char *haystack, const char *needle)
+{
+ const char *h, *n;
+
+ if (!haystack || !needle)
+ {
+ xa_debug (1, "ERROR: ice_strcasestr() called with NULL pointers");
+ return NULL;
+ }
+
+ if (*needle == '\0')
+ return haystack;
+
+ for (; *haystack; haystack++)
+ {
+ h = haystack;
+ n = needle;
+
+ while (*h && *n && (tolower((unsigned char)*h) == tolower((unsigned char)*n)))
+ {
+ h++;
+ n++;
+ }
+
+ if (*n == '\0')
+ return haystack;
+ }
+
+ return NULL;
+}
+
const char *
nullcheck_string (const char *string)
{
diff --git a/ntripcaster/src/ntrip_string.h b/ntripcaster/src/ntrip_string.h
index ce556d1..6c5b8f0 100644
--- a/ntripcaster/src/ntrip_string.h
+++ b/ntripcaster/src/ntrip_string.h
@@ -53,6 +53,7 @@ char *nice_time(unsigned long int seconds, char *buf);
int ice_strcasecmp (const char *s1, const char *s2);
int ice_strncmp (const char *s1, const char *s2, size_t n);
int ice_strcmp (const char *s1, const char *s2);
+const char *ice_strcasestr (const char *haystack, const char *needle);
size_t ice_strlen (const char *string);
char *nice_time_minutes (unsigned long int minutes, char *buf);
const char *nullcheck_string (const char *string);