Changeset 1850


Ignore:
Timestamp:
Mar 8, 2008 12:54:04 AM (16 years ago)
Author:
bennylp
Message:

More work on ticket #485: more TURN-07 work

Location:
pjproject/trunk/pjnath
Files:
1 added
5 edited

Legend:

Unmodified
Added
Removed
  • pjproject/trunk/pjnath/build/pjturn_srv.dsp

    r1812 r1850  
    8888# Begin Source File 
    8989 
     90SOURCE="..\src\pjturn-srv\allocation.c" 
     91# End Source File 
     92# Begin Source File 
     93 
    9094SOURCE="..\src\pjturn-srv\listener_udp.c" 
    9195# End Source File 
  • pjproject/trunk/pjnath/include/pjnath/stun_msg.h

    r1811 r1850  
    6868 
    6969    /** 
    70      * STUN/TURN Send Indication as defined by draft-ietf-behave-turn 
     70     * STUN/TURN Refresh method as defined by draft-ietf-behave-turn 
    7171     */ 
    7272    PJ_STUN_REFRESH_METHOD                  = 4, 
     73 
     74    /** 
     75     * STUN/TURN Send indication as defined by draft-ietf-behave-turn 
     76     */ 
     77    PJ_STUN_SEND_METHOD                     = 6, 
     78 
     79    /** 
     80     * STUN/TURN Data indication as defined by draft-ietf-behave-turn 
     81     */ 
     82    PJ_STUN_DATA_METHOD                     = 7, 
     83 
     84    /** 
     85     * STUN/TURN ChannelBind as defined by draft-ietf-behave-turn 
     86     */ 
     87    PJ_STUN_CHANNEL_BIND_METHOD             = 9, 
    7388 
    7489    /** 
     
    177192    PJ_STUN_BINDING_ERROR_RESPONSE          = 0x0111, 
    178193 
     194 
    179195    /** 
    180196     * STUN SHARED-SECRET reqeust. 
     
    192208    PJ_STUN_SHARED_SECRET_ERROR_RESPONSE    = 0x0112, 
    193209 
     210 
    194211    /** 
    195212     * STUN/TURN Allocate Request 
     
    207224    PJ_STUN_ALLOCATE_ERROR_RESPONSE         = 0x0113, 
    208225 
     226 
    209227    /** 
    210228     * STUN/TURN REFRESH Request 
     
    220238     * Error response to STUN REFRESH request. 
    221239     */ 
    222     PJ_STUN_REFRESH_ERROR_RESPONSE          = 0x0114 
     240    PJ_STUN_REFRESH_ERROR_RESPONSE          = 0x0114, 
     241 
     242 
     243    /** 
     244     * TURN Send indication 
     245     */ 
     246    PJ_STUN_SEND_INDICATION                 = 0x0016, 
     247 
     248 
     249    /** 
     250     * TURN Data indication 
     251     */ 
     252    PJ_STUN_DATA_INDICATION                 = 0x0017, 
     253 
     254 
     255    /** 
     256     * STUN/TURN ChannelBind Request 
     257     */ 
     258    PJ_STUN_CHANNEL_BIND_REQUEST            = 0x0009, 
     259 
     260    /** 
     261     * Successful response to STUN ChannelBind request 
     262     */ 
     263    PJ_STUN_CHANNEL_BIND_RESPONSE           = 0x0109, 
     264 
     265    /** 
     266     * Error response to STUN ChannelBind request. 
     267     */ 
     268    PJ_STUN_CHANNEL_BIND_ERROR_RESPONSE     = 0x0119 
    223269 
    224270} pj_stun_msg_type; 
     
    948994 * number is returned in host byte order. 
    949995 */ 
    950 #define PJ_STUN_GET_RPP_PORT(u32)   pj_ntons((pj_uint16_t)(u32 & 0x0000FFFFL)) 
     996#define PJ_STUN_GET_RPP_PORT(u32)   pj_ntohs((pj_uint16_t)(u32 & 0x0000FFFFL)) 
    951997 
    952998/** 
  • pjproject/trunk/pjnath/src/pjnath/stun_msg.c

    r1811 r1850  
    3838    "Unknown",                  /* 0 */ 
    3939    "Binding",                  /* 1 */ 
    40     "Shared Secret",            /* 2 */ 
     40    "SharedSecret",             /* 2 */ 
    4141    "Allocate",                 /* 3 */ 
    4242    "Refresh",                  /* 4 */ 
     43    "???",                      /* 5 */ 
     44    "Send",                     /* 6 */ 
     45    "Data",                     /* 7 */ 
     46    "???",                      /* 8 */ 
     47    "ChannelBind",              /* 9 */ 
    4348}; 
    4449 
  • pjproject/trunk/pjnath/src/pjturn-srv/server.c

    r1812 r1850  
    3636 
    3737 
    38 /* Globals */ 
    39 PJ_DEF_DATA(int) PJTURN_TP_UDP = 1; 
    40 PJ_DEF_DATA(int) PJTURN_TP_TCP = 2; 
    41 PJ_DEF_DATA(int) PJTURN_TP_TLS = 3; 
    42  
    4338/* Prototypes */ 
    4439static pj_status_t on_tx_stun_msg( pj_stun_session *sess, 
     
    5449                                      unsigned src_addr_len); 
    5550 
     51/* 
     52 * Get transport type name. 
     53 */ 
     54PJ_DEF(const char*) pjturn_tp_type_name(int tp_type) 
     55{ 
     56    /* Must be 3 characters long! */ 
     57    if (tp_type == PJTURN_TP_UDP) 
     58        return "UDP"; 
     59    else if (tp_type == PJTURN_TP_TCP) 
     60        return "TCP"; 
     61    else 
     62        return "???"; 
     63} 
    5664 
    5765/* 
     
    98106    srv->tables.alloc = pj_hash_create(pool, MAX_CLIENTS); 
    99107    srv->tables.res = pj_hash_create(pool, MAX_CLIENTS); 
    100     srv->tables.peer = pj_hash_create(pool, MAX_CLIENTS*MAX_PEERS_PER_CLIENT); 
    101108 
    102109    /* Init ports settings */ 
     
    162169    lis->id = index; 
    163170    srv->core.lis_cnt++; 
     171 
     172    return PJ_SUCCESS; 
     173} 
     174 
     175/** 
     176 * Register an allocation. 
     177 */ 
     178PJ_DEF(pj_status_t) pjturn_srv_register_allocation(pjturn_srv *srv, 
     179                                                   pjturn_allocation *alloc) 
     180{ 
     181    /* Add to hash tables */ 
     182    pj_lock_acquire(srv->core.lock); 
     183    pj_hash_set(alloc->pool, srv->tables.alloc, 
     184                &alloc->hkey, sizeof(alloc->hkey), 0, alloc); 
     185    pj_hash_set(alloc->pool, srv->tables.res, 
     186                &alloc->relay.hkey, sizeof(alloc->relay.hkey), 0, 
     187                &alloc->relay); 
     188    pj_lock_release(srv->core.lock); 
     189 
     190    return PJ_SUCCESS; 
     191} 
     192 
     193/** 
     194 * Unregister an allocation. 
     195 */ 
     196PJ_DEF(pj_status_t) pjturn_srv_unregister_allocation(pjturn_srv *srv, 
     197                                                     pjturn_allocation *alloc) 
     198{ 
     199    /* Unregister from hash tables */ 
     200    pj_lock_acquire(srv->core.lock); 
     201    pj_hash_set(alloc->pool, srv->tables.alloc, 
     202                &alloc->hkey, sizeof(alloc->hkey), 0, NULL); 
     203    pj_hash_set(alloc->pool, srv->tables.res, 
     204                &alloc->relay.hkey, sizeof(alloc->relay.hkey), 0, NULL); 
     205    pj_lock_release(srv->core.lock); 
    164206 
    165207    return PJ_SUCCESS; 
     
    185227 
    186228/* Create and send error response */ 
    187 static pj_status_t respond_error(pj_stun_sess *sess, const pj_stun_msg *req, 
    188                                  pj_bool_t cache, int code, const char *err_msg, 
    189                                  const pj_sockaddr_t *addr, unsigned addr_len) 
     229static pj_status_t respond_error(pj_stun_session *sess, const pj_stun_msg *req, 
     230                                 pj_bool_t cache, int code, const char *errmsg, 
     231                                 const pj_sockaddr_t *dst_addr,  
     232                                 unsigned addr_len) 
    190233{ 
    191234    pj_status_t status; 
     
    194237 
    195238    status = pj_stun_session_create_res(sess, req,  
    196                                         code, (err_msg?pj_cstr(&reason,err_msg):NULL),  
     239                                        code, (errmsg?pj_cstr(&reason,errmsg):NULL),  
    197240                                        &tdata); 
    198241    if (status != PJ_SUCCESS) 
    199         return statys; 
     242        return status; 
    200243 
    201244    status = pj_stun_session_send_msg(sess, cache, dst_addr,  addr_len, tdata); 
     
    221264 
    222265    /* Get BANDWIDTH attribute, if any. */ 
    223     attr_bw = pj_stun_msg_find_attr(msg, PJ_STUN_BANDWIDTH_ATTR, 0); 
     266    attr_bw = (pj_stun_uint_attr*) 
     267              pj_stun_msg_find_attr(req, PJ_STUN_ATTR_BANDWIDTH, 0); 
    224268    if (attr_bw) { 
    225269        cfg->bandwidth = attr_bw->value; 
     
    230274    /* Check if we can satisfy the bandwidth */ 
    231275    if (cfg->bandwidth > MAX_CLIENT_BANDWIDTH) { 
    232         respond_error(sess, msg, PJ_FALSE,  
     276        respond_error(sess, req, PJ_FALSE,  
    233277                      PJ_STUN_SC_ALLOCATION_QUOTA_REACHED,  
    234278                      "Invalid bandwidth", src_addr, src_addr_len); 
     
    237281 
    238282    /* Get REQUESTED-TRANSPORT attribute, is any */ 
    239     attr_req_tp = pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_REQ_TRANSPORT, 0); 
     283    attr_req_tp = (pj_stun_uint_attr*) 
     284                  pj_stun_msg_find_attr(req, PJ_STUN_ATTR_REQ_TRANSPORT, 0); 
    240285    if (attr_req_tp) { 
    241286        cfg->tp_type = PJ_STUN_GET_RT_PROTO(attr_req_tp->value); 
     
    246291    /* Can only support UDP for now */ 
    247292    if (cfg->tp_type != PJTURN_TP_UDP) { 
    248         respond_error(sess, msg, PJ_FALSE,  
     293        respond_error(sess, req, PJ_FALSE,  
    249294                      PJ_STUN_SC_UNSUPP_TRANSPORT_PROTO,  
    250295                      NULL, src_addr, src_addr_len); 
     
    253298 
    254299    /* Get REQUESTED-IP attribute, if any */ 
    255     attr_req_ip = pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_REQ_IP, 0); 
     300    attr_req_ip = (pj_stun_sockaddr_attr*) 
     301                  pj_stun_msg_find_attr(req, PJ_STUN_ATTR_REQ_IP, 0); 
    256302    if (attr_req_ip) { 
    257         pj_memcpy(&cfg->addr, &attr_req_ip->sockaddr,  
    258                   sizeof(attr_req_ip->sockaddr)); 
     303        pj_sockaddr_print(&attr_req_ip->sockaddr, cfg->addr,  
     304                          sizeof(cfg->addr), 0); 
    259305    } 
    260306 
    261307    /* Get REQUESTED-PORT-PROPS attribute, if any */ 
    262     attr_rpp = pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_REQ_PORT_PROPS, 0); 
     308    attr_rpp = (pj_stun_uint_attr*) 
     309               pj_stun_msg_find_attr(req, PJ_STUN_ATTR_REQ_PORT_PROPS, 0); 
    263310    if (attr_rpp) { 
    264311        cfg->rpp_bits = PJ_STUN_GET_RPP_BITS(attr_rpp->value); 
     
    270317 
    271318    /* Get LIFETIME attribute */ 
    272     attr_lifetime = pj_stun_msg_find_attr(msg, PJ_STUN_ATTR_LIFETIME, 0); 
     319    attr_lifetime = (pj_stun_uint_attr*) 
     320                    pj_stun_msg_find_attr(req, PJ_STUN_ATTR_LIFETIME, 0); 
    273321    if (attr_lifetime) { 
    274322        cfg->lifetime = attr_lifetime->value; 
    275323        if (cfg->lifetime < MIN_LIFETIME || cfg->lifetime > MAX_LIFETIME) { 
    276             respond_error(sess, msg, PJ_FALSE,  
     324            respond_error(sess, req, PJ_FALSE,  
    277325                          PJ_STUN_SC_BAD_REQUEST,  
    278326                          "Invalid LIFETIME value", src_addr,  
     
    296344{ 
    297345    pjturn_listener *listener; 
     346    pjturn_srv *srv; 
    298347    pjturn_allocation_req req; 
     348    pjturn_allocation *alloc; 
     349    pj_stun_tx_data *tdata; 
    299350    pj_status_t status; 
    300351 
    301352    listener = (pjturn_listener*) pj_stun_session_get_user_data(sess); 
     353    srv = listener->server; 
    302354 
    303355    /* Handle strayed REFRESH request */ 
     
    322374        return status; 
    323375 
    324     /* Ready to allocate now */ 
    325  
     376    /* Create new allocation. The relay resource will be allocated 
     377     * in this function. 
     378     */ 
     379    status = pjturn_allocation_create(listener, src_addr, src_addr_len, 
     380                                      msg, &req, &alloc); 
     381    if (status != PJ_SUCCESS) { 
     382        char errmsg[PJ_ERR_MSG_SIZE]; 
     383 
     384        pj_strerror(status, errmsg, sizeof(errmsg)); 
     385        return respond_error(sess, msg, PJ_FALSE, PJ_STUN_SC_SERVER_ERROR, 
     386                             errmsg, src_addr, src_addr_len); 
     387    } 
     388 
     389    /* Respond the original ALLOCATE request */ 
     390    status = pj_stun_session_create_res(srv->core.stun_sess[listener->id], 
     391                                        msg, 0, NULL, &tdata); 
     392    if (status != PJ_SUCCESS) { 
     393        char errmsg[PJ_ERR_MSG_SIZE]; 
     394 
     395        pjturn_allocation_destroy(alloc); 
     396 
     397        pj_strerror(status, errmsg, sizeof(errmsg)); 
     398        return respond_error(sess, msg, PJ_FALSE, PJ_STUN_SC_SERVER_ERROR, 
     399                             errmsg, src_addr, src_addr_len); 
     400    } 
     401 
     402    /* Add RELAYED-ADDRESS attribute */ 
     403    pj_stun_msg_add_sockaddr_attr(tdata->pool, tdata->msg, 
     404                                  PJ_STUN_ATTR_RELAY_ADDR, PJ_TRUE, 
     405                                  &alloc->relay.hkey.addr, 
     406                                  pj_sockaddr_get_len(&alloc->relay.hkey.addr)); 
     407 
     408    /* Add LIFETIME. */ 
     409    pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, 
     410                              PJ_STUN_ATTR_LIFETIME,  
     411                              (unsigned)alloc->relay.lifetime); 
     412 
     413    /* Add BANDWIDTH */ 
     414    pj_stun_msg_add_uint_attr(tdata->pool, tdata->msg, 
     415                              PJ_STUN_ATTR_BANDWIDTH, 
     416                              alloc->bandwidth); 
     417 
     418    /* Add RESERVATION-TOKEN */ 
     419    PJ_TODO(ADD_RESERVATION_TOKEN); 
     420 
     421    /* Add XOR-MAPPED-ADDRESS */ 
     422    pj_stun_msg_add_sockaddr_attr(tdata->pool, tdata->msg, 
     423                                  PJ_STUN_ATTR_XOR_MAPPED_ADDR, PJ_TRUE, 
     424                                  &alloc->hkey.clt_addr, 
     425                                  pj_sockaddr_get_len(&alloc->hkey.clt_addr)); 
     426     
     427    /* Send the response */ 
     428    pj_stun_session_send_msg(srv->core.stun_sess[listener->id], PJ_TRUE, 
     429                             src_addr, src_addr_len, tdata); 
     430 
     431    /* Done. */ 
     432    return PJ_SUCCESS; 
    326433} 
    327434 
     
    331438                               pjturn_pkt *pkt) 
    332439{ 
    333     pj_stun_msg *req, *res; 
    334440    unsigned options, lis_id; 
    335441    pj_status_t status; 
     
    392498     */ 
    393499    if (alloc) { 
    394         pjturn_allocation_on_rx_pkt(alloc, pkt); 
     500        pjturn_allocation_on_rx_client_pkt(alloc, pkt); 
    395501    } else { 
    396502        /* Otherwise this is a new client */ 
  • pjproject/trunk/pjnath/src/pjturn-srv/turn.h

    r1812 r1850  
    3434#define PJTURN_NO_TIMEOUT           ((long)0x7FFFFFFF) 
    3535#define PJTURN_MAX_PKT_LEN          3000 
     36#define PJTURN_PERM_TIMEOUT         300 
     37#define PJTURN_CHANNEL_TIMEOUT      600 
    3638 
    3739/** Transport types */ 
     
    4143}; 
    4244 
     45/**  
     46 * Get transport type name string. 
     47 */ 
     48PJ_DECL(const char*) pjturn_tp_type_name(int tp_type); 
    4349 
    4450/** 
     
    5561        /** Transport/relay address */ 
    5662        pj_sockaddr         addr; 
    57     } key; 
    58  
    59     /** Pool for this resource. */ 
    60     pj_pool_t       *pool; 
    61  
    62     /** Mutex */ 
    63     pj_lock_t       *lock; 
     63    } hkey; 
    6464 
    6565    /** Allocation who requested or reserved this resource. */ 
    6666    pjturn_allocation *allocation; 
    6767 
    68     /** Time when this resource times out */ 
    69     pj_time_val     timeout; 
    70  
    7168    /** Username used in credential */ 
    7269    pj_str_t        user; 
     
    7572    pj_str_t        realm; 
    7673 
    77     /** Transport/relay socket */ 
    78     pj_sock_t       sock; 
     74    /** Lifetime, in seconds. */ 
     75    unsigned        lifetime; 
     76 
     77    /** Relay/allocation expiration time */ 
     78    pj_time_val     expiry; 
     79 
     80    /** Timeout timer entry */ 
     81    pj_timer_entry  timer; 
     82 
     83    /** Transport. */ 
     84    struct { 
     85        /** Transport/relay socket */ 
     86        pj_sock_t           sock; 
     87 
     88        /** Transport/relay ioqueue */ 
     89        pj_ioqueue_key_t    *key; 
     90 
     91        /** Read operation key. */ 
     92        pj_ioqueue_op_key_t read_key; 
     93 
     94        /** The incoming packet buffer */ 
     95        char                rx_pkt[PJTURN_MAX_PKT_LEN]; 
     96 
     97        /** Source address of the packet. */ 
     98        pj_sockaddr         src_addr; 
     99 
     100        /** Source address length */ 
     101        int                 src_addr_len; 
     102 
     103        /** The outgoing packet buffer. This must be 3wbit aligned. */ 
     104        char                tx_pkt[PJTURN_MAX_PKT_LEN+4]; 
     105    } tp; 
    79106}; 
    80107 
     
    105132 
    106133    /** Requested IP */ 
    107     pj_sockaddr         addr; 
     134    char                addr[PJ_INET6_ADDRSTRLEN]; 
    108135 
    109136    /** Requested bandwidth */ 
     
    128155{ 
    129156    /** Hash table key to identify client. */ 
    130     pjturn_allocation_key key; 
     157    pjturn_allocation_key hkey; 
    131158 
    132159    /** Pool for this allocation. */ 
    133160    pj_pool_t           *pool; 
    134161 
     162    /** Object name for logging identification */ 
     163    char                *obj_name; 
     164 
     165    /** Client info (IP address and port) */ 
     166    char                info[80]; 
     167 
    135168    /** Mutex */ 
    136169    pj_lock_t           *lock; 
     
    148181    pjturn_relay_res    *resv; 
    149182 
    150 }; 
     183    /** Requested bandwidth */ 
     184    unsigned            bandwidth; 
     185 
     186    /** STUN session for this client */ 
     187    pj_stun_session     *sess; 
     188 
     189    /** Peer hash table (keyed by peer address) */ 
     190    pj_hash_table_t     *peer_table; 
     191 
     192    /** Channel hash table (keyed by channel number) */ 
     193    pj_hash_table_t     *ch_table; 
     194}; 
     195 
     196 
     197/** 
     198 * This structure describes the hash table key to lookup TURN 
     199 * permission. 
     200 */ 
     201typedef struct pjturn_permission_key 
     202{ 
     203    /** Peer address. */ 
     204    pj_sockaddr         peer_addr; 
     205 
     206} pjturn_permission_key; 
    151207 
    152208 
     
    157213{ 
    158214    /** Hash table key */ 
    159     struct { 
    160         /** Transport type. */ 
    161         pj_uint16_t             tp_type; 
    162  
    163         /** Transport socket. If TCP is used, the value will be the actual 
    164          *  TCP socket. If UDP is used, the value will be the relay address 
    165          */ 
    166         pj_sock_t               sock; 
    167  
    168         /** Peer address. */ 
    169         pj_sockaddr             peer_addr; 
    170     } key; 
    171  
    172     /** Pool for this permission. */ 
    173     pj_pool_t           *pool; 
    174  
    175     /** Mutex */ 
    176     pj_lock_t           *lock; 
     215    pjturn_permission_key hkey; 
     216 
     217    /** Transport socket. If TCP is used, the value will be the actual 
     218     *  TCP socket. If UDP is used, the value will be the relay address 
     219     */ 
     220    pj_sock_t           sock; 
    177221 
    178222    /** TURN allocation that owns this permission/channel */ 
     
    184228    pj_uint16_t         channel; 
    185229 
    186     /** Permission timeout. */ 
    187     pj_time_val         timeout; 
    188 }; 
    189  
    190 /** 
    191  * Handle incoming packet. 
    192  */ 
    193 PJ_DECL(void) pjturn_allocation_on_rx_pkt(pjturn_allocation *alloc, 
    194                                           pjturn_pkt *pkt); 
    195  
     230    /** Permission expiration time. */ 
     231    pj_time_val         expiry; 
     232}; 
     233 
     234/** 
     235 * Create new allocation. 
     236 */ 
     237PJ_DECL(pj_status_t) pjturn_allocation_create(pjturn_listener *listener, 
     238                                              const pj_sockaddr_t *src_addr, 
     239                                              unsigned src_addr_len, 
     240                                              const pj_stun_msg *msg, 
     241                                              const pjturn_allocation_req *req, 
     242                                              pjturn_allocation **p_alloc); 
     243/** 
     244 * Destroy allocation. 
     245 */ 
     246PJ_DECL(void) pjturn_allocation_destroy(pjturn_allocation *alloc); 
     247 
     248/** 
     249 * Create relay. 
     250 */ 
     251PJ_DECL(pj_status_t) pjturn_allocation_create_relay(pjturn_srv *srv, 
     252                                                    pjturn_allocation *alloc, 
     253                                                    const pj_stun_msg *msg, 
     254                                                    const pjturn_allocation_req *req, 
     255                                                    pjturn_relay_res *relay); 
     256 
     257/** 
     258 * Handle incoming packet from client. 
     259 */ 
     260PJ_DECL(void) pjturn_allocation_on_rx_client_pkt(pjturn_allocation *alloc, 
     261                                                 pjturn_pkt *pkt); 
    196262 
    197263/****************************************************************************/ 
     
    251317    pjturn_listener         *listener; 
    252318 
    253     /** Packet buffer. */ 
     319    /** Packet buffer (must be 32bit aligned). */ 
    254320    pj_uint8_t              pkt[PJTURN_MAX_PKT_LEN]; 
    255321 
     
    358424        pj_hash_table_t *res; 
    359425 
    360         /** Permission hash table, indexed by transport type, socket handle, 
    361          *  and peer address. 
    362          */ 
    363         pj_hash_table_t *peer; 
    364  
    365426    } tables; 
    366427 
     
    408469 
    409470/** 
     471 * Register an allocation. 
     472 */ 
     473PJ_DECL(pj_status_t) pjturn_srv_register_allocation(pjturn_srv *srv, 
     474                                                    pjturn_allocation *alloc); 
     475 
     476/** 
     477 * Unregister an allocation. 
     478 */ 
     479PJ_DECL(pj_status_t) pjturn_srv_unregister_allocation(pjturn_srv *srv, 
     480                                                      pjturn_allocation *alloc); 
     481 
     482/** 
    410483 * This callback is called by UDP listener on incoming packet. 
    411484 */ 
Note: See TracChangeset for help on using the changeset viewer.