Commit a1c17a29 authored by Adrien Béraud's avatar Adrien Béraud

tls: transmit certificate and cipher to client

Refs #66534

Change-Id: I1ae97785690f37ae6a11e1632d25347ddd4115d0
parent f2d8655d
......@@ -649,6 +649,22 @@
</arg>
</method>
<method name="validateCertificateRaw" tp:name-for-bindings="validateCertificateRaw">
<arg type="s" name="accountId" direction="in"></arg>
<arg type="ay" name="certificateRaw" direction="in">
<tp:docstring>
<p>A certificate path</p>
</tp:docstring>
</arg>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="MapStringString"/>
<arg type="a{ss}" name="details" direction="out">
<tp:docstring>
<p>A key-value list of all certificate validation</p>
The constants used as keys are defined in the "security.h" constants header file
</tp:docstring>
</arg>
</method>
<method name="getCertificateDetails" tp:name-for-bindings="getCertificateDetails">
<arg type="s" name="certificatePath" direction="in">
<tp:docstring>
......@@ -664,6 +680,21 @@
</arg>
</method>
<method name="getCertificateDetailsRaw" tp:name-for-bindings="getCertificateDetailsRaw">
<arg type="ay" name="certificateRaw" direction="in">
<tp:docstring>
<p>A raw certificate</p>
</tp:docstring>
</arg>
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="MapStringString"/>
<arg type="a{ss}" name="details" direction="out">
<tp:docstring>
<p>A key-value list of all certificate details</p>
The constants used as keys are defined in the "security.h" constants header file
</tp:docstring>
</arg>
</method>
<method name="getAddrFromInterfaceName" tp:name-for-bindings="getAddrFromInterfaceName">
<arg type="s" name="interface" direction="in">
</arg>
......
......@@ -360,12 +360,24 @@ DBusConfigurationManager::validateCertificate(const std::string& accountId, cons
return DRing::validateCertificate(accountId, certificate, privateKey);
}
std::map<std::string, std::string>
DBusConfigurationManager::validateCertificateRaw(const std::string& accountId, const std::vector<uint8_t>& certificate)
{
return DRing::validateCertificateRaw(accountId, certificate);
}
auto
DBusConfigurationManager::getCertificateDetails(const std::string& certificate) -> decltype(DRing::getCertificateDetails(certificate))
{
return DRing::getCertificateDetails(certificate);
}
auto
DBusConfigurationManager::getCertificateDetailsRaw(const std::vector<uint8_t>& certificate) -> decltype(DRing::getCertificateDetailsRaw(certificate))
{
return DRing::getCertificateDetailsRaw(certificate);
}
void
DBusConfigurationManager::setTlsSettings(const std::map<std::string, std::string>& details)
{
......
......@@ -128,8 +128,9 @@ class DBusConfigurationManager :
void setVolume(const std::string& device, const double& value);
double getVolume(const std::string& device);
std::map<std::string, std::string> validateCertificate(const std::string& accountId, const std::string& certificate, const std::string& privateKey);
std::map<std::string, std::string> validateCertificateRaw(const std::string& accountId, const std::vector<uint8_t>& certificate);
std::map<std::string, std::string> getCertificateDetails(const std::string& certificate);
std::map<std::string, std::string> getCertificateDetailsRaw(const std::vector<uint8_t>& certificate);
};
#endif // __RING_DBUSCONFIGURATIONMANAGER_H__
# OPENDHT
OPENDHT_VERSION := 13902b0f19f702c43bc34e24a8d42db6a8d81a31
OPENDHT_VERSION := 6e01255bc00da5ea75c6ee48526743ba56d1f8df
OPENDHT_URL := https://github.com/savoirfairelinux/opendht/archive/$(OPENDHT_VERSION).tar.gz
PKGS += opendht
......
......@@ -24,13 +24,13 @@ ssl_sock_gtls: avoid NULL dereference
pjlib/include/pj/compat/os_auto.h.in | 3 +
pjlib/include/pj/config.h | 4 +-
pjlib/src/pj/ssl_sock_common.c | 5 +
pjlib/src/pj/ssl_sock_gtls.c | 2778 ++++++++++++++++++++++++++++++++++
pjlib/src/pj/ssl_sock_gtls.c | 2782 ++++++++++++++++++++++++++++++++++
pjlib/src/pj/ssl_sock_ossl.c | 6 +-
8 files changed, 3019 insertions(+), 62 deletions(-)
8 files changed, 3023 insertions(+), 62 deletions(-)
create mode 100644 pjlib/src/pj/ssl_sock_gtls.c
diff --git a/aconfigure b/aconfigure
index 24de91d..37e0914 100755
index a296266..03f727f 100755
--- a/aconfigure
+++ b/aconfigure
@@ -637,6 +637,8 @@ ac_no_opencore_amrnb
......@@ -40,9 +40,9 @@ index 24de91d..37e0914 100755
+libgnutls_present
+gnutls_h_present
ac_no_ssl
ac_openh264_ldflags
ac_openh264_cflags
@@ -1462,8 +1464,8 @@ Optional Features:
ac_v4l2_ldflags
ac_v4l2_cflags
@@ -1457,8 +1459,8 @@ Optional Features:
package and samples location using IPPROOT and
IPPSAMPLES env var or with --with-ipp and
--with-ipp-samples options
......@@ -53,7 +53,7 @@ index 24de91d..37e0914 100755
--disable-opencore-amr Exclude OpenCORE AMR support from the build
(default: autodetect)
@@ -7482,33 +7484,159 @@ fi
@@ -7380,33 +7382,159 @@ fi
# Check whether --enable-ssl was given.
if test "${enable_ssl+set}" = set; then :
......@@ -227,7 +227,7 @@ index 24de91d..37e0914 100755
$as_echo_n "checking for ERR_load_BIO_strings in -lcrypto... " >&6; }
if ${ac_cv_lib_crypto_ERR_load_BIO_strings+:} false; then :
$as_echo_n "(cached) " >&6
@@ -7548,7 +7676,7 @@ if test "x$ac_cv_lib_crypto_ERR_load_BIO_strings" = xyes; then :
@@ -7446,7 +7574,7 @@ if test "x$ac_cv_lib_crypto_ERR_load_BIO_strings" = xyes; then :
libcrypto_present=1 && LIBS="$LIBS -lcrypto"
fi
......@@ -236,7 +236,7 @@ index 24de91d..37e0914 100755
$as_echo_n "checking for SSL_library_init in -lssl... " >&6; }
if ${ac_cv_lib_ssl_SSL_library_init+:} false; then :
$as_echo_n "(cached) " >&6
@@ -7588,22 +7716,23 @@ if test "x$ac_cv_lib_ssl_SSL_library_init" = xyes; then :
@@ -7486,22 +7614,23 @@ if test "x$ac_cv_lib_ssl_SSL_library_init" = xyes; then :
libssl_present=1 && LIBS="$LIBS -lssl"
fi
......@@ -271,10 +271,10 @@ index 24de91d..37e0914 100755
if test "${with_opencore_amrnb+set}" = set; then :
withval=$with_opencore_amrnb; as_fn_error $? "This option is obsolete and replaced by --with-opencore-amr=DIR" "$LINENO" 5
diff --git a/aconfigure.ac b/aconfigure.ac
index 6c35836..3be55c5 100644
index cd71a7a..465285e 100644
--- a/aconfigure.ac
+++ b/aconfigure.ac
@@ -1418,38 +1418,76 @@ fi
@@ -1346,38 +1346,76 @@ fi
dnl # Include SSL support
AC_SUBST(ac_no_ssl)
......@@ -383,7 +383,7 @@ index 6c35836..3be55c5 100644
dnl # Obsolete option --with-opencore-amrnb
AC_ARG_WITH(opencore-amrnb,
diff --git a/pjlib/build/Makefile b/pjlib/build/Makefile
index 1e64950..e650a31 100644
index a75fa65..529e0ff 100644
--- a/pjlib/build/Makefile
+++ b/pjlib/build/Makefile
@@ -35,7 +35,7 @@ export PJLIB_OBJS += $(OS_OBJS) $(M_OBJS) $(CC_OBJS) $(HOST_OBJS) \
......@@ -396,7 +396,7 @@ index 1e64950..e650a31 100644
export PJLIB_CFLAGS += $(_CFLAGS)
export PJLIB_CXXFLAGS += $(_CXXFLAGS)
diff --git a/pjlib/include/pj/compat/os_auto.h.in b/pjlib/include/pj/compat/os_auto.h.in
index 77980d3..6d6a625 100644
index 18df2bf..9295740 100644
--- a/pjlib/include/pj/compat/os_auto.h.in
+++ b/pjlib/include/pj/compat/os_auto.h.in
@@ -206,6 +206,9 @@
......@@ -410,7 +410,7 @@ index 77980d3..6d6a625 100644
#endif /* __PJ_COMPAT_OS_AUTO_H__ */
diff --git a/pjlib/include/pj/config.h b/pjlib/include/pj/config.h
index c191ef1..4ace1bc 100644
index 31020a3..90aefe2 100644
--- a/pjlib/include/pj/config.h
+++ b/pjlib/include/pj/config.h
@@ -854,13 +854,15 @@
......@@ -431,7 +431,7 @@ index c191ef1..4ace1bc 100644
diff --git a/pjlib/src/pj/ssl_sock_common.c b/pjlib/src/pj/ssl_sock_common.c
index 67a8d63..602a1af 100644
index 768a640..b116f1b 100644
--- a/pjlib/src/pj/ssl_sock_common.c
+++ b/pjlib/src/pj/ssl_sock_common.c
@@ -34,7 +34,12 @@ PJ_DEF(void) pj_ssl_sock_param_default(pj_ssl_sock_param *param)
......@@ -449,10 +449,10 @@ index 67a8d63..602a1af 100644
#endif
diff --git a/pjlib/src/pj/ssl_sock_gtls.c b/pjlib/src/pj/ssl_sock_gtls.c
new file mode 100644
index 0000000..c2a4349
index 0000000..7b4b941
--- /dev/null
+++ b/pjlib/src/pj/ssl_sock_gtls.c
@@ -0,0 +1,2778 @@
@@ -0,0 +1,2782 @@
+/* $Id$ */
+/*
+ * Copyright (C) 2014 Savoir-Faire Linux. (http://www.savoirfairelinux.com)
......@@ -1594,6 +1594,8 @@ index 0000000..c2a4349
+ goto us_out;
+
+ tls_cert_get_info(ssock->pool, &ssock->local_cert_info, cert);
+ const pj_str_t local_crt_raw = {(char*)us->data, (pj_ssize_t)us->size};
+ pj_strdup(ssock->pool, &ssock->local_cert_info.cert_raw, &local_crt_raw);
+
+us_out:
+ tls_last_error = ret;
......@@ -1620,6 +1622,8 @@ index 0000000..c2a4349
+ goto peer_out;
+
+ tls_cert_get_info(ssock->pool, &ssock->remote_cert_info, cert);
+ const pj_str_t remote_crt_raw = {(char*)certs->data, (pj_ssize_t)certs->size};
+ pj_strdup(ssock->pool, &ssock->remote_cert_info.cert_raw, &remote_crt_raw);
+
+peer_out:
+ tls_last_error = ret;
......@@ -3232,7 +3236,7 @@ index 0000000..c2a4349
+
+#endif /* PJ_HAS_SSL_SOCK */
diff --git a/pjlib/src/pj/ssl_sock_ossl.c b/pjlib/src/pj/ssl_sock_ossl.c
index 7c7b6d0..e466bb7 100644
index ba4ae0b..98bbfee 100644
--- a/pjlib/src/pj/ssl_sock_ossl.c
+++ b/pjlib/src/pj/ssl_sock_ossl.c
@@ -31,8 +31,10 @@
......
......@@ -53,6 +53,7 @@ endif
$(APPLY) $(SRC)/pjproject/aconfigureupdate.patch
$(APPLY) $(SRC)/pjproject/endianness.patch
$(APPLY) $(SRC)/pjproject/unknowncipher.patch
$(APPLY) $(SRC)/pjproject/tls_cert.patch
$(APPLY) $(SRC)/pjproject/gnutls.patch
$(APPLY) $(SRC)/pjproject/notestsapps.patch
$(APPLY) $(SRC)/pjproject/ipv6.patch
......
--- a/pjlib/include/pj/ssl_sock.h
+++ b/pjlib/include/pj/ssl_sock.h
@@ -181,6 +181,8 @@ typedef struct pj_ssl_cert_info {
} subj_alt_name; /**< Subject alternative
name extension */
+ pj_str_t cert_raw;
+
} pj_ssl_cert_info;
......@@ -80,7 +80,7 @@ Call::setConnectionState(ConnectionState state)
}
Call::ConnectionState
Call::getConnectionState()
Call::getConnectionState() const
{
std::lock_guard<std::mutex> lock(callMutex_);
return connectionState_;
......@@ -137,14 +137,14 @@ Call::setState(CallState state)
}
Call::CallState
Call::getState()
Call::getState() const
{
std::lock_guard<std::mutex> lock(callMutex_);
return callState_;
}
std::string
Call::getStateStr()
Call::getStateStr() const
{
switch (getState()) {
case ACTIVE:
......@@ -238,7 +238,7 @@ std::string Call::getTypeStr() const
}
std::map<std::string, std::string>
Call::getDetails()
Call::getDetails() const
{
return {
{DRing::Call::Details::CALL_TYPE, ring::to_string(type_)},
......
......@@ -167,7 +167,7 @@ class Call : public Recordable, public std::enable_shared_from_this<Call> {
* Get the connection state of the call (protected by mutex)
* @return ConnectionState The connection state
*/
ConnectionState getConnectionState();
ConnectionState getConnectionState() const;
/**
* Set the state of the call (protected by mutex)
......@@ -180,9 +180,9 @@ class Call : public Recordable, public std::enable_shared_from_this<Call> {
* Get the call state of the call (protected by mutex)
* @return CallState The call state
*/
CallState getState();
CallState getState() const;
std::string getStateStr();
std::string getStateStr() const;
void setIPToIP(bool IPToIP) {
isIPToIP_ = IPToIP;
......@@ -231,7 +231,7 @@ class Call : public Recordable, public std::enable_shared_from_this<Call> {
unsigned int getLocalVideoPort() const;
void time_stop();
virtual std::map<std::string, std::string> getDetails();
virtual std::map<std::string, std::string> getDetails() const;
static std::map<std::string, std::string> getNullDetails();
virtual bool toggleRecording();
......
......@@ -168,6 +168,23 @@ validateCertificate(const std::string&,
#endif
}
std::map<std::string, std::string>
validateCertificateRaw(const std::string&,
const std::vector<uint8_t>& certificate_raw)
{
#if HAVE_TLS && HAVE_DHT
try {
return TlsValidator{certificate_raw}.getSerializedChecks();
} catch(const std::runtime_error& e) {
RING_WARN("Certificate loading failed");
return {{Certificate::ChecksNames::EXIST, Certificate::CheckValuesNames::FAILED}};
}
#else
RING_WARN("TLS not supported");
return {};
#endif
}
std::map<std::string, std::string>
getCertificateDetails(const std::string& certificate)
{
......@@ -180,7 +197,22 @@ getCertificateDetails(const std::string& certificate)
#else
RING_WARN("TLS not supported");
#endif
return std::map<std::string, std::string>();
return {};
}
std::map<std::string, std::string>
getCertificateDetailsRaw(const std::vector<uint8_t>& certificate_raw)
{
#if HAVE_TLS && HAVE_DHT
try {
return TlsValidator{certificate_raw}.getSerializedDetails();
} catch(const std::runtime_error& e) {
RING_WARN("Certificate loading failed");
}
#else
RING_WARN("TLS not supported");
#endif
return {};
}
void
......
......@@ -169,7 +169,10 @@ double getVolume(const std::string& device);
*/
std::map<std::string, std::string> validateCertificate(const std::string& accountId,
const std::string& certificate, const std::string& privateKey);
std::map<std::string, std::string> validateCertificateRaw(const std::string& accountId,
const std::vector<uint8_t>& certificate);
std::map<std::string, std::string> getCertificateDetails(const std::string& certificate);
std::map<std::string, std::string> getCertificateDetailsRaw(const std::vector<uint8_t>& certificate);
} // namespace DRing
......
......@@ -389,6 +389,10 @@ SipsIceTransport::certGetInfo(pj_pool_t *pool, pj_ssl_cert_info *ci, const gnutl
/* Update cert info */
pj_bzero(ci, sizeof(pj_ssl_cert_info));
/* Full raw certificate */
const pj_str_t raw_crt_pjstr {(char*)crt_raw.data, crt_raw.size};
pj_strdup(pool, &ci->cert_raw, &raw_crt_pjstr);
/* Version */
ci->version = gnutls_x509_crt_get_version(crt.cert);
......
......@@ -52,6 +52,8 @@
#include "im/instant_messaging.h"
#endif
#include "dring/call_const.h"
#ifdef RING_VIDEO
#include "client/videomanager.h"
#include "video/video_rtp_session.h"
......@@ -869,6 +871,18 @@ SIPCall::openPortsUPnP()
}
}
std::map<std::string, std::string>
SIPCall::getDetails() const
{
auto details = Call::getDetails();
if (transport_ and transport_->isSecure()) {
const auto& tlsInfos = transport_->getTlsInfos();
details.emplace(DRing::Call::Details::TLS_PEER_CERT, tlsInfos.peerCert.toString());
details.emplace(DRing::Call::Details::TLS_CIPHER, pj_ssl_cipher_name(tlsInfos.cipher));
}
return details;
}
void
SIPCall::setSecure(bool sec)
{
......
......@@ -193,6 +193,8 @@ class SIPCall : public Call
void openPortsUPnP();
virtual std::map<std::string, std::string> getDetails() const;
private:
NON_COPYABLE(SIPCall);
......
......@@ -45,6 +45,7 @@
#include <pjsip/sip_types.h>
#if HAVE_TLS
#include <pjsip/sip_transport_tls.h>
#include <pj/ssl_sock.h>
#endif
#include <pjnath.h>
#include <pjnath/stun_config.h>
......@@ -131,6 +132,25 @@ void
SipTransport::stateCallback(pjsip_transport_state state,
const pjsip_transport_state_info *info)
{
#if HAVE_TLS
auto extInfo = static_cast<const pjsip_tls_state_info*>(info->ext_info);
if (isSecure() && extInfo && extInfo->ssl_sock_info && extInfo->ssl_sock_info->established) {
auto tlsInfo = extInfo->ssl_sock_info;
tlsInfos_.proto = tlsInfo->proto;
tlsInfos_.cipher = tlsInfo->cipher;
tlsInfos_.verifyStatus = (pj_ssl_cert_verify_flag_t)tlsInfo->verify_status;
const auto& peer_crt = tlsInfo->remote_cert_info->cert_raw;
if (peer_crt.ptr && peer_crt.slen)
tlsInfos_.peerCert = {std::vector<uint8_t>(peer_crt.ptr, peer_crt.ptr + peer_crt.slen)};
else
tlsInfos_.peerCert = {};
} else {
tlsInfos_.proto = PJ_SSL_SOCK_PROTO_DEFAULT;
tlsInfos_.cipher = PJ_TLS_UNKNOWN_CIPHER;
tlsInfos_.peerCert = {};
}
#endif
std::vector<SipTransportStateCallback> cbs;
{
std::lock_guard<std::mutex> lock(stateListenersMutex_);
......
......@@ -39,6 +39,8 @@
#include "noncopyable.h"
#include "logger.h"
#include <opendht/crypto.h>
#include <pjsip.h>
#include <pjnath/stun_config.h>
......@@ -103,6 +105,13 @@ private:
pjsip_tpfactory* listener {nullptr};
};
struct TlsInfos {
pj_ssl_cipher cipher;
pj_ssl_sock_proto proto;
pj_ssl_cert_verify_flag_t verifyStatus;
dht::crypto::Certificate peerCert;
};
using SipTransportStateCallback = std::function<void(pjsip_transport_state, const pjsip_transport_state_info*)>;
/**
......@@ -131,6 +140,10 @@ class SipTransport
return PJSIP_TRANSPORT_IS_SECURE(transport_);
}
const TlsInfos& getTlsInfos() const {
return tlsInfos_;
}
static bool isAlive(const std::shared_ptr<SipTransport>&, pjsip_transport_state state);
private:
......@@ -142,6 +155,8 @@ class SipTransport
std::shared_ptr<TlsListener> tlsListener_;
std::map<uintptr_t, SipTransportStateCallback> stateListeners_;
std::mutex stateListenersMutex_;
TlsInfos tlsInfos_;
};
class IpAddr;
......
......@@ -212,8 +212,9 @@ const Matrix2D<TlsValidator::CheckValuesType , TlsValidator::CheckValues , bool>
TlsValidator::TlsValidator(const std::string& certificate, const std::string& privatekey) :
certificatePath_(certificate), privateKeyPath_(privatekey), certificateFound_(false), caCert_(nullptr),
caChecked_(false)
certificatePath_(certificate),
privateKeyPath_(privatekey),
certificateFound_(false)
{
int err = gnutls_global_init();
if (err != GNUTLS_E_SUCCESS)
......@@ -236,6 +237,22 @@ caChecked_(false)
}
}
TlsValidator::TlsValidator(const std::vector<uint8_t>& certificate_raw) :
certificateFound_(true)
{
int err = gnutls_global_init();
if (err != GNUTLS_E_SUCCESS)
throw TlsValidatorException(gnutls_strerror(err));
try {
x509crt_ = {certificate_raw};
certificateContent_ = x509crt_.getPacked();
certificateFound_ = true;
} catch (const std::exception& e) {
throw TlsValidatorException("Can't load certificate");
}
}
TlsValidator::~TlsValidator()
{
gnutls_global_deinit();
......@@ -367,11 +384,12 @@ static TlsValidator::CheckResult checkError(int err, char* copy_buffer, size_t s
* ASCII-hexadecimal representation before being sent to DBus as it will cause the
* process to assert
*/
static std::string binaryToHex(const char* input, size_t input_sz)
static std::string binaryToHex(const uint8_t* input, size_t input_sz)
{
std::ostringstream ret;
ret << std::hex;
for (size_t i=0; i<input_sz; i++)
ret << std::hex << std::setfill('0') << std::setw(2) << std::uppercase << (unsigned)input[i];
ret << std::setfill('0') << std::setw(2) << (unsigned)input[i];
return ret.str();
}
......@@ -394,7 +412,7 @@ static TlsValidator::CheckResult formatDate(const time_t time)
static TlsValidator::CheckResult checkBinaryError(int err, char* copy_buffer, size_t resultSize)
{
if (err == GNUTLS_E_SUCCESS)
return TlsValidator::CheckResult(TlsValidator::CheckValues::CUSTOM, binaryToHex(copy_buffer, resultSize));
return TlsValidator::CheckResult(TlsValidator::CheckValues::CUSTOM, binaryToHex(reinterpret_cast<uint8_t*>(copy_buffer), resultSize));
else
return TlsValidator::CheckResult(TlsValidator::CheckValues::UNSUPPORTED, "");
}
......@@ -970,7 +988,7 @@ TlsValidator::CheckResult TlsValidator::getSerialNumber()
// gnutls_x509_crt_get_authority_key_gn_serial
size_t resultSize = sizeof(copy_buffer);
int err = gnutls_x509_crt_get_serial(x509crt_.cert, copy_buffer, &resultSize);
return checkError(err, copy_buffer, resultSize);
return checkBinaryError(err, copy_buffer, resultSize);
}
/**
......@@ -1080,8 +1098,8 @@ TlsValidator::CheckResult TlsValidator::getSha1Fingerprint()
*/
TlsValidator::CheckResult TlsValidator::getPublicKeyId()
{
size_t resultSize = sizeof(copy_buffer);
static unsigned char unsigned_copy_buffer[4096];
size_t resultSize = sizeof(unsigned_copy_buffer);
int err = gnutls_x509_crt_get_key_id(x509crt_.cert,0,unsigned_copy_buffer,&resultSize);
// TODO check for GNUTLS_E_SHORT_MEMORY_BUFFER and increase the buffer size
......
......@@ -143,6 +143,8 @@ public:
TlsValidator(const std::string& certificate,
const std::string& privatekey = "");
TlsValidator(const std::vector<uint8_t>& certificate_raw);
~TlsValidator();
bool hasCa() const;
......@@ -233,9 +235,9 @@ private:
dht::crypto::Certificate x509crt_;
bool certificateFound_;
bool privateKeyFound_;
TlsValidator* caCert_;
bool caChecked_;
bool privateKeyFound_ {false};
TlsValidator* caCert_ {nullptr};
bool caChecked_ {false};
unsigned int caValidationOutput_;
mutable char copy_buffer[4096];
......
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