This commit is contained in:
Bob-IT 2025-02-10 08:47:30 +08:00 committed by GitHub
commit 7d2de0c95b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 2063 additions and 75 deletions

View File

@ -127,7 +127,6 @@ typedef struct GncPluginPageReportPrivate
/// the gnc_html abstraction this PluginPage contains /// the gnc_html abstraction this PluginPage contains
// gnc_html *html; // gnc_html *html;
GncHtml *html; GncHtml *html;
gboolean webkit2;
/// the container the above HTML widget is in. /// the container the above HTML widget is in.
GtkContainer *container; GtkContainer *container;
@ -519,11 +518,6 @@ gnc_plugin_page_report_create_widget( GncPluginPage *page )
report = GNC_PLUGIN_PAGE_REPORT(page); report = GNC_PLUGIN_PAGE_REPORT(page);
priv = GNC_PLUGIN_PAGE_REPORT_GET_PRIVATE(report); priv = GNC_PLUGIN_PAGE_REPORT_GET_PRIVATE(report);
#ifndef WEBKIT1
/* Hide the ExportPdf action for Webkit2 */
priv->webkit2 = TRUE;
#endif
topLvl = gnc_ui_get_main_window (nullptr); topLvl = gnc_ui_get_main_window (nullptr);
// priv->html = gnc_html_new( topLvl ); // priv->html = gnc_html_new( topLvl );
priv->html = gnc_html_factory_create_html(); priv->html = gnc_html_factory_create_html();
@ -1249,8 +1243,6 @@ gnc_plugin_page_report_menu_update (GncPluginPage *plugin_page,
static void static void
gnc_plugin_page_report_menu_updates (GncPluginPage *plugin_page) gnc_plugin_page_report_menu_updates (GncPluginPage *plugin_page)
{ {
GncPluginPageReportPrivate *priv;
GncPluginPageReport *report;
GncMainWindow *window; GncMainWindow *window;
action_toolbar_labels tooltip_list[3]; action_toolbar_labels tooltip_list[3];
GAction *action; GAction *action;
@ -1263,9 +1255,6 @@ gnc_plugin_page_report_menu_updates (GncPluginPage *plugin_page)
_("Add the current report's configuration to the 'Reports->Saved Report Configurations' menu. " _("Add the current report's configuration to the 'Reports->Saved Report Configurations' menu. "
"The report configuration will be saved in the file %s."), saved_reports_path); "The report configuration will be saved in the file %s."), saved_reports_path);
report = GNC_PLUGIN_PAGE_REPORT(plugin_page);
priv = GNC_PLUGIN_PAGE_REPORT_GET_PRIVATE(report);
window = (GncMainWindow*)gnc_plugin_page_get_window (GNC_PLUGIN_PAGE(plugin_page)); window = (GncMainWindow*)gnc_plugin_page_get_window (GNC_PLUGIN_PAGE(plugin_page));
tooltip_list[0] = { "ReportSaveAction", N_("Save _Report Configuration"), report_save_str }; tooltip_list[0] = { "ReportSaveAction", N_("Save _Report Configuration"), report_save_str };
@ -1278,11 +1267,6 @@ gnc_plugin_page_report_menu_updates (GncPluginPage *plugin_page)
action = gnc_main_window_find_action (window, "FilePrintAction"); action = gnc_main_window_find_action (window, "FilePrintAction");
g_simple_action_set_enabled (G_SIMPLE_ACTION(action), true); g_simple_action_set_enabled (G_SIMPLE_ACTION(action), true);
if (priv->webkit2)
{
GtkWidget *pdf_item = gnc_main_window_menu_find_menu_item (window, "FilePrintPDFAction");
gtk_widget_hide (pdf_item);
}
g_free (saved_reports_path); g_free (saved_reports_path);
g_free (report_save_str); g_free (report_save_str);
g_free (report_saveas_str); g_free (report_saveas_str);
@ -1299,7 +1283,6 @@ gnc_plugin_page_report_constr_init (GncPluginPageReport *plugin_page, gint repor
DEBUG("property reportId=%d", reportId); DEBUG("property reportId=%d", reportId);
priv = GNC_PLUGIN_PAGE_REPORT_GET_PRIVATE(plugin_page); priv = GNC_PLUGIN_PAGE_REPORT_GET_PRIVATE(plugin_page);
priv->reportId = reportId; priv->reportId = reportId;
priv->webkit2 = FALSE;
gnc_plugin_page_report_setup( GNC_PLUGIN_PAGE(plugin_page)); gnc_plugin_page_report_setup( GNC_PLUGIN_PAGE(plugin_page));
@ -1991,11 +1974,7 @@ gnc_plugin_page_report_print_cb (GSimpleAction *simple,
//g_warning("Setting job name=%s", job_name); //g_warning("Setting job name=%s", job_name);
#ifdef WEBKIT1
gnc_html_print (priv->html, job_name, FALSE);
#else
gnc_html_print (priv->html, job_name); gnc_html_print (priv->html, job_name);
#endif
g_free (job_name); g_free (job_name);
} }
@ -2037,11 +2016,7 @@ gnc_plugin_page_report_exportpdf_cb (GSimpleAction *simple,
//g_warning("Setting job name=%s", job_name); //g_warning("Setting job name=%s", job_name);
#ifdef WEBKIT1
gnc_html_print (priv->html, job_name, TRUE);
#else
gnc_html_print (priv->html, job_name); gnc_html_print (priv->html, job_name);
#endif
if (owner) if (owner)
{ {

View File

@ -5,8 +5,10 @@ set (html_HEADERS
gnc-html-p.h gnc-html-p.h
gnc-html-factory.h gnc-html-factory.h
gnc-html-extras.h gnc-html-extras.h
gnc-html-webkit-p.h gnc-html-browser-p.h
gnc-html-webkit.h gnc-html-browser.h
gnc-ws-server.h
gnc-ws-protocol.h
) )
# Command to generate the swig-gnc-html.c wrapper file # Command to generate the swig-gnc-html.c wrapper file
@ -19,20 +21,11 @@ set (html_SOURCES
gnc-html.c gnc-html.c
gnc-html-history.c gnc-html-history.c
gnc-html-factory.c gnc-html-factory.c
gnc-html-browser.c
gnc-ws-server.c
gnc-ws-protocol.c
) )
if (WEBKIT1)
list(APPEND html_HEADERS gnc-html-webkit1.h)
list(APPEND html_SOURCES gnc-html-webkit1.c)
set(html_EXTRA_DIST gnc-html-webkit2.h gnc-html-webkit2.c)
else ()
list(APPEND html_HEADERS gnc-html-webkit2.h)
list(APPEND html_SOURCES gnc-html-webkit2.c)
set(html_EXTRA_DIST gnc-html-webkit1.h gnc-html-webkit1.c)
endif()
set (gnc_html_SCHEME html.scm) set (gnc_html_SCHEME html.scm)
set(GUILE_OUTPUT_DIR gnucash) set(GUILE_OUTPUT_DIR gnucash)
@ -57,7 +50,6 @@ target_link_libraries(gnc-html
gnc-engine gnc-engine
gnc-gnome-utils gnc-gnome-utils
PkgConfig::GTK3 PkgConfig::GTK3
PkgConfig::WEBKIT
${GUILE_LDFLAGS}) ${GUILE_LDFLAGS})
target_compile_definitions(gnc-html PRIVATE -DG_LOG_DOMAIN=\"gnc.html\") target_compile_definitions(gnc-html PRIVATE -DG_LOG_DOMAIN=\"gnc.html\")

View File

@ -0,0 +1,40 @@
/********************************************************************
* gnc-html-browser-p.h -- display html with gnc special tags *
* Copyright (C) 2024 Bob Fewell *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
\********************************************************************/
#ifndef GNC_HTML_BROWSER_P_H
#define GNC_HTML_BROWSER_P_H
#include "gnc-html-p.h"
struct _GncHtmlBrowserPrivate
{
struct _GncHtmlPrivate base;
GtkWidget *web_view;
gchar* html_string; /* html string being displayed */
GncWsServer *gws;
const gchar *file_name;
};
#endif

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,64 @@
/********************************************************************
* gnc-html-browser.h -- display html with gnc special tags *
* Copyright (C) 2024 Bob Fewell *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
\********************************************************************/
#ifndef GNC_HTML_BROWSER_H
#define GNC_HTML_BROWSER_H
#include <glib-object.h>
#include "gnc-html.h"
G_BEGIN_DECLS
#define GNC_TYPE_HTML_BROWSER (gnc_html_browser_get_type())
#define GNC_HTML_BROWSER(o) (G_TYPE_CHECK_INSTANCE_CAST ((o), GNC_TYPE_HTML_BROWSER, GncHtmlBrowser))
#define GNC_HTML_BROWSER_CLASS(k) (G_TYPE_CHECK_CLASS_CAST((k), GNC_TYPE_HTML_BROWSER, GncHtmlBrowserClass))
#define GNC_IS_HTML_BROWSER(o) (G_TYPE_CHECK_INSTANCE_TYPE((o), GNC_TYPE_HTML_BROWSER))
#define GNC_IS_HTML_BROWSER_CLASS(k) (G_TYPE_CHECK_CLASS_TYPE((k), GNC_TYPE_HTML_BROWSER))
#define GNC_HTML_BROWSER_GET_CLASS(o) (G_TYPE_INSTANCE_GET_CLASS((o), GNC_TYPE_HTML_BROWSER, GncHtmlBrowserClass))
typedef struct _GncHtmlBrowser GncHtmlBrowser;
typedef struct _GncHtmlBrowserClass GncHtmlBrowserClass;
typedef struct _GncHtmlBrowserPrivate GncHtmlBrowserPrivate;
/** Key for saving the PDF-export directory in the print settings */
#define GNC_GTK_PRINT_SETTINGS_EXPORT_DIR "gnc-pdf-export-directory"
struct _GncHtmlBrowser
{
GncHtml parent_instance;
/*< private >*/
GncHtmlBrowserPrivate* priv;
};
struct _GncHtmlBrowserClass
{
GncHtmlClass parent_class;
};
GType gnc_html_browser_get_type (void);
GncHtml* gnc_html_browser_new (void);
G_END_DECLS
#endif

View File

@ -26,7 +26,7 @@
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include "gnc-html.h" #include "gnc-html.h"
#include "gnc-html-webkit.h" #include "gnc-html-browser.h"
#include "qoflog.h" #include "qoflog.h"
#include "gnc-engine.h" #include "gnc-engine.h"
@ -35,13 +35,13 @@
/* indicates the debugging module that this .o belongs to. */ /* indicates the debugging module that this .o belongs to. */
G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_HTML; G_GNUC_UNUSED static QofLogModule log_module = GNC_MOD_HTML;
GncHtml* gnc_html_factory_create_html( void ) GncHtml* gnc_html_factory_create_html (void)
{ {
return gnc_html_webkit_new(); return gnc_html_browser_new ();
} }
gboolean gboolean
gnc_html_engine_supports_css( void ) gnc_html_engine_supports_css (void)
{ {
return TRUE; return TRUE;
} }

View File

@ -528,13 +528,9 @@ gnc_html_export_to_file( GncHtml* self, const gchar* filepath )
return FALSE; return FALSE;
} }
} }
#ifdef WEBKIT1
void
gnc_html_print (GncHtml* self, const char *jobname, gboolean export_pdf)
#else
void void
gnc_html_print (GncHtml* self, const char *jobname) gnc_html_print (GncHtml* self, const char *jobname)
#endif
{ {
g_return_if_fail( self != NULL ); g_return_if_fail( self != NULL );
g_return_if_fail( jobname != NULL ); g_return_if_fail( jobname != NULL );
@ -542,11 +538,7 @@ gnc_html_print (GncHtml* self, const char *jobname)
if ( GNC_HTML_GET_CLASS(self)->print != NULL ) if ( GNC_HTML_GET_CLASS(self)->print != NULL )
{ {
#ifdef WEBKIT1
GNC_HTML_GET_CLASS(self)->print (self, jobname, export_pdf);
#else
GNC_HTML_GET_CLASS(self)->print (self, jobname); GNC_HTML_GET_CLASS(self)->print (self, jobname);
#endif
} }
else else
{ {
@ -589,17 +581,13 @@ gnc_html_get_webview( GncHtml* self )
if (sw_list) // the scroll window has only one child if (sw_list) // the scroll window has only one child
{ {
#ifdef WEBKIT1
webview = sw_list->data;
#else
GList *vp_list = gtk_container_get_children (GTK_CONTAINER(sw_list->data)); GList *vp_list = gtk_container_get_children (GTK_CONTAINER(sw_list->data));
if (vp_list) // the viewport has only one child if (vp_list) // the viewport has only one child
{ {
webview = vp_list->data; webview = vp_list->data;
g_list_free (vp_list); g_list_free (vp_list);
} }
#endif
} }
g_list_free (sw_list); g_list_free (sw_list);
return webview; return webview;

View File

@ -138,11 +138,7 @@ struct _GncHtmlClass
void (*reload)( GncHtml* html, gboolean force_rebuild ); void (*reload)( GncHtml* html, gboolean force_rebuild );
void (*copy_to_clipboard)( GncHtml* html ); void (*copy_to_clipboard)( GncHtml* html );
gboolean (*export_to_file)( GncHtml* html, const gchar* file ); gboolean (*export_to_file)( GncHtml* html, const gchar* file );
#ifdef WEBKIT1
void (*print) (GncHtml* html, const gchar* jobname, gboolean export_pdf);
#else
void (*print) (GncHtml* html, const gchar* jobname); void (*print) (GncHtml* html, const gchar* jobname);
#endif
void (*cancel)( GncHtml* html ); void (*cancel)( GncHtml* html );
URLType (*parse_url)( GncHtml* html, const gchar* url, URLType (*parse_url)( GncHtml* html, const gchar* url,
gchar** url_location, gchar** url_label ); gchar** url_location, gchar** url_label );
@ -203,25 +199,13 @@ void gnc_html_copy_to_clipboard( GncHtml* html );
*/ */
gboolean gnc_html_export_to_file( GncHtml* html, const gchar* filename ); gboolean gnc_html_export_to_file( GncHtml* html, const gchar* filename );
#ifdef WEBKIT1
/**
* Prints the report.
*
* @param html GncHtml object
* @param jobname A jobname for identifying the print job or to provide
* an output filename.
* @param export_pdf If TRUE write a PDF file using the jobname for a
* filename; otherwise put up a print dialog.
*/
void gnc_html_print (GncHtml* html, const char* jobname, gboolean export_pdf);
#else
/** /**
* Prints the report. * Prints the report.
* *
* @param html GncHtml object * @param html GncHtml object
*/ */
void gnc_html_print (GncHtml* html, const char* jobname); void gnc_html_print (GncHtml* html, const char* jobname);
#endif
/** /**
* Cancels the current operation * Cancels the current operation
* *

View File

@ -0,0 +1,338 @@
/********************************************************************
* gnc-ws-protocol.c -- basic websocket server protocol *
* Copyright (C) 2024 Bob Fewell *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
********************************************************************/
#include <config.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include "gnc-ws-protocol.h"
#define WS_KEY "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
#define WS_HS_ACCEPT "HTTP/1.1 101 Switching Protocols\r\n"\
"Upgrade: websocket\r\n"\
"Connection: Upgrade\r\n"\
"Sec-WebSocket-Accept: "
#ifdef skip
static void
print_hex (const gchar *s)
{
while (*s)
g_printf("%02x", (unsigned int) *s++);
g_printf("\n");
}
#endif
#ifdef skip
static void
print_bytes (GBytes *bytes_in)
{
gsize length;
const guint8 *array = g_bytes_get_data (bytes_in, &length);
GString *printable;
guint i = 0;
printable = g_string_new ("[");
while (i < length)
{
if (i > 0)
g_string_append_c (printable, ' ');
g_string_append_printf (printable, "%02x", array[i++]);
}
g_string_append_c (printable, ']');
gchar *test = g_string_free (printable, FALSE);
g_print("Print bytes: '%s' %" G_GUINT64_FORMAT "\n", test, length);
g_free (test);
}
#endif
static gchar*
find_header_text (gchar **header, const gchar *find_text)
{
gchar *ret_text = NULL;
guint parts = g_strv_length (header);
for (guint i = 0; i < parts; i++)
{
if (g_strstr_len (header[i], -1, find_text))
{
ret_text = g_strdup (header[i]);
break;
}
}
return ret_text;
}
#define BUFFER_LEN 200 // only dealing with max message of 125
static GBytes *
make_frame (guint8 opcode, const gchar *message)
{
guint8 first_byte = (0x80 | opcode);
guint8 msg_len = strlen (message);
if (msg_len > 125)
{
g_print("## Not supporting messages lonmger than 125 characters ##\n");
return NULL;
}
guint8 header[2];
header[0] = first_byte;
header[1] = msg_len;
static guint8 array_buf[BUFFER_LEN];
memset (array_buf, 0, BUFFER_LEN);
memcpy (array_buf, header, 2);
memcpy (array_buf + 2, message, msg_len);
GBytes *test = g_bytes_new (array_buf, msg_len + 2);
return test;
}
GBytes *
gnc_ws_send_message (const gchar *message)
{
return make_frame (OPCODE_TEXT, message);
}
static gboolean
parse_frame (GBytes *bytes_in, gint *opcode, GBytes **bytes_out)
{
gsize length;
const guint8 *array = g_bytes_get_data (bytes_in, &length);
const guint8 first_byte = array[0];
const guint8 second_byte = array[1];
gboolean fin = (first_byte >> 7) & 1;
gboolean rsv1 = (first_byte >> 6) & 1;
gboolean rsv2 = (first_byte >> 5) & 1;
gboolean rsv3 = (first_byte >> 4) & 1;
*opcode = first_byte & 0xf;
if (rsv1 || rsv2 || rsv3)
{
g_print("WebSocketError: Received frame with non-zero reserved bits\n");
return FALSE;
}
if (fin == 0 && *opcode == OPCODE_CONTINUATION)
{
g_print("WebSocketError: Received new fragment frame with non-zero opcode\n");
return FALSE;
}
gboolean has_mask = (second_byte >> 7) & 1;
guint8 payload_len = second_byte & 0x7f;
gint offset = 2;
if ((*opcode > 0x7) && (payload_len > 125))
{
g_print("WebSocketError: Control frame payload cannot be larger than 125 bytes\n");
return FALSE;
}
if (payload_len > 125)
{
g_print("## Payload length greater than 125 bytes, not suported ##\n");
return FALSE;
}
if (has_mask)
{
guint8 masks[4];
gint i = 0;
masks[0] = array[offset];
masks[1] = array[offset + 1];
masks[2] = array[offset + 2];
masks[3] = array[offset + 3];
offset = offset + 4;
guint8 mypayload[length];
while ((i + offset) < length)
{
guint8 data = array[i + offset] ^ masks[i % 4];
mypayload[i] = data;
i++;
}
*bytes_out = g_bytes_new (mypayload, length - offset);
}
else
*bytes_out = g_bytes_new_from_bytes (bytes_in, offset, length - offset);
return TRUE;
}
gchar *
gnc_ws_parse_bytes_in (GBytes *bytes_in, gint *opcode_out)
{
GBytes *bytes_out = NULL;
gint opcode;
gboolean ok = FALSE;
ok = parse_frame (bytes_in, &opcode, &bytes_out);
if (!ok)
{
g_print("## Parsing bytes in failed ##\n");
return NULL;
}
*opcode_out = opcode;
if (opcode == OPCODE_TEXT)
{
gsize length;
const guint8 *array = g_bytes_get_data (bytes_out, &length);
GString *data_out = g_string_new (NULL);
gint i = 0;
while (i < length)
{
guint8 data = array[i];
g_string_append_printf (data_out, "%c", data);
i++;
}
return g_string_free (data_out, FALSE);
}
return NULL;
}
gchar *
gnc_ws_make_handshake (const gchar *message, gchar **id)
{
gchar **message_split = g_strsplit (message, "\r\n", -1);
gchar *message_out = NULL;
gchar *key = NULL;
gchar *message_test = find_header_text (message_split, "GET");
if (!message_test)
{
g_print(" Error: No Get\n");
return NULL;
}
else
{
gchar *ptr = g_strrstr (message_test, " ");
if (ptr)
*id = g_utf8_substring (message_test, 5, ptr - message_test);
}
g_free (message_test);
message_test = find_header_text (message_split, "Upgrade");
if (!message_test)
{
g_print(" Error: No upgrade hdr\n");
return NULL;
}
g_free (message_test);
message_test = find_header_text (message_split, "Sec-WebSocket-Version");
if (message_test)
{
if (!g_strstr_len (message_test , -1, "13"))
{
g_print(" Error: Unsupported version\n");
g_free (message_test);
return NULL;
}
}
else
{
g_print(" Error: Did not find websocket version\n");
return NULL;
}
g_free (message_test);
message_test = find_header_text (message_split, "Sec-WebSocket-Key");
if (message_test)
{
gchar *ptr = g_strstr_len (message_test , -1, ":");
if (ptr)
{
key = g_strstrip (g_strdup (ptr + 1));
gsize out_len;
guchar *decoded = g_base64_decode (key, &out_len);
if (out_len != 16)
{
g_print(" Error: Key wrong length\n");
g_free (message_test);
return NULL;
}
g_free (decoded);
}
else
{
g_print(" Error: Key not found\n");
g_free (message_test);
return NULL;
}
}
else
{
g_print(" Error: Key not found\n");
g_free (message_test);
return NULL;
}
g_free (message_test);
guint8 buf[100];
gsize bsize = 100;
GChecksum *cs = g_checksum_new (G_CHECKSUM_SHA1);
gchar *digest = g_strconcat (key, WS_KEY, NULL);
g_checksum_update (cs, (guint8*)digest, -1);
g_checksum_get_digest (cs, buf, &bsize);
gchar *str = g_base64_encode (buf, bsize);
message_out = g_strconcat (WS_HS_ACCEPT, str, "\r\n\r\n", NULL);
g_checksum_free (cs);
g_strfreev (message_split);
g_free (key);
return message_out;
}

View File

@ -0,0 +1,45 @@
/********************************************************************
* gnc-ws-protocol.h -- basic websocket server protocol *
* Copyright (C) 2024 Bob Fewell *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
********************************************************************/
#ifndef WS_PROTOCOL_H
#define WS_PROTOCOL_H
#include <config.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#define OPCODE_CONTINUATION 0x0
#define OPCODE_TEXT 0x1
#define OPCODE_BINARY 0x2
#define OPCODE_CLOSE 0x8
#define OPCODE_PING 0x9
#define OPCODE_PONG 0xa
gchar *gnc_ws_make_handshake (const gchar *message, gchar **id);
gchar *gnc_ws_parse_bytes_in (GBytes *bytes_in, gint *opcode);
GBytes *gnc_ws_send_message (const gchar *message);
#endif

View File

@ -0,0 +1,399 @@
/********************************************************************
* gnc-ws-server.c -- basic websocket server *
* Copyright (C) 2024 Bob Fewell *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
********************************************************************/
#include <config.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <gio/gio.h>
#include "gnc-ws-server.h"
#include "gnc-ws-protocol.h"
#define GNC_WS_SERVER_PATH "gnc-ws-server-path"
G_DEFINE_TYPE (GncWsServer, gnc_ws_server, G_TYPE_OBJECT)
#define PORT 8080
#define BLOCK_SIZE 1024
static GncWsServer *gws = NULL;
/* Signal codes */
enum
{
OPEN,
MESSAGE,
CLOSE,
LAST_SIGNAL
};
static guint ws_server_signals [LAST_SIGNAL] = { 0 };
static void
gnc_ws_server_init (GncWsServer *self)
{
g_print("%s called\n",__FUNCTION__);
}
static void
gnc_ws_server_dispose (GObject *object)
{
g_print("%s called\n",__FUNCTION__);
g_return_if_fail (object != NULL);
GncWsServer *gws = (GncWsServer*)object;
g_print("ws service active %d\n", g_socket_service_is_active (gws->service));
if (gws->incoming_id != 0)
g_signal_handler_disconnect (G_OBJECT(gws->service), gws->incoming_id);
gws->incoming_id = 0;
g_socket_service_stop (gws->service);
g_print(" ws service active %d\n", g_socket_service_is_active (gws->service));
G_OBJECT_CLASS(gnc_ws_server_parent_class)->dispose (object);
}
static void
gnc_ws_server_finalize (GObject *object)
{
g_print("%s called\n",__FUNCTION__);
gws = NULL;
G_OBJECT_CLASS(gnc_ws_server_parent_class)->finalize (object);
}
static void
gnc_ws_server_class_init (GncWsServerClass *klass)
{
GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
g_print("%s called\n",__FUNCTION__);
gobject_class->dispose = gnc_ws_server_dispose;
gobject_class->finalize = gnc_ws_server_finalize;
ws_server_signals [OPEN] =
g_signal_new ("open",
G_OBJECT_CLASS_TYPE(gobject_class),
G_SIGNAL_RUN_FIRST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_STRING);
ws_server_signals [MESSAGE] =
g_signal_new ("message",
G_OBJECT_CLASS_TYPE(gobject_class),
G_SIGNAL_RUN_FIRST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
2,
G_TYPE_STRING,
G_TYPE_STRING);
ws_server_signals [CLOSE] =
g_signal_new ("close",
G_OBJECT_CLASS_TYPE(gobject_class),
G_SIGNAL_RUN_FIRST,
0,
NULL,
NULL,
NULL,
G_TYPE_NONE,
1,
G_TYPE_STRING);
}
static gboolean
send_message_bytes (GSocketConnection *connection, GBytes *bytes)
{
gboolean ret = TRUE;
GError *error = NULL;
GOutputStream *ostream = g_io_stream_get_output_stream (G_IO_STREAM(connection));
gssize data_out = g_output_stream_write_bytes (ostream, bytes, NULL, &error);
#ifdef G_OS_WIN32
g_print("%s called, connection %p, data out sent %d\n",__FUNCTION__, connection, data_out);
#else
g_print("%s called, connection %p, data out sent %ld\n",__FUNCTION__, connection, data_out);
#endif
if (error != NULL)
{
g_error ("%s", error->message);
g_print("send error '%s'\n", error->message);
g_clear_error (&error);
ret = FALSE;
}
return ret;
}
struct ConnHashData
{
GSocketConnection *connection;
const gchar *id;
};
static gboolean
connection_hash_table_find (gpointer key, gpointer value, gpointer user_data)
{
struct ConnHashData *data = user_data;
if (g_str_has_suffix (data->id, value))
{
data->connection = key;
return TRUE;
}
return FALSE;
}
void
gnc_ws_server_send_message (GncWsServer *gws, const gchar *id, const gchar *message)
{
struct ConnHashData *data = g_new (struct ConnHashData, 1);
data->id = id;
data->connection = NULL;
g_print("%s called, id '%s', message '%s'\n",__FUNCTION__, id, message);
g_hash_table_foreach (gws->connections_hash,
(GHFunc)connection_hash_table_find,
data);
if (data->connection)
{
GBytes *bytes = gnc_ws_send_message (message);
send_message_bytes (data->connection, bytes);
g_bytes_unref (bytes);
}
g_free (data);
}
static gboolean
send_handshake_message (GSocketConnection *connection, const gchar *message)
{
gboolean ret = TRUE;
GError *error = NULL;
GOutputStream *ostream = g_io_stream_get_output_stream (G_IO_STREAM(connection));
#ifdef G_OS_WIN32
g_print("%s called, connection %p, msg len %d\n",__FUNCTION__, connection, strlen (message));
#else
g_print("%s called, connection %p, msg len %ld\n",__FUNCTION__, connection, strlen (message));
#endif
g_output_stream_write (ostream,
message,
strlen (message),
NULL,
&error);
if (error != NULL)
{
g_error ("%s", error->message);
g_print("send error '%s'\n", error->message);
g_clear_error (&error);
ret = FALSE;
}
return ret;
}
struct ConnData
{
GncWsServer *gws;
GSocketConnection *connection;
char message[BLOCK_SIZE];
};
static void
bytes_ready_cb (GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
GInputStream *istream = G_INPUT_STREAM(source_object);
GError *error = NULL;
struct ConnData *data = user_data;
GncWsServer *gws = data->gws;
GBytes *bytes_in = g_input_stream_read_bytes_finish (istream, res, &error);
g_print("%s called, istream %p\n",__FUNCTION__, istream);
if (error != NULL)
{
g_error ("%s", error->message);
g_print("bytes service error is: %s\n", error->message);
g_clear_error (&error);
return;
}
if (g_bytes_get_size (bytes_in) != 0)
{
gint opcode;
gchar *message = gnc_ws_parse_bytes_in (bytes_in, &opcode);
if ((opcode == OPCODE_TEXT) && message)
{
gchar *id = g_hash_table_lookup (gws->connections_hash, data->connection);
if (id)
g_signal_emit_by_name (data->gws, "message", id, message);
else
g_print("Error: Lookup Error\n");
}
g_free (message);
g_bytes_unref (bytes_in);
if (opcode == OPCODE_CLOSE)
{
gchar *id = g_hash_table_lookup (gws->connections_hash, data->connection);
if (id)
g_signal_emit_by_name (data->gws, "close", id);
else
g_print("Error: Lookup Error\n");
g_hash_table_remove (gws->connections_hash, data->connection);
return;
}
g_input_stream_read_bytes_async (istream, 8192, G_PRIORITY_DEFAULT,
NULL, bytes_ready_cb, user_data);
}
}
static void
message_ready_cb (GObject *source_object,
GAsyncResult *res,
gpointer user_data)
{
GInputStream *istream = G_INPUT_STREAM(source_object);
GError *error = NULL;
struct ConnData *data = user_data;
int count;
count = g_input_stream_read_finish (istream, res, &error);
g_print("%s called, istream %p, count %d\n",__FUNCTION__, istream, count);
if (count == -1)
{
g_print ("Error: receiving message\n");
if (error != NULL)
{
g_print("Error: incoming stream error: %s\n", error->message);
g_clear_error (&error);
return;
}
}
gchar *id = NULL;
gchar *handshake_message = gnc_ws_make_handshake (data->message, &id);
if (handshake_message)
{
if (send_handshake_message (data->connection, handshake_message))
g_input_stream_read_bytes_async (istream, 8192, G_PRIORITY_DEFAULT,
NULL, bytes_ready_cb, user_data);
}
if (id)
{
g_hash_table_insert (gws->connections_hash, data->connection, g_strdup(id));
g_signal_emit_by_name (data->gws, "open", id);
}
g_free (handshake_message);
g_free (id);
// g_object_unref (G_SOCKET_CONNECTION(data->connection));
// g_free (data);
}
static gboolean
incoming_callback (GSocketService *service,
GSocketConnection *connection,
GObject *source_object,
gpointer user_data)
{
GncWsServer *gws = user_data;
GInputStream *istream = g_io_stream_get_input_stream (G_IO_STREAM(connection));
struct ConnData *data = g_new (struct ConnData, 1);
g_print("%s called, connection %p\n",__FUNCTION__, connection);
data->connection = g_object_ref (connection);
data->gws = gws;
g_input_stream_read_async (istream, data->message, sizeof (data->message),
G_PRIORITY_DEFAULT, NULL, message_ready_cb, data);
return FALSE;
}
GncWsServer *
gnc_ws_server_new (void)
{
g_print("%s called\n",__FUNCTION__);
if (gws)
return gws;
gws = g_object_new (GNC_TYPE_WS_SERVER, NULL);
g_print(" ws gws %p\n", gws);
gws->service = g_socket_service_new ();
GError *error = NULL;
gboolean ret = g_socket_listener_add_inet_port (G_SOCKET_LISTENER(gws->service),
PORT, NULL, &error);
if (ret && error != NULL)
{
g_error ("%s", error->message);
g_print("service error is: %s\n", error->message);
g_clear_error (&error);
return NULL;
}
gws->connections_hash = g_hash_table_new_full (g_direct_hash, g_direct_equal,
NULL, g_free);
g_socket_service_start (gws->service);
gws->incoming_id = g_signal_connect (gws->service, "incoming",
G_CALLBACK(incoming_callback), gws);
g_print(" ws service active %d\n", g_socket_service_is_active (gws->service));
return gws;
}

View File

@ -0,0 +1,57 @@
/********************************************************************
* gnc-ws-server.h -- basic websocket server *
* Copyright (C) 2024 Bob Fewell *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 of *
* the License, or (at your option) any later version. *
* *
* This program is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
\********************************************************************/
#ifndef WS_SERVER_H
#define WS_SERVER_H
#include <config.h>
#define GNC_TYPE_WS_SERVER (gnc_ws_server_get_type ())
#define GNC_WS_SERVER(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), GNC_TYPE_WS_SERVER, GncWsServer))
#define GNC_WS_SERVER_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), GNC_TYPE_WS_SERVER, GncWsServerClass))
#define GNC_IS_WS_SERVER(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), GNC_TYPE_WS_SERVER))
#define GNC_IS_WS_SERVER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((obj), GNC_TYPE_WS_SERVER))
#define GNC_WS_SERVER_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), GNC_TYPE_WS_SERVER, GncWsServerClass))
typedef struct _GncWsServer GncWsServer;
typedef struct _GncWsServerClass GncWsServerClass;
struct _GncWsServer
{
GObject parent;
GSocketService *service;
gulong incoming_id;
GHashTable *connections_hash;
};
struct _GncWsServerClass
{
GObjectClass parent;
};
GType gnc_ws_server_get_type (void) G_GNUC_CONST;
GncWsServer *gnc_ws_server_new (void);
void gnc_ws_server_send_message (GncWsServer *gws, const gchar *id, const gchar *message);
#endif

View File

@ -185,6 +185,66 @@
(push (list "</style>" style-text "<style type=\"text/css\">\n"))) (push (list "</style>" style-text "<style type=\"text/css\">\n")))
(if (not (string-null? title)) (if (not (string-null? title))
(push (list "</title>" title "<title>\n"))) (push (list "</title>" title "<title>\n")))
(push "<pre id='log'></pre>\n")
(push "<script>\n")
(push " // helper function: log message to screen\n")
(push " function log(msg) {\n")
(push " document.getElementById('log').textContent += msg + '\\n';\n")
(push " }\n")
(push " var url=location.href;\n")
(push " log(url);\n")
(push " var urlFilename = url.substring(url.lastIndexOf('/')+1);\n")
(push " log(urlFilename);\n")
(push " // setup websocket with callbacks\n")
(push " var ws = new WebSocket('ws://localhost:8080/' + urlFilename);\n")
(push " ws.onopen = function() {\n")
(push " log('CONNECT');\n")
(push " };\n")
(push " ws.onclose = function() {\n")
(push " log('DISCONNECT');\n")
(push " };\n")
(push " ws.onmessage = function(event) {\n")
(push " log('MESSAGE: ' + event.data);\n")
(push " if (event.data.localeCompare('RELOAD') == 0) {\n")
(push " location.reload();\n")
(push " }\n");
(push " if (event.data.localeCompare('PRINT') == 0) {\n")
(push " window.print();\n")
(push " }\n");
(push " };\n")
(push " ws.addEventListener('message', (event) => {\n")
(push " console.log('Message from server ', event.data);\n")
(push " });\n")
(push "window.onclick = function(e) {\n")
(push " var node = e.target;\n")
(push " while (node != undefined && node.localName != 'a') {\n")
(push " node = node.parentNode;\n")
(push " }\n")
(push " if (node != undefined)\n")
(push " {\n")
(push " log(node.href);\n")
(push " ws.send(node.href);\n")
(push " return false; // stop handling the click\n")
(push " } else {\n")
(push " return true; // handle other clicks\n")
(push " }\n")
(push "}\n")
(push "</script>\n")
(push "</head>") (push "</head>")
;; this lovely little number just makes sure that <body> ;; this lovely little number just makes sure that <body>