mirror of
https://github.com/zeek/zeek.git
synced 2025-10-10 02:28:21 +00:00
630 lines
18 KiB
JavaScript
630 lines
18 KiB
JavaScript
# $Id:$
|
|
|
|
# Analyzer for SSL messages (general part).
|
|
# To be used in conjunction with an SSL record-layer analyzer.
|
|
# Separation is necessary due to possible fragmentation of SSL records.
|
|
|
|
######################################################################
|
|
# General definitions
|
|
######################################################################
|
|
|
|
type uint24 = record {
|
|
byte1 : uint8;
|
|
byte2 : uint8;
|
|
byte3 : uint8;
|
|
};
|
|
|
|
%header{
|
|
class to_int {
|
|
public:
|
|
int operator()(uint24 * num) const
|
|
{
|
|
return (num->byte1() << 16) | (num->byte2() << 8) | num->byte3();
|
|
}
|
|
};
|
|
%}
|
|
|
|
extern type to_int;
|
|
|
|
######################################################################
|
|
# state management according to Section 7.3. in spec
|
|
######################################################################
|
|
|
|
enum AnalyzerState {
|
|
STATE_INITIAL,
|
|
STATE_CLIENT_HELLO_RCVD,
|
|
STATE_IN_SERVER_HELLO,
|
|
STATE_SERVER_HELLO_DONE,
|
|
STATE_CLIENT_CERT,
|
|
STATE_CLIENT_KEY_WITH_CERT,
|
|
STATE_CLIENT_KEY_NO_CERT,
|
|
STATE_CLIENT_CERT_VERIFIED,
|
|
STATE_CLIENT_ENCRYPTED,
|
|
STATE_CLIENT_FINISHED,
|
|
STATE_ABBREV_SERVER_ENCRYPTED,
|
|
STATE_ABBREV_SERVER_FINISHED,
|
|
STATE_COMM_ENCRYPTED,
|
|
STATE_CONN_ESTABLISHED,
|
|
STATE_V2_CL_MASTER_KEY_EXPECTED,
|
|
|
|
STATE_TRACK_LOST,
|
|
STATE_ANY
|
|
};
|
|
|
|
%code{
|
|
string state_label(int state_nr)
|
|
{
|
|
switch ( state_nr ) {
|
|
case STATE_INITIAL:
|
|
return string("INITIAL");
|
|
case STATE_CLIENT_HELLO_RCVD:
|
|
return string("CLIENT_HELLO_RCVD");
|
|
case STATE_IN_SERVER_HELLO:
|
|
return string("IN_SERVER_HELLO");
|
|
case STATE_SERVER_HELLO_DONE:
|
|
return string("SERVER_HELLO_DONE");
|
|
case STATE_CLIENT_CERT:
|
|
return string("CLIENT_CERT");
|
|
case STATE_CLIENT_KEY_WITH_CERT:
|
|
return string("CLIENT_KEY_WITH_CERT");
|
|
case STATE_CLIENT_KEY_NO_CERT:
|
|
return string("CLIENT_KEY_NO_CERT");
|
|
case STATE_CLIENT_CERT_VERIFIED:
|
|
return string("CLIENT_CERT_VERIFIED");
|
|
case STATE_CLIENT_ENCRYPTED:
|
|
return string("CLIENT_ENCRYPTED");
|
|
case STATE_CLIENT_FINISHED:
|
|
return string("CLIENT_FINISHED");
|
|
case STATE_ABBREV_SERVER_ENCRYPTED:
|
|
return string("ABBREV_SERVER_ENCRYPTED");
|
|
case STATE_ABBREV_SERVER_FINISHED:
|
|
return string("ABBREV_SERVER_FINISHED");
|
|
case STATE_COMM_ENCRYPTED:
|
|
return string("COMM_ENCRYPTED");
|
|
case STATE_CONN_ESTABLISHED:
|
|
return string("CONN_ESTABLISHED");
|
|
case STATE_V2_CL_MASTER_KEY_EXPECTED:
|
|
return string("STATE_V2_CL_MASTER_KEY_EXPECTED");
|
|
case STATE_TRACK_LOST:
|
|
return string("TRACK_LOST");
|
|
case STATE_ANY:
|
|
return string("ANY");
|
|
|
|
default:
|
|
return string(fmt("UNKNOWN (%d)", state_nr));
|
|
}
|
|
}
|
|
|
|
string orig_label(bool is_orig)
|
|
{
|
|
return string(is_orig ? "originator" :"responder");
|
|
}
|
|
%}
|
|
|
|
######################################################################
|
|
# SSLv3 Handshake Protocols (7.)
|
|
######################################################################
|
|
|
|
enum HandshakeType {
|
|
HELLO_REQUEST = 0,
|
|
CLIENT_HELLO = 1,
|
|
SERVER_HELLO = 2,
|
|
CERTIFICATE = 11,
|
|
SERVER_KEY_EXCHANGE = 12,
|
|
CERTIFICATE_REQUEST = 13,
|
|
SERVER_HELLO_DONE = 14,
|
|
CERTIFICATE_VERIFY = 15,
|
|
CLIENT_KEY_EXCHANGE = 16,
|
|
FINISHED = 20
|
|
};
|
|
|
|
%code{
|
|
string handshake_type_label(int type)
|
|
{
|
|
switch ( type ) {
|
|
case HELLO_REQUEST: return string("HELLO_REQUEST");
|
|
case CLIENT_HELLO: return string("CLIENT_HELLO");
|
|
case SERVER_HELLO: return string("SERVER_HELLO");
|
|
case CERTIFICATE: return string("CERTIFICATE");
|
|
case SERVER_KEY_EXCHANGE: return string("SERVER_KEY_EXCHANGE");
|
|
case CERTIFICATE_REQUEST: return string("CERTIFICATE_REQUEST");
|
|
case SERVER_HELLO_DONE: return string("SERVER_HELLO_DONE");
|
|
case CERTIFICATE_VERIFY: return string("CERTIFICATE_VERIFY");
|
|
case CLIENT_KEY_EXCHANGE: return string("CLIENT_KEY_EXCHANGE");
|
|
case FINISHED: return string("FINISHED");
|
|
default: return string(fmt("UNKNOWN (%d)", type));
|
|
}
|
|
}
|
|
%}
|
|
|
|
|
|
######################################################################
|
|
# V3 Change Cipher Spec Protocol (7.1.)
|
|
######################################################################
|
|
|
|
type ChangeCipherSpec = record {
|
|
type : uint8;
|
|
} &length = 1, &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_CLIENT_FINISHED,
|
|
STATE_COMM_ENCRYPTED, false) ||
|
|
$context.analyzer.transition(STATE_IN_SERVER_HELLO,
|
|
STATE_ABBREV_SERVER_ENCRYPTED, false) ||
|
|
$context.analyzer.transition(STATE_CLIENT_KEY_NO_CERT,
|
|
STATE_CLIENT_ENCRYPTED, true) ||
|
|
$context.analyzer.transition(STATE_CLIENT_CERT_VERIFIED,
|
|
STATE_CLIENT_ENCRYPTED, true) ||
|
|
$context.analyzer.transition(STATE_CLIENT_KEY_WITH_CERT,
|
|
STATE_CLIENT_ENCRYPTED, true) ||
|
|
$context.analyzer.transition(STATE_ABBREV_SERVER_FINISHED,
|
|
STATE_COMM_ENCRYPTED, true) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Alert Protocol (7.2.)
|
|
######################################################################
|
|
|
|
type Alert = record {
|
|
level : uint8;
|
|
description: uint8;
|
|
} &length = 2;
|
|
|
|
|
|
######################################################################
|
|
# V2 Error Records (SSLv2 2.7.)
|
|
######################################################################
|
|
|
|
type V2Error = record {
|
|
error_code : uint16;
|
|
} &length = 2;
|
|
|
|
|
|
######################################################################
|
|
# V3 Application Data
|
|
######################################################################
|
|
|
|
# Application data should always be encrypted, so we should not
|
|
# reach this point.
|
|
type ApplicationData = empty &let {
|
|
discard: bool = $context.flow.discard_data();
|
|
};
|
|
|
|
######################################################################
|
|
# Handshake Protocol (7.4.)
|
|
######################################################################
|
|
|
|
######################################################################
|
|
# V3 Hello Request (7.4.1.1.)
|
|
######################################################################
|
|
|
|
# Hello Request is empty
|
|
type HelloRequest = empty &let {
|
|
hr: bool = $context.analyzer.set_hello_requested(true);
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Client Hello (7.4.1.2.)
|
|
######################################################################
|
|
|
|
type ClientHello = record {
|
|
client_version : uint16;
|
|
gmt_unix_time : uint32;
|
|
random_bytes : bytestring &length = 28 &transient;
|
|
session_len : uint8;
|
|
session_id : uint8[session_len];
|
|
csuit_len : uint16 &check(csuit_len > 1 && csuit_len % 2 == 0);
|
|
csuits : uint16[csuit_len/2];
|
|
cmeth_len : uint8 &check(cmeth_len > 0);
|
|
cmeths : uint8[cmeth_len];
|
|
} &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_INITIAL,
|
|
STATE_CLIENT_HELLO_RCVD, true) ||
|
|
($context.analyzer.hello_requested() &&
|
|
$context.analyzer.transition(STATE_ANY, STATE_CLIENT_HELLO_RCVD, true)) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V2 Client Hello (SSLv2 2.5.)
|
|
######################################################################
|
|
|
|
type V2ClientHello = record {
|
|
client_version : uint16;
|
|
csuit_len : uint16;
|
|
session_len : uint16;
|
|
chal_len : uint16;
|
|
ciphers : uint24[csuit_len/3];
|
|
session_id : uint8[session_len];
|
|
challenge : bytestring &length = chal_len;
|
|
} &length = 8 + csuit_len + session_len + chal_len, &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_INITIAL,
|
|
STATE_CLIENT_HELLO_RCVD, true) ||
|
|
($context.analyzer.hello_requested() &&
|
|
$context.analyzer.transition(STATE_ANY, STATE_CLIENT_HELLO_RCVD, true)) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Server Hello (7.4.1.3.)
|
|
######################################################################
|
|
|
|
type ServerHello = record {
|
|
server_version : uint16;
|
|
gmt_unix_time : uint32;
|
|
random_bytes : bytestring &length = 28 &transient;
|
|
session_len : uint8;
|
|
session_id : uint8[session_len];
|
|
cipher_suite : uint16[1];
|
|
compression_method : uint8;
|
|
} &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_CLIENT_HELLO_RCVD,
|
|
STATE_IN_SERVER_HELLO, false) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V2 Server Hello (SSLv2 2.6.)
|
|
######################################################################
|
|
|
|
type V2ServerHello = record {
|
|
session_id_hit : uint8;
|
|
cert_type : uint8;
|
|
server_version : uint16;
|
|
cert_len : uint16;
|
|
ciph_len : uint16;
|
|
conn_id_len : uint16;
|
|
cert_data : bytestring &length = cert_len;
|
|
ciphers : uint24[ciph_len/3];
|
|
conn_id_data : bytestring &length = conn_id_len;
|
|
} &length = 10 + cert_len + ciph_len + conn_id_len, &let {
|
|
state_changed : bool =
|
|
(session_id_hit > 0 ?
|
|
$context.analyzer.transition(STATE_CLIENT_HELLO_RCVD,
|
|
STATE_CONN_ESTABLISHED, false) :
|
|
$context.analyzer.transition(STATE_CLIENT_HELLO_RCVD,
|
|
STATE_V2_CL_MASTER_KEY_EXPECTED, false)) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Server Certificate (7.4.2.)
|
|
######################################################################
|
|
|
|
type X509Certificate = record {
|
|
length : uint24;
|
|
certificate : bytestring &length = to_int()(length);
|
|
};
|
|
|
|
type CertificateList = X509Certificate[] &until($input.length() == 0);
|
|
|
|
type Certificate = record {
|
|
length : uint24;
|
|
certificates : CertificateList &length = to_int()(length);
|
|
} &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_IN_SERVER_HELLO,
|
|
STATE_IN_SERVER_HELLO, false) ||
|
|
$context.analyzer.transition(STATE_SERVER_HELLO_DONE,
|
|
STATE_CLIENT_CERT, true) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Server Key Exchange Message (7.4.3.)
|
|
######################################################################
|
|
|
|
# For now ignore details; just eat up complete message
|
|
type ServerKeyExchange = record {
|
|
cont : bytestring &restofdata &transient;
|
|
} &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_IN_SERVER_HELLO,
|
|
STATE_IN_SERVER_HELLO, false) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Certificate Request (7.4.4.)
|
|
######################################################################
|
|
|
|
# For now, ignore Certificate Request Details; just eat up message.
|
|
type CertificateRequest = record {
|
|
cont : bytestring &restofdata &transient;
|
|
} &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_IN_SERVER_HELLO,
|
|
STATE_IN_SERVER_HELLO, false) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Server Hello Done (7.4.5.)
|
|
######################################################################
|
|
|
|
# Server Hello Done is empty
|
|
type ServerHelloDone = empty &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_IN_SERVER_HELLO,
|
|
STATE_SERVER_HELLO_DONE, false) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Client Certificate (7.4.6.)
|
|
######################################################################
|
|
|
|
# Client Certificate is identical to Server Certificate;
|
|
# no further definition here
|
|
|
|
|
|
######################################################################
|
|
# V3 Client Key Exchange Message (7.4.7.)
|
|
######################################################################
|
|
|
|
# For now ignore details of ClientKeyExchange (most of it is
|
|
# encrypted anyway); just eat up message.
|
|
type ClientKeyExchange = record {
|
|
cont : bytestring &restofdata &transient;
|
|
} &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_SERVER_HELLO_DONE,
|
|
STATE_CLIENT_KEY_NO_CERT, true) ||
|
|
$context.analyzer.transition(STATE_CLIENT_CERT,
|
|
STATE_CLIENT_KEY_WITH_CERT, true) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
######################################################################
|
|
# V2 Client Master Key (SSLv2 2.5.)
|
|
######################################################################
|
|
|
|
type V2ClientMasterKey = record {
|
|
cipher_kind : uint24;
|
|
cl_key_len : uint16;
|
|
en_key_len : uint16;
|
|
key_arg_len : uint16;
|
|
cl_key_data : bytestring &length = cl_key_len &transient;
|
|
en_key_data : bytestring &length = en_key_len &transient;
|
|
key_arg_data : bytestring &length = key_arg_len &transient;
|
|
} &length = 9 + cl_key_len + en_key_len + key_arg_len, &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_V2_CL_MASTER_KEY_EXPECTED,
|
|
STATE_CONN_ESTABLISHED, true) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Certificate Verify (7.4.8.)
|
|
######################################################################
|
|
|
|
# For now, ignore Certificate Verify; just eat up the message.
|
|
type CertificateVerify = record {
|
|
cont : bytestring &restofdata &transient;
|
|
} &let {
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_CLIENT_KEY_WITH_CERT,
|
|
STATE_CLIENT_CERT_VERIFIED, true) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# V3 Finished (7.4.9.)
|
|
######################################################################
|
|
|
|
# The Finished messages are always sent after encryption is in effect,
|
|
# so we will not be able to read those message
|
|
|
|
|
|
######################################################################
|
|
# V3 Handshake Protocol (7.)
|
|
######################################################################
|
|
|
|
type UnknownHandshake(msg_type : uint8) = record {
|
|
cont : bytestring &restofdata &transient;
|
|
} &let {
|
|
state_changed : bool = $context.analyzer.lost_track();
|
|
};
|
|
|
|
type Handshake = record {
|
|
msg_type : uint8;
|
|
length : uint24;
|
|
|
|
body : case msg_type of {
|
|
HELLO_REQUEST -> hello_request : HelloRequest;
|
|
CLIENT_HELLO -> client_hello : ClientHello;
|
|
SERVER_HELLO -> server_hello : ServerHello;
|
|
CERTIFICATE -> certificate : Certificate;
|
|
SERVER_KEY_EXCHANGE -> server_key_exchange : ServerKeyExchange;
|
|
CERTIFICATE_REQUEST -> certificate_request : CertificateRequest;
|
|
SERVER_HELLO_DONE -> server_hello_done : ServerHelloDone;
|
|
CERTIFICATE_VERIFY -> certificate_verify : CertificateVerify;
|
|
CLIENT_KEY_EXCHANGE -> client_key_exchange : ClientKeyExchange;
|
|
default -> unknown_handshake : UnknownHandshake(msg_type);
|
|
};
|
|
} &length = 4 + to_int()(length);
|
|
|
|
|
|
######################################################################
|
|
# Fragmentation (6.2.1.)
|
|
######################################################################
|
|
|
|
type UnknownRecord = record {
|
|
cont : empty;
|
|
} &let {
|
|
discard : bool = $context.flow.discard_data();
|
|
state_changed : bool = $context.analyzer.lost_track();
|
|
};
|
|
|
|
type PlaintextRecord = case $context.analyzer.current_record_type() of {
|
|
CHANGE_CIPHER_SPEC -> ch_cipher : ChangeCipherSpec;
|
|
ALERT -> alert : Alert;
|
|
HANDSHAKE -> handshakes : Handshake;
|
|
APPLICATION_DATA -> app_data : ApplicationData;
|
|
V2_ERROR -> v2_error : V2Error;
|
|
V2_CLIENT_HELLO -> v2_client_hello : V2ClientHello;
|
|
V2_CLIENT_MASTER_KEY -> v2_client_master_key : V2ClientMasterKey;
|
|
V2_SERVER_HELLO -> v2_server_hello : V2ServerHello;
|
|
UNKNOWN_OR_V2_ENCRYPTED -> unknown_record : UnknownRecord;
|
|
};
|
|
|
|
type CiphertextRecord = empty &let {
|
|
discard : bool = $context.flow.discard_data();
|
|
state_changed : bool =
|
|
$context.analyzer.transition(STATE_ABBREV_SERVER_ENCRYPTED,
|
|
STATE_ABBREV_SERVER_FINISHED, false) ||
|
|
$context.analyzer.transition(STATE_CLIENT_ENCRYPTED,
|
|
STATE_CLIENT_FINISHED, true) ||
|
|
$context.analyzer.transition(STATE_COMM_ENCRYPTED,
|
|
STATE_CONN_ESTABLISHED, false) ||
|
|
$context.analyzer.transition(STATE_COMM_ENCRYPTED,
|
|
STATE_CONN_ESTABLISHED, true) ||
|
|
$context.analyzer.transition(STATE_CONN_ESTABLISHED,
|
|
STATE_CONN_ESTABLISHED, false) ||
|
|
$context.analyzer.transition(STATE_CONN_ESTABLISHED,
|
|
STATE_CONN_ESTABLISHED, true) ||
|
|
$context.analyzer.lost_track();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# initial datatype for binpac
|
|
######################################################################
|
|
|
|
type SSLPDU = case $context.analyzer.state() of {
|
|
STATE_ABBREV_SERVER_ENCRYPTED, STATE_CLIENT_ENCRYPTED,
|
|
STATE_COMM_ENCRYPTED, STATE_CONN_ESTABLISHED
|
|
-> ciphertext : CiphertextRecord;
|
|
default
|
|
-> plaintext : PlaintextRecord;
|
|
} &byteorder = bigendian, &let {
|
|
consumed : bool = $context.flow.consume_data();
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# binpac analyzer for SSL including
|
|
######################################################################
|
|
|
|
analyzer SSLAnalyzer {
|
|
upflow = SSLFlow(true);
|
|
downflow = SSLFlow(false);
|
|
|
|
%member{
|
|
int current_record_type_;
|
|
int current_record_version_;
|
|
int current_record_length_;
|
|
bool current_record_is_orig_;
|
|
int state_;
|
|
int old_state_;
|
|
bool hello_requested_;
|
|
%}
|
|
|
|
%init{
|
|
current_record_type_ = -1;
|
|
current_record_version_ = -1;
|
|
current_record_length_ = -1;
|
|
current_record_is_orig_ = false;
|
|
state_ = STATE_INITIAL;
|
|
old_state_ = STATE_INITIAL;
|
|
hello_requested_ = false;
|
|
%}
|
|
|
|
function current_record_type() : int
|
|
%{ return current_record_type_; %}
|
|
function current_record_version() : int
|
|
%{ return current_record_version_; %}
|
|
function current_record_length() : int
|
|
%{ return current_record_length_; %}
|
|
function current_record_is_orig() : bool
|
|
%{ return current_record_is_orig_; %}
|
|
|
|
function next_record(rec : const_bytestring, type : int,
|
|
version : int, is_orig : bool) : bool
|
|
%{
|
|
current_record_type_ = type;
|
|
current_record_version_ = version;
|
|
current_record_length_ = rec.length();
|
|
current_record_is_orig_ = is_orig;
|
|
|
|
NewData(is_orig, rec.begin(), rec.end());
|
|
|
|
return true;
|
|
%}
|
|
|
|
function state() : int %{ return state_; %}
|
|
function old_state() : int %{ return old_state_; %}
|
|
|
|
function transition(olds : AnalyzerState, news : AnalyzerState,
|
|
is_orig : bool) : bool
|
|
%{
|
|
if ( (olds != STATE_ANY && olds != state_) ||
|
|
current_record_is_orig_ != is_orig )
|
|
return false;
|
|
|
|
old_state_ = state_;
|
|
state_ = news;
|
|
|
|
return true;
|
|
%}
|
|
|
|
function lost_track() : bool
|
|
%{
|
|
state_ = STATE_TRACK_LOST;
|
|
return false;
|
|
%}
|
|
|
|
function hello_requested() : bool
|
|
%{
|
|
bool ret = hello_requested_;
|
|
hello_requested_ = false;
|
|
return ret;
|
|
%}
|
|
|
|
function set_hello_requested(val : bool) : bool
|
|
%{
|
|
hello_requested_ = val;
|
|
return val;
|
|
%}
|
|
};
|
|
|
|
|
|
######################################################################
|
|
# binpac flow for SSL
|
|
######################################################################
|
|
|
|
flow SSLFlow(is_orig : bool) {
|
|
flowunit = SSLPDU withcontext(connection, this);
|
|
|
|
function discard_data() : bool
|
|
%{
|
|
flow_buffer_->DiscardData();
|
|
return true;
|
|
%}
|
|
|
|
function data_available() : bool
|
|
%{
|
|
return flow_buffer_->data_available();
|
|
%}
|
|
|
|
function consume_data() : bool
|
|
%{
|
|
flow_buffer_->NewFrame(0, false);
|
|
return true;
|
|
%}
|
|
};
|