Navigation


Changeset 706:4ffbc9f1e922 in freeDiameter for libfdcore/p_ce.c


Ignore:
Timestamp:
Feb 9, 2011, 3:26:58 PM (13 years ago)
Author:
Sebastien Decugis <sdecugis@nict.go.jp>
Branch:
default
Phase:
public
Message:

Large UNTESTED commit with the following changes:

  • Improved DiameterIdentity? handling (esp. interationalization issues), and improve efficiency of some string operations in peers, sessions, and dictionary modules (closes #7)
  • Cleanup in the session module to free only unreferenced sessions (#16)
  • Removed fd_cpu_flush_cache(), replaced by more robust alternatives.
  • Improved peer state machine algorithm to counter SCTP multistream race condition.
File:
1 edited

Legend:

Unmodified
Added
Removed
  • libfdcore/p_ce.c

    r688 r706  
    3838/* This file contains code to handle Capabilities Exchange messages (CER and CEA) and election process */
    3939
    40 /* Compilation option:
    41  USE_CEA_BROADCAST
    42         Define this to enable sending multiple copies of the CEA in case of SCTP connection.
    43         This avoids a race condition when sending an application message over a different stream
    44         than the CEA, it might be delivered first and thus ignored.
    45 */
    46 
    4740/* Save a connection as peer's principal */
    4841static int set_peer_cnx(struct fd_peer * peer, struct cnxctx **cnx)
     
    8881}
    8982
    90 /* Election: compare the Diameter Ids, return true if the election is won */
     83/* Election: compare the Diameter Ids by lexical order, return true if the election is won */
    9184static __inline__ int election_result(struct fd_peer * peer)
    9285{
     
    245238static void cleanup_remote_CE_info(struct fd_peer * peer)
    246239{
     240        /* free linked information */
    247241        free(peer->p_hdr.info.runtime.pir_realm);
    248         peer->p_hdr.info.runtime.pir_realm = NULL;
    249         peer->p_hdr.info.runtime.pir_vendorid = 0;
    250         peer->p_hdr.info.runtime.pir_orstate = 0;
    251242        free(peer->p_hdr.info.runtime.pir_prodname);
    252         peer->p_hdr.info.runtime.pir_prodname = NULL;
    253         peer->p_hdr.info.runtime.pir_firmrev = 0;
    254         peer->p_hdr.info.runtime.pir_relay = 0;
    255         peer->p_hdr.info.runtime.pir_isi = 0;
    256243        while (!FD_IS_LIST_EMPTY(&peer->p_hdr.info.runtime.pir_apps)) {
    257244                struct fd_list * li = peer->p_hdr.info.runtime.pir_apps.next;
     
    259246                free(li);
    260247        }
    261        
    262         fd_ep_clearflags( &peer->p_hdr.info.pi_endpoints, EP_FL_ADV /* Remove previously advertised endpoints */ );
     248        /* note: pir_cert_list needs not be freed (belongs to gnutls) */
     249       
     250        /* cleanup the area */
     251        memset(&peer->p_hdr.info.runtime, 0, sizeof(peer->p_hdr.info.runtime));
     252       
     253        /* reinit the list */
     254        fd_list_init(&peer->p_hdr.info.runtime.pir_apps, peer);
     255
     256        /* Remove previously advertised endpoints */
     257        fd_ep_clearflags( &peer->p_hdr.info.pi_endpoints, EP_FL_ADV );
    263258}
    264259
    265260/* Extract information sent by the remote peer and save it in our peer structure */
    266 static int save_remote_CE_info(struct msg * msg, struct fd_peer * peer, char ** error_code, uint32_t *rc)
     261static int save_remote_CE_info(struct msg * msg, struct fd_peer * peer, struct fd_pei * error, uint32_t *rc)
    267262{
    268263        struct avp * avp = NULL;
     
    309304                               
    310305                                /* We check that the value matches what we know, otherwise disconnect the peer */
    311                                 /* here also, using strcasecmp on (supposed) UTF8 data might be bad idea... to be improved */
    312                                 if (strncasecmp((char *)hdr->avp_value->os.data, peer->p_hdr.info.pi_diamid, hdr->avp_value->os.len)) {
     306                                if (fd_os_almostcasecmp(hdr->avp_value->os.data, hdr->avp_value->os.len,
     307                                                        peer->p_hdr.info.pi_diamid, peer->p_hdr.info.pi_diamidlen)) {
    313308                                        TRACE_DEBUG(INFO, "Received a message with Origin-Host set to '%.*s' while expecting '%s'\n",
    314309                                                        hdr->avp_value->os.len, hdr->avp_value->os.data, peer->p_hdr.info.pi_diamid);
    315                                         *error_code = "DIAMETER_UNKNOWN_PEER";
     310                                        error->pei_errcode = "ER_DIAMETER_AVP_NOT_ALLOWED";
     311                                        error->pei_message = "Your Origin-Host value does not match my configuration.";
     312                                        error->pei_avp = avp;
    316313                                        return EINVAL;
    317314                                }
     
    330327                                /* In case of multiple AVPs */
    331328                                if (peer->p_hdr.info.runtime.pir_realm) {
    332                                         TRACE_DEBUG(INFO, "Ignored multiple instances of the Origin-Realm AVP");
    333                                         goto next;
    334                                 }
    335                                
    336                                 /* Save the value -- we don't change the case to avoid risking breaking UTF-8 with poor tolower() impls. */
    337                                 CHECK_MALLOC(  peer->p_hdr.info.runtime.pir_realm = calloc( hdr->avp_value->os.len + 1, 1 )  );
    338                                 memcpy(peer->p_hdr.info.runtime.pir_realm, hdr->avp_value->os.data, hdr->avp_value->os.len);
     329                                        TRACE_DEBUG(INFO, "Multiple instances of the Origin-Realm AVP");
     330                                        error->pei_errcode = "ER_DIAMETER_AVP_OCCURS_TOO_MANY_TIMES";
     331                                        error->pei_message = "I found several Origin-Realm AVPs";
     332                                        error->pei_avp = avp;
     333                                        return EINVAL;
     334                                }
     335                               
     336                                /* If the octet string contains a \0 */
     337                                if (!fd_os_is_valid_DiameterIdentity(hdr->avp_value->os.data, hdr->avp_value->os.len)) {
     338                                        error->pei_errcode = "ER_DIAMETER_INVALID_AVP_VALUE";
     339                                        error->pei_message = "Your Origin-Realm contains invalid characters.";
     340                                        error->pei_avp = avp;
     341                                        return EINVAL;
     342                                }
     343                               
     344                                /* Save the value */
     345                                CHECK_MALLOC(  peer->p_hdr.info.runtime.pir_realm = os0dup( hdr->avp_value->os.data, hdr->avp_value->os.len )  );
     346                                peer->p_hdr.info.runtime.pir_realmlen = hdr->avp_value->os.len;
    339347                                break;
    340348
     
    352360                                        /* Get the sockaddr value */
    353361                                        memset(&ss, 0, sizeof(ss));
    354                                         CHECK_FCT( fd_msg_avp_value_interpret( avp, &ss) );
     362                                        CHECK_FCT_DO( fd_msg_avp_value_interpret( avp, &ss),
     363                                                {
     364                                                        /* in case of error, assume the AVP value was wrong */
     365                                                        error->pei_errcode = "ER_DIAMETER_INVALID_AVP_VALUE";
     366                                                        error->pei_avp = avp;
     367                                                        return EINVAL;
     368                                                } );
    355369
    356370                                        /* Save this endpoint in the list as advertized */
     
    370384                                /* In case of multiple AVPs */
    371385                                if (peer->p_hdr.info.runtime.pir_vendorid) {
    372                                         TRACE_DEBUG(INFO, "Ignored multiple instances of the Vendor-Id AVP");
    373                                         goto next;
     386                                        TRACE_DEBUG(INFO, "Multiple instances of the Vendor-Id AVP");
     387                                        error->pei_errcode = "ER_DIAMETER_AVP_OCCURS_TOO_MANY_TIMES";
     388                                        error->pei_message = "I found several Vendor-Id AVPs";
     389                                        error->pei_avp = avp;
     390                                        return EINVAL;
    374391                                }
    375392                               
     
    388405                                /* In case of multiple AVPs */
    389406                                if (peer->p_hdr.info.runtime.pir_prodname) {
    390                                         TRACE_DEBUG(INFO, "Ignored multiple instances of the Product-Name AVP");
    391                                         goto next;
     407                                        TRACE_DEBUG(INFO, "Multiple instances of the Product-Name AVP");
     408                                        error->pei_errcode = "ER_DIAMETER_AVP_OCCURS_TOO_MANY_TIMES";
     409                                        error->pei_message = "I found several Product-Name AVPs";
     410                                        error->pei_avp = avp;
     411                                        return EINVAL;
    392412                                }
    393413
     
    407427                                /* In case of multiple AVPs */
    408428                                if (peer->p_hdr.info.runtime.pir_orstate) {
    409                                         TRACE_DEBUG(INFO, "Ignored multiple instances of the Origin-State-Id AVP");
    410                                         goto next;
     429                                        TRACE_DEBUG(INFO, "Multiple instances of the Origin-State-Id AVP");
     430                                        error->pei_errcode = "ER_DIAMETER_AVP_OCCURS_TOO_MANY_TIMES";
     431                                        error->pei_message = "I found several Origin-State-Id AVPs";
     432                                        error->pei_avp = avp;
     433                                        return EINVAL;
    411434                                }
    412435                               
     
    423446                                }
    424447                               
    425                                 TRACE_DEBUG(FULL, "'%s' supports a subset of vendor %d features.", peer->p_hdr.info.pi_diamid, hdr->avp_value->u32);
     448                                TRACE_DEBUG(FULL, "'%s' claims support for a subset of vendor %d features.", peer->p_hdr.info.pi_diamid, hdr->avp_value->u32);
     449                                /* not that it makes a difference for us...
     450                                 -- if an application actually needs this info, we could save it somewhere.
     451                                */
    426452                                break;
    427453
     
    477503                                                TRACE_DEBUG(FULL, "Invalid Vendor-Specific-Application-Id AVP received, ignored");
    478504                                                fd_msg_dump_one(FULL, avp);
     505                                                error->pei_errcode = "ER_DIAMETER_INVALID_AVP_VALUE";
     506                                                error->pei_avp = avp;
     507                                                return EINVAL;
    479508                                        } else {
    480509                                                /* Add an entry in the list */
     
    510539                               
    511540                                if (hdr->avp_value->u32 == AI_RELAY) {
     541                                        /* Not clear if the relay application can be inside this AVP... */
    512542                                        peer->p_hdr.info.runtime.pir_relay = 1;
    513543                                } else {
    514                                         /* Not clear if the relay application can be inside this AVP... */
    515544                                        CHECK_FCT( fd_app_merge(&peer->p_hdr.info.runtime.pir_apps, hdr->avp_value->u32, 0, 0, 1) );
    516545                                }
     
    537566                                        goto next;
    538567                                }
    539                                 ASSERT( hdr->avp_value->u32 < 32 ); /* if false, we have to change the code bellow */
     568                                if (hdr->avp_value->u32 >= 32 ) {
     569                                        error->pei_errcode = "ER_DIAMETER_INVALID_AVP_VALUE";
     570                                        error->pei_message = "I don't support this Inband-Security-Id value (yet).";
     571                                        error->pei_avp = avp;
     572                                        return EINVAL;
     573                                }
    540574                                peer->p_hdr.info.runtime.pir_isi |= (1 << hdr->avp_value->u32);
    541575                                break;
     
    559593        CHECK_FCT( fd_msg_new ( fd_dict_cmd_CER, MSGFL_ALLOC_ETEID, cer ) );
    560594       
    561         /* Do we need Inband-Security-Id AVPs ? */
     595        /* Do we need Inband-Security-Id AVPs ? If we're already using TLS, we don't... */
    562596        if (!fd_cnx_getTLS(cnx)) {
    563                 isi_none = peer->p_hdr.info.config.pic_flags.sec & PI_SEC_NONE; /* we add it event if the peer does not use the old mechanism */
     597                isi_none = peer->p_hdr.info.config.pic_flags.sec & PI_SEC_NONE; /* we add it even if the peer does not use the old mechanism, it is impossible to distinguish */
    564598                isi_tls  = peer->p_hdr.info.config.pic_flags.sec & PI_SEC_TLS_OLD;
    565599        }
     
    587621
    588622/* Reject an incoming connection attempt */
    589 static void receiver_reject(struct cnxctx * recv_cnx, struct msg ** cer, char * rescode, char * errormsg)
     623static void receiver_reject(struct cnxctx ** recv_cnx, struct msg ** cer, struct fd_pei * error)
    590624{
    591625        /* Create and send the CEA with appropriate error code */
    592626        CHECK_FCT_DO( fd_msg_new_answer_from_req ( fd_g_config->cnf_dict, cer, MSGFL_ANSW_ERROR ), goto destroy );
    593         CHECK_FCT_DO( fd_msg_rescode_set(*cer, rescode, errormsg, NULL, 1 ), goto destroy );
    594         CHECK_FCT_DO( fd_out_send(cer, recv_cnx, NULL, FD_CNX_ORDERED), goto destroy );
     627        CHECK_FCT_DO( fd_msg_rescode_set(*cer, error->pei_errcode, error->pei_message, error->pei_avp, 1 ), goto destroy );
     628        CHECK_FCT_DO( fd_out_send(cer, *recv_cnx, NULL, FD_CNX_ORDERED), goto destroy );
    595629       
    596630        /* And now destroy this connection */
    597631destroy:
    598         fd_cnx_destroy(recv_cnx);
     632        fd_cnx_destroy(*recv_cnx);
     633        *recv_cnx = NULL;
    599634        if (*cer) {
    600635                fd_msg_log(FD_MSG_LOG_DROPPED, *cer, "An error occurred while rejecting a CER.");
     
    614649       
    615650        /* Are we doing an election ? */
    616         fd_cpu_flush_cache();
    617         if (peer->p_hdr.info.runtime.pir_state == STATE_WAITCNXACK_ELEC) {
     651        if (fd_peer_getstate(peer) == STATE_WAITCNXACK_ELEC) {
    618652                if (election_result(peer)) {
    619653                        /* Close initiator connection */
     
    624658
    625659                } else {
     660                        struct fd_pei pei;
     661                        memset(&pei, 0, sizeof(pei));
     662                        pei.pei_errcode = "ELECTION_LOST";
    626663
    627664                        /* Answer an ELECTION LOST to the receiver side */
    628                         receiver_reject(peer->p_receiver, &peer->p_cer, "ELECTION_LOST", NULL);
    629                         peer->p_receiver = NULL;
     665                        receiver_reject(&peer->p_receiver, &peer->p_cer, &pei);
    630666                        CHECK_FCT( to_waitcea(peer, initiator) );
    631667                }
     
    641677int fd_p_ce_msgrcv(struct msg ** msg, int req, struct fd_peer * peer)
    642678{
    643         char * ec;
    644679        uint32_t rc = 0;
     680        int st;
     681        struct fd_pei pei;
     682       
    645683        TRACE_ENTRY("%p %p", msg, peer);
    646684        CHECK_PARAMS( msg && *msg && CHECK_PEER(peer) );
     
    656694               
    657695                /* Set the error code */
    658                 CHECK_FCT( fd_msg_rescode_set(*msg, "DIAMETER_COMMAND_UNSUPPORTED", "No CER allowed in current state", NULL, 1 ) );
     696                CHECK_FCT( fd_msg_rescode_set(*msg, "ER_DIAMETER_UNABLE_TO_COMPLY", "No CER allowed in current state", NULL, 1 ) );
    659697
    660698                /* msg now contains an answer message to send back */
     
    663701       
    664702        /* If the state is not WAITCEA, just discard the message */
    665         fd_cpu_flush_cache();
    666         if (req || (peer->p_hdr.info.runtime.pir_state != STATE_WAITCEA)) {
     703        if (req || ((st = fd_peer_getstate(peer)) != STATE_WAITCEA)) {
    667704                if (*msg) {
    668                         fd_msg_log( FD_MSG_LOG_DROPPED, *msg, "Received CER/CEA while in '%s' state.\n", STATE_STR(peer->p_hdr.info.runtime.pir_state));
     705                        fd_msg_log( FD_MSG_LOG_DROPPED, *msg, "Received CER/CEA while in '%s' state.\n", STATE_STR(st));
    669706                        CHECK_FCT_DO( fd_msg_free(*msg), /* continue */);
    670707                        *msg = NULL;
     
    674711        }
    675712       
     713        memset(&pei, 0, sizeof(pei));
     714       
    676715        /* Save info from the CEA into the peer */
    677         CHECK_FCT_DO( save_remote_CE_info(*msg, peer, &ec, &rc), goto cleanup );
     716        CHECK_FCT_DO( save_remote_CE_info(*msg, peer, &pei, &rc), goto cleanup );
    678717       
    679718        /* Dispose of the message, we don't need it anymore */
     
    701740                default:
    702741                        /* In any other case, we abort all attempts to connect to this peer */
    703                         TRACE_DEBUG(INFO, "Peer %s replied a CEA with Result-Code AVP %d, aborting connection attempts.", peer->p_hdr.info.pi_diamid, rc);
     742                        TRACE_DEBUG(INFO, "Peer %s replied a CEA with Result-Code %d, aborting connection attempts.", peer->p_hdr.info.pi_diamid, rc);
    704743                        return EINVAL;
    705744        }
     
    751790}
    752791
    753 /* Handle the receiver side to go to OPEN state (any election is resolved) */
     792/* Handle the receiver side to go to OPEN or OPEN_NEW state (any election is resolved) */
    754793int fd_p_ce_process_receiver(struct fd_peer * peer)
    755794{
    756         char * ec = NULL;
     795        struct fd_pei pei;
    757796        struct msg * msg = NULL;
    758797        int isi = 0;
    759798        int fatal = 0;
     799        int tls_sync=0;
    760800       
    761801        TRACE_ENTRY("%p", peer);
     
    765805        peer->p_cer = NULL;
    766806       
     807        memset(&pei, 0, sizeof(pei));
     808       
    767809        /* Parse the content of the received CER */
    768         CHECK_FCT_DO( save_remote_CE_info(msg, peer, &ec, NULL), goto error_abort );
     810        CHECK_FCT_DO( save_remote_CE_info(msg, peer, &pei, NULL), goto error_abort );
     811       
     812        /* Validate the realm if needed */
     813        if (peer->p_hdr.info.config.pic_realm) {
     814                size_t len = strlen(peer->p_hdr.info.config.pic_realm);
     815                if (fd_os_almostcasecmp(peer->p_hdr.info.config.pic_realm, len, peer->p_hdr.info.runtime.pir_realm, peer->p_hdr.info.runtime.pir_realmlen)) {
     816                        TRACE_DEBUG(INFO, "Rejected CER from peer '%s', realm mismatch with configured value (returning DIAMETER_UNKNOWN_PEER).\n", peer->p_hdr.info.pi_diamid);
     817                        pei.pei_errcode = "DIAMETER_UNKNOWN_PEER"; /* maybe AVP_NOT_ALLOWED would be better fit? */
     818                        goto error_abort;
     819                }
     820        }
    769821       
    770822        /* Validate the peer if needed */
     
    773825                if (res < 0) {
    774826                        TRACE_DEBUG(INFO, "Rejected CER from peer '%s', validation failed (returning DIAMETER_UNKNOWN_PEER).\n", peer->p_hdr.info.pi_diamid);
    775                         ec = "DIAMETER_UNKNOWN_PEER";
     827                        pei.pei_errcode = "DIAMETER_UNKNOWN_PEER";
    776828                        goto error_abort;
    777829                }
     
    785837                if (!got_common) {
    786838                        TRACE_DEBUG(INFO, "No common application with peer '%s', sending DIAMETER_NO_COMMON_APPLICATION", peer->p_hdr.info.pi_diamid);
    787                         ec = "DIAMETER_NO_COMMON_APPLICATION";
     839                        pei.pei_errcode = "DIAMETER_NO_COMMON_APPLICATION";
    788840                        fatal = 1;
    789841                        goto error_abort;
     
    835887                if (!isi) {
    836888                        TRACE_DEBUG(INFO, "No common security mechanism with '%s', sending DIAMETER_NO_COMMON_SECURITY", peer->p_hdr.info.pi_diamid);
    837                         ec = "DIAMETER_NO_COMMON_SECURITY";
     889                        pei.pei_errcode = "DIAMETER_NO_COMMON_SECURITY";
    838890                        fatal = 1;
    839891                        goto error_abort;
     
    849901        CHECK_FCT( fd_msg_rescode_set(msg, "DIAMETER_SUCCESS", NULL, NULL, 0 ) );
    850902        CHECK_FCT( add_CE_info(msg, peer->p_cnxctx, isi & PI_SEC_TLS_OLD, isi & PI_SEC_NONE) );
    851 #ifdef USE_CEA_BROADCAST
    852         CHECK_FCT( fd_out_send(&msg, peer->p_cnxctx, peer, (isi & PI_SEC_TLS_OLD) ? FD_CNX_ORDERED : FD_CNX_BROADCAST) ); /* Broadcast in order to avoid further messages sent over a different stream be delivered first... */
    853 #else /* USE_CEA_BROADCAST */
    854903        CHECK_FCT( fd_out_send(&msg, peer->p_cnxctx, peer, FD_CNX_ORDERED ) );
    855 #endif /* USE_CEA_BROADCAST */
    856904       
    857905        /* Handshake if needed */
     
    878926                                }  );
    879927                }
    880                
     928                tls_sync = 1;
    881929        } else {
    882930                if ( ! fd_cnx_getTLS(peer->p_cnxctx) ) {
     
    891939                CHECK_FCT( fd_p_dw_reopen(peer) );
    892940        } else {
    893                 fd_psm_change_state(peer, STATE_OPEN );
    894                 fd_psm_next_timeout(peer, 1, peer->p_hdr.info.config.pic_twtimer ?: fd_g_config->cnf_timer_tw);
     941                if ((!tls_sync) && (fd_cnx_isMultichan(peer->p_cnxctx))) {
     942                        fd_psm_change_state(peer, STATE_OPEN_NEW );
     943                        /* send DWR */
     944                        CHECK_FCT( fd_p_dw_timeout(peer) );
     945                } else {
     946
     947                        fd_psm_change_state(peer, STATE_OPEN );
     948                        fd_psm_next_timeout(peer, 1, peer->p_hdr.info.config.pic_twtimer ?: fd_g_config->cnf_timer_tw);
     949                }
    895950        }
    896951       
     
    898953
    899954error_abort:
    900         if (ec) {
    901                 /* Create the error message */
    902                 CHECK_FCT( fd_msg_new_answer_from_req ( fd_g_config->cnf_dict, &msg, MSGFL_ANSW_ERROR ) );
    903 
    904                 /* Set the error code */
    905                 CHECK_FCT( fd_msg_rescode_set(msg, ec, NULL, NULL, 1 ) );
    906 
    907                 /* msg now contains an answer message to send back */
    908                 CHECK_FCT_DO( fd_out_send(&msg, peer->p_cnxctx, peer, FD_CNX_ORDERED), /* In case of error the message has already been dumped */ );
     955        if (pei.pei_errcode) {
     956                /* Send the error */
     957                receiver_reject(&peer->p_cnxctx, &msg, &pei);
    909958        }
    910959       
     
    925974int fd_p_ce_handle_newCER(struct msg ** msg, struct fd_peer * peer, struct cnxctx ** cnx, int valid)
    926975{
    927         fd_cpu_flush_cache();
    928         switch (peer->p_hdr.info.runtime.pir_state) {
     976        struct fd_pei pei;
     977        int cur_state = fd_peer_getstate(peer);
     978        memset(&pei, 0, sizeof(pei));
     979       
     980        switch (cur_state) {
    929981                case STATE_CLOSED:
    930982                        peer->p_receiver = *cnx;
     
    9601012
    9611013                                /* Answer an ELECTION LOST to the receiver side and continue */
    962                                 receiver_reject(*cnx, msg, "ELECTION_LOST", "Please answer my CER instead, you won the election.");
    963                                 *cnx = NULL;
     1014                                pei.pei_errcode = "ELECTION_LOST";
     1015                                pei.pei_message = "Please answer my CER instead, you won the election.";
     1016                                receiver_reject(cnx, msg, &pei);
    9641017                        }
    9651018                        break;
    9661019
    9671020                default:
    968                         receiver_reject(*cnx, msg, "DIAMETER_UNABLE_TO_COMPLY", "Invalid state to receive a new connection attempt");
    969                         *cnx = NULL;
     1021                        pei.pei_errcode = "DIAMETER_UNABLE_TO_COMPLY"; /* INVALID COMMAND? in case of Capabilities-Updates? */
     1022                        pei.pei_message = "Invalid state to receive a new connection attempt.";
     1023                        receiver_reject(cnx, msg, &pei);
    9701024        }
    9711025                               
Note: See TracChangeset for help on using the changeset viewer.