Logo Search packages:      
Sourcecode: kannel version File versions  Download package

wsp_session.c

/* ==================================================================== 
 * The Kannel Software License, Version 1.0 
 * 
 * Copyright (c) 2001-2004 Kannel Group  
 * Copyright (c) 1998-2001 WapIT Ltd.   
 * All rights reserved. 
 * 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions 
 * are met: 
 * 
 * 1. Redistributions of source code must retain the above copyright 
 *    notice, this list of conditions and the following disclaimer. 
 * 
 * 2. Redistributions in binary form must reproduce the above copyright 
 *    notice, this list of conditions and the following disclaimer in 
 *    the documentation and/or other materials provided with the 
 *    distribution. 
 * 
 * 3. The end-user documentation included with the redistribution, 
 *    if any, must include the following acknowledgment: 
 *       "This product includes software developed by the 
 *        Kannel Group (http://www.kannel.org/)." 
 *    Alternately, this acknowledgment may appear in the software itself, 
 *    if and wherever such third-party acknowledgments normally appear. 
 * 
 * 4. The names "Kannel" and "Kannel Group" must not be used to 
 *    endorse or promote products derived from this software without 
 *    prior written permission. For written permission, please  
 *    contact org@kannel.org. 
 * 
 * 5. Products derived from this software may not be called "Kannel", 
 *    nor may "Kannel" appear in their name, without prior written 
 *    permission of the Kannel Group. 
 * 
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED 
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 
 * DISCLAIMED.  IN NO EVENT SHALL THE KANNEL GROUP OR ITS CONTRIBUTORS 
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,  
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT  
 * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR  
 * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,  
 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE  
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,  
 * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 * ==================================================================== 
 * 
 * This software consists of voluntary contributions made by many 
 * individuals on behalf of the Kannel Group.  For more information on  
 * the Kannel Group, please see <http://www.kannel.org/>. 
 * 
 * Portions of this software are based upon software originally written at  
 * WapIT Ltd., Helsinki, Finland for the Kannel project.  
 */ 

/*
 * wsp_session.c - Implement WSP session oriented service
 *
 * Lars Wirzenius
 * Stipe Tolj
 */


#include <string.h>
#include <limits.h>

#include "gwlib/gwlib.h"
#include "wsp.h"
#include "wsp_pdu.h"
#include "wsp_headers.h"
#include "wsp_caps.h"
#include "wsp_strings.h"
#include "cookies.h"
#include "wap.h"
#include "wtp.h"


typedef enum {
      #define STATE_NAME(name) name,
      #define ROW(state, event, condition, action, next_state)
      #include "wsp_server_session_states.def"

      #define STATE_NAME(name) name,
      #define ROW(state, event, condition, action, next_state)
      #include "wsp_server_method_states.def"

        #define STATE_NAME(name) name,
        #define ROW(state, event, condition, action, next_state)
        #include "wsp_server_push_states.def"

      WSPState_count
} WSPState;


/*
 * Give the status the module:
 *
 *    limbo
 *          not running at all
 *    running
 *          operating normally
 *    terminating
 *          waiting for operations to terminate, returning to limbo
 */
static enum { limbo, running, terminating } run_status = limbo;

static wap_dispatch_func_t *dispatch_to_wtp_resp;
static wap_dispatch_func_t *dispatch_to_wtp_init;
static wap_dispatch_func_t *dispatch_to_appl;
static wap_dispatch_func_t *dispatch_to_ota;

/*
 * True iff "Session resume facility" is enabled.  This means we are
 * willing to let sessions go to SUSPENDED state, and later resume them.
 * Currently we always support it, but this may become configurable
 * at some point.
 */

static int resume_enabled = 1;

static List *queue = NULL;
static List *session_machines = NULL;
static Counter *session_id_counter = NULL;


static WSPMachine *find_session_machine(WAPEvent *event, WSP_PDU *pdu);
static void handle_session_event(WSPMachine *machine, WAPEvent *event, 
                         WSP_PDU *pdu);
static WSPMachine *machine_create(void);
static void machine_destroy(void *p);

static void handle_method_event(WSPMachine *session, WSPMethodMachine *machine, WAPEvent *event, WSP_PDU *pdu);
static void cant_handle_event(WSPMachine *sm, WAPEvent *event);
static WSPMethodMachine *method_machine_create(WSPMachine *, long);
static void method_machine_destroy(void *msm);

static void handle_push_event(WSPMachine *session, WSPPushMachine *machine,
                              WAPEvent *e);
static WSPPushMachine *push_machine_create(WSPMachine *session, long id);
static void push_machine_destroy(void *p);

static char *state_name(WSPState state);
static unsigned long next_wsp_session_id(void);

static List *make_capabilities_reply(WSPMachine *m);
static List *make_reply_headers(WSPMachine *m);
static Octstr *make_connectreply_pdu(WSPMachine *m);
static Octstr *make_resume_reply_pdu(WSPMachine *m, List *headers);
static WSP_PDU *make_confirmedpush_pdu(WAPEvent *e);
static WSP_PDU *make_push_pdu(WAPEvent *e);

static int transaction_belongs_to_session(void *session, void *tuple);
static int find_by_session_id(void *session, void *idp);
static int same_client(void *sm1, void *sm2);
static WSPMethodMachine *find_method_machine(WSPMachine *, long id);
static WSPPushMachine *find_push_machine(WSPMachine *m, long id);

static List *unpack_new_headers(WSPMachine *sm, Octstr *hdrs);

static void disconnect_other_sessions(WSPMachine *sm);
static void send_abort(long reason, long handle);
static void indicate_disconnect(WSPMachine *sm, long reason);
static void indicate_suspend(WSPMachine *sm, long reason);
static void indicate_resume(WSPMachine *sm, WAPAddrTuple *tuple, 
                            List *client_headers);

static void release_holding_methods(WSPMachine *sm);
static void abort_methods(WSPMachine *sm, long reason);
static void abort_pushes(WSPMachine *sm, long reason);

static void method_abort(WSPMethodMachine *msm, long reason);
static void indicate_method_abort(WSPMethodMachine *msm, long reason);

static WAPEvent *make_abort(long reason, long handle);
static void send_invoke(WSPMachine *session, WSP_PDU *pdu, WAPEvent *e,
                        long class);
static void send_abort_to_initiator(long reason, long handle);
static void indicate_pushabort(WSPPushMachine *machine, long reason);
static void confirm_push(WSPPushMachine *machine);

static void main_thread(void *);
static int id_belongs_to_session (void *, void *);
static int wsp_encoding_string_to_version(Octstr *enc);
static Octstr *wsp_encoding_version_to_string(int version);


/***********************************************************************
 * Public functions.
 */


void wsp_session_init(wap_dispatch_func_t *responder_dispatch,
                      wap_dispatch_func_t *initiator_dispatch,
                      wap_dispatch_func_t *application_dispatch,
                      wap_dispatch_func_t *push_ota_dispatch) {
      queue = list_create();
      list_add_producer(queue);
      session_machines = list_create();
      session_id_counter = counter_create();
      dispatch_to_wtp_resp = responder_dispatch;
      dispatch_to_wtp_init = initiator_dispatch;
      dispatch_to_appl = application_dispatch;
        dispatch_to_ota = push_ota_dispatch;
        wsp_strings_init();
      run_status = running;
      gwthread_create(main_thread, NULL);
}


void wsp_session_shutdown(void) {
      gw_assert(run_status == running);
      run_status = terminating;
      list_remove_producer(queue);
      gwthread_join_every(main_thread);

      list_destroy(queue, wap_event_destroy_item);

      debug("wap.wsp", 0, "WSP: %ld session machines left.",
            list_len(session_machines));
      list_destroy(session_machines, machine_destroy);

      counter_destroy(session_id_counter);
        wsp_strings_shutdown();
}


void wsp_session_dispatch_event(WAPEvent *event) {
      wap_event_assert(event);
      list_produce(queue, event);
}


/***********************************************************************
 * Local functions
 */


static void main_thread(void *arg) {
      WAPEvent *e;
      WSPMachine *sm;
      WSP_PDU *pdu;
      
      while (run_status == running && (e = list_consume(queue)) != NULL) {
            wap_event_assert(e);
            switch (e->type) {
            case TR_Invoke_Ind:
                  pdu = wsp_pdu_unpack(e->u.TR_Invoke_Ind.user_data);
                  if (pdu == NULL) {
                        warning(0, "WSP: Broken PDU ignored.");
                        wap_event_destroy(e);
                        continue;
                  }
                  break;
      
            default:
                  pdu = NULL;
                  break;
            }
      
            sm = find_session_machine(e, pdu);
            if (sm == NULL) {
                  wap_event_destroy(e);
            } else {
                  handle_session_event(sm, e, pdu);
            }
            
            wsp_pdu_destroy(pdu);
      }
}


static WSPMachine *find_session_machine(WAPEvent *event, WSP_PDU *pdu) {
      WSPMachine *sm;
      long session_id;
      WAPAddrTuple *tuple;
      
      tuple = NULL;
      session_id = -1;
      
      switch (event->type) {
      case TR_Invoke_Ind:
            tuple = wap_addr_tuple_duplicate(
                        event->u.TR_Invoke_Ind.addr_tuple);
            break;

        case TR_Invoke_Cnf:
                tuple = wap_addr_tuple_duplicate(
                        event->u.TR_Invoke_Cnf.addr_tuple);
              break;

      case TR_Result_Cnf:
            tuple = wap_addr_tuple_duplicate(
                        event->u.TR_Result_Cnf.addr_tuple);
            break;

      case TR_Abort_Ind:
            tuple = wap_addr_tuple_duplicate(
                        event->u.TR_Abort_Ind.addr_tuple);
            break;

      case S_Connect_Res:
            session_id = event->u.S_Connect_Res.session_id;
            break;

      case S_Resume_Res:
            session_id = event->u.S_Resume_Res.session_id;
            break;

      case Disconnect_Event:
            session_id = event->u.Disconnect_Event.session_handle;
            break;

      case Suspend_Event:
            session_id = event->u.Suspend_Event.session_handle;
            break;

      case S_MethodInvoke_Res:
            session_id = event->u.S_MethodInvoke_Res.session_id;
            break;

      case S_MethodResult_Req:
            session_id = event->u.S_MethodResult_Req.session_id;
            break;

      case S_ConfirmedPush_Req:
                session_id = event->u.S_ConfirmedPush_Req.session_id;
              break;

        case S_Push_Req:
                session_id = event->u.S_Push_Req.session_id;
              break;

      default:
            error(0, "WSP: Cannot find machine for %s event",
                  wap_event_name(event->type));
      }
      
      gw_assert(tuple != NULL || session_id != -1);

      /* Pre-state-machine tests, according to 7.1.5.  After the tests,
       * caller will pass the event to sm if sm is not NULL. */
      sm = NULL;
      /* First test is for MRUEXCEEDED, and we don't have a MRU */

      /* Second test is for class 2 TR-Invoke.ind with Connect PDU */
      if (event->type == TR_Invoke_Ind &&
          event->u.TR_Invoke_Ind.tcl == 2 &&
          pdu->type == Connect) {
                  /* Create a new session, even if there is already
                   * a session open for this address.  The new session
                   * will take care of killing the old ones. */
                  sm = machine_create();
                  gw_assert(tuple != NULL);
                  sm->addr_tuple = wap_addr_tuple_duplicate(tuple);
                  sm->connect_handle = event->u.TR_Invoke_Ind.handle;
      /* Third test is for class 2 TR-Invoke.ind with Resume PDU */
      } else if (event->type == TR_Invoke_Ind &&
               event->u.TR_Invoke_Ind.tcl == 2 &&
               pdu->type == Resume) {
            /* Pass to session identified by session id, not
             * the address tuple. */
            session_id = pdu->u.Resume.sessionid;
            sm = list_search(session_machines, &session_id,
                        find_by_session_id);
            if (sm == NULL) {
                  /* No session; TR-Abort.req(DISCONNECT) */
                  send_abort(WSP_ABORT_DISCONNECT,
                        event->u.TR_Invoke_Ind.handle);
            }
      /* Fourth test is for a class 1 or 2 TR-Invoke.Ind with no
       * session for that address tuple.  We also handle class 0
       * TR-Invoke.ind here by ignoring them; this seems to be
       * an omission in the spec table. */
      } else if (event->type == TR_Invoke_Ind) {
            sm = list_search(session_machines, tuple,
                         transaction_belongs_to_session);
            if (sm == NULL && (event->u.TR_Invoke_Ind.tcl == 1 ||
                        event->u.TR_Invoke_Ind.tcl == 2)) {
                  send_abort(WSP_ABORT_DISCONNECT,
                        event->u.TR_Invoke_Ind.handle);
            }
      /* Other tests are for events not handled by the state tables;
       * do those later, after we've tried to handle them. */
      } else {
            if (session_id != -1) {
                  sm = list_search(session_machines, &session_id,
                        find_by_session_id);
            } else {
                  sm = list_search(session_machines, tuple,
                        transaction_belongs_to_session);
            }
            /* The table doesn't really say what we should do with
             * non-Invoke events for which there is no session.  But
             * such a situation means there is an error _somewhere_
             * in the gateway. */
            if (sm == NULL) {
                  error(0, "WSP: Cannot find session machine for event.");
                  wap_event_dump(event);
            }
      }

      wap_addr_tuple_destroy(tuple);
      return sm;
}


static void handle_session_event(WSPMachine *sm, WAPEvent *current_event, 
WSP_PDU *pdu) {
      debug("wap.wsp", 0, "WSP: machine %p, state %s, event %s",
            (void *) sm,
            state_name(sm->state), 
            wap_event_name(current_event->type));

      #define STATE_NAME(name)
      #define ROW(state_name, event, condition, action, next_state) \
            { \
                  struct event *e; \
                  e = &current_event->u.event; \
                  if (sm->state == state_name && \
                     current_event->type == event && \
                     (condition)) { \
                        action \
                        sm->state = next_state; \
                        debug("wap.wsp", 0, "WSP %ld: New state %s", \
                              sm->session_id, #next_state); \
                        goto end; \
                  } \
            }
      #include "wsp_server_session_states.def"
      
      cant_handle_event(sm, current_event);

end:
      wap_event_destroy(current_event);

      if (sm->state == NULL_SESSION)
            machine_destroy(sm);
}


static void cant_handle_event(WSPMachine *sm, WAPEvent *event) {
      /* We do the rest of the pre-state-machine tests here.  The first
       * four were done in find_session_machine().  The fifth is a
       * class 1 or 2 TR-Invoke.ind not handled by the state tables. */
      if (event->type == TR_Invoke_Ind &&
          (event->u.TR_Invoke_Ind.tcl == 1 ||
           event->u.TR_Invoke_Ind.tcl == 2)) {
            warning(0, "WSP: Can't handle TR-Invoke.ind, aborting transaction.");
            debug("wap.wsp", 0, "WSP: The unhandled event:");
            wap_event_dump(event);
            send_abort(WSP_ABORT_PROTOERR,
                  event->u.TR_Invoke_Ind.handle);
      /* The sixth is a class 0 TR-Invoke.ind not handled by state tables. */
      } else if (event->type == TR_Invoke_Ind) {
            warning(0, "WSP: Can't handle TR-Invoke.ind, ignoring.");
            debug("wap.wsp", 0, "WSP: The ignored event:");
            wap_event_dump(event);
      /* The seventh is any other event not handled by state tables. */
      } else {
            error(0, "WSP: Can't handle event. Aborting session.");
            debug("wap.wsp", 0, "WSP: The unhandled event:");
            wap_event_dump(event);
            /* TR-Abort.req(PROTOERR) if it is some other transaction
             * event than abort. */
            /* Currently that means TR-Result.cnf, because we already
             * tested for Invoke. */
            /* FIXME We need a better way to get at event values than
             * by hardcoding the types. */
            if (event->type == TR_Result_Cnf) {
                  send_abort(WSP_ABORT_PROTOERR,
                        event->u.TR_Result_Cnf.handle);
            }
            /* Abort(PROTOERR) all method and push transactions */
            abort_methods(sm, WSP_ABORT_PROTOERR);
                abort_pushes(sm, WSP_ABORT_PROTOERR);
            /* S-Disconnect.ind(PROTOERR) */
            indicate_disconnect(sm, WSP_ABORT_PROTOERR);
      }
}


static WSPMachine *machine_create(void) {
      WSPMachine *p;
      
      p = gw_malloc(sizeof(WSPMachine));
      debug("wap.wsp", 0, "WSP: Created WSPMachine %p", (void *) p);
      
      #define INTEGER(name) p->name = 0;
      #define OCTSTR(name) p->name = NULL;
      #define HTTPHEADERS(name) p->name = NULL;
      #define ADDRTUPLE(name) p->name = NULL;
      #define MACHINESLIST(name) p->name = list_create();
      #define CAPABILITIES(name) p->name = NULL;
      #define COOKIES(name) p->name = NULL;
      #define REFERER(name) p->name = NULL;
      #define MACHINE(fields) fields
      #include "wsp_server_session_machine.def"
      
      p->state = NULL_SESSION;

      /* set capabilities to default values (defined in 1.1) */

      p->client_SDU_size = 1400;
      p->MOR_push = 1;
      
      /* Insert new machine at the _front_, because 1) it's more likely
       * to get events than old machines are, so this speeds up the linear
       * search, and 2) we want the newest machine to get any method
       * invokes that come through before the Connect is established. */
      list_insert(session_machines, 0, p);

      return p;
}


static void destroy_methodmachines(List *machines) {
      if (list_len(machines) > 0) {
            warning(0, "Destroying WSP session with %ld active methods\n",
                  list_len(machines));
      }

      list_destroy(machines, method_machine_destroy);
}

static void destroy_pushmachines(List *machines) {
      if (list_len(machines) > 0) {
            warning(0, "Destroying WSP session with %ld active pushes\n",
                  list_len(machines));
      }

      list_destroy(machines, push_machine_destroy);
}

static void machine_destroy(void *pp) {
      WSPMachine *p;
      
      p = pp;
      debug("wap.wsp", 0, "Destroying WSPMachine %p", pp);
      list_delete_equal(session_machines, p);

      #define INTEGER(name) p->name = 0;
      #define OCTSTR(name) octstr_destroy(p->name);
      #define HTTPHEADERS(name) http_destroy_headers(p->name);
      #define ADDRTUPLE(name) wap_addr_tuple_destroy(p->name);
      #define MACHINESLIST(name) destroy_##name(p->name);
      #define CAPABILITIES(name) wsp_cap_destroy_list(p->name);
      #define COOKIES(name) cookies_destroy(p->name);
      #define REFERER(name) octstr_destroy(p->name);
      #define MACHINE(fields) fields
      #include "wsp_server_session_machine.def"
      gw_free(p);
}


struct msm_pattern {
      WAPAddrTuple *addr_tuple;
      long msmid, tid;
};


/* This function does NOT consume its event; it leaves that task up
 * to the parent session */
static void handle_method_event(WSPMachine *sm, WSPMethodMachine *msm, 
WAPEvent *current_event, WSP_PDU *pdu) {

      if (msm == NULL) {
            warning(0, "No method machine for event.");
            wap_event_dump(current_event);
            return;
      }
            
      debug("wap.wsp", 0, "WSP: method %ld, state %s, event %s",
            msm->transaction_id, state_name(msm->state), 
            wap_event_name(current_event->type));

      gw_assert(sm->session_id == msm->session_id);

      #define STATE_NAME(name)
      #define ROW(state_name, event, condition, action, next_state) \
            { \
                  struct event *e; \
                  e = &current_event->u.event; \
                  if (msm->state == state_name && \
                     current_event->type == event && \
                     (condition)) { \
                        action \
                        msm->state = next_state; \
                        debug("wap.wsp", 0, "WSP %ld/%ld: New method state %s", \
                              msm->session_id, msm->transaction_id, #next_state); \
                        goto end; \
                  } \
            }
      #include "wsp_server_method_states.def"
      
      cant_handle_event(sm, current_event);

end:
      if (msm->state == NULL_METHOD) {
            method_machine_destroy(msm);
            list_delete_equal(sm->methodmachines, msm);
      }
}


static WSPMethodMachine *method_machine_create(WSPMachine *sm,
                  long wtp_handle) {
      WSPMethodMachine *msm;
      
      msm = gw_malloc(sizeof(*msm));
      
      #define INTEGER(name) msm->name = 0;
      #define ADDRTUPLE(name) msm->name = NULL;
      #define EVENT(name) msm->name = NULL;
      #define MACHINE(fields) fields
      #include "wsp_server_method_machine.def"
      
      msm->transaction_id = wtp_handle;
      msm->state = NULL_METHOD;
      msm->addr_tuple = wap_addr_tuple_duplicate(sm->addr_tuple);
      msm->session_id = sm->session_id;

      list_append(sm->methodmachines, msm);

      return msm;
}



static void method_machine_destroy(void *p) {
      WSPMethodMachine *msm;

      if (p == NULL)
            return;

      msm = p;

      debug("wap.wsp", 0, "Destroying WSPMethodMachine %ld",
                  msm->transaction_id);

      #define INTEGER(name)
      #define ADDRTUPLE(name) wap_addr_tuple_destroy(msm->name);
      #define EVENT(name) wap_event_destroy(msm->name);
      #define MACHINE(fields) fields
      #include "wsp_server_method_machine.def"

      gw_free(msm);
}

static void handle_push_event(WSPMachine *sm, WSPPushMachine *pm, 
                              WAPEvent *current_event)
{
        if (pm == NULL) {
            warning(0, "No push machine for event.");
            wap_event_dump(current_event);
            return;
      }

        debug("wap.wsp", 0, "WSP(tid/pid): push %ld/%ld, state %s, event %s",
            pm->transaction_id, pm->server_push_id, state_name(pm->state), 
            wap_event_name(current_event->type));
      gw_assert(sm->session_id == pm->session_id);

        #define STATE_NAME(name)
      #define ROW(state_name, event, condition, action, next_state) \
            { \
                if (pm->state == state_name && \
                  current_event->type == event && \
                  (condition)) { \
                       action \
                       pm->state = next_state; \
                       debug("wap.wsp", 0, "WSP %ld/%ld: New push state %s", \
                             pm->session_id, pm->transaction_id, #next_state); \
                        goto end; \
                  } \
            }
      #include "wsp_server_push_states.def"

        cant_handle_event(sm, current_event);
end:
        if (pm->state == SERVER_PUSH_NULL_STATE) {
            push_machine_destroy(pm);
            list_delete_equal(sm->pushmachines, pm);
      }
}

static WSPPushMachine *push_machine_create(WSPMachine *sm, 
        long pid)
{
        WSPPushMachine *m;

        m = gw_malloc(sizeof(WSPPushMachine));

        #define INTEGER(name) m->name = 0;
        #define ADDRTUPLE(name) m->name = NULL;
        #define HTTPHEADER(name) m->name = http_create_empty_headers();
        #define MACHINE(fields) fields
        #include "wsp_server_push_machine.def"

        m->server_push_id = pid;
        m->transaction_id = pid;
      m->state = SERVER_PUSH_NULL_STATE;
      m->addr_tuple = wap_addr_tuple_duplicate(sm->addr_tuple);
      m->session_id = sm->session_id;

      list_append(sm->pushmachines, m);

      return m;      
}

static void push_machine_destroy(void *p)
{
        WSPPushMachine *m = NULL;   

      if (p == NULL)
             return;  
        m = p;
        debug("wap.wsp", 0, "Destroying WSPPushMachine %ld",
                  m->transaction_id);
        #define INTEGER(name) 
        #define ADDRTUPLE(name) wap_addr_tuple_destroy(m->name);
        #define HTTPHEADER(name) http_destroy_headers(m->name);
        #define MACHINE(fields) fields
        #include "wsp_server_push_machine.def"

        gw_free(m);
}

static char *state_name(WSPState state) {
      switch (state) {
      #define STATE_NAME(name) case name: return #name;
      #define ROW(state, event, cond, stmt, next_state)
      #include "wsp_server_session_states.def"

      #define STATE_NAME(name) case name: return #name;
      #define ROW(state, event, cond, stmt, next_state)
      #include "wsp_server_method_states.def"

        #define STATE_NAME(name) case name: return #name;
        #define ROW(state, event, cond, stmt, next_state)
        #include "wsp_server_push_states.def"

      default:
            return "unknown wsp state";
      }
}


static unsigned long next_wsp_session_id(void) {
      return counter_increase(session_id_counter);
}


static void sanitize_capabilities(List *caps, WSPMachine *m) {
      long i;
      Capability *cap;
      unsigned long ui;

      for (i = 0; i < list_len(caps); i++) {
            cap = list_get(caps, i);

            /* We only know numbered capabilities.  Let the application
             * layer negotiate whatever it wants for unknown ones. */
            if (cap->name != NULL)
                  continue;

            switch (cap->id) {
            case WSP_CAPS_CLIENT_SDU_SIZE:
                  /* Check if it's a valid uintvar.  The value is the
                   * max SDU size we will send, and there's no
                   * internal limit to that, so accept any value. */
                  if (cap->data != NULL &&
                      octstr_extract_uintvar(cap->data, &ui, 0) < 0)
                        goto bad_cap;
                  else
                        m->client_SDU_size = ui;
                  break;

            case WSP_CAPS_SERVER_SDU_SIZE:
                  /* Check if it's a valid uintvar */
                  if (cap->data != NULL &&
                      (octstr_extract_uintvar(cap->data, &ui, 0) < 0))
                        goto bad_cap;
                  /* XXX Our MRU is not quite unlimited, since we
                   * use signed longs in the library functions --
                   * should we make sure we limit the reply value
                   * to LONG_MAX?  (That's already a 2GB packet) */
                  break;

            case WSP_CAPS_PROTOCOL_OPTIONS:
                  /* Currently we don't support any Push, nor
                   * session resume, nor acknowledgement headers,
                   * so make sure those bits are not set. */
                  if (cap->data != NULL && octstr_len(cap->data) > 0
                     && (octstr_get_char(cap->data, 0) & 0xf0) != 0) {
                        warning(0, "WSP: Application layer tried to "
                              "negotiate protocol options.");
                        octstr_set_bits(cap->data, 0, 4, 0);
                  }
                  break;

            case WSP_CAPS_EXTENDED_METHODS:
                  /* XXX Check format here */
                  break;

            
            case WSP_CAPS_HEADER_CODE_PAGES:
                  /* We don't support any yet, so don't let this
                   * be negotiated. */
                  if (cap->data)
                        goto bad_cap;
                  break;
            }
            continue;

      bad_cap:
            error(0, "WSP: Found illegal value in capabilities reply.");
            wsp_cap_dump(cap);
            list_delete(caps, i, 1);
            i--;
            wsp_cap_destroy(cap);
            continue;
      }
}


static void reply_known_capabilities(List *caps, List *req, WSPMachine *m) {
      unsigned long ui;
      Capability *cap;
      Octstr *data;

      if (wsp_cap_count(caps, WSP_CAPS_CLIENT_SDU_SIZE, NULL) == 0) {
            if (wsp_cap_get_client_sdu(req, &ui) > 0) {
                  /* Accept value if it is not silly. */
                  if ((ui >= 256 && ui < LONG_MAX) || ui == 0) {
                        m->client_SDU_size = ui;
                  }
            }
            /* Reply with the client SDU we decided on */
            data = octstr_create("");
            octstr_append_uintvar(data, m->client_SDU_size);
            cap = wsp_cap_create(WSP_CAPS_CLIENT_SDU_SIZE,
                  NULL, data);
            list_append(caps, cap);
      }

      if (wsp_cap_count(caps, WSP_CAPS_SERVER_SDU_SIZE, NULL) == 0) {
            /* Accept whatever size the client is willing
             * to send.  If the client did not specify anything,
             * then use the default. */
            if (wsp_cap_get_server_sdu(req, &ui) <= 0) {
                  ui = 1400;
            }
            data = octstr_create("");
            octstr_append_uintvar(data, ui);
            cap = wsp_cap_create(WSP_CAPS_SERVER_SDU_SIZE, NULL, data);
            list_append(caps, cap);
      }

      /* Currently we cannot handle any protocol options */
      if (wsp_cap_count(caps, WSP_CAPS_PROTOCOL_OPTIONS, NULL) == 0) {
            data = octstr_create("");
            octstr_append_char(data, 0);
            cap = wsp_cap_create(WSP_CAPS_PROTOCOL_OPTIONS, NULL, data);
            list_append(caps, cap);
      }

      /* Accept any Method-MOR the client sent; if it sent none,
       * use the default. */
      if (wsp_cap_count(caps, WSP_CAPS_METHOD_MOR, NULL) == 0) {
            if (wsp_cap_get_method_mor(req, &ui) <= 0) {
                  ui = 1;
            }
            data = octstr_create("");
            octstr_append_char(data, ui);
            cap = wsp_cap_create(WSP_CAPS_METHOD_MOR, NULL, data);
            list_append(caps, cap);
      }

      /* We will never send any Push requests because we don't support
       * that yet.  But we already specified that in protocol options;
       * so, pretend we do, and handle the value that way. */
      if (wsp_cap_count(caps, WSP_CAPS_PUSH_MOR, NULL) == 0) {
            if (wsp_cap_get_push_mor(req, &ui) > 0) {
                  m->MOR_push = ui;
            }
            data = octstr_create("");
            octstr_append_char(data, m->MOR_push);
            cap = wsp_cap_create(WSP_CAPS_PUSH_MOR, NULL, data);
            list_append(caps, cap);
      }

      /* Supporting extended methods is up to the application layer,
       * not up to us.  If the application layer didn't specify any,
       * then we refuse whatever the client requested.  The default
       * is to support none, so we don't really have to add anything here. */

      /* We do not support any header code pages.  sanitize_capabilities
       * must have already deleted any reply that indicates otherwise.
       * Again, not adding anything here is the same as refusing support. */

      /* Listing aliases is something the application layer can do if
       * it wants to.  We don't care. */
}


/* Generate a refusal for all requested capabilities that are not
 * replied to. */
static void refuse_unreplied_capabilities(List *caps, List *req) {
      long i, len;
      Capability *cap;

      len = list_len(req);
      for (i = 0; i < len; i++) {
            cap = list_get(req, i);
            if (wsp_cap_count(caps, cap->id, cap->name) == 0) {
                  cap = wsp_cap_create(cap->id, cap->name, NULL);
                  list_append(caps, cap);
            }
      }
}


static int is_default_cap(Capability *cap) {
      unsigned long ui;

      /* All unknown values are empty by default */
      if (cap->name != NULL || cap->id < 0 || cap->id >= WSP_NUM_CAPS)
            return cap->data == NULL || octstr_len(cap->data) == 0;

      switch (cap->id) {
      case WSP_CAPS_CLIENT_SDU_SIZE:
      case WSP_CAPS_SERVER_SDU_SIZE:
            return (cap->data != NULL &&
                octstr_extract_uintvar(cap->data, &ui, 0) >= 0 &&
                ui == 1400);
      case WSP_CAPS_PROTOCOL_OPTIONS:
            return cap->data != NULL && octstr_get_char(cap->data, 0) == 0;
      case WSP_CAPS_METHOD_MOR:
      case WSP_CAPS_PUSH_MOR:
            return cap->data != NULL && octstr_get_char(cap->data, 0) == 1;
      case WSP_CAPS_EXTENDED_METHODS:
      case WSP_CAPS_HEADER_CODE_PAGES:
      case WSP_CAPS_ALIASES:
            return cap->data == NULL || octstr_len(cap->data) == 0;
      default:
            return 0;
      }
}


/* Remove any replies that have no corresponding request and that
 * are equal to the default. */
static void strip_default_capabilities(List *caps, List *req) {
      long i;
      Capability *cap;
      int count;

      /* Hmm, this is an O(N*N) operation, which may be bad. */

      i = 0;
      while (i < list_len(caps)) {
            cap = list_get(caps, i);

            count = wsp_cap_count(req, cap->id, cap->name);
            if (count == 0 && is_default_cap(cap)) {
                  list_delete(caps, i, 1);
                  wsp_cap_destroy(cap);
            } else {
                  i++;
            }
      }
}


static List *make_capabilities_reply(WSPMachine *m) {
      List *caps;

      /* In principle, copy the application layer's capabilities
       * response, add refusals for all unknown requested capabilities,
       * and add responses for all known capabilities that are
       * not already responded to.  Then eliminate any replies that
       * would have no effect because they are equal to the default. */

      caps = wsp_cap_duplicate_list(m->reply_caps);

      /* Don't let the application layer negotiate anything we
       * cannot handle.  Also parse the values it set if we're
       * interested. */
      sanitize_capabilities(caps, m);

      /* Add capability records for all capabilities we know about
       * that are not already in the reply list. */
      reply_known_capabilities(caps, m->request_caps, m);

      /* All remaining capabilities in the request list that are
       * not in the reply list at this point must be unknown ones
       * that we want to refuse. */
      refuse_unreplied_capabilities(caps, m->request_caps);

      /* Now eliminate replies that would be equal to the requested
       * value, or (if there was none) to the default value. */
      strip_default_capabilities(caps, m->request_caps);

      return caps;
}


static List *make_reply_headers(WSPMachine *m)
{
    List *headers;
    Octstr *encoding_version;

    /* Add all server wsp level hop-by-hop headers. Currently only 
     * Encoding-Version, as defined by wsp, chapter 8.4.2.70. 
     * What headers belong to which version is defined in appendix A,
     * table 39.. 
    encoding_version = request_version = NULL;
     * Essentially, if the client sends us an Encoding-Version
     * higher than ours (1.3) we send our version number to it,
     * if it is lower, we left version number intact. */
    /* First the case that we have no Encoding-Version header at all. 
     * This case we must assume that the client supports version 1.2
     * or lower. */

    headers = http_create_empty_headers();
    encoding_version = wsp_encoding_version_to_string(m->encoding_version);
    http_header_add(headers, "Encoding-Version", octstr_get_cstr(encoding_version));
    octstr_destroy(encoding_version);

    return headers;
}

static Octstr *make_connectreply_pdu(WSPMachine *m) 
{
    WSP_PDU *pdu;
    Octstr *os;
    List *caps;
    List *reply_headers;
      
    pdu = wsp_pdu_create(ConnectReply);

    pdu->u.ConnectReply.sessionid = m->session_id;

    caps = make_capabilities_reply(m);
    pdu->u.ConnectReply.capabilities = wsp_cap_pack_list(caps);
    wsp_cap_destroy_list(caps);

    reply_headers = make_reply_headers(m);
    pdu->u.ConnectReply.headers = 
        wsp_headers_pack(reply_headers, 0, m->encoding_version);
    http_destroy_headers(reply_headers);
      
    os = wsp_pdu_pack(pdu);
    wsp_pdu_destroy(pdu);

    return os;
}


static Octstr *make_resume_reply_pdu(WSPMachine *m, List *headers) 
{
    WSP_PDU *pdu;
    Octstr *os;

    pdu = wsp_pdu_create(Reply);

    /* Not specified for Resume replies */
    pdu->u.Reply.status = wsp_convert_http_status_to_wsp_status(HTTP_OK);
    if (headers == NULL) {
        headers = http_create_empty_headers();
        pdu->u.Reply.headers = wsp_headers_pack(headers, 1, m->encoding_version);
        http_destroy_headers(headers);
    } else {
        pdu->u.Reply.headers = wsp_headers_pack(headers, 1, m->encoding_version);
    }
    pdu->u.Reply.data = octstr_create("");

    os = wsp_pdu_pack(pdu);
    wsp_pdu_destroy(pdu);

    return os;
}

static WSP_PDU *make_confirmedpush_pdu(WAPEvent *e)
{
        WSP_PDU *pdu;
        List *headers;

        pdu = wsp_pdu_create(ConfirmedPush);
/*
 * Both push headers and push body are optional. 
 */
        if (e->u.S_ConfirmedPush_Req.push_headers == NULL) {
          headers = http_create_empty_headers();
            pdu->u.ConfirmedPush.headers = wsp_headers_pack(headers, 1, WSP_1_2);
            http_destroy_headers(headers);
        } else
            pdu->u.ConfirmedPush.headers = 
                wsp_headers_pack(e->u.S_ConfirmedPush_Req.push_headers, 1, WSP_1_2);
   
        if (e->u.S_ConfirmedPush_Req.push_body == NULL)
          pdu->u.ConfirmedPush.data = octstr_create("");
        else
          pdu->u.ConfirmedPush.data = 
                octstr_duplicate(e->u.S_ConfirmedPush_Req.push_body);        

        return pdu;
}

static WSP_PDU *make_push_pdu(WAPEvent *e)
{
        WSP_PDU *pdu;
        List *headers;

        pdu = wsp_pdu_create(Push);
/*
 * Both push headers and push body are optional
 */
        if (e->u.S_Push_Req.push_headers == NULL) {
          headers = http_create_empty_headers();
            pdu->u.Push.headers = wsp_headers_pack(headers, 1, WSP_1_2);
            http_destroy_headers(headers);
        } else
            pdu->u.Push.headers = 
                wsp_headers_pack(e->u.S_Push_Req.push_headers, 1, WSP_1_2);
   
        if (e->u.S_Push_Req.push_body == NULL)
          pdu->u.Push.data = octstr_create("");
        else
          pdu->u.Push.data = 
                octstr_duplicate(e->u.S_Push_Req.push_body);        

        return pdu;
}

static int transaction_belongs_to_session(void *wsp_ptr, void *tuple_ptr) {
      WSPMachine *wsp;
      WAPAddrTuple *tuple;
      
      wsp = wsp_ptr;
      tuple = tuple_ptr;

      return wap_addr_tuple_same(wsp->addr_tuple, tuple);
}


static int find_by_session_id(void *wsp_ptr, void *id_ptr) {
      WSPMachine *wsp = wsp_ptr;
      long *idp = id_ptr;
      
      return wsp->session_id == *idp;
}


static int find_by_method_id(void *wspm_ptr, void *id_ptr) {
      WSPMethodMachine *msm = wspm_ptr;
      long *idp = id_ptr;

      return msm->transaction_id == *idp;
}

static int find_by_push_id(void *m_ptr, void *id_ptr) {
      WSPPushMachine *m = m_ptr;
      long *idp = id_ptr;

      return m->transaction_id == *idp;
}

static WSPMethodMachine *find_method_machine(WSPMachine *sm, long id) {
      return list_search(sm->methodmachines, &id, find_by_method_id);
}

static WSPPushMachine *find_push_machine(WSPMachine *m, long id)
{
       return list_search(m->pushmachines, &id, find_by_push_id);
}

static int same_client(void *a, void *b) {
      WSPMachine *sm1, *sm2;
      
      sm1 = a;
      sm2 = b;
      return wap_addr_tuple_same(sm1->addr_tuple, sm2->addr_tuple);
}


static void disconnect_other_sessions(WSPMachine *sm) {
      List *old_sessions;
      WAPEvent *disconnect;
      WSPMachine *sm2;
      long i;

      old_sessions = list_search_all(session_machines, sm, same_client);
      if (old_sessions == NULL)
            return;

      for (i = 0; i < list_len(old_sessions); i++) {
            sm2 = list_get(old_sessions, i);
            if (sm2 != sm) {
                  disconnect = wap_event_create(Disconnect_Event);
                  handle_session_event(sm2, disconnect, NULL);
            }
      }

      list_destroy(old_sessions, NULL);
}


static List *unpack_new_headers(WSPMachine *sm, Octstr *hdrs) {
      List *new_headers;

      if (hdrs && octstr_len(hdrs) > 0) {
            new_headers = wsp_headers_unpack(hdrs, 0);
            if (sm->http_headers == NULL)
                  sm->http_headers = http_create_empty_headers();
            http_header_combine(sm->http_headers, new_headers);
            return new_headers;
      }
      return NULL;
}

static WAPEvent *make_abort(long reason, long handle)
{
        WAPEvent *wtp_event;

        wtp_event = wap_event_create(TR_Abort_Req);
        wtp_event->u.TR_Abort_Req.abort_type = 0x01;
        wtp_event->u.TR_Abort_Req.abort_reason = reason;
        wtp_event->u.TR_Abort_Req.handle = handle;

        return wtp_event;
}

static void send_abort(long reason, long handle) {
        WAPEvent *wtp_event;

      wtp_event = make_abort(reason, handle);
      dispatch_to_wtp_resp(wtp_event);
}

static void send_abort_to_initiator(long reason, long handle)
{
       WAPEvent *wtp_event;

       wtp_event = make_abort(reason, handle);
       dispatch_to_wtp_init(wtp_event);
}

/*
 * The server sends invoke (to be exact, makes TR-Invoke.req) only when it is 
 * pushing. (Only the client disconnects sessions.)
 */ 
static void send_invoke(WSPMachine *m, WSP_PDU *pdu, WAPEvent *e, long class)
{
        WAPEvent *wtp_event;

        wtp_event = wap_event_create(TR_Invoke_Req);
        wtp_event->u.TR_Invoke_Req.addr_tuple = 
          wap_addr_tuple_duplicate(m->addr_tuple);
/*
 * There is no mention of acknowledgement type in the specs. But because 
 * confirmed push is confirmed after response from OTA, provider acknowledge-
 * ments seem redundant.
 */
      wtp_event->u.TR_Invoke_Req.up_flag = USER_ACKNOWLEDGEMENT;
        wtp_event->u.TR_Invoke_Req.tcl = class;
        if (e->type == S_ConfirmedPush_Req)
           wtp_event->u.TR_Invoke_Req.handle = 
               e->u.S_ConfirmedPush_Req.server_push_id;
      wtp_event->u.TR_Invoke_Req.user_data = wsp_pdu_pack(pdu);

        wsp_pdu_destroy(pdu);
        dispatch_to_wtp_init(wtp_event);
}

static void indicate_disconnect(WSPMachine *sm, long reason) {
      WAPEvent *new_event;

      new_event = wap_event_create(S_Disconnect_Ind);
      new_event->u.S_Disconnect_Ind.reason_code = reason;
      new_event->u.S_Disconnect_Ind.redirect_security = 0;
      new_event->u.S_Disconnect_Ind.redirect_addresses = 0;
      new_event->u.S_Disconnect_Ind.error_headers = NULL;
      new_event->u.S_Disconnect_Ind.error_body = NULL;
      new_event->u.S_Disconnect_Ind.session_handle = sm->session_id;
      dispatch_to_appl(new_event);
}


static void indicate_suspend(WSPMachine *sm, long reason) {
      WAPEvent *new_event;

      new_event = wap_event_create(S_Suspend_Ind);
      new_event->u.S_Suspend_Ind.reason = reason;
      new_event->u.S_Suspend_Ind.session_id = sm->session_id;
      dispatch_to_appl(new_event);
}


static void indicate_resume(WSPMachine *sm,
                                WAPAddrTuple *tuple, List *headers) {
      WAPEvent *new_event;

      new_event = wap_event_create(S_Resume_Ind);
      new_event->u.S_Resume_Ind.addr_tuple = wap_addr_tuple_duplicate(tuple);
      new_event->u.S_Resume_Ind.client_headers = http_header_duplicate(headers);
      new_event->u.S_Resume_Ind.session_id = sm->session_id;
      dispatch_to_appl(new_event);
}

static void indicate_pushabort(WSPPushMachine *spm, long reason)
{
       WAPEvent *ota_event;
 
       ota_event = wap_event_create(S_PushAbort_Ind);
       ota_event->u.S_PushAbort_Ind.push_id = spm->server_push_id;
       ota_event->u.S_PushAbort_Ind.reason = reason;
       ota_event->u.S_PushAbort_Ind.session_id = spm->session_id;
       dispatch_to_appl(ota_event);
}

static void confirm_push(WSPPushMachine *m)
{
       WAPEvent *ota_event;

       ota_event = wap_event_create(S_ConfirmedPush_Cnf);
       ota_event->u.S_ConfirmedPush_Cnf.server_push_id = m->server_push_id;
       ota_event->u.S_ConfirmedPush_Cnf.session_id = m->session_id;
       dispatch_to_appl(ota_event);
}

static void method_abort(WSPMethodMachine *msm, long reason) {
      WAPEvent *wtp_event;

      /* Send TR-Abort.req(reason) */
      wtp_event = wap_event_create(TR_Abort_Req);
      /* FIXME: Specs are unclear about this; we may indeed have to
       * guess abort whether this is a WSP or WTP level abort code */
      if (reason < WSP_ABORT_PROTOERR) {
            wtp_event->u.TR_Abort_Req.abort_type = 0x00;
      } else {
            wtp_event->u.TR_Abort_Req.abort_type = 0x01;
      }
      wtp_event->u.TR_Abort_Req.abort_reason = reason;
      wtp_event->u.TR_Abort_Req.handle = msm->transaction_id;

      dispatch_to_wtp_resp(wtp_event);
}


static void indicate_method_abort(WSPMethodMachine *msm, long reason) {
      WAPEvent *new_event;

      /* Send S-MethodAbort.ind(reason) */
      new_event = wap_event_create(S_MethodAbort_Ind);
      new_event->u.S_MethodAbort_Ind.transaction_id = msm->transaction_id;
      new_event->u.S_MethodAbort_Ind.reason = reason;
      new_event->u.S_MethodAbort_Ind.session_handle = msm->session_id;
      dispatch_to_appl(new_event);
}

      
static int method_is_holding(void *item, void *pattern) {
      WSPMethodMachine *msm = item;

      return msm->state == HOLDING;
}


static void release_holding_methods(WSPMachine *sm) {
      WAPEvent *release;
      WSPMethodMachine *msm;
      List *holding;
      long i, len;

      holding = list_search_all(sm->methodmachines, NULL, method_is_holding);
      if (holding == NULL)
            return;

      /* We can re-use this because wsp_handle_method_event does not
       * destroy its event */
      release = wap_event_create(Release_Event);

      len = list_len(holding);
      for (i = 0; i < len; i++) {
            msm = list_get(holding, i);
            handle_method_event(sm, msm, release, NULL);
      }
      list_destroy(holding, NULL);
      wap_event_destroy(release);
}


static void abort_methods(WSPMachine *sm, long reason) {
      WAPEvent *ab;
      WSPMethodMachine *msm;
      long i, len;

      ab = wap_event_create(Abort_Event);
      ab->u.Abort_Event.reason = reason;

      /* This loop goes backward because it has to deal with the
       * possibility of method machines disappearing after their event. */
      len = list_len(sm->methodmachines);
      for (i = len - 1; i >= 0; i--) {
            msm = list_get(sm->methodmachines, i);
            handle_method_event(sm, msm, ab, NULL);
      }

      wap_event_destroy(ab);
}


static void abort_pushes(WSPMachine *sm, long reason)
{
        WAPEvent *ab;
      WSPPushMachine *psm;
      long i, len;

        ab = wap_event_create(Abort_Event);
      ab->u.Abort_Event.reason = reason;

        len = list_len(sm->pushmachines);
      for (i = len - 1; i >= 0; i--) {
            psm = list_get(sm->pushmachines, i);
            handle_push_event(sm, psm, ab);
      }

        wap_event_destroy(ab);
}


WSPMachine *find_session_machine_by_id (int id) {

      return list_search(session_machines, &id, id_belongs_to_session);
}


static int id_belongs_to_session (void *wsp_ptr, void *pid) {
      WSPMachine *wsp;
      int *id;

      wsp = wsp_ptr;
      id = (int *) pid;

      if (*id == wsp->session_id) return 1;
      return 0;
}


static int wsp_encoding_string_to_version(Octstr *enc) 
{
    int v;
    
    /* default will be WSP 1.2, as defined by WAPWSP */
    v = WSP_1_2;    

    if (octstr_compare(enc, octstr_imm("1.1")) == 0) {
        v = WSP_1_1;
    }
    else if (octstr_compare(enc, octstr_imm("1.2")) == 0) {
        v = WSP_1_2;
    }
    else if (octstr_compare(enc, octstr_imm("1.3")) == 0) {
        v = WSP_1_3;
    }
    else if (octstr_compare(enc, octstr_imm("1.4")) == 0) {
        v = WSP_1_4;
    }
    else if (octstr_compare(enc, octstr_imm("1.5")) == 0) {
        v = WSP_1_5;
    }

    return v;
}

static Octstr *wsp_encoding_version_to_string(int version) 
{
    Octstr *os;
    
    switch (version) {
        case WSP_1_1:
            os = octstr_create("1.1");
            break;
        case WSP_1_2:
            os = octstr_create("1.2");
            break;
        case WSP_1_3:
            os = octstr_create("1.3");
            break;
        case WSP_1_4:
            os = octstr_create("1.4");
            break;
        case WSP_1_5:
            os = octstr_create("1.5");
            break;
        default:
            os = octstr_create("1.2");
            break;
    }
    
    return os;
}


Generated by  Doxygen 1.6.0   Back to index