Commit 26ada606 authored by johan's avatar johan

Complete LIME integration into liblinphone

- add a boolean into linphoneCore to check if encryption is requested
- move encryption/decryption from chat.c to sal_op_message.c
- check encrypted message validity
- return error to application when enable to encrypt/decrypt
parent 3ccc7fb4
......@@ -18,6 +18,11 @@ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
*/
#include "sal_impl.h"
#include "linphonecore.h"
#include "private.h"
#include "lime.h"
#include <libxml/xmlwriter.h>
static void process_error( SalOp* op) {
if (op->dir == SalOpDirOutgoing) {
op->base.root->callbacks.text_delivery_update(op, SalTextDeliveryFailed);
......@@ -59,6 +64,11 @@ static bool_t is_plain_text(belle_sip_header_content_type_t* content_type) {
return strcmp("text",belle_sip_header_content_type_get_type(content_type))==0
&& strcmp("plain",belle_sip_header_content_type_get_subtype(content_type))==0;
}
static bool_t is_cipher_xml(belle_sip_header_content_type_t* content_type) {
return strcmp("xml",belle_sip_header_content_type_get_type(content_type))==0
&& strcmp("cipher",belle_sip_header_content_type_get_subtype(content_type))==0;
}
static bool_t is_external_body(belle_sip_header_content_type_t* content_type) {
return strcmp("message",belle_sip_header_content_type_get_type(content_type))==0
&& strcmp("external-body",belle_sip_header_content_type_get_subtype(content_type))==0;
......@@ -69,7 +79,7 @@ static bool_t is_im_iscomposing(belle_sip_header_content_type_t* content_type) {
}
static void add_message_accept(belle_sip_message_t *msg){
belle_sip_message_add_header(msg,belle_sip_header_create("Accept","text/plain, message/external-body, application/im-iscomposing+xml"));
belle_sip_message_add_header(msg,belle_sip_header_create("Accept","text/plain, message/external-body, application/im-iscomposing+xml, xml/cipher"));
}
void sal_process_incoming_message(SalOp *op,const belle_sip_request_event_t *event){
......@@ -85,11 +95,58 @@ void sal_process_incoming_message(SalOp *op,const belle_sip_request_event_t *eve
char* from;
bool_t plain_text=FALSE;
bool_t external_body=FALSE;
bool_t cipher_xml=FALSE;
from_header=belle_sip_message_get_header_by_type(BELLE_SIP_MESSAGE(req),belle_sip_header_from_t);
content_type=belle_sip_message_get_header_by_type(BELLE_SIP_MESSAGE(req),belle_sip_header_content_type_t);
/* check if we have a xml/cipher message to be decrypted */
uint8_t *decryptedMessage = NULL;
if (content_type && (cipher_xml=is_cipher_xml(content_type))) {
/* access the zrtp cache to get keys needed to decipher the message */
LinphoneCore *lc=(LinphoneCore *)sal_get_user_pointer(sal_op_get_sal(op));
FILE *CACHEFD = fopen(lc->zrtp_secrets_cache, "r+");
ms_message("Cache file is %s", lc->zrtp_secrets_cache);
if (CACHEFD == NULL) {
ms_warning("Unable to access ZRTP ZID cache to decrypt message");
} else {
fseek(CACHEFD, 0L, SEEK_END); /* Position to end of file */
int cacheSize = ftell(CACHEFD); /* Get file length */
rewind(CACHEFD); /* Back to start of file */
uint8_t *cacheString = (uint8_t *)malloc(cacheSize*sizeof(uint8_t)+1); /* string must be null terminated */
fread(cacheString, 1, cacheSize, CACHEFD);
cacheString[cacheSize] = '\0';
cacheSize += 1;
fclose(CACHEFD);
xmlDocPtr cacheXml = xmlParseDoc(cacheString);
free(cacheString);
int retval = lime_decryptMultipartMessage(cacheXml, (uint8_t *)belle_sip_message_get_body(BELLE_SIP_MESSAGE(req)), &decryptedMessage);
if (retval != 0) {
ms_warning("JOHAN: Unable to decrypt message, reason %x", retval);
free(decryptedMessage);
xmlFreeDoc(cacheXml);
resp = belle_sip_response_create_from_request(req,488);
belle_sip_server_transaction_send_response(server_transaction,resp);
return;
} else {
ms_warning("JOHAN : Yes we did decrypt the message and it is %s", decryptedMessage);
/* dump updated cache to a string */
xmlChar *xmlStringOutput;
int xmlStringLength;
xmlDocDumpFormatMemoryEnc(cacheXml, &xmlStringOutput, &xmlStringLength, "UTF-8", 0);
/* write it to the cache file */
CACHEFD = fopen(lc->zrtp_secrets_cache, "w+");
fwrite(xmlStringOutput, 1, xmlStringLength, CACHEFD);
xmlFree(xmlStringOutput);
fclose(CACHEFD);
}
xmlFreeDoc(cacheXml);
}
}
if (content_type && ((plain_text=is_plain_text(content_type))
|| (external_body=is_external_body(content_type)))) {
|| (external_body=is_external_body(content_type))
|| decryptedMessage!=NULL)) {
SalMessage salmsg;
char message_id[256]={0};
......@@ -104,7 +161,7 @@ void sal_process_incoming_message(SalOp *op,const belle_sip_request_event_t *eve
,belle_sip_header_call_id_get_call_id(call_id)
,belle_sip_header_cseq_get_seq_number(cseq));
salmsg.from=from;
salmsg.text=plain_text?belle_sip_message_get_body(BELLE_SIP_MESSAGE(req)):NULL;
salmsg.text=plain_text?belle_sip_message_get_body(BELLE_SIP_MESSAGE(req)):(cipher_xml?(char *)decryptedMessage:NULL);
salmsg.url=NULL;
if (external_body && belle_sip_parameters_get_parameter(BELLE_SIP_PARAMETERS(content_type),"URL")) {
size_t url_length=strlen(belle_sip_parameters_get_parameter(BELLE_SIP_PARAMETERS(content_type),"URL"));
......@@ -143,11 +200,12 @@ static void process_request_event(void *op_base, const belle_sip_request_event_t
sal_process_incoming_message(op,event);
}
int sal_message_send(SalOp *op, const char *from, const char *to, const char* content_type, const char *msg){
int sal_message_send(SalOp *op, const char *from, const char *to, const char* content_type, const char *msg, const char *peer_uri){
belle_sip_request_t* req;
char content_type_raw[256];
size_t content_length = msg?strlen(msg):0;
time_t curtime=time(NULL);
uint8_t *multipartEncryptedMessage = NULL;
if (op->dialog){
/*for SIP MESSAGE that are sent in call's dialog*/
......@@ -165,11 +223,56 @@ int sal_message_send(SalOp *op, const char *from, const char *to, const char* co
belle_sip_message_add_header(BELLE_SIP_MESSAGE(req),BELLE_SIP_HEADER(sal_op_create_contact(op)));
}
}
/* shall we try to encrypt the message?*/
if (strcmp(content_type, "xml/cipher") == 0) {
/* access the zrtp cache to get keys needed to cipher the message */
LinphoneCore *lc=(LinphoneCore *)sal_get_user_pointer(sal_op_get_sal(op));
FILE *CACHEFD = fopen(lc->zrtp_secrets_cache, "r+");
ms_message("Cache file is %s", lc->zrtp_secrets_cache);
if (CACHEFD == NULL) {
ms_warning("Unable to access ZRTP ZID cache to encrypt message");
} else {
fseek(CACHEFD, 0L, SEEK_END); /* Position to end of file */
int cacheSize = ftell(CACHEFD); /* Get file length */
rewind(CACHEFD); /* Back to start of file */
uint8_t *cacheString = (uint8_t *)malloc(cacheSize*sizeof(uint8_t)+1); /* string must be null terminated */
fread(cacheString, 1, cacheSize, CACHEFD);
cacheString[cacheSize] = '\0';
cacheSize += 1;
fclose(CACHEFD);
xmlDocPtr cacheXml = xmlParseDoc(cacheString);
free(cacheString);
int retval = lime_createMultipartMessage(cacheXml, (uint8_t *)msg, (uint8_t *)peer_uri, &multipartEncryptedMessage);
if (retval != 0) {
ms_warning("Unable to encrypt message to %s error %x", peer_uri, retval);
xmlFreeDoc(cacheXml);
free(multipartEncryptedMessage);
sal_error_info_set(&op->error_info, SalReasonNotAcceptable, 488, "Unable to encrypt IM", NULL);
op->base.root->callbacks.text_delivery_update(op,SalTextDeliveryFailed);
return 0;
} else {
ms_warning("Succes in encrypting message for %s to %s", to, peer_uri);
/* dump updated cache to a string */
xmlChar *xmlStringOutput;
int xmlStringLength;
xmlDocDumpFormatMemoryEnc(cacheXml, &xmlStringOutput, &xmlStringLength, "UTF-8", 0);
/* write it to the cache file */
CACHEFD = fopen(lc->zrtp_secrets_cache, "w+");
fwrite(xmlStringOutput, 1, xmlStringLength, CACHEFD);
xmlFree(xmlStringOutput);
fclose(CACHEFD);
content_length = strlen((const char *)multipartEncryptedMessage);
}
xmlFreeDoc(cacheXml);
}
}
snprintf(content_type_raw,sizeof(content_type_raw),BELLE_SIP_CONTENT_TYPE ": %s",content_type);
belle_sip_message_add_header(BELLE_SIP_MESSAGE(req),BELLE_SIP_HEADER(belle_sip_header_content_type_parse(content_type_raw)));
belle_sip_message_add_header(BELLE_SIP_MESSAGE(req),BELLE_SIP_HEADER(belle_sip_header_content_length_create(content_length)));
belle_sip_message_add_header(BELLE_SIP_MESSAGE(req),BELLE_SIP_HEADER(belle_sip_header_date_create_from_time(&curtime)));
belle_sip_message_set_body(BELLE_SIP_MESSAGE(req),msg,content_length);
belle_sip_message_set_body(BELLE_SIP_MESSAGE(req),(multipartEncryptedMessage==NULL)?msg:(const char *)multipartEncryptedMessage,content_length);
return sal_op_send_request(op,req);
}
......@@ -186,7 +289,7 @@ int sal_message_reply(SalOp *op, SalReason reason){
}
int sal_text_send(SalOp *op, const char *from, const char *to, const char *msg) {
return sal_message_send(op,from,to,"text/plain",msg);
return sal_message_send(op,from,to,"text/plain",msg, NULL);
}
static belle_sip_listener_callbacks_t op_message_callbacks={0};
......
......@@ -26,10 +26,6 @@
#include "private.h"
#include "lpconfig.h"
#include <libxml/xmlwriter.h>
#include "lime.h"
#define COMPOSING_DEFAULT_IDLE_TIMEOUT 15
#define COMPOSING_DEFAULT_REFRESH_TIMEOUT 60
#define COMPOSING_DEFAULT_REMOTE_REFRESH_TIMEOUT 120
......@@ -203,54 +199,21 @@ static void _linphone_chat_room_send_message(LinphoneChatRoom *cr, LinphoneChatM
linphone_configure_op(cr->lc,op,cr->peer_url,msg->custom_headers,lp_config_get_int(cr->lc->config,"sip","chat_msg_with_contact",0));
sal_op_set_user_pointer(op, msg); /*if out of call, directly store msg*/
}
if (msg->external_body_url) {
content_type=ms_strdup_printf("message/external-body; access-type=URL; URL=\"%s\"",msg->external_body_url);
sal_message_send(op,identity,cr->peer,content_type, NULL);
sal_message_send(op,identity,cr->peer,content_type, NULL, NULL);
ms_free(content_type);
} else {
uint8_t *multipartEncryptedMessage = NULL;
/* shall we try to encrypt the message?*/
if (1) { /* TODO : set a flag for message encryption into LinphoneChatRoom structure */
/* get the zrtp cache and parse it into an xml doc */
FILE *CACHEFD = fopen(cr->lc->zrtp_secrets_cache, "r+");
ms_message("Cache file is %s", cr->lc->zrtp_secrets_cache);
if (CACHEFD == NULL) {
ms_warning("Unable to access ZRTP ZID cache to encrypt message");
} else {
fseek(CACHEFD, 0L, SEEK_END); /* Position to end of file */
int cacheSize = ftell(CACHEFD); /* Get file length */
rewind(CACHEFD); /* Back to start of file */
uint8_t *cacheString = (uint8_t *)malloc(cacheSize*sizeof(uint8_t)+1); /* string must be null terminated */
fread(cacheString, 1, cacheSize, CACHEFD);
cacheString[cacheSize] = '\0';
cacheSize += 1;
fclose(CACHEFD);
xmlDocPtr cacheXml = xmlParseDoc(cacheString);
int retval = lime_createMultipartMessage(cacheXml, (uint8_t *)msg->message, (uint8_t *)linphone_address_as_string_uri_only(linphone_chat_room_get_peer_address(cr)), &multipartEncryptedMessage);
if (retval != 0) {
ms_warning("Unable to encrypt message to %s error %x", linphone_address_as_string_uri_only(linphone_chat_room_get_peer_address(cr)), retval);
}
/* dump updated cache to a string */
xmlChar *xmlStringOutput;
int xmlStringLength;
xmlDocDumpFormatMemoryEnc(cacheXml, &xmlStringOutput, &xmlStringLength, "UTF-8", 0);
/* write it to the cache file */
CACHEFD = fopen(cr->lc->zrtp_secrets_cache, "w+");
fwrite(xmlStringOutput, 1, xmlStringLength, CACHEFD);
xmlFree(xmlStringOutput);
fclose(CACHEFD);
xmlFreeDoc(cacheXml);
}
}
if (multipartEncryptedMessage!=NULL) {
sal_text_send(op, identity, cr->peer,(const char *)multipartEncryptedMessage);
free(multipartEncryptedMessage);
if (cr->lc->lime == 1) { /* shall we try to encrypt messages? */
linphone_chat_message_ref(msg); /* ref the message or it may be destroyed by callback if the encryption failed */
sal_message_send(op, identity, cr->peer, "xml/cipher", msg->message, linphone_address_as_string_uri_only(linphone_chat_room_get_peer_address(cr)));
} else {
sal_text_send(op, identity, cr->peer,msg->message);
}
}
msg->dir=LinphoneChatMessageOutgoing;
msg->from=linphone_address_new(identity);
msg->storage_id=linphone_chat_message_store(msg);
......@@ -306,7 +269,6 @@ void linphone_core_message_received(LinphoneCore *lc, SalOp *op, const SalMessag
LinphoneChatRoom *cr=NULL;
LinphoneAddress *addr;
char *cleanfrom;
char *from;
LinphoneChatMessage* msg;
const SalCustomHeader *ch;
......@@ -314,53 +276,12 @@ void linphone_core_message_received(LinphoneCore *lc, SalOp *op, const SalMessag
linphone_address_clean(addr);
cr=linphone_core_get_chat_room(lc,addr);
cleanfrom=linphone_address_as_string(addr);
from=linphone_address_as_string_uri_only(addr);
if (cr==NULL){
/* create a new chat room */
cr=linphone_core_create_chat_room(lc,cleanfrom);
}
/* shall we try to decrypt the message */
uint8_t *decryptedMessage = NULL;
if (1) { /* TODO : set a flag for message encryption into LinphoneChatRoom structure */
/* get the zrtp cache and parse it into an xml doc */
FILE *CACHEFD = fopen(cr->lc->zrtp_secrets_cache, "r+");
ms_message("Cache file is %s", lc->zrtp_secrets_cache);
if (CACHEFD == NULL) {
ms_warning("Unable to access ZRTP ZID cache to decrypt message");
} else {
fseek(CACHEFD, 0L, SEEK_END); /* Position to end of file */
int cacheSize = ftell(CACHEFD); /* Get file length */
rewind(CACHEFD); /* Back to start of file */
uint8_t *cacheString = (uint8_t *)malloc(cacheSize*sizeof(uint8_t)+1); /* string must be null terminated */
fread(cacheString, 1, cacheSize, CACHEFD);
cacheString[cacheSize] = '\0';
cacheSize += 1;
fclose(CACHEFD);
xmlDocPtr cacheXml = xmlParseDoc(cacheString);
int retval = lime_decryptMultipartMessage(cacheXml, (uint8_t *)(sal_msg->text), &decryptedMessage);
if (retval != 0) {
ms_warning("Unable to decrypt message error %x", retval);
}
/* dump updated cache to a string */
xmlChar *xmlStringOutput;
int xmlStringLength;
xmlDocDumpFormatMemoryEnc(cacheXml, &xmlStringOutput, &xmlStringLength, "UTF-8", 0);
/* write it to the cache file */
CACHEFD = fopen(lc->zrtp_secrets_cache, "w+");
fwrite(xmlStringOutput, 1, xmlStringLength, CACHEFD);
xmlFree(xmlStringOutput);
fclose(CACHEFD);
xmlFreeDoc(cacheXml);
}
}
if (decryptedMessage == NULL) {
msg = linphone_chat_room_create_message(cr, sal_msg->text);
} else {
msg = linphone_chat_room_create_message(cr, (const char *)decryptedMessage);
}
msg = linphone_chat_room_create_message(cr, sal_msg->text);
linphone_chat_message_set_from(msg, cr->peer_url);
......@@ -384,7 +305,6 @@ void linphone_core_message_received(LinphoneCore *lc, SalOp *op, const SalMessag
msg->storage_id=linphone_chat_message_store(msg);
linphone_chat_room_message_received(cr,lc,msg);
ms_free(cleanfrom);
ms_free(from);
}
static int linphone_chat_room_remote_refresh_composing_expired(void *data, unsigned int revents) {
......@@ -646,7 +566,7 @@ static void linphone_chat_room_send_is_composing_notification(LinphoneChatRoom *
}
content = linphone_chat_room_create_is_composing_xml(cr);
if (content != NULL) {
sal_message_send(op, identity, cr->peer, "application/im-iscomposing+xml", content);
sal_message_send(op, identity, cr->peer, "application/im-iscomposing+xml", content, NULL);
ms_free(content);
}
}
......
......@@ -557,6 +557,11 @@ int lime_createMultipartMessage(xmlDocPtr cacheBuffer, uint8_t *message, uint8_t
}
int lime_decryptMultipartMessage(xmlDocPtr cacheBuffer, uint8_t *message, uint8_t **output) {
int retval;
if (cacheBuffer == NULL) {
return LIME_INVALID_CACHE;
}
/* retrieve selfZIDHex from cache(return a 24 char hexa string + null termination) */
uint8_t selfZidHex[25];
if (lime_getSelfZid(cacheBuffer, selfZidHex) != 0) {
......@@ -566,7 +571,14 @@ int lime_decryptMultipartMessage(xmlDocPtr cacheBuffer, uint8_t *message, uint8_
lime_strToUint8(selfZid, selfZidHex, 24);
/* parse the message into an xml doc */
xmlDocPtr xmlEncryptedMessage = xmlParseDoc(message);
/* make sure we have a valid xml message before trying to parse it */
if (memcmp(message, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>", 38) != 0 ) {
return LIME_INVALID_ENCRYPTED_MESSAGE;
}
xmlDocPtr xmlEncryptedMessage = xmlParseDoc((const xmlChar *)message);
if (xmlEncryptedMessage == NULL) {
return LIME_INVALID_ENCRYPTED_MESSAGE;
}
/* retrieve the sender ZID which is the first child of root */
limeKey_t associatedKey;
......@@ -588,7 +600,7 @@ int lime_decryptMultipartMessage(xmlDocPtr cacheBuffer, uint8_t *message, uint8_
if (peerZidHex != NULL) {
/* get from cache the matching key */
int retval = lime_getCachedRcvKeyByZid(cacheBuffer, &associatedKey);
retval = lime_getCachedRcvKeyByZid(cacheBuffer, &associatedKey);
if (retval == 0) {
/* retrieve the portion of message which is encrypted with our key */
......@@ -633,15 +645,34 @@ int lime_decryptMultipartMessage(xmlDocPtr cacheBuffer, uint8_t *message, uint8_
}
/* shall we derive our key before going for decryption */
if (usedSessionIndex>associatedKey.sessionIndex) {
/* TODO */
if (usedSessionIndex < associatedKey.sessionIndex) {
/* something wen't wrong with the cache, this shall never happend */
free(encryptedMessage);
return LIME_UNABLE_TO_DECRYPT_MESSAGE;
}
if ((usedSessionIndex - associatedKey.sessionIndex > MAX_DERIVATION_NUMBER) ) {
/* we missed to many messages, ask for a cache reset via a ZRTP call */
free(encryptedMessage);
return LIME_UNABLE_TO_DECRYPT_MESSAGE;
}
while (usedSessionIndex>associatedKey.sessionIndex) {
lime_deriveKey(&associatedKey);
}
/* decrypt the message */
*output = (uint8_t *)malloc(encryptedMessageLength - 16 +1); /* plain message is same length than encrypted one with 16 bytes less for the tag + 1 to add the null termination char */
lime_decryptMessage(&associatedKey, encryptedMessage, encryptedMessageLength, selfZid, *output);
retval = lime_decryptMessage(&associatedKey, encryptedMessage, encryptedMessageLength, selfZid, *output);
free(encryptedMessage);
if (retval!=0 ) {
free(*output);
*output = NULL;
return LIME_UNABLE_TO_DECRYPT_MESSAGE;
}
/* update used key */
lime_deriveKey(&associatedKey);
lime_setCachedKey(cacheBuffer, &associatedKey, LIME_RECEIVER);
......
......@@ -6,6 +6,10 @@
#define LIME_UNABLE_TO_ENCRYPT_MESSAGE 0x1004
#define LIME_UNABLE_TO_DECRYPT_MESSAGE 0x1008
#define LIME_NO_KEY_FOUND_FOR_PEER 0x1010
#define LIME_INVALID_ENCRYPTED_MESSAGE 0x1020
/* this define the maximum key derivation number allowed to get the caches back in sync in case of missed messages */
#define MAX_DERIVATION_NUMBER 100
#define LIME_SENDER 0x01
#define LIME_RECEIVER 0x02
......
......@@ -697,6 +697,8 @@ static void sip_config_read(LinphoneCore *lc)
tmp=lp_config_get_int(lc->config,"sip","guess_hostname",1);
linphone_core_set_guess_hostname(lc,tmp);
tmp=lp_config_get_int(lc->config,"sip","lime",0);
linphone_core_set_lime(lc,tmp);
tmp=lp_config_get_int(lc->config,"sip","inc_timeout",30);
linphone_core_set_inc_timeout(lc,tmp);
......@@ -1585,6 +1587,15 @@ bool_t linphone_core_get_guess_hostname(LinphoneCore *lc){
return lc->sip_conf.guess_hostname;
}
/**
* Tells to LinphoneCore to use Linphone Instant Messaging encryption
*
*/
void linphone_core_set_lime(LinphoneCore *lc, bool_t val){
lc->lime=val;
lp_config_set_int(lc->config,"sip","lime",val);
}
/**
* Same as linphone_core_get_primary_contact() but the result is a LinphoneAddress object
* instead of const char*
......
......@@ -1498,6 +1498,8 @@ LINPHONE_PUBLIC const char * linphone_core_get_identity(LinphoneCore *lc);
LINPHONE_PUBLIC void linphone_core_set_guess_hostname(LinphoneCore *lc, bool_t val);
LINPHONE_PUBLIC bool_t linphone_core_get_guess_hostname(LinphoneCore *lc);
LINPHONE_PUBLIC void linphone_core_set_lime(LinphoneCore *lc, bool_t val);
LINPHONE_PUBLIC bool_t linphone_core_ipv6_enabled(LinphoneCore *lc);
LINPHONE_PUBLIC void linphone_core_enable_ipv6(LinphoneCore *lc, bool_t val);
......
......@@ -691,6 +691,7 @@ struct _LinphoneCore
belle_tls_verify_policy_t *http_verify_policy;
MSList *tones;
LinphoneReason chat_deny_code;
bool_t lime;
};
......
......@@ -618,7 +618,7 @@ int sal_unregister(SalOp *h);
/*Messaging */
int sal_text_send(SalOp *op, const char *from, const char *to, const char *text);
int sal_message_send(SalOp *op, const char *from, const char *to, const char* content_type, const char *msg);
int sal_message_send(SalOp *op, const char *from, const char *to, const char* content_type, const char *msg, const char *peer_uri);
int sal_message_reply(SalOp *op, SalReason reason);
/*presence Subscribe/notify*/
......
oRTP @ ebd87c21
Subproject commit beee1f20d69af3d04cd27fcc00b3830620066367
Subproject commit ebd87c21d3d5c8b4775ef08af18e27c4bf4fa3ac
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment