[squid-dev] [PATCH] LockingPointer API update
Amos Jeffries
squid3 at treenet.co.nz
Tue Jun 21 10:00:11 UTC 2016
With GnuTLS support it is sometimes more useful to use a TidyPointer
where a LockingPointer is used by OpenSSL.
This patch converts the LockingPointer resetAndLock() to a virtual
reset() so callers can use the right one without needing to care which
type of pointer they are handling.
Doing this has already uncovered two instances of the
TidyPointer::reset() being wrongly used on cert LockingPointer objects.
There may be more hidden away, I only noticed the ones that were being
used near a resetAndLock() on the same variable. That could have been
leading to early unexpected free'ing of those certs in some code paths.
This change needs to be checked by someone with an OpenSSL bumping
installation please.
Amos
-------------- next part --------------
=== modified file 'src/base/TidyPointer.h'
--- src/base/TidyPointer.h 2016-03-31 23:33:45 +0000
+++ src/base/TidyPointer.h 2016-06-20 22:24:54 +0000
@@ -12,53 +12,53 @@
/**
* A pointer that deletes the object it points to when the pointer's owner or
* context is gone. Similar to std::unique_ptr but without confusing assignment
* and with a customizable cleanup method. Prevents memory leaks in
* the presence of exceptions and processing short cuts.
*/
template <typename T, void (*DeAllocator)(T *t)> class TidyPointer
{
public:
/// Delete callback.
typedef void DCB (T *t);
TidyPointer(T *t = NULL)
: raw(t) {}
public:
bool operator !() const { return !raw; }
explicit operator bool() const { return raw; }
/// Returns raw and possibly NULL pointer
T *get() const { return raw; }
/// Reset raw pointer - delete last one and save new one.
- void reset(T *t) {
+ virtual void reset(T *t) {
deletePointer();
raw = t;
}
/// Forget the raw pointer without freeing it. Become a nil pointer.
T *release() {
T *ret = raw;
raw = NULL;
return ret;
}
/// Deallocate raw pointer.
- ~TidyPointer() {
+ virtual ~TidyPointer() {
deletePointer();
}
private:
/// Forbidden copy constructor.
TidyPointer(TidyPointer<T, DeAllocator> const &);
/// Forbidden assigment operator.
TidyPointer <T, DeAllocator> & operator = (TidyPointer<T, DeAllocator> const &);
/// Deallocate raw pointer. Become a nil pointer.
void deletePointer() {
if (raw) {
DeAllocator(raw);
}
raw = NULL;
}
T *raw; ///< pointer to T object or NULL
};
/// DeAllocator for pointers that need free(3) from the std C library
template<typename T> void tidyFree(T *p)
{
=== modified file 'src/client_side.cc'
--- src/client_side.cc 2016-05-20 13:20:27 +0000
+++ src/client_side.cc 2016-06-20 22:36:29 +0000
@@ -2879,55 +2879,55 @@
SSL_CTX *sslContext = SSL_get_SSL_CTX(ssl);
Ssl::configureUnconfiguredSslContext(sslContext, signAlgorithm, *port);
} else {
auto ctx = Ssl::generateSslContextUsingPkeyAndCertFromMemory(reply_message.getBody().c_str(), *port);
getSslContextDone(ctx, true);
}
return;
}
}
}
getSslContextDone(NULL);
}
void ConnStateData::buildSslCertGenerationParams(Ssl::CertificateProperties &certProperties)
{
certProperties.commonName = sslCommonName_.isEmpty() ? sslConnectHostOrIp.termedBuf() : sslCommonName_.c_str();
// fake certificate adaptation requires bump-server-first mode
if (!sslServerBump) {
assert(port->signingCert.get());
- certProperties.signWithX509.resetAndLock(port->signingCert.get());
+ certProperties.signWithX509.reset(port->signingCert.get());
if (port->signPkey.get())
- certProperties.signWithPkey.resetAndLock(port->signPkey.get());
+ certProperties.signWithPkey.reset(port->signPkey.get());
certProperties.signAlgorithm = Ssl::algSignTrusted;
return;
}
// In case of an error while connecting to the secure server, use a fake
// trusted certificate, with no mimicked fields and no adaptation
// algorithms. There is nothing we can mimic so we want to minimize the
// number of warnings the user will have to see to get to the error page.
assert(sslServerBump->entry);
if (sslServerBump->entry->isEmpty()) {
if (X509 *mimicCert = sslServerBump->serverCert.get())
- certProperties.mimicCert.resetAndLock(mimicCert);
+ certProperties.mimicCert.reset(mimicCert);
ACLFilledChecklist checklist(NULL, sslServerBump->request.getRaw(),
clientConnection != NULL ? clientConnection->rfc931 : dash_str);
checklist.sslErrors = cbdataReference(sslServerBump->sslErrors());
for (sslproxy_cert_adapt *ca = Config.ssl_client.cert_adapt; ca != NULL; ca = ca->next) {
// If the algorithm already set, then ignore it.
if ((ca->alg == Ssl::algSetCommonName && certProperties.setCommonName) ||
(ca->alg == Ssl::algSetValidAfter && certProperties.setValidAfter) ||
(ca->alg == Ssl::algSetValidBefore && certProperties.setValidBefore) )
continue;
if (ca->aclList && checklist.fastCheck(ca->aclList) == ACCESS_ALLOWED) {
const char *alg = Ssl::CertAdaptAlgorithmStr[ca->alg];
const char *param = ca->param;
// For parameterless CN adaptation, use hostname from the
// CONNECT request.
if (ca->alg == Ssl::algSetCommonName) {
if (!param)
@@ -2946,48 +2946,48 @@
certProperties.signAlgorithm = Ssl::algSignEnd;
for (sslproxy_cert_sign *sg = Config.ssl_client.cert_sign; sg != NULL; sg = sg->next) {
if (sg->aclList && checklist.fastCheck(sg->aclList) == ACCESS_ALLOWED) {
certProperties.signAlgorithm = (Ssl::CertSignAlgorithm)sg->alg;
break;
}
}
} else {// if (!sslServerBump->entry->isEmpty())
// Use trusted certificate for a Squid-generated error
// or the user would have to add a security exception
// just to see the error page. We will close the connection
// so that the trust is not extended to non-Squid content.
certProperties.signAlgorithm = Ssl::algSignTrusted;
}
assert(certProperties.signAlgorithm != Ssl::algSignEnd);
if (certProperties.signAlgorithm == Ssl::algSignUntrusted) {
assert(port->untrustedSigningCert.get());
- certProperties.signWithX509.resetAndLock(port->untrustedSigningCert.get());
- certProperties.signWithPkey.resetAndLock(port->untrustedSignPkey.get());
+ certProperties.signWithX509.reset(port->untrustedSigningCert.get());
+ certProperties.signWithPkey.reset(port->untrustedSignPkey.get());
} else {
assert(port->signingCert.get());
- certProperties.signWithX509.resetAndLock(port->signingCert.get());
+ certProperties.signWithX509.reset(port->signingCert.get());
if (port->signPkey.get())
- certProperties.signWithPkey.resetAndLock(port->signPkey.get());
+ certProperties.signWithPkey.reset(port->signPkey.get());
}
signAlgorithm = certProperties.signAlgorithm;
certProperties.signHash = Ssl::DefaultSignHash;
}
void
ConnStateData::getSslContextStart()
{
// XXX starting SSL with a pipeline of requests still waiting for non-SSL replies?
assert(pipeline.count() < 2); // the CONNECT is okay for now. Anything else is a bug.
pipeline.terminateAll(0);
/* careful: terminateAll(0) above frees request, host, etc. */
if (port->generateHostCertificates) {
Ssl::CertificateProperties certProperties;
buildSslCertGenerationParams(certProperties);
sslBumpCertKey = certProperties.dbKey().c_str();
assert(sslBumpCertKey.size() > 0 && sslBumpCertKey[0] != '\0');
=== modified file 'src/security/LockingPointer.h'
--- src/security/LockingPointer.h 2016-03-31 23:33:45 +0000
+++ src/security/LockingPointer.h 2016-06-20 22:54:50 +0000
@@ -32,58 +32,61 @@
extern "C++" inline void function ## _cpp(argument a) { \
function(a); \
}
namespace Security
{
/**
* Add SSL locking (a.k.a. reference counting) and assignment to TidyPointer
*/
template <typename T, void (*DeAllocator)(T *t), int lock>
class LockingPointer: public TidyPointer<T, DeAllocator>
{
public:
typedef TidyPointer<T, DeAllocator> Parent;
typedef LockingPointer<T, DeAllocator, lock> SelfType;
explicit LockingPointer(T *t = nullptr): Parent(t) {}
explicit LockingPointer(const SelfType &o): Parent() {
- resetAndLock(o.get());
+ reset(o.get());
}
+ virtual ~LockingPointer() = default;
SelfType &operator =(const SelfType & o) {
- resetAndLock(o.get());
+ reset(o.get());
return *this;
}
#if __cplusplus >= 201103L
explicit LockingPointer(LockingPointer<T, DeAllocator, lock> &&o): Parent(o.release()) {
}
LockingPointer<T, DeAllocator, lock> &operator =(LockingPointer<T, DeAllocator, lock> &&o) {
if (o.get() != this->get())
this->reset(o.release());
return *this;
}
#endif
- void resetAndLock(T *t) {
+ virtual void reset(T *t) override {
if (t != this->get()) {
- this->reset(t);
+ // initial part must match TidyPointer::reset
+ this->TidyPointer<T, DeAllocator>::reset(t);
+ // then the locking
#if USE_OPENSSL
if (t)
CRYPTO_add(&t->references, 1, lock);
#elif USE_GNUTLS
// XXX: GnuTLS does not provide locking ?
#else
assert(false);
#endif
}
}
};
} // namespace Security
#endif /* SQUID_SRC_SECURITY_LOCKINGPOINTER_H */
=== modified file 'src/ssl/ErrorDetail.cc'
--- src/ssl/ErrorDetail.cc 2016-01-01 00:12:18 +0000
+++ src/ssl/ErrorDetail.cc 2016-06-20 22:39:28 +0000
@@ -611,48 +611,48 @@
code_len = convert(++p, &t);
if (code_len)
errDetailStr.append(t);
else
errDetailStr.append("%");
s = p + code_len;
}
errDetailStr.append(s, strlen(s));
}
const String &Ssl::ErrorDetail::toString() const
{
if (errDetailStr.size() == 0)
buildDetail();
return errDetailStr;
}
Ssl::ErrorDetail::ErrorDetail( Ssl::ssl_error_t err_no, X509 *cert, X509 *broken, const char *aReason): error_no (err_no), lib_error_no(SSL_ERROR_NONE), errReason(aReason)
{
if (cert)
- peer_cert.resetAndLock(cert);
+ peer_cert.reset(cert);
if (broken)
- broken_cert.resetAndLock(broken);
+ broken_cert.reset(broken);
else
- broken_cert.resetAndLock(cert);
+ broken_cert.reset(cert);
detailEntry.error_no = SSL_ERROR_NONE;
}
Ssl::ErrorDetail::ErrorDetail(Ssl::ErrorDetail const &anErrDetail)
{
error_no = anErrDetail.error_no;
request = anErrDetail.request;
if (anErrDetail.peer_cert.get()) {
- peer_cert.resetAndLock(anErrDetail.peer_cert.get());
+ peer_cert.reset(anErrDetail.peer_cert.get());
}
if (anErrDetail.broken_cert.get()) {
- broken_cert.resetAndLock(anErrDetail.broken_cert.get());
+ broken_cert.reset(anErrDetail.broken_cert.get());
}
detailEntry = anErrDetail.detailEntry;
lib_error_no = anErrDetail.lib_error_no;
}
=== modified file 'src/ssl/PeekingPeerConnector.cc'
--- src/ssl/PeekingPeerConnector.cc 2016-05-18 16:35:36 +0000
+++ src/ssl/PeekingPeerConnector.cc 2016-06-20 22:40:27 +0000
@@ -212,41 +212,41 @@
}
}
return ssl;
}
void
Ssl::PeekingPeerConnector::noteNegotiationDone(ErrorState *error)
{
Security::SessionPtr ssl = fd_table[serverConnection()->fd].ssl.get();
// Check the list error with
if (!request->clientConnectionManager.valid() || ! ssl)
return;
// remember the server certificate from the ErrorDetail object
if (Ssl::ServerBump *serverBump = request->clientConnectionManager->serverBump()) {
if (!serverBump->serverCert.get()) {
// remember the server certificate from the ErrorDetail object
if (error && error->detail && error->detail->peerCert())
- serverBump->serverCert.resetAndLock(error->detail->peerCert());
+ serverBump->serverCert.reset(error->detail->peerCert());
else {
handleServerCertificate();
}
}
if (error) {
// For intercepted connections, set the host name to the server
// certificate CN. Otherwise, we just hope that CONNECT is using
// a user-entered address (a host name or a user-entered IP).
const bool isConnectRequest = !request->clientConnectionManager->port->flags.isIntercepted();
if (request->flags.sslPeek && !isConnectRequest) {
if (X509 *srvX509 = serverBump->serverCert.get()) {
if (const char *name = Ssl::CommonHostName(srvX509)) {
request->url.host(name);
debugs(83, 3, "reset request host: " << name);
}
}
}
}
}
@@ -333,41 +333,41 @@
Security::SessionPtr ssl = fd_table[fd].ssl.get();
Security::CertPointer serverCert(SSL_get_peer_certificate(ssl));
if (!serverCert.get())
return;
serverCertificateHandled = true;
// remember the server certificate for later use
if (Ssl::ServerBump *serverBump = csd->serverBump()) {
serverBump->serverCert.reset(serverCert.release());
}
}
}
void
Ssl::PeekingPeerConnector::serverCertificateVerified()
{
if (ConnStateData *csd = request->clientConnectionManager.valid()) {
Security::CertPointer serverCert;
if(Ssl::ServerBump *serverBump = csd->serverBump())
- serverCert.resetAndLock(serverBump->serverCert.get());
+ serverCert.reset(serverBump->serverCert.get());
else {
const int fd = serverConnection()->fd;
Security::SessionPtr ssl = fd_table[fd].ssl.get();
serverCert.reset(SSL_get_peer_certificate(ssl));
}
if (serverCert.get()) {
csd->resetSslCommonName(Ssl::CommonHostName(serverCert.get()));
debugs(83, 5, "HTTPS server CN: " << csd->sslCommonName() <<
" bumped: " << *serverConnection());
}
}
}
void
Ssl::PeekingPeerConnector::tunnelInsteadOfNegotiating()
{
Must(callback != NULL);
CbDialer *dialer = dynamic_cast<CbDialer*>(callback->getDialer());
Must(dialer);
dialer->answer().tunneled = true;
=== modified file 'src/ssl/ServerBump.cc'
--- src/ssl/ServerBump.cc 2016-02-13 12:12:10 +0000
+++ src/ssl/ServerBump.cc 2016-06-20 22:41:30 +0000
@@ -40,33 +40,33 @@
// later, but an entry without any client will trim all its contents away.
sc = storeClientListAdd(entry, this);
}
Ssl::ServerBump::~ServerBump()
{
debugs(33, 4, HERE << "destroying");
if (entry) {
debugs(33, 4, HERE << *entry);
storeUnregister(sc, entry, this);
entry->unlock("Ssl::ServerBump");
}
}
void
Ssl::ServerBump::attachServerSSL(SSL *ssl)
{
if (serverSSL.get())
return;
- serverSSL.resetAndLock(ssl);
+ serverSSL.reset(ssl);
}
const Ssl::CertErrors *
Ssl::ServerBump::sslErrors() const
{
if (!serverSSL.get())
return NULL;
const Ssl::CertErrors *errs = static_cast<const Ssl::CertErrors*>(SSL_get_ex_data(serverSSL.get(), ssl_ex_index_ssl_errors));
return errs;
}
=== modified file 'src/ssl/cert_validate_message.cc'
--- src/ssl/cert_validate_message.cc 2016-01-05 10:55:14 +0000
+++ src/ssl/cert_validate_message.cc 2016-06-20 22:41:38 +0000
@@ -199,52 +199,52 @@
id = old.id;
error_no = old.error_no;
error_reason = old.error_reason;
error_depth = old.error_depth;
setCert(old.cert.get());
}
Ssl::CertValidationResponse::RecvdError & Ssl::CertValidationResponse::RecvdError::operator = (const RecvdError &old)
{
id = old.id;
error_no = old.error_no;
error_reason = old.error_reason;
error_depth = old.error_depth;
setCert(old.cert.get());
return *this;
}
void
Ssl::CertValidationResponse::RecvdError::setCert(X509 *aCert)
{
- cert.resetAndLock(aCert);
+ cert.reset(aCert);
}
Ssl::CertValidationMsg::CertItem::CertItem(const CertItem &old)
{
name = old.name;
setCert(old.cert.get());
}
Ssl::CertValidationMsg::CertItem & Ssl::CertValidationMsg::CertItem::operator = (const CertItem &old)
{
name = old.name;
setCert(old.cert.get());
return *this;
}
void
Ssl::CertValidationMsg::CertItem::setCert(X509 *aCert)
{
- cert.resetAndLock(aCert);
+ cert.reset(aCert);
}
const std::string Ssl::CertValidationMsg::code_cert_validate("cert_validate");
const std::string Ssl::CertValidationMsg::param_domain("domain");
const std::string Ssl::CertValidationMsg::param_cert("cert_");
const std::string Ssl::CertValidationMsg::param_error_name("error_name_");
const std::string Ssl::CertValidationMsg::param_error_reason("error_reason_");
const std::string Ssl::CertValidationMsg::param_error_cert("error_cert_");
const std::string Ssl::CertValidationMsg::param_error_depth("error_depth_");
const std::string Ssl::CertValidationMsg::param_proto_version("proto_version");
const std::string Ssl::CertValidationMsg::param_cipher("cipher");
=== modified file 'src/ssl/gadgets.cc'
--- src/ssl/gadgets.cc 2016-06-14 18:12:14 +0000
+++ src/ssl/gadgets.cc 2016-06-20 22:41:58 +0000
@@ -492,41 +492,41 @@
if (X509_add_ext(cert.get(), ext, -1))
++addedExtensions;
}
}
addedExtensions += mimicExtensions(cert, properties.mimicCert, properties.signWithX509);
// According to RFC 5280, using extensions requires v3 certificate.
if (addedExtensions)
X509_set_version(cert.get(), 2); // value 2 means v3
}
return true;
}
static bool generateFakeSslCertificate(Security::CertPointer & certToStore, Ssl::EVP_PKEY_Pointer & pkeyToStore, Ssl::CertificateProperties const &properties, Ssl::BIGNUM_Pointer const &serial)
{
Ssl::EVP_PKEY_Pointer pkey;
// Use signing certificates private key as generated certificate private key
if (properties.signWithPkey.get())
- pkey.resetAndLock(properties.signWithPkey.get());
+ pkey.reset(properties.signWithPkey.get());
else // if not exist generate one
pkey.reset(Ssl::createSslPrivateKey());
if (!pkey)
return false;
Security::CertPointer cert(X509_new());
if (!cert)
return false;
// Set pub key and serial given by the caller
if (!X509_set_pubkey(cert.get(), pkey.get()))
return false;
if (!setSerialNumber(X509_get_serialNumber(cert.get()), serial.get()))
return false;
// Fill the certificate with the required properties
if (!buildCertificate(cert, properties))
return false;
=== modified file 'src/ssl/support.cc'
--- src/ssl/support.cc 2016-06-02 09:52:43 +0000
+++ src/ssl/support.cc 2016-06-20 22:43:17 +0000
@@ -289,41 +289,41 @@
if (!SSL_set_ex_data(ssl, ssl_ex_index_ssl_errors, (void *)errs)) {
debugs(83, 2, "Failed to set ssl error_no in ssl_verify_cb: Certificate " << buffer);
delete errs;
errs = NULL;
}
} else // remember another error number
errs->push_back_unique(Ssl::CertError(error_no, broken_cert));
if (const char *err_descr = Ssl::GetErrorDescr(error_no))
debugs(83, 5, err_descr << ": " << buffer);
else
debugs(83, DBG_IMPORTANT, "SSL unknown certificate error " << error_no << " in " << buffer);
// Check if the certificate error can be bypassed.
// Infinity validation loop errors can not bypassed.
if (error_no != SQUID_X509_V_ERR_INFINITE_VALIDATION) {
if (check) {
ACLFilledChecklist *filledCheck = Filled(check);
assert(!filledCheck->sslErrors);
filledCheck->sslErrors = new Ssl::CertErrors(Ssl::CertError(error_no, broken_cert));
- filledCheck->serverCert.resetAndLock(peer_cert);
+ filledCheck->serverCert.reset(peer_cert);
if (check->fastCheck() == ACCESS_ALLOWED) {
debugs(83, 3, "bypassing SSL error " << error_no << " in " << buffer);
ok = 1;
} else {
debugs(83, 5, "confirming SSL error " << error_no);
}
delete filledCheck->sslErrors;
filledCheck->sslErrors = NULL;
filledCheck->serverCert.reset(NULL);
}
// If the certificate validator is used then we need to allow all errors and
// pass them to certficate validator for more processing
else if (Ssl::TheConfig.ssl_crt_validator) {
ok = 1;
}
}
}
if (Ssl::TheConfig.ssl_crt_validator) {
// Check if we have stored certificates chain. Store if not.
@@ -1282,42 +1282,42 @@
}
bool Ssl::generateUntrustedCert(Security::CertPointer &untrustedCert, EVP_PKEY_Pointer &untrustedPkey, Security::CertPointer const &cert, EVP_PKEY_Pointer const & pkey)
{
// Generate the self-signed certificate, using a hard-coded subject prefix
Ssl::CertificateProperties certProperties;
if (const char *cn = CommonHostName(cert.get())) {
certProperties.commonName = "Not trusted by \"";
certProperties.commonName += cn;
certProperties.commonName += "\"";
} else if (const char *org = getOrganization(cert.get())) {
certProperties.commonName = "Not trusted by \"";
certProperties.commonName += org;
certProperties.commonName += "\"";
} else
certProperties.commonName = "Not trusted";
certProperties.setCommonName = true;
// O, OU, and other CA subject fields will be mimicked
// Expiration date and other common properties will be mimicked
certProperties.signAlgorithm = Ssl::algSignSelf;
- certProperties.signWithPkey.resetAndLock(pkey.get());
- certProperties.mimicCert.resetAndLock(cert.get());
+ certProperties.signWithPkey.reset(pkey.get());
+ certProperties.mimicCert.reset(cert.get());
return Ssl::generateSslCertificate(untrustedCert, untrustedPkey, certProperties);
}
SSL *
SslCreate(Security::ContextPtr sslContext, const int fd, Ssl::Bio::Type type, const char *squidCtx)
{
if (fd < 0) {
debugs(83, DBG_IMPORTANT, "Gone connection");
return NULL;
}
const char *errAction = NULL;
int errCode = 0;
if (auto ssl = SSL_new(sslContext)) {
// without BIO, we would call SSL_set_fd(ssl, fd) instead
if (BIO *bio = Ssl::Bio::Create(fd, type)) {
Ssl::Bio::Link(ssl, bio); // cannot fail
fd_table[fd].ssl.reset(ssl);
fd_table[fd].read_method = &ssl_read_method;
@@ -1335,53 +1335,53 @@
debugs(83, DBG_IMPORTANT, "ERROR: " << squidCtx << ' ' << errAction <<
": " << ERR_error_string(errCode, NULL));
return NULL;
}
SSL *
Ssl::CreateClient(Security::ContextPtr sslContext, const int fd, const char *squidCtx)
{
return SslCreate(sslContext, fd, Ssl::Bio::BIO_TO_SERVER, squidCtx);
}
SSL *
Ssl::CreateServer(Security::ContextPtr sslContext, const int fd, const char *squidCtx)
{
return SslCreate(sslContext, fd, Ssl::Bio::BIO_TO_CLIENT, squidCtx);
}
Ssl::CertError::CertError(ssl_error_t anErr, X509 *aCert, int aDepth): code(anErr), depth(aDepth)
{
- cert.resetAndLock(aCert);
+ cert.reset(aCert);
}
Ssl::CertError::CertError(CertError const &err): code(err.code), depth(err.depth)
{
- cert.resetAndLock(err.cert.get());
+ cert.reset(err.cert.get());
}
Ssl::CertError &
Ssl::CertError::operator = (const CertError &old)
{
code = old.code;
- cert.resetAndLock(old.cert.get());
+ cert.reset(old.cert.get());
return *this;
}
bool
Ssl::CertError::operator == (const CertError &ce) const
{
return code == ce.code && cert.get() == ce.cert.get();
}
bool
Ssl::CertError::operator != (const CertError &ce) const
{
return code != ce.code || cert.get() != ce.cert.get();
}
static int
store_session_cb(SSL *ssl, SSL_SESSION *session)
{
if (!Ssl::SessionCache)
return 0;
More information about the squid-dev
mailing list