modules/websocket: First attempt a module for websocket support
authorPeter Dunkley <peter.dunkley@crocodile-rcs.com>
Thu, 14 Jun 2012 23:40:28 +0000 (00:40 +0100)
committerPeter Dunkley <peter.dunkley@crocodile-rcs.com>
Thu, 14 Jun 2012 23:40:28 +0000 (00:40 +0100)
- So far this is:
  - Module boiler-plate
  - WebSocket handshake
  - Example/test kamailio.cfg

modules/websocket/Makefile [new file with mode: 0644]
modules/websocket/example/kamailio.cfg [new file with mode: 0644]
modules/websocket/ws_handshake.c [new file with mode: 0644]
modules/websocket/ws_handshake.h [new file with mode: 0644]
modules/websocket/ws_mod.c [new file with mode: 0644]
modules/websocket/ws_mod.h [new file with mode: 0644]

diff --git a/modules/websocket/Makefile b/modules/websocket/Makefile
new file mode 100644 (file)
index 0000000..36543a0
--- /dev/null
@@ -0,0 +1,27 @@
+# $Id$
+#
+# 
+# WARNING: do not run this directly, it should be run by the master Makefile
+
+include ../../Makefile.defs
+auto_gen=
+NAME=websocket.so
+
+BUILDER = $(shell which pkg-config)
+ifeq ($(BUILDER),)
+        DEFS+= -I$(LOCALBASE)/ssl/include
+        LIBS=  -L$(LOCALBASE)/lib -L$(LOCALBASE)/ssl/lib \
+                       -L$(LOCALBASE)/lib64 -L$(LOCALBASE)/ssl/lib64 \
+                       -lssl
+else
+       DEFS+= $(shell pkg-config --cflags libssl)
+       LIBS=  $(shell pkg-config --libs libssl)
+endif
+
+DEFS+=-DOPENSER_MOD_INTERFACE
+
+SERLIBPATH=../../lib
+SER_LIBS+=$(SERLIBPATH)/kcore/kcore
+
+include ../../Makefile.modules
+
diff --git a/modules/websocket/example/kamailio.cfg b/modules/websocket/example/kamailio.cfg
new file mode 100644 (file)
index 0000000..4b772b1
--- /dev/null
@@ -0,0 +1,252 @@
+#!KAMAILIO
+
+#!define DBURL "sqlite:////etc/kamailio/db.sqlite"
+
+####### Global Parameters #########
+
+debug=2
+fork=yes
+children=4
+
+alias="example.com"
+listen=192.168.111.12
+port=5060
+listen=192.168.111.12
+port=80
+
+tcp_connection_lifetime=3604
+tcp_accept_no_cl=yes
+
+syn_branch=0
+
+#mpath="/usr/lib64/kamailio/modules_k/:/usr/lib64/kamailio/modules/"
+mpath="modules_k:modules"
+
+loadmodule "db_sqlite.so"
+loadmodule "tm.so"
+loadmodule "sl.so"
+loadmodule "rr.so"
+loadmodule "pv.so"
+loadmodule "maxfwd.so"
+loadmodule "usrloc.so"
+loadmodule "registrar.so"
+loadmodule "textops.so"
+loadmodule "siputils.so"
+loadmodule "xlog.so"
+loadmodule "sanity.so"
+loadmodule "ctl.so"
+loadmodule "auth.so"
+loadmodule "auth_db.so"
+loadmodule "xhttp.so"
+loadmodule "kex.so"
+loadmodule "websocket.so"
+
+# ----------------- setting module-specific parameters ---------------
+
+# ----- tm params -----
+# auto-discard branches from previous serial forking leg
+modparam("tm", "failure_reply_mode", 3)
+# default retransmission timeout: 30sec
+modparam("tm", "fr_timer", 30000)
+# default invite retransmission timeout after 1xx: 120sec
+modparam("tm", "fr_inv_timer", 120000)
+
+# ----- rr params -----
+# add value to ;lr param to cope with most of the UAs
+modparam("rr", "enable_full_lr", 1)
+# do not append from tag to the RR (no need for this script)
+modparam("rr", "append_fromtag", 0)
+
+# ----- registrar params -----
+modparam("registrar", "method_filtering", 1)
+modparam("registrar", "max_expires", 3600)
+modparam("registrar", "gruu_enabled", 0)
+
+# ----- usrloc params -----
+modparam("usrloc", "db_url", DBURL)
+modparam("usrloc", "db_mode", 0)
+
+# ----- auth_db params -----
+modparam("auth_db", "db_url", DBURL)
+modparam("auth_db", "calculate_ha1", yes)
+modparam("auth_db", "password_column", "password")
+modparam("auth_db", "load_credentials", "")
+
+
+####### Routing Logic ########
+
+
+# Main SIP request routing logic
+# - processing of any incoming SIP request starts with this route
+# - note: this is the same as route { ... }
+request_route {
+
+       # per request initial checks
+       route(REQINIT);
+
+       # handle requests within SIP dialogs
+       route(WITHINDLG);
+
+       ### only initial requests (no To tag)
+
+       # CANCEL processing
+       if (is_method("CANCEL"))
+       {
+               if (t_check_trans())
+                       t_relay();
+               exit;
+       }
+
+       t_check_trans();
+
+       # authentication
+       route(AUTH);
+
+       # record routing for dialog forming requests (in case they are routed)
+       # - remove preloaded route headers
+       remove_hf("Route");
+       if (is_method("INVITE|SUBSCRIBE"))
+               record_route();
+
+       # handle registrations
+       route(REGISTRAR);
+
+       if ($rU==$null)
+       {
+               # request with no Username in RURI
+               sl_send_reply("484","Address Incomplete");
+               exit;
+       }
+
+       # user location service
+       route(LOCATION);
+
+       route(RELAY);
+}
+
+route[RELAY] {
+       if (!t_relay()) {
+               sl_reply_error();
+       }
+       exit;
+}
+
+# Per SIP request initial checks
+route[REQINIT] {
+       if (!mf_process_maxfwd_header("10")) {
+               sl_send_reply("483","Too Many Hops");
+               exit;
+       }
+
+       if(!sanity_check("1511", "7"))
+       {
+               xlog("Malformed SIP message from $si:$sp\n");
+               exit;
+       }
+}
+
+# Handle requests within SIP dialogs
+route[WITHINDLG] {
+       if (has_totag()) {
+               # sequential request withing a dialog should
+               # take the path determined by record-routing
+               if (loose_route()) {
+                       route(RELAY);
+               } else {
+                       if ( is_method("ACK") ) {
+                               if ( t_check_trans() ) {
+                                       # no loose-route, but stateful ACK;
+                                       # must be an ACK after a 487
+                                       # or e.g. 404 from upstream server
+                                       t_relay();
+                                       exit;
+                               } else {
+                                       # ACK without matching transaction...
+                                       # ignore and discard
+                                       exit;
+                               }
+                       }
+                       sl_send_reply("404","Not here");
+               }
+               exit;
+       }
+}
+
+# Handle SIP registrations
+route[REGISTRAR] {
+       if (is_method("REGISTER"))
+       {
+               if (!save("location"))
+                       sl_reply_error();
+
+               exit;
+       }
+}
+
+# USER location service
+route[LOCATION] {
+       if (!lookup("location")) {
+               $var(rc) = $rc;
+               t_newtran();
+               switch ($var(rc)) {
+                       case -1:
+                       case -3:
+                               send_reply("404", "Not Found");
+                               exit;
+                       case -2:
+                               send_reply("405", "Method Not Allowed");
+                               exit;
+               }
+       }
+}
+
+# Authentication route
+route[AUTH] {
+       if (is_method("REGISTER") || from_uri==myself)
+       {
+               # authenticate requests
+               if (!auth_check("$fd", "subscriber", "1")) {
+                       auth_challenge("$fd", "0");
+                       exit;
+               }
+               # user authenticated - remove auth header
+               if(!is_method("REGISTER|PUBLISH"))
+                       consume_credentials();
+       }
+       # if caller is not local subscriber, then check if it calls
+       # a local destination, otherwise deny, not an open relay here
+       if (from_uri!=myself && uri!=myself)
+       {
+               sl_send_reply("403","Not relaying");
+               exit;
+       }
+}
+
+event_route[xhttp:request] {
+       if ($Rp != "80") {
+               xlog("L_WARN", "HTTP request received on $Rp\n");
+               xhttp_reply("403", "Forbidden", "", "");
+               exit;
+       }
+
+       if ($hdr(Upgrade)=~"websocket"
+                       && $hdr(Connection)=~"Upgrade"
+                       && $rm=~"GET") {
+               xlog("L_INFO", "WebSocket\n");
+               xlog("L_INFO", " Host: $hdr(Host)\n");
+               xlog("L_INFO", " Origin: $hdr(Origin)\n");
+
+               if ($hdr(Host) == $null || !is_myself($hdr(Host))) {
+                       xlog("L_WARN", "Bad host $hdr(Host)\n");
+                       xhttp_reply("403", "Forbidden", "", "");
+                       exit;
+               }
+
+               # Optional... validate Origin
+               # Optional... perform HTTP authentication
+
+               ws_handle_handshake();
+       }
+
+       xhttp_reply("404", "Not found", "", "");
+}
diff --git a/modules/websocket/ws_handshake.c b/modules/websocket/ws_handshake.c
new file mode 100644 (file)
index 0000000..313187c
--- /dev/null
@@ -0,0 +1,186 @@
+/*
+ * $Id$
+ *
+ * Copyright (C) 2012 Crocodile RCS Ltd
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio 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
+ *
+ * Kamailio 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, write to the Free Software 
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+#include <openssl/sha.h>
+
+#include "../../basex.h"
+#include "../../data_lump_rpl.h"
+#include "../../dprint.h"
+#include "../../lib/kcore/cmpapi.h"
+#include "../../parser/msg_parser.h"
+#include "../sl/sl.h"
+#include "ws_handshake.h"
+#include "ws_mod.h"
+
+#define WS_VERSION             (13)
+
+#define SEC_WEBSOCKET_KEY      (1<<0)
+#define SEC_WEBSOCKET_PROTOCOL (1<<1)
+#define SEC_WEBSOCKET_VERSION  (1<<2)
+
+#define REQUIRED_HEADERS       (SEC_WEBSOCKET_KEY | SEC_WEBSOCKET_PROTOCOL\
+                                       | SEC_WEBSOCKET_VERSION)
+
+static str str_sip = str_init("sip");
+static str str_ws_guid = str_init("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
+
+static str str_switching_protocols = str_init("Switching Protocols");
+static str str_bad_request = str_init("Bad Request");
+static str str_upgrade_required = str_init("Upgrade Required");
+static str str_internal_server_error = str_init("Internal Server Error");
+
+#define HDR_BUF_LEN            (256)
+static char headers_buf[HDR_BUF_LEN];
+
+#define KEY_BUF_LEN            (28)
+static char key_buf[KEY_BUF_LEN];
+
+static int ws_send_reply(sip_msg_t *msg, int code, str *reason, str *hdrs)
+{
+       if (hdrs && hdrs->len > 0)
+       {
+               if (add_lump_rpl(msg, hdrs->s, hdrs->len, LUMP_RPL_HDR) == 0)
+               {
+                       LM_ERR("inserting extra-headers lump\n");
+                       return -1;
+               }
+       }
+
+       if (ws_slb.freply(msg, code, reason) < 0)
+       {
+               LM_ERR("sending reply\n");
+               return -1;
+       }
+
+       return 0;
+}
+
+int ws_handle_handshake(struct sip_msg *msg)
+{
+       str key = {0, 0}, headers = {0, 0}, reply_key = {0, 0};
+       unsigned char sha1[20];
+       unsigned int hdr_flags = 0;
+       int version;
+       struct hdr_field *hdr = msg->headers;
+
+       while (hdr != NULL)
+       {
+               /* Decode and validate Sec-WebSocket-Key */
+               if (cmp_hdrname_strzn(&hdr->name,
+                               "Sec-WebSocket-Key", 17) == 0) 
+               {
+                       if (hdr_flags & SEC_WEBSOCKET_KEY)
+                       {
+                               LM_WARN("%.*s found multiple times\n",
+                                       hdr->name.len, hdr->name.s);
+                               ws_send_reply(msg, 400, &str_bad_request, NULL);
+                               return 0;
+                       }
+
+                       key = hdr->body;
+                       hdr_flags |= SEC_WEBSOCKET_KEY;
+               }
+               /* Decode and validate Sec-WebSocket-Protocol */
+               else if (cmp_hdrname_strzn(&hdr->name,
+                               "Sec-WebSocket-Protocol", 22) == 0)
+               {
+                       if (str_search(&hdr->body, &str_sip) != NULL)
+                               hdr_flags |= SEC_WEBSOCKET_PROTOCOL;
+               }
+               /* Decode and validate Sec-WebSocket-Version */
+               else if (cmp_hdrname_strzn(&hdr->name,
+                               "Sec-WebSocket-Version", 21) == 0)
+               {
+                       if (hdr_flags & SEC_WEBSOCKET_VERSION)
+                       {
+                               LM_WARN("%.*s found multiple times\n",
+                                       hdr->name.len, hdr->name.s);
+                               ws_send_reply(msg, 400, &str_bad_request, NULL);
+                               return 0;
+                       }
+
+                       str2sint(&hdr->body, &version);
+
+                       if (version != WS_VERSION)
+                       {
+                               LM_WARN("Unsupported protocol version %.*s\n",
+                                       hdr->body.len, hdr->body.s);
+                               headers.s = headers_buf;
+                               headers.len = snprintf(headers.s, HDR_BUF_LEN,
+                                       "Sec-WebSocket-Version: %u\r\n",
+                                       WS_VERSION);
+                               ws_send_reply(msg, 426, &str_upgrade_required,
+                                               &headers);
+                               return 0;
+                       }
+
+                       hdr_flags |= SEC_WEBSOCKET_VERSION;
+               }
+
+               hdr = hdr->next;
+       }
+
+       /* Final check that all required headers/values were found */
+       if (hdr_flags != REQUIRED_HEADERS)
+       {
+               LM_WARN("all required headers not present\n");
+               ws_send_reply(msg, 400, &str_bad_request, NULL);
+               return 0;
+       }
+
+       /* Construct reply_key */
+       reply_key.s = (char *) pkg_malloc(
+                               (key.len + str_ws_guid.len) * sizeof(char)); 
+       if (reply_key.s == NULL)
+       {
+               LM_ERR("allocating pkg memory\n");
+               ws_send_reply(msg, 500, &str_internal_server_error, NULL);
+               return 0;
+       }
+       memcpy(reply_key.s, key.s, key.len);
+       memcpy(reply_key.s + key.len, str_ws_guid.s, str_ws_guid.len);
+       reply_key.len = key.len + str_ws_guid.len;
+       SHA1((const unsigned char *) reply_key.s, reply_key.len, sha1);
+       pkg_free(reply_key.s);
+       reply_key.s = key_buf;
+       reply_key.len = base64_enc(sha1, 20,
+                               (unsigned char *) reply_key.s, KEY_BUF_LEN);
+
+       /* Build headers for reply */
+       headers.s = headers_buf;
+       headers.len = snprintf(headers.s, HDR_BUF_LEN,
+                       "Sec-WebSocket-Key: %.*s\r\n"
+                       "Sec-WebSocket-Protocol: %.*s\r\n"
+                       "Sec-WebSocket-Version: %u\r\n",
+                       reply_key.len, reply_key.s,
+                       str_sip.len, str_sip.s,
+                       WS_VERSION);
+
+       /* TODO: make sure Kamailio core sends future requests on this
+                connection directly to this module */
+
+       /* Send reply */
+       ws_send_reply(msg, 101, &str_switching_protocols, &headers);
+
+       return 0;
+}
diff --git a/modules/websocket/ws_handshake.h b/modules/websocket/ws_handshake.h
new file mode 100644 (file)
index 0000000..b63e799
--- /dev/null
@@ -0,0 +1,31 @@
+/*
+ * $Id$
+ *
+ * Copyright (C) 2012 Crocodile RCS Ltd
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio 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
+ *
+ * Kamailio 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, write to the Free Software 
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+#ifndef _WS_HANDSHAKE_H
+#define _WS_HANDSHAKE_H
+
+#include "../../parser/msg_parser.h"
+
+int ws_handle_handshake(struct sip_msg *msg);
+
+#endif /* _WS_HANDSHAKE_H */
diff --git a/modules/websocket/ws_mod.c b/modules/websocket/ws_mod.c
new file mode 100644 (file)
index 0000000..004bed4
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * $Id$
+ *
+ * Copyright (C) 2012 Crocodile RCS Ltd
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio 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
+ *
+ * Kamailio 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, write to the Free Software 
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+#include "../../dprint.h"
+#include "../../sr_module.h"
+#include "../../parser/msg_parser.h"
+#include "ws_handshake.h"
+#include "ws_mod.h"
+
+MODULE_VERSION
+
+static int mod_init(void);
+
+sl_api_t ws_slb;
+int ws_ping_interval = 25;     /* time (in seconds) after which a Ping will be
+                                  sent on an idle connection */
+int ws_ping_timeout = 1;       /* time (in seconds) to wait for a Pong in
+                                  response to a Ping before closing a
+                                  connection */
+
+static param_export_t params[]=
+{
+       {"ping_interval",       INT_PARAM, &ws_ping_interval},
+       {"ping_timeout",        INT_PARAM, &ws_ping_timeout},
+};
+
+static cmd_export_t cmds[]= 
+{
+    {"ws_handle_handshake", (cmd_function)ws_handle_handshake, 0,
+       0, 0,
+       ANY_ROUTE},
+    {0, 0, 0, 0, 0, 0}
+};
+
+struct module_exports exports= 
+{
+       "websocket",
+       DEFAULT_DLFLAGS,        /* dlopen flags */
+       cmds,                   /* Exported functions */
+       params,                 /* Exported parameters */
+       0,                      /* exported statistics */
+       0,                      /* exported MI functions */
+       0,                      /* exported pseudo-variables */
+       0,                      /* extra processes */
+       mod_init,               /* module initialization function */
+       0,                      /* response function */
+       0,                      /* destroy function */
+       0                       /* per-child initialization function */
+};
+
+static int mod_init(void)
+{
+       if (sl_load_api(&ws_slb) != 0)
+       {
+               LM_ERR("cannot bind to SL\n");
+               return -1;
+       }
+
+       /* TODO: register module with core to receive WS/WSS messages */
+
+       return 0;
+}
diff --git a/modules/websocket/ws_mod.h b/modules/websocket/ws_mod.h
new file mode 100644 (file)
index 0000000..8b5817f
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * $Id$
+ *
+ * Copyright (C) 2012 Crocodile RCS Ltd
+ *
+ * This file is part of Kamailio, a free SIP server.
+ *
+ * Kamailio 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
+ *
+ * Kamailio 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, write to the Free Software 
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ *
+ */
+
+#ifndef _WS_MOD_H
+#define _WS_MOD_H
+
+#include "../sl/sl.h"
+
+extern sl_api_t ws_slb;
+extern int ws_ping_interval;   /* time (in seconds) after which a Ping will be
+                                  sent on an idle connection */
+extern int ws_ping_timeout;    /* time (in seconds) to wait for a Pong in
+                                  response to a Ping before closing a
+                                  connection */
+#endif /* _WS_MOD_H */