diff --git a/.gitignore b/.gitignore index 6a32990..76c1e67 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ bin_* mcp/locale.* MEMORY.md .DS_Store +mcc/amissl_sdk/ diff --git a/AUTHORS b/AUTHORS index ee21782..0ef5350 100644 --- a/AUTHORS +++ b/AUTHORS @@ -2,7 +2,7 @@ HTMLview.mcc - HTMLview MUI Custom Class Copyright (C) 1997-2000 Allan Odgaard - Copyright (C) 2005-2010 by HTMLview.mcc Open Source Team + Copyright (C) 2005-2026 by HTMLview.mcc Open Source Team This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public @@ -25,6 +25,7 @@ design provided by the following people: Alfonso Ranieri Allan Odgaard +Dimitris Panokostas Dwight Meese Ilkka Lehtoranta Jens Langner diff --git a/ChangeLog b/ChangeLog index f5fc9ea..a6f4824 100644 --- a/ChangeLog +++ b/ChangeLog @@ -5,6 +5,36 @@ MUI HTMLview MCC class - ChangeLog $Id$ $URL$ +2026-04-18 Dimitris Panokostas + + * mcc/test_image_hook.h: HTTPS support via AmiSSL. The shared test-program + image hook now handles https:// URLs and follows http->https redirects + (up to five hops). TLS is wrapped through amisslmaster.library + + amissl.library (jens-maus AmiSSL 5.27 SDK), using the inline macros + against task-local library bases -- no stub lib link and no global + AmiSSLBase symbol required. Certificate verification is currently + VERIFY_NONE; CA bundle wiring is a follow-up. + * mcc/test_image_hook.h: Replaced unbounded sprintf into log buffers with + snprintf; a long Location: header or redirect target could otherwise + overflow the stack log buffer. + * mcc/Makefile: On-demand AmiSSL SDK download (lha xq). HTTPS is enabled + for OS3 and OS4 builds; MorphOS stays on plain HTTP. + * mcc/SimpleTest.c, mcc/LibLoad_Test.c: Added network-image test entries + (plain HTTP from aminet, http->https redirect on amigaworld, direct + HTTPS from aminet). + * mcc/LibLoad_Test.c: New test program that exercises the same hook as + SimpleTest but through dlopen-style library loading, making it easier + to verify the shipped HTMLview.mcc on each platform. + +2026-04-17 Dimitris Panokostas + + * mcc: Fix MorphOS MCP and OS4 MCC build issues; OS4 build now makes + -ldebug conditional and stubs kprintf when not building with DEBUG=1; + enabled all three platform builds (OS3, OS4, MorphOS) in CI. + * mcc/ImageManager.cpp, mcc/IM_Render.cpp, mcc/Dispatcher.cpp: Local + image rendering fixes uncovered by the new test programs (decoder + handshake, frame cleanup, hook dispatcher wiring on OS4). + 2016-07-29 Thore Böckelmann * mcc: lots of changes to get this beast buildable with our Linux based cross diff --git a/README b/README index a9e752b..0acd400 100644 --- a/README +++ b/README @@ -2,7 +2,7 @@ HTMLview.mcc - HTMLview MUI Custom Class Copyright (C) 1997-2000 Allan Odgaard - Copyright (C) 2005-2007 by HTMLview.mcc Open Source Team + Copyright (C) 2005-2026 by HTMLview.mcc Open Source Team This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public diff --git a/TODO b/TODO index e58ea4e..3fa4961 100644 --- a/TODO +++ b/TODO @@ -2,7 +2,7 @@ HTMLview.mcc - HTMLview MUI Custom Class Copyright (C) 1997-2000 Allan Odgaard - Copyright (C) 2005-2007 by HTMLview.mcc Open Source Team + Copyright (C) 2005-2026 by HTMLview.mcc Open Source Team This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public diff --git a/doc/MCC_HTMLview.doc b/doc/MCC_HTMLview.doc index 2ce2999..7e34adc 100644 --- a/doc/MCC_HTMLview.doc +++ b/doc/MCC_HTMLview.doc @@ -141,6 +141,14 @@ ToDo: FUNCTION Setup a hook used for image-loading. + Only the hook performs I/O; HTMLview.mcc itself never + touches the network or the filesystem. The hook therefore + decides which URL schemes are supported. The bundled + SimpleTest / LibLoad_Test programs in mcc/ ship a reference + hook that handles PROGDIR:, file://, http:// and (when + compiled against the AmiSSL SDK) https:// with http->https + redirect following. + SEE ALSO MUIA_HTMLview_LoadHook @@ -160,6 +168,10 @@ ToDo: FUNCTION Setup a hook used for (page)-loading. + See MUIA_HTMLview_ImageLoadHook for a note on what the + hook is responsible for and where to find a reference + implementation that also covers HTTPS via AmiSSL. + The hook is called with a pointer to itself in a0, a pointer to a struct HTMLview_LoadMsg in a1 and a pointer to the calling object in a2. diff --git a/doc/MCC_HTMLview.readme b/doc/MCC_HTMLview.readme index 910dcc7..0f149a3 100644 --- a/doc/MCC_HTMLview.readme +++ b/doc/MCC_HTMLview.readme @@ -51,7 +51,7 @@ LEGALESE HTMLview.mcc was originally written in 1995 and is Copyright (C) 1995-2000 by Allan Odgaard. As of version 13.4, released in December 2007, the gadget is -maintained and Copyright (C) 2005-2007 by the HTMLview.mcc Open Source Team. +maintained and Copyright (C) 2005-2026 by the HTMLview.mcc Open Source Team. HTMLview.mcc is distributed under the GNU Lesser General Public License (LGPL) and the development is hosted at SourceForge.net: diff --git a/mcc/HTMLview_mcc.h b/mcc/HTMLview_mcc.h index 133a5f0..c591751 100644 --- a/mcc/HTMLview_mcc.h +++ b/mcc/HTMLview_mcc.h @@ -165,6 +165,10 @@ * to itself in a0, a pointer to a struct HTMLview_LoadMsg in a1 and a * pointer to the calling object in a2. * + * See MUIA_HTMLview_ImageLoadHook for a note on what the hook is + * responsible for and where to find a reference implementation that + * also covers HTTPS via AmiSSL. + * * This hook will be called from a separate task, so the only MUI method * that you can use is MUIM_Application_PushMethod. The hook may very well * be called by sevaral tasks at the same time, so your code needs to be @@ -619,6 +623,13 @@ * * Setup a hook used for image-loading. * + * Only the hook performs I/O; HTMLview.mcc itself never touches the + * network or the filesystem. The hook therefore decides which URL + * schemes are supported. The bundled SimpleTest / LibLoad_Test + * programs in mcc/ ship a reference hook that handles PROGDIR:, + * file://, http:// and (when compiled against the AmiSSL SDK) + * https:// with http->https redirect following. + * * SEE ALSO * * MUIA_HTMLview_LoadHook diff --git a/mcc/LibLoad_Test.c b/mcc/LibLoad_Test.c index aa0df24..3682a0c 100644 --- a/mcc/LibLoad_Test.c +++ b/mcc/LibLoad_Test.c @@ -52,7 +52,12 @@ static const char *test_html = "

Images - Network (HTTP)

" "

Network images require bsdsocket.library:

" "

\"Aminet

" - "

\"AmigaWorld

" + "

Exercises http->https redirect (requires AmiSSL):

" + "

\"AmigaWorld

" + + "

Images - Network (HTTPS)

" + "

Direct TLS fetch. Needs amisslmaster.library + amissl.library installed:

" + "

\"Aminet

" "

Text

" "

Bold, italic, under, strike, tt

" diff --git a/mcc/Makefile b/mcc/Makefile index f1ed190..b2b5f54 100644 --- a/mcc/Makefile +++ b/mcc/Makefile @@ -152,6 +152,11 @@ endif LDFLAGS = $(CPU) $(DEBUGSYM) LDLIBS = +# Per-program extras; populated by the AmiSSL block below. +TEST_CFLAGS = +TEST_LDLIBS = +TEST_DEPS = + # different options per target OS ifeq ($(OS), os4) @@ -241,6 +246,45 @@ endif endif endif +########################################################################### +# AmiSSL SDK — enables HTTPS in SimpleTest / LibLoad_Test. +# +# The SDK is downloaded from jens-maus/amissl on first build. Pass +# USE_AMISSL=0 to disable; the test programs then speak plain HTTP only. +# No SDK is distributed for MorphOS, so USE_AMISSL is forced off there. + +AMISSL_VERSION ?= 5.27 +AMISSL_SDK_DIR ?= amissl_sdk +AMISSL_SDK_READY = $(AMISSL_SDK_DIR)/.ready +AMISSL_INC = $(AMISSL_SDK_DIR)/AmiSSL/Developer/include +AMISSL_URL = https://github.com/jens-maus/amissl/releases/download/$(AMISSL_VERSION)/AmiSSL-$(AMISSL_VERSION)-SDK.lha + +ifeq ($(OS), os3) + AMISSL_LIBDIR = $(AMISSL_SDK_DIR)/AmiSSL/Developer/lib/AmigaOS3 + USE_AMISSL ?= 1 +else ifeq ($(OS), os4) + AMISSL_LIBDIR = $(AMISSL_SDK_DIR)/AmiSSL/Developer/lib/AmigaOS4/newlib + USE_AMISSL ?= 1 +else + USE_AMISSL = 0 +endif + +ifeq ($(USE_AMISSL), 1) + # Headers only: the inline macros in dispatch through + # a task-local library base, so no stub lib (libamisslstubs.a) is needed + # and there are no global AmiSSLBase / AmiSSLMasterBase symbols to + # satisfy at link time. + TEST_CFLAGS += -DHAVE_AMISSL -I$(AMISSL_INC) + TEST_DEPS += $(AMISSL_SDK_READY) +endif + +$(AMISSL_SDK_READY): + @echo " GET AmiSSL SDK $(AMISSL_VERSION)" + @mkdir -p $(AMISSL_SDK_DIR) + @cd $(AMISSL_SDK_DIR) && curl -fsSL -o sdk.lha "$(AMISSL_URL)" + @cd $(AMISSL_SDK_DIR) && lha xq sdk.lha && rm -f sdk.lha + @touch $@ + ########################################################################### # Here starts all stuff that is common for all target platforms and # hosts. @@ -447,6 +491,16 @@ $(OBJDIR)/mccclass_68k.o: ../include/mccclass_68k.c @echo " CC $<" @$(CC) $(CFLAGS) $< -o $@ +# Test programs get the AmiSSL include path (if enabled). The explicit +# rules below shadow the generic %.o: %.c rule so TEST_CFLAGS applies. +$(OBJDIR)/SimpleTest.o: SimpleTest.c test_image_hook.h $(TEST_DEPS) + @echo " CC $<" + @$(CC) $(CFLAGS) $(TEST_CFLAGS) $< -o $@ + +$(OBJDIR)/LibLoad_Test.o: LibLoad_Test.c test_image_hook.h $(TEST_DEPS) + @echo " CC $<" + @$(CC) $(CFLAGS) $(TEST_CFLAGS) $< -o $@ + # # Link against all MCC objects EXCEPT library.o (globals conflict) @@ -456,18 +510,18 @@ LIBOBJS = $(filter-out $(OBJDIR)/library.o $(OBJDIR)/crtclasses_begin.o $(OBJDIR $(SIMPLETARGET): $(OBJDIR)/SimpleTest.o @echo " LD $@" ifneq (,$(DEBUG)) - @$(CXX) $(filter-out -nostartfiles,$(LDFLAGS)) -o $@.debug $(OBJDIR)/SimpleTest.o $(LDLIBS) -ldebug -Wl,-Map,$@.map + @$(CXX) $(filter-out -nostartfiles,$(LDFLAGS)) -o $@.debug $(OBJDIR)/SimpleTest.o $(LDLIBS) $(TEST_LDLIBS) -ldebug -Wl,-Map,$@.map else - @$(CXX) $(filter-out -nostartfiles,$(LDFLAGS)) -o $@.debug $(OBJDIR)/SimpleTest.o $(LDLIBS) -Wl,-Map,$@.map + @$(CXX) $(filter-out -nostartfiles,$(LDFLAGS)) -o $@.debug $(OBJDIR)/SimpleTest.o $(LDLIBS) $(TEST_LDLIBS) -Wl,-Map,$@.map endif @$(STRIP) --preserve-dates -R.comment -R.sdata2 -S -o $@ $@.debug $(LIBLOADTARGET): $(OBJDIR)/LibLoad_Test.o @echo " LD $@" ifneq (,$(DEBUG)) - @$(CXX) $(filter-out -nostartfiles,$(LDFLAGS)) -o $@.debug $(OBJDIR)/LibLoad_Test.o $(LDLIBS) -ldebug -Wl,-Map,$@.map + @$(CXX) $(filter-out -nostartfiles,$(LDFLAGS)) -o $@.debug $(OBJDIR)/LibLoad_Test.o $(LDLIBS) $(TEST_LDLIBS) -ldebug -Wl,-Map,$@.map else - @$(CXX) $(filter-out -nostartfiles,$(LDFLAGS)) -o $@.debug $(OBJDIR)/LibLoad_Test.o $(LDLIBS) -Wl,-Map,$@.map + @$(CXX) $(filter-out -nostartfiles,$(LDFLAGS)) -o $@.debug $(OBJDIR)/LibLoad_Test.o $(LDLIBS) $(TEST_LDLIBS) -Wl,-Map,$@.map endif @$(STRIP) --preserve-dates -R.comment -R.sdata2 -S -o $@ $@.debug diff --git a/mcc/SimpleTest.c b/mcc/SimpleTest.c index ce4753c..64f136c 100644 --- a/mcc/SimpleTest.c +++ b/mcc/SimpleTest.c @@ -57,7 +57,12 @@ static const char *test_html = "

Images - Network (HTTP)

" "

These need bsdsocket.library and a working network stack:

" "

\"Aminet

" - "

\"AmigaWorld

" + "

Exercises http->https redirect (requires AmiSSL):

" + "

\"AmigaWorld

" + + "

Images - Network (HTTPS)

" + "

Direct TLS fetch. Needs amisslmaster.library + amissl.library installed:

" + "

\"Aminet

" "

Text Formatting

" "

Bold text, italic text, " diff --git a/mcc/test_image_hook.h b/mcc/test_image_hook.h index 52e1cfb..0c253cd 100644 --- a/mcc/test_image_hook.h +++ b/mcc/test_image_hook.h @@ -2,23 +2,26 @@ * test_image_hook.h -- Shared image-load hook used by SimpleTest and * LibLoad_Test to feed picture data to HTMLview.mcc. * - * The hook resolves three kinds of URLs: - * - PROGDIR:, DH0:, etc. (direct dos Open) - * - file:// (stripped and dos Open) - * - http://host[:port]/p (bsdsocket.library GET, chunked aware) + * URL schemes resolved by the hook: + * - PROGDIR:, DH0:, etc. -- direct dos Open + * - file:// -- stripped and dos Open + * - http://host[:port]/p -- bsdsocket GET, chunked-aware, redirect-aware + * - https://host[:port]/p -- AmiSSL-wrapped GET (only if HAVE_AMISSL) * - * https:// URLs return failure (no TLS in the stock Amiga bsdsocket). + * The hook follows up to 5 consecutive 3xx redirects (absolute URLs only; + * relative Location: headers are NOT supported yet) and upgrades http->https + * transparently when a server redirects to TLS. * - * The header is intentionally single-include, single-translation-unit. - * Include it from exactly one .c file in a test program; the program owns - * the SocketBase / ISocket globals (so it can close them on exit). + * HAVE_AMISSL is defined by the Makefile when the AmiSSL SDK is present. + * Without it, https:// URLs and http->https redirects fail cleanly. * - * Threading notes: the hook runs in the HTMLview decoder thread, not the - * main task. On m68k every task that uses bsdsocket must OpenLibrary the - * library itself -- the library base opened by main() isn't valid for our - * thread. We therefore open a task-local SocketBase in the hook and shadow - * the file-scope globals so the proto/socket.h inline macros dispatch - * through the local base. + * Threading: runs in the HTMLview decoder task. On m68k every task that uses + * bsdsocket must OpenLibrary the library itself, so this file opens a task- + * local SocketBase (and AmiSSLMasterBase / AmiSSLBase) in the hook and + * shadows the file-scope globals so the proto/socket.h inline macros + * dispatch through the local base. + * + * Include this header from exactly one translation unit per test program. */ #ifndef HTMLVIEW_TEST_IMAGE_HOOK_H @@ -33,6 +36,14 @@ #include #include "HTMLview_mcc.h" +#ifdef HAVE_AMISSL +#include +#include +#include +#include +#include +#endif + #ifndef TRUE #define TRUE 1 #endif @@ -40,8 +51,9 @@ #define FALSE 0 #endif -/* The test programs carry their own globals for this; only used by the - main task. The hook uses its own task-local bases instead (see below). */ + +/* Owned by the test program's main task. The hook uses its own task-local + library bases and never touches these from the decoder thread. */ #if defined(__amigaos4__) extern struct Library *SocketBase; extern struct SocketIFace *ISocket; @@ -59,20 +71,34 @@ struct THL_State ULONG buflen; int chunked; /* non-zero => Transfer-Encoding: chunked */ ULONG chunk_left; + int use_tls; /* non-zero => route I/O through SSL_* */ - /* Task-local bsdsocket bases -- opened by the decoder thread so it can + /* Task-local bsdsocket bases. Opened by the decoder thread so it can actually call socket/recv/etc.; closed on hook-Close. */ struct Library *SBase; #if defined(__amigaos4__) struct SocketIFace *SIFace; #endif + +#ifdef HAVE_AMISSL + struct Library *AMSBase; /* amisslmaster.library */ + struct Library *ASBase; /* amissl.library */ +#if defined(__amigaos4__) + struct AmiSSLMasterIFace *IAMSMaster; + struct AmiSSLIFace *IAS; +#endif + SSL_CTX *ssl_ctx; + SSL *ssl; + int ssl_initialized; /* CleanupAmiSSLA required on teardown */ + int sni_errno; /* storage for AmiSSL_ErrNoPtr */ +#endif }; /* --- logging ----------------------------------------------------------- */ /* Accumulating-buffer + rewrite pattern (same as ImageManager DTLog) so the log file stays coherent across OS3 and OS4 without relying on Seek. */ -static char THL_LogBuf[8192]; +static char THL_LogBuf[16384]; static ULONG THL_LogLen = 0; static void THL_Log(const char *line) @@ -87,23 +113,63 @@ static void THL_Log(const char *line) if (f) { Write(f, THL_LogBuf, THL_LogLen); Close(f); } } -/* --- low-level helpers ------------------------------------------------- */ +/* --- low-level I/O (plain TCP or TLS depending on st->use_tls) --------- */ -static LONG THL_ReadChunked(struct THL_State *st, UBYTE *out, LONG want); +static LONG THL_Recv(struct THL_State *st, APTR buf, LONG len) +{ +#ifdef HAVE_AMISSL + if (st->use_tls && st->ssl) + { + /* Inline macros in resolve AMISSL_BASE_NAME + (default: AmiSSLBase) by normal C name lookup -- shadow the + file-scope extern with our task-local base. */ + struct Library *AmiSSLBase = st->ASBase; +#if defined(__amigaos4__) + struct AmiSSLIFace *IAmiSSL = st->IAS; +#endif + int n = SSL_read(st->ssl, buf, (int)len); + return n > 0 ? n : 0; + } +#endif + { + struct Library *SocketBase = st->SBase; +#if defined(__amigaos4__) + struct SocketIFace *ISocket = st->SIFace; +#endif + LONG got = recv(st->socket, buf, len, 0); + return got > 0 ? got : 0; + } +} -static LONG THL_RecvLine(struct THL_State *st, char *buf, LONG maxlen) +static LONG THL_Send(struct THL_State *st, const APTR buf, LONG len) { - /* Shadow file-scope bases with our task-local ones for proto/socket.h. */ - struct Library *SocketBase = st->SBase; +#ifdef HAVE_AMISSL + if (st->use_tls && st->ssl) + { + struct Library *AmiSSLBase = st->ASBase; +#if defined(__amigaos4__) + struct AmiSSLIFace *IAmiSSL = st->IAS; +#endif + int n = SSL_write(st->ssl, buf, (int)len); + return n > 0 ? n : -1; + } +#endif + { + struct Library *SocketBase = st->SBase; #if defined(__amigaos4__) - struct SocketIFace *ISocket = st->SIFace; + struct SocketIFace *ISocket = st->SIFace; #endif + return send(st->socket, buf, len, 0); + } +} +static LONG THL_RecvLine(struct THL_State *st, char *buf, LONG maxlen) +{ LONG i = 0; while (i < maxlen - 1) { UBYTE c; - LONG got = recv(st->socket, (APTR)&c, 1, 0); + LONG got = THL_Recv(st, (APTR)&c, 1); if (got <= 0) return -1; if (c == '\n') { buf[i] = 0; return i; } if (c != '\r') buf[i++] = c; @@ -115,11 +181,6 @@ static LONG THL_RecvLine(struct THL_State *st, char *buf, LONG maxlen) /* Reads up to `want` bytes, unwrapping chunked transfer encoding. */ static LONG THL_ReadChunked(struct THL_State *st, UBYTE *out, LONG want) { - struct Library *SocketBase = st->SBase; -#if defined(__amigaos4__) - struct SocketIFace *ISocket = st->SIFace; -#endif - LONG produced = 0; while (produced < want) { @@ -141,7 +202,7 @@ static LONG THL_ReadChunked(struct THL_State *st, UBYTE *out, LONG want) LONG take = want - produced; if ((ULONG)take > st->chunk_left) take = (LONG)st->chunk_left; - LONG got = recv(st->socket, (APTR)(out + produced), take, 0); + LONG got = THL_Recv(st, (APTR)(out + produced), take); if (got <= 0) return produced; produced += got; st->chunk_left -= got; @@ -149,13 +210,17 @@ static LONG THL_ReadChunked(struct THL_State *st, UBYTE *out, LONG want) return produced; } -/* Parses "http://host[:port]/path" into components (buffers owned by caller). */ -static int THL_ParseHttp(CONST_STRPTR url, char *host, ULONG hlen, - char *path, ULONG plen, ULONG *portp) +/* Parses http:// or https:// URLs. Sets *is_https from scheme. */ +static int THL_ParseUrl(CONST_STRPTR url, char *host, ULONG hlen, + char *path, ULONG plen, ULONG *portp, int *is_https) { const char *p = url; - if (strncmp(p, "http://", 7) != 0) return 0; - p += 7; + ULONG default_port = 80; + *is_https = 0; + + if (strncmp(p, "https://", 8) == 0) { p += 8; default_port = 443; *is_https = 1; } + else if (strncmp(p, "http://", 7) == 0) { p += 7; } + else return 0; const char *hs = p; while (*p && *p != '/' && *p != ':') p++; @@ -165,7 +230,7 @@ static int THL_ParseHttp(CONST_STRPTR url, char *host, ULONG hlen, memcpy(host, hs, n); host[n] = 0; - ULONG port = 80; + ULONG port = default_port; if (*p == ':') { p++; @@ -182,26 +247,186 @@ static int THL_ParseHttp(CONST_STRPTR url, char *host, ULONG hlen, return 1; } -/* Opens an HTTP connection, reads and parses response headers, stashes any - body bytes that came with the header recv. Returns 1 on success. */ -static LONG THL_HttpOpen(CONST_STRPTR url, struct THL_State *st) +/* --- connection teardown (shared by error paths and redirect loop) ----- */ + +static void THL_Disconnect(struct THL_State *st) { - char logbuf[320]; +#ifdef HAVE_AMISSL + if (st->use_tls) + { + struct Library *AmiSSLBase = st->ASBase; + struct Library *AmiSSLMasterBase = st->AMSBase; +#if defined(__amigaos4__) + struct AmiSSLIFace *IAmiSSL = st->IAS; + struct AmiSSLMasterIFace *IAmiSSLMaster = st->IAMSMaster; +#endif + if (st->ssl) { SSL_shutdown(st->ssl); SSL_free(st->ssl); st->ssl = NULL; } + if (st->ssl_ctx) { SSL_CTX_free(st->ssl_ctx); st->ssl_ctx = NULL; } + if (st->ssl_initialized) { CleanupAmiSSLA(NULL); st->ssl_initialized = 0; } +#if defined(__amigaos4__) + if (st->IAS) { DropInterface((struct Interface *)st->IAS); st->IAS = NULL; } +#endif + if (st->ASBase) { CloseAmiSSL(); st->ASBase = NULL; AmiSSLBase = NULL; } +#if defined(__amigaos4__) + if (st->IAMSMaster) { DropInterface((struct Interface *)st->IAMSMaster); st->IAMSMaster = NULL; } +#endif + if (st->AMSBase) { CloseLibrary(st->AMSBase); st->AMSBase = NULL; } + (void)AmiSSLBase; (void)AmiSSLMasterBase; + } +#endif - char host[256], path[1024]; - ULONG port = 80; - if (!THL_ParseHttp(url, host, sizeof(host), path, sizeof(path), &port)) + if (st->socket >= 0 && st->SBase) { - THL_Log("http: parse URL failed"); + struct Library *SocketBase = st->SBase; +#if defined(__amigaos4__) + struct SocketIFace *ISocket = st->SIFace; +#endif + CloseSocket(st->socket); + } + st->socket = -1; + +#if defined(__amigaos4__) + if (st->SIFace) { DropInterface((struct Interface *)st->SIFace); st->SIFace = NULL; } +#endif + if (st->SBase) { CloseLibrary(st->SBase); st->SBase = NULL; } + + st->use_tls = 0; + st->chunked = 0; + st->chunk_left = 0; + if (st->buffer) { free(st->buffer); st->buffer = NULL; } + st->buflen = st->bufpos = 0; +} + +/* --- AmiSSL-wrapped connect (only linked when HAVE_AMISSL) ------------- */ + +#ifdef HAVE_AMISSL +static int THL_TlsWrap(struct THL_State *st, const char *host) +{ + char logbuf[256]; + + /* All inline SSL calls resolve their library base via normal C name + lookup. We declare these early and reassign as bases come up, so + every call below sees the right local. */ + struct Library *AmiSSLMasterBase = NULL; + struct Library *AmiSSLBase = NULL; +#if defined(__amigaos4__) + struct AmiSSLMasterIFace *IAmiSSLMaster = NULL; + struct AmiSSLIFace *IAmiSSL = NULL; + struct SocketIFace *ISocket = st->SIFace; +#endif + + st->AMSBase = OpenLibrary((STRPTR)"amisslmaster.library", AMISSLMASTER_MIN_VERSION); + if (!st->AMSBase) + { + THL_Log("https: OpenLibrary amisslmaster.library failed"); + return 0; + } + AmiSSLMasterBase = st->AMSBase; +#if defined(__amigaos4__) + st->IAMSMaster = (struct AmiSSLMasterIFace *) + GetInterface(st->AMSBase, "main", 1, NULL); + if (!st->IAMSMaster) { THL_Log("https: GetInterface IAmiSSLMaster failed"); return 0; } + IAmiSSLMaster = st->IAMSMaster; +#endif + + if (!InitAmiSSLMaster(AMISSL_CURRENT_VERSION, TRUE)) + { + THL_Log("https: InitAmiSSLMaster failed (library too old)"); + return 0; + } + + st->ASBase = OpenAmiSSL(); + if (!st->ASBase) + { + THL_Log("https: OpenAmiSSL failed"); + return 0; + } + AmiSSLBase = st->ASBase; +#if defined(__amigaos4__) + st->IAS = (struct AmiSSLIFace *)GetInterface(st->ASBase, "main", 1, NULL); + if (!st->IAS) { THL_Log("https: GetInterface IAmiSSL failed"); return 0; } + IAmiSSL = st->IAS; +#endif + + /* Use the A-suffixed variant: OS3 builds pass -DNO_INLINE_STDARG, + which disables the varargs InitAmiSSL() macro. */ + { + struct TagItem init_tags[] = { + { AmiSSL_ErrNoPtr, (ULONG)&st->sni_errno }, +#if defined(__amigaos4__) + { AmiSSL_ISocket, (ULONG)st->SIFace }, +#else + { AmiSSL_SocketBase, (ULONG)st->SBase }, +#endif + { TAG_DONE, 0 } + }; + if (InitAmiSSLA(init_tags) != 0) + { + THL_Log("https: InitAmiSSL failed"); + return 0; + } + } + st->ssl_initialized = 1; + + OPENSSL_init_ssl(OPENSSL_INIT_SSL_DEFAULT + | OPENSSL_INIT_ADD_ALL_CIPHERS + | OPENSSL_INIT_ADD_ALL_DIGESTS, NULL); + + /* Minimal entropy seeding -- enough for client handshake; see the + AmiSSL sample for a proper implementation if needed. */ + { + unsigned char seed[32]; + ULONG t = (ULONG)FindTask(NULL); + for (ULONG i = 0; i < sizeof(seed); i++) + seed[i] = (unsigned char)(t >> ((i & 3) * 8)) ^ (unsigned char)i; + RAND_seed(seed, sizeof(seed)); + } + + st->ssl_ctx = SSL_CTX_new(TLS_client_method()); + if (!st->ssl_ctx) { THL_Log("https: SSL_CTX_new failed"); return 0; } + + /* Disable peer verification for the test programs. Strict verification + needs ENVARC:AmiSSL/cacert.pem installed; relaxing lets the tests run + on a barebones setup without silently masking real TLS errors -- any + real protocol failure still shows up in SSL_connect below. */ + SSL_CTX_set_verify(st->ssl_ctx, SSL_VERIFY_NONE, NULL); + + st->ssl = SSL_new(st->ssl_ctx); + if (!st->ssl) { THL_Log("https: SSL_new failed"); return 0; } + + SSL_set_fd(st->ssl, (int)st->socket); + SSL_set_tlsext_host_name(st->ssl, host); + + int h = SSL_connect(st->ssl); + if (h <= 0) + { + int err = SSL_get_error(st->ssl, h); + snprintf(logbuf, sizeof(logbuf), "https: SSL_connect failed rc=%d err=%d", h, err); + THL_Log(logbuf); return 0; } - sprintf(logbuf, "http: host=%s port=%lu path=%s", host, port, path); + snprintf(logbuf, sizeof(logbuf), "https: handshake OK cipher=%s", SSL_get_cipher(st->ssl)); THL_Log(logbuf); + return 1; +} +#endif /* HAVE_AMISSL */ + +/* --- per-hop connect (TCP + optional TLS). Does NOT send the request. - */ + +static int THL_Connect(struct THL_State *st, const char *host, ULONG port, int use_tls) +{ + char logbuf[256]; + st->use_tls = use_tls; + +#ifndef HAVE_AMISSL + if (use_tls) + { + THL_Log("https: no AmiSSL support compiled in"); + return 0; + } +#endif - /* Open bsdsocket fresh for THIS task -- the decoder thread. On m68k the - library is opened per-task; the main task's SocketBase is not usable - from us. */ struct Library *SocketBase = OpenLibrary((STRPTR)"bsdsocket.library", 4); #if defined(__amigaos4__) struct SocketIFace *ISocket = SocketBase @@ -213,12 +438,10 @@ static LONG THL_HttpOpen(CONST_STRPTR url, struct THL_State *st) THL_Log("http: OpenLibrary bsdsocket.library failed"); return 0; } + st->SBase = SocketBase; st->SIFace = ISocket; #else - if (!SocketBase) - { - THL_Log("http: OpenLibrary bsdsocket.library failed"); - return 0; - } + if (!SocketBase) { THL_Log("http: OpenLibrary bsdsocket.library failed"); return 0; } + st->SBase = SocketBase; #endif struct hostent *he; @@ -229,25 +452,13 @@ static LONG THL_HttpOpen(CONST_STRPTR url, struct THL_State *st) #endif if (!he) { - sprintf(logbuf, "http: gethostbyname(%s) failed", host); + snprintf(logbuf, sizeof(logbuf), "http: gethostbyname(%s) failed", host); THL_Log(logbuf); -#if defined(__amigaos4__) - if (ISocket) DropInterface((struct Interface *)ISocket); -#endif - CloseLibrary(SocketBase); return 0; } - LONG s = socket(AF_INET, SOCK_STREAM, 0); - if (s < 0) - { - THL_Log("http: socket() failed"); -#if defined(__amigaos4__) - if (ISocket) DropInterface((struct Interface *)ISocket); -#endif - CloseLibrary(SocketBase); - return 0; - } + st->socket = socket(AF_INET, SOCK_STREAM, 0); + if (st->socket < 0) { THL_Log("http: socket() failed"); return 0; } struct sockaddr_in sin; memset(&sin, 0, sizeof(sin)); @@ -255,56 +466,54 @@ static LONG THL_HttpOpen(CONST_STRPTR url, struct THL_State *st) sin.sin_port = htons((UWORD)port); sin.sin_addr.s_addr = *((ULONG *)he->h_addr_list[0]); - if (connect(s, (struct sockaddr *)&sin, sizeof(sin)) < 0) + if (connect(st->socket, (struct sockaddr *)&sin, sizeof(sin)) < 0) { THL_Log("http: connect() failed"); - CloseSocket(s); -#if defined(__amigaos4__) - if (ISocket) DropInterface((struct Interface *)ISocket); -#endif - CloseLibrary(SocketBase); return 0; } +#ifdef HAVE_AMISSL + if (use_tls) + { + if (!THL_TlsWrap(st, host)) return 0; + } +#endif + return 1; +} + +/* --- build + send GET; read headers; stash any body bytes in st->buffer -- + Returns: 1 = headers OK, body follows + 0 = error / non-200 + -1 = redirect; next URL copied into redirect_out (size plen). */ +static int THL_DoRequest(struct THL_State *st, const char *host, + const char *path, char *redirect_out, ULONG rlen) +{ + char logbuf[320]; + char request[2048]; - int rlen = sprintf(request, + int rlen_s = snprintf(request, sizeof(request), "GET %s HTTP/1.1\r\n" "Host: %s\r\n" - "User-Agent: HTMLview-TestHook/1.1\r\n" + "User-Agent: HTMLview-TestHook/1.2\r\n" "Accept: */*\r\n" "Connection: close\r\n" "\r\n", path, host); - if (send(s, request, rlen, 0) < 0) + if (THL_Send(st, request, rlen_s) < 0) { THL_Log("http: send() failed"); - CloseSocket(s); -#if defined(__amigaos4__) - if (ISocket) DropInterface((struct Interface *)ISocket); -#endif - CloseLibrary(SocketBase); return 0; } - /* Read a moderate chunk so we're very likely to have all headers plus - some body in a single recv. */ UBYTE *hdr = (UBYTE *)malloc(16384); - if (!hdr) - { - CloseSocket(s); -#if defined(__amigaos4__) - if (ISocket) DropInterface((struct Interface *)ISocket); -#endif - CloseLibrary(SocketBase); - return 0; - } + if (!hdr) return 0; LONG have = 0; char *hdrend = NULL; while (have < 16384 - 1) { - LONG got = recv(s, (APTR)(hdr + have), 16384 - 1 - have, 0); + LONG got = THL_Recv(st, (APTR)(hdr + have), 16384 - 1 - have); if (got <= 0) break; have += got; hdr[have] = 0; @@ -316,88 +525,81 @@ static LONG THL_HttpOpen(CONST_STRPTR url, struct THL_State *st) { THL_Log("http: no header terminator in reply"); free(hdr); - CloseSocket(s); -#if defined(__amigaos4__) - if (ISocket) DropInterface((struct Interface *)ISocket); -#endif - CloseLibrary(SocketBase); return 0; } - /* hdrend points at the first byte of the header/body separator. - Separator is either "\r\n\r\n" (4 bytes) or "\n\n" (2 bytes). - Compute body offset BEFORE null-terminating. */ ULONG header_len = (*hdrend == '\r') ? (ULONG)(hdrend - (char *)hdr) + 4 : (ULONG)(hdrend - (char *)hdr) + 2; - - /* Null-terminate the header block so strstr on header values is safe. - Writing at hdrend clobbers only the first byte of the separator, - which we no longer need -- NOT the first body byte. */ *hdrend = 0; + /* Parse status line. */ + char status[160]; + ULONG n = 0; + while (n < sizeof(status) - 1 && n < header_len && + hdr[n] != '\r' && hdr[n] != '\n') { - /* Log the HTTP status line so we can spot 301/302 redirects, 404s, - etc. The header block also tells us whether the server returned - HTML instead of an image. */ - char status[120]; - ULONG n = 0; - while (n < sizeof(status) - 1 && n < header_len && - hdr[n] != '\r' && hdr[n] != '\n') - { - status[n] = (char)hdr[n]; - n++; - } - status[n] = 0; - sprintf(logbuf, "http: status=%s", status); - THL_Log(logbuf); + status[n] = (char)hdr[n]; + n++; + } + status[n] = 0; + snprintf(logbuf, sizeof(logbuf), "http: status=%s", status); + THL_Log(logbuf); - /* Bail on non-200 responses -- redirects, 404s and 5xx must not - reach the decoder as "image body". Parse the numeric code. */ - int code = 0; - const char *sp = strchr(status, ' '); - if (sp) - { - while (*sp == ' ') sp++; - while (*sp >= '0' && *sp <= '9') { code = code*10 + (*sp - '0'); sp++; } - } - if (code != 200) - { - sprintf(logbuf, "http: aborting (status=%d)", code); - THL_Log(logbuf); - free(hdr); - CloseSocket(s); -#if defined(__amigaos4__) - if (ISocket) DropInterface((struct Interface *)ISocket); -#endif - CloseLibrary(SocketBase); - return 0; - } + int code = 0; + const char *sp = strchr(status, ' '); + if (sp) + { + while (*sp == ' ') sp++; + while (*sp >= '0' && *sp <= '9') { code = code*10 + (*sp - '0'); sp++; } + } - /* Helper: dig out a header value by name, log as "http: =". */ - const char *hdrs[] = { "Location:", "location:", - "Content-Type:", "content-type:", - "Content-Length:", "content-length:", - "Content-Encoding:", "content-encoding:" }; - const char *tags[] = { "location", "location", - "content-type", "content-type", - "content-length", "content-length", - "content-encoding", "content-encoding" }; - for (ULONG i = 0; i < sizeof(hdrs)/sizeof(hdrs[0]); i++) - { - const char *v = strstr((char *)hdr, hdrs[i]); - if (!v) continue; - v += strlen(hdrs[i]); - while (*v == ' ' || *v == '\t') v++; - const char *e = v; - while (*e && *e != '\r' && *e != '\n') e++; - ULONG L = (ULONG)(e - v); - if (L > sizeof(status) - 1) L = sizeof(status) - 1; - memcpy(status, v, L); - status[L] = 0; - sprintf(logbuf, "http: %s=%s", tags[i], status); - THL_Log(logbuf); - } + /* Helper: extract a single named header into `out`. Case-aware: accepts + either capitalised or lowercase names (lazy match of the common + Amiga-side server responses). */ + #define GRAB_HDR(upper, lower, out, outsz) do { \ + const char *v = strstr((char *)hdr, upper); \ + if (!v) v = strstr((char *)hdr, lower); \ + if (v) { \ + v += strlen(upper); \ + while (*v == ' ' || *v == '\t') v++; \ + const char *e = v; \ + while (*e && *e != '\r' && *e != '\n') e++; \ + ULONG L = (ULONG)(e - v); \ + if (L > (ULONG)(outsz) - 1) L = (ULONG)(outsz) - 1; \ + memcpy(out, v, L); out[L] = 0; \ + } else out[0] = 0; \ + } while (0) + + char v_loc[512], v_type[128], v_len[32], v_enc[32]; + GRAB_HDR("Location:", "location:", v_loc, sizeof(v_loc)); + GRAB_HDR("Content-Type:", "content-type:", v_type, sizeof(v_type)); + GRAB_HDR("Content-Length:", "content-length:", v_len, sizeof(v_len)); + GRAB_HDR("Content-Encoding:","content-encoding:", v_enc, sizeof(v_enc)); + if (v_loc[0]) { snprintf(logbuf, sizeof(logbuf), "http: location=%s", v_loc); THL_Log(logbuf); } + if (v_type[0]) { snprintf(logbuf, sizeof(logbuf), "http: content-type=%s", v_type); THL_Log(logbuf); } + if (v_len[0]) { snprintf(logbuf, sizeof(logbuf), "http: content-length=%s", v_len); THL_Log(logbuf); } + if (v_enc[0]) { snprintf(logbuf, sizeof(logbuf), "http: content-encoding=%s", v_enc); THL_Log(logbuf); } + #undef GRAB_HDR + + /* 3xx + Location => signal caller to reconnect. We don't handle relative + redirects yet; the request will fail on next parse if Location isn't + a full URL. */ + if ((code == 301 || code == 302 || code == 303 || + code == 307 || code == 308) && v_loc[0]) + { + strncpy(redirect_out, v_loc, rlen - 1); + redirect_out[rlen - 1] = 0; + free(hdr); + return -1; + } + + if (code != 200) + { + snprintf(logbuf, sizeof(logbuf), "http: aborting (status=%d)", code); + THL_Log(logbuf); + free(hdr); + return 0; } if (strstr((char *)hdr, "Transfer-Encoding: chunked") || @@ -416,33 +618,73 @@ static LONG THL_HttpOpen(CONST_STRPTR url, struct THL_State *st) } } - sprintf(logbuf, "http: OK header_len=%lu body_have=%lu chunked=%d", - header_len, body_have, st->chunked); + snprintf(logbuf, sizeof(logbuf), "http: OK header_len=%lu body_have=%lu chunked=%d tls=%d", + header_len, body_have, st->chunked, st->use_tls); THL_Log(logbuf); - /* Log the first 16 body bytes in hex -- lets us see the magic number - of the actual payload so we can tell PNG / GIF / HTML / gzip apart - at a glance. */ if (body_have > 0) { UBYTE *bp = hdr + header_len; ULONG show = body_have < 16 ? body_have : 16; - char hex[80]; + char hex[96]; int off = sprintf(hex, "http: body[0..%lu]=", show); for (ULONG i = 0; i < show; i++) off += sprintf(hex + off, "%02x ", bp[i]); THL_Log(hex); } - st->socket = s; - st->SBase = SocketBase; -#if defined(__amigaos4__) - st->SIFace = ISocket; -#endif free(hdr); return 1; } +/* Entry point: opens a connection and reads headers, following up to 5 + redirects across http / https. Returns 1 on success, 0 on any error. */ +static LONG THL_HttpOpen(CONST_STRPTR url, struct THL_State *st) +{ + char logbuf[384]; + char current[1024]; + strncpy(current, url, sizeof(current) - 1); + current[sizeof(current) - 1] = 0; + + for (int hop = 0; hop < 5; hop++) + { + char host[256], path[1024]; + ULONG port; + int is_https; + if (!THL_ParseUrl(current, host, sizeof(host), + path, sizeof(path), &port, &is_https)) + { + snprintf(logbuf, sizeof(logbuf), "http: unparseable URL '%s'", current); + THL_Log(logbuf); + return 0; + } + + snprintf(logbuf, sizeof(logbuf), "http: [hop %d] %s://%s:%lu%s", + hop, is_https ? "https" : "http", host, port, path); + THL_Log(logbuf); + + if (!THL_Connect(st, host, port, is_https)) + { + THL_Disconnect(st); + return 0; + } + + char next[1024]; + int r = THL_DoRequest(st, host, path, next, sizeof(next)); + if (r == 1) return 1; + if (r == 0) { THL_Disconnect(st); return 0; } + + /* 3xx -- tear down and try the new URL. */ + THL_Disconnect(st); + strncpy(current, next, sizeof(current) - 1); + current[sizeof(current) - 1] = 0; + } + + THL_Log("http: too many redirects"); + THL_Disconnect(st); + return 0; +} + /* --- the hook function ------------------------------------------------- */ static ULONG TestImageHookFunc(struct Hook *hook, APTR obj, @@ -464,13 +706,8 @@ static ULONG TestImageHookFunc(struct Hook *hook, APTR obj, st->socket = -1; msg->lm_Userdata = st; - if (strncmp(url, "https://", 8) == 0) - { - THL_Log("hook: https:// unsupported"); - free(st); msg->lm_Userdata = NULL; return 0; - } - - if (strncmp(url, "http://", 7) == 0) + if (strncmp(url, "http://", 7) == 0 || + strncmp(url, "https://", 8) == 0) { if (!THL_HttpOpen(url, st)) { @@ -520,17 +757,15 @@ static ULONG TestImageHookFunc(struct Hook *hook, APTR obj, if (st->socket >= 0) { - struct Library *SocketBase = st->SBase; -#if defined(__amigaos4__) - struct SocketIFace *ISocket = st->SIFace; -#endif LONG rd = st->chunked ? THL_ReadChunked(st, (UBYTE *)out, len) - : recv(st->socket, out, len, 0); + : THL_Recv(st, out, len); { char logbuf[96]; - sprintf(logbuf, "read: recv(sock=%ld want=%ld) => %ld%s", - st->socket, len, rd, st->chunked ? " (chunked)" : ""); + snprintf(logbuf, sizeof(logbuf), "read: recv(sock=%ld want=%ld) => %ld%s%s", + st->socket, len, rd, + st->chunked ? " (chunked)" : "", + st->use_tls ? " (tls)" : ""); THL_Log(logbuf); } return rd > 0 ? rd : 0; @@ -548,23 +783,7 @@ static ULONG TestImageHookFunc(struct Hook *hook, APTR obj, if (st) { if (st->file) Close(st->file); - - if (st->socket >= 0 && st->SBase) - { - struct Library *SocketBase = st->SBase; -#if defined(__amigaos4__) - struct SocketIFace *ISocket = st->SIFace; -#endif - CloseSocket(st->socket); - } - -#if defined(__amigaos4__) - if (st->SIFace) - DropInterface((struct Interface *)st->SIFace); -#endif - if (st->SBase) CloseLibrary(st->SBase); - - if (st->buffer) free(st->buffer); + THL_Disconnect(st); free(st); msg->lm_Userdata = NULL; }