Commit 2dfb9923 authored by Evan Hunt's avatar Evan Hunt

[master] new-zones-directory option

4610.	[func]		The "new-zones-directory" option specifies the
			location of NZF or NZD files for storing
			configuration of zones added by "rndc addzone".
			Thanks to Petr Menšík. [RT #44853]
parent 67e1f8fa
4610. [func] The "new-zones-directory" option specifies the
location of NZF or NZD files for storing
configuration of zones added by "rndc addzone".
Thanks to Petr Menšík. [RT #44853]
4609. [cleanup] Rearrange makefiles to enable parallel execution
(i.e. "make -j"). [RT #45078]
......
......@@ -5642,7 +5642,6 @@ configure_zone(const cfg_obj_t *config, const cfg_obj_t *zconfig,
/*
* Configure built-in zone for storing managed-key data.
*/
static isc_result_t
add_keydata_zone(dns_view_t *view, const char *directory, isc_mem_t *mctx) {
isc_result_t result;
......@@ -6630,6 +6629,8 @@ setup_newzones(dns_view_t *view, cfg_obj_t *config, cfg_obj_t *vconfig,
const cfg_obj_t *maps[4];
const cfg_obj_t *options = NULL, *voptions = NULL;
const cfg_obj_t *nz = NULL;
const cfg_obj_t *nzdir = NULL;
const char *dir = NULL;
int i = 0;
REQUIRE (config != NULL);
......@@ -6647,6 +6648,22 @@ setup_newzones(dns_view_t *view, cfg_obj_t *config, cfg_obj_t *vconfig,
result = ns_config_get(maps, "allow-new-zones", &nz);
if (result == ISC_R_SUCCESS)
allow = cfg_obj_asboolean(nz);
result = ns_config_get(maps, "new-zones-directory", &nzdir);
if (result == ISC_R_SUCCESS) {
dir = cfg_obj_asstring(nzdir);
if (dir != NULL) {
result = isc_file_isdirectory(dir);
}
if (result != ISC_R_SUCCESS) {
isc_log_write(ns_g_lctx, DNS_LOGCATEGORY_SECURITY,
NS_LOGMODULE_SERVER, ISC_LOG_ERROR,
"invalid new-zones-directory %s: %s",
dir, isc_result_totext(result));
return (result);
}
dns_view_setnewzonedir(view, dir);
}
/*
* A non-empty catalog-zones statement implies allow-new-zones
......@@ -6677,7 +6694,7 @@ setup_newzones(dns_view_t *view, cfg_obj_t *config, cfg_obj_t *vconfig,
memset(nzcfg, 0, sizeof(*nzcfg));
result = dns_view_setnewzones(view, allow, nzcfg,
result = dns_view_setnewzones(view, ISC_TRUE, nzcfg,
newzone_cfgctx_destroy);
if (result != ISC_R_SUCCESS) {
isc_mem_free(view->mctx, nzcfg);
......@@ -11038,7 +11055,8 @@ nzf_writeconf(const cfg_obj_t *config, dns_view_t *view) {
char tmp[1024];
isc_result_t result;
result = isc_file_template("", "nzf-XXXXXXXX", tmp, sizeof(tmp));
result = isc_file_template(view->new_zone_file, "nzf-XXXXXXXX",
tmp, sizeof(tmp));
if (result == ISC_R_SUCCESS)
result = isc_file_openunique(tmp, &fp);
if (result != ISC_R_SUCCESS)
......
......@@ -242,12 +242,14 @@
</para>
<para>
The configuration is saved in a file called
<filename><replaceable>name</replaceable>.nzf</filename>,
where <replaceable>name</replaceable> is the
name of the view, or if it contains characters
that are incompatible with use as a file name, a
cryptographic hash generated from the name
of the view.
<filename><replaceable>viewname</replaceable>.nzf</filename>
(or, if <command>named</command> is compiled with
liblmdb, an LMDB database file called
<filename><replaceable>viewname</replaceable>.nzd</filename>).
<replaceable>viewname</replaceable> is the
name of the view, unless the view name contains characters
that are incompatible with use as a file name, in which case
a cryptographic hash of the view name is used instead.
When <command>named</command> is
restarted, the file will be loaded into the view
configuration, so that zones that were added
......
......@@ -14,16 +14,17 @@ rm -f showzone.out*
rm -f zonestatus.out*
rm -f ns2/named.conf
rm -f */named.memstats
rm -f ns1/*.nzf
rm -f ns1/*.nzf~
rm -f ns1/*.nzf ns1/*.nzf~
rm -f ns1/*.nzd ns1/*.nzd-lock
rm -f ns2/*.nzf
rm -f ns2/*.nzf~
rm -f ns2/*.nzf ns2/*.nzf~
rm -f ns2/*.nzd ns2/*.nzd-lock
rm -f ns3/*.nzf ns3/*.nzf~
rm -f ns3/*.nzd ns3/*.nzd-lock
rm -f ns2/core*
rm -f ns2/inline.db.jbk
rm -f ns2/inline.db.signed
rm -f ns2/inlineslave.bk*
rm -rf ns2/new-zones
rm -f ns*/named.lock
rm -f ns*/named.run
rm -f ns2/nzf-*
......@@ -31,3 +32,4 @@ rm -f ns1/redirect.db
rm -f ns2/redirect.db
rm -f ns2/redirect.bk
rm -f ns3/redirect.db
/*
* Copyright (C) 2010, 2011, 2016 Internet Systems Consortium, Inc. ("ISC")
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
controls { /* empty */ };
include "../../common/controls.conf";
options {
port 5300;
pid-file "named.pid";
listen-on { 10.53.0.2; 10.53.0.4; 10.53.0.5; };
listen-on-v6 { none; };
recursion no;
new-zones-directory "new-zones";
};
view internal {
match-clients { 10.53.0.2; };
allow-new-zones no;
recursion yes;
response-policy { zone "policy"; };
zone "." {
type hint;
file "../../common/root.hint";
};
zone "policy" {
type master;
file "normal.db";
};
};
view directory {
match-clients { 10.53.0.5; };
allow-new-zones yes;
zone "." {
type hint;
file "../../common/root.hint";
};
};
view external {
match-clients { any; };
allow-new-zones yes;
zone "." {
type hint;
file "../../common/root.hint";
};
};
# This view is only here to test that configuration context is cleaned
# up correctly when using multiple named ACLs (regression test for RT #22739)
acl match { none; };
acl nobody { none; };
view extra {
match-clients { match; };
allow-new-zones yes;
allow-transfer { nobody; };
allow-query { nobody; };
allow-recursion { nobody; };
};
......@@ -16,3 +16,4 @@ cp -f ns2/redirect.db.1 ns2/redirect.db
cp -f ns3/redirect.db.1 ns3/redirect.db
cp -f ns2/named1.conf ns2/named.conf
cp -f ns2/default.nzf.in ns2/3bf305731dd26307.nzf
mkdir ns2/new-zones
......@@ -551,6 +551,87 @@ n=`expr $n + 1`
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
echo "I:adding new zone again to external view ($n)"
ret=0
$RNDC -c ../common/rndc.conf -s 10.53.0.2 -p 9953 addzone 'added.example in external { type master; file "added.db"; };' 2>&1 | sed 's/^/I:ns2 /'
$DIG +norec $DIGOPTS @10.53.0.2 -b 10.53.0.2 a.added.example a > dig.out.ns2.int.$n || ret=1
grep 'status: NOERROR' dig.out.ns2.int.$n > /dev/null || ret=1
$DIG +norec $DIGOPTS @10.53.0.4 -b 10.53.0.4 a.added.example a > dig.out.ns2.ext.$n || ret=1
grep 'status: NOERROR' dig.out.ns2.ext.$n > /dev/null || ret=1
grep '^a.added.example' dig.out.ns2.ext.$n > /dev/null || ret=1
n=`expr $n + 1`
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
echo "I:reconfiguring server with multiple views and new-zones-directory"
rm -f ns2/named.conf
cp -f ns2/named3.conf ns2/named.conf
$RNDC -c ../common/rndc.conf -s 10.53.0.2 -p 9953 reconfig 2>&1 | sed 's/^/I:ns2 /'
sleep 5
echo "I:checking new zone is still loaded after dir change ($n)"
ret=0
$DIG +norec $DIGOPTS @10.53.0.4 -b 10.53.0.4 a.added.example a > dig.out.ns2.ext.$n || ret=1
grep 'status: NOERROR' dig.out.ns2.ext.$n > /dev/null || ret=1
grep '^a.added.example' dig.out.ns2.ext.$n > /dev/null || ret=1
n=`expr $n + 1`
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
echo "I:deleting newly added zone from external ($n)"
ret=0
$RNDC -c ../common/rndc.conf -s 10.53.0.2 -p 9953 delzone 'added.example in external' 2>&1 | sed 's/^/I:ns2 /'
$DIG $DIGOPTS @10.53.0.4 -b 10.53.0.4 a.added.example a > dig.out.ns2.$n || ret=1
grep 'status: REFUSED' dig.out.ns2.$n > /dev/null || ret=1
grep '^a.added.example' dig.out.ns2.$n > /dev/null && ret=1
n=`expr $n + 1`
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
echo "I:adding new zone to directory view ($n)"
ret=0
$DIG +norec $DIGOPTS @10.53.0.2 -b 10.53.0.2 a.added.example a > dig.out.ns2.intpre.$n || ret=1
grep 'status: NOERROR' dig.out.ns2.intpre.$n > /dev/null || ret=1
$DIG +norec $DIGOPTS @10.53.0.4 -b 10.53.0.4 a.added.example a > dig.out.ns2.extpre.$n || ret=1
grep 'status: REFUSED' dig.out.ns2.extpre.$n > /dev/null || ret=1
$DIG +norec $DIGOPTS @10.53.0.5 -b 10.53.0.5 a.added.example a > dig.out.ns2.dirpre.$n || ret=1
grep 'status: REFUSED' dig.out.ns2.dirpre.$n > /dev/null || ret=1
$RNDC -c ../common/rndc.conf -s 10.53.0.2 -p 9953 addzone 'added.example in directory { type master; file "added.db"; };' 2>&1 | sed 's/^/I:ns2 /'
$DIG +norec $DIGOPTS @10.53.0.2 -b 10.53.0.2 a.added.example a > dig.out.ns2.int.$n || ret=1
grep 'status: NOERROR' dig.out.ns2.int.$n > /dev/null || ret=1
$DIG +norec $DIGOPTS @10.53.0.4 -b 10.53.0.4 a.added.example a > dig.out.ns2.ext.$n || ret=1
grep 'status: REFUSED' dig.out.ns2.ext.$n > /dev/null || ret=1
$DIG +norec $DIGOPTS @10.53.0.5 -b 10.53.0.5 a.added.example a > dig.out.ns2.dir.$n || ret=1
grep 'status: NOERROR' dig.out.ns2.dir.$n > /dev/null || ret=1
grep '^a.added.example' dig.out.ns2.dir.$n > /dev/null || ret=1
n=`expr $n + 1`
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
if [ -n "$NZD" ]; then
echo "I:checking NZD file was created in new-zones-directory ($n)"
expect=ns2/new-zones/directory.nzd
else
echo "I:checking NZF file was created in new-zones-directory ($n)"
expect=ns2/new-zones/directory.nzf
fi
$RNDC -c ../common/rndc.conf -s 10.53.0.2 -p 9953 sync 'added.example IN directory' 2>&1 | sed 's/^/I:ns2 /'
sleep 2
[ -e "$expect" ] || ret=1
n=`expr $n + 1`
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
echo "I:deleting newly added zone from directory ($n)"
ret=0
$RNDC -c ../common/rndc.conf -s 10.53.0.2 -p 9953 delzone 'added.example in directory' 2>&1 | sed 's/^/I:ns2 /'
$DIG $DIGOPTS @10.53.0.5 -b 10.53.0.5 a.added.example a > dig.out.ns2.$n || ret=1
grep 'status: REFUSED' dig.out.ns2.$n > /dev/null || ret=1
grep '^a.added.example' dig.out.ns2.$n > /dev/null && ret=1
n=`expr $n + 1`
if [ $ret != 0 ]; then echo "I:failed"; fi
status=`expr $status + $ret`
echo "I:ensure the configuration context is cleaned up correctly ($n)"
ret=0
$RNDC -c ../common/rndc.conf -s 10.53.0.2 -p 9953 reconfig > /dev/null 2>&1 || ret=1
......
......@@ -4432,6 +4432,7 @@ badresp:1,adberr:0,findfail:0,valfail:0]
[ <command>geoip-directory</command> <replaceable>path_name</replaceable> ; ]
[ <command>key-directory</command> <replaceable>path_name</replaceable> ; ]
[ <command>managed-keys-directory</command> <replaceable>path_name</replaceable> ; ]
[ <command>new-zones-directory</command> <replaceable>path_name</replaceable> ; ]
[ <command>named-xfer</command> <replaceable>path_name</replaceable> ; ]
[ <command>tkey-gssapi-keytab</command> <replaceable>path_name</replaceable> ; ]
[ <command>tkey-gssapi-credential</command> <replaceable>principal</replaceable> ; ]
......@@ -5113,6 +5114,17 @@ badresp:1,adberr:0,findfail:0,valfail:0]
</listitem>
</varlistentry>
<varlistentry>
<term><command>new-zones-directory</command></term>
<listitem>
<para>
Specifies the directory in which to store the configuration
parameters for zones added via <command>rndc addzone</command>.
By default, this is the working directory.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term><command>named-xfer</command></term>
<listitem>
......@@ -5928,6 +5940,20 @@ options {
added at runtime via <command>rndc addzone</command>.
The default is <userinput>no</userinput>.
</para>
<para>
Newly added zones' configuration parameters
are stored so that they can persist after the
server is restarted. The configuration information
is saved in a file called
<filename><replaceable>viewname</replaceable>.nzf</filename>
(or, if <command>named</command> is compiled with
liblmdb, in an LMDB database file called
<filename><replaceable>viewname</replaceable>.nzd</filename>).
<replaceable>viewname</replaceable> is the name of the
view, unless the view name contains characters that are
incompatible with use as a file name, in which case a
cryptographic hash of the view name is used instead.
</para>
</listitem>
</varlistentry>
......
<!DOCTYPE book [
<!ENTITY Scaron "&#x160;">
<!ENTITY scaron "&#x161;">
<!ENTITY ccaron "&#x10D;">
<!ENTITY aacute "&#x0E1;">
<!ENTITY iacute "&#x0ED;">
<!ENTITY mdash "&#8212;">
<!ENTITY ouml "&#xf6;">]>
<!--
......@@ -149,6 +151,16 @@
<section xml:id="relnotes_features"><info><title>New Features</title></info>
<itemizedlist>
<listitem>
<para>
The <command>new-zones-directory</command> option allows
<command>named</command> to store configuration parameters
for zones added via <command>rndc addzone</command> in a
location other than the working directory. Thanks to Petr
Men&scaron;&iacute;k of Red Hat for the contribution.
[RT #44853]
</para>
</listitem>
<listitem>
<para>
Many aspects of <command>named</command> have been modified
......
......@@ -208,8 +208,9 @@ struct dns_view {
* XXX: This should be a pointer to an opaque type that
* named implements.
*/
char * new_zone_dir;
char * new_zone_file;
char * new_zone_db;
char * new_zone_db;
void * new_zone_dbenv;
void * new_zone_config;
void (*cfg_destroy)(void **);
......@@ -1238,6 +1239,19 @@ dns_view_setnewzones(dns_view_t *view, isc_boolean_t allow, void *cfgctx,
* \li ISC_R_NOSPACE
*/
void
dns_view_setnewzonedir(dns_view_t *view, const char *dir);
const char *
dns_view_getnewzonedir(dns_view_t *view);
/*%<
* Set/get the path to the directory in which NZF or NZD files should
* be stored. If the path was previously set to a non-NULL value,
* the previous value is freed.
*
* Requires:
* \li 'view' is valid.
*/
void
dns_view_restorekeyring(dns_view_t *view);
......
......@@ -232,6 +232,7 @@ dns_view_create(isc_mem_t *mctx, dns_rdataclass_t rdclass,
view->sendcookie = ISC_TRUE;
view->requireservercookie = ISC_FALSE;
view->trust_anchor_telemetry = ISC_TRUE;
view->new_zone_dir = NULL;
view->new_zone_file = NULL;
view->new_zone_db = NULL;
view->new_zone_dbenv = NULL;
......@@ -507,6 +508,10 @@ destroy(dns_view_t *view) {
isc_mem_free(view->mctx, view->new_zone_file);
view->new_zone_file = NULL;
}
if (view->new_zone_dir != NULL) {
isc_mem_free(view->mctx, view->new_zone_dir);
view->new_zone_dir = NULL;
}
#ifdef HAVE_LMDB
if (view->new_zone_dbenv != NULL)
mdb_env_close((MDB_env *) view->new_zone_dbenv);
......@@ -1974,6 +1979,56 @@ dns_view_untrust(dns_view_t *view, const dns_name_t *keyname,
dst_key_free(&key);
}
/*
* Create path to a directory and a filename contructed from viewname.
* This is a front-end to isc_file_sanitize(), allowing backward
* compatibility to older versions when a file couldn't be expected
* to be in the specified directory but might be in the current working
* directory instead.
*
* It first tests for the existence of a file <viewname>.<suffix> in
* 'directory'. If the file does not exist, it checks again in the
* current working directory. If it does not exist there either,
* return the path inside the directory.
*
* Returns ISC_R_SUCCESS if a path to an existing file is found or
* a new path is created; returns ISC_R_NOSPACE if the path won't
* fit in 'buflen'.
*/
static isc_result_t
nz_legacy(const char *directory, const char *viewname,
const char *suffix, char *buffer, size_t buflen)
{
isc_result_t result;
char newbuf[PATH_MAX];
result = isc_file_sanitize(directory, viewname, suffix,
buffer, buflen);
if (result != ISC_R_SUCCESS) {
return (result);
} else if (directory == NULL || isc_file_exists(buffer)) {
return (ISC_R_SUCCESS);
} else {
/* Save buffer */
strlcpy(newbuf, buffer, sizeof(newbuf));
}
/*
* It isn't in the specified directory; check CWD.
*/
result = isc_file_sanitize(NULL, viewname, suffix, buffer, buflen);
if (result != ISC_R_SUCCESS || isc_file_exists(buffer)) {
return (result);
}
/*
* File does not exist in either 'directory' or CWD,
* so use the path in 'directory'.
*/
strlcpy(buffer, newbuf, buflen);
return (ISC_R_SUCCESS);
}
isc_result_t
dns_view_setnewzones(dns_view_t *view, isc_boolean_t allow, void *cfgctx,
void (*cfg_destroy)(void **))
......@@ -2010,18 +2065,19 @@ dns_view_setnewzones(dns_view_t *view, isc_boolean_t allow, void *cfgctx,
view->cfg_destroy = NULL;
}
if (!allow)
if (!allow) {
return (ISC_R_SUCCESS);
}
result = isc_file_sanitize(NULL, view->name, "nzf",
buffer, sizeof(buffer));
result = nz_legacy(view->new_zone_dir, view->name, "nzf",
buffer, sizeof(buffer));
if (result != ISC_R_SUCCESS)
goto out;
view->new_zone_file = isc_mem_strdup(view->mctx, buffer);
#ifdef HAVE_LMDB
result = isc_file_sanitize(NULL, view->name, "nzd",
buffer, sizeof(buffer));
result = nz_legacy(view->new_zone_dir, view->name, "nzd",
buffer, sizeof(buffer));
if (result != ISC_R_SUCCESS)
goto out;
view->new_zone_db = isc_mem_strdup(view->mctx, buffer);
......@@ -2068,6 +2124,29 @@ dns_view_setnewzones(dns_view_t *view, isc_boolean_t allow, void *cfgctx,
return (result);
}
void
dns_view_setnewzonedir(dns_view_t *view, const char *dir) {
REQUIRE(DNS_VIEW_VALID(view));
if (view->new_zone_dir != NULL) {
isc_mem_free(view->mctx, view->new_zone_dir);
view->new_zone_dir = NULL;
}
if (dir == NULL) {
return;
}
view->new_zone_dir = isc_mem_strdup(view->mctx, dir);
}
const char *
dns_view_getnewzonedir(dns_view_t *view) {
REQUIRE(DNS_VIEW_VALID(view));
return (view->new_zone_dir);
}
isc_result_t
dns_view_searchdlz(dns_view_t *view, const dns_name_t *name,
unsigned int minlabels, dns_clientinfomethods_t *methods,
......
......@@ -77,11 +77,57 @@ ATF_TC_BODY(isc_file_sanitize, tc) {
unlink(F(SHA));
}
ATF_TC(isc_file_template);
ATF_TC_HEAD(isc_file_template, tc) {
atf_tc_set_md_var(tc, "descr", "file template");
}
ATF_TC_BODY(isc_file_template, tc) {
isc_result_t result;
char buf[1024];
ATF_CHECK(chdir(TESTS) != -1);
result = isc_file_template("/absolute/path", "file-XXXXXXXX",
buf, sizeof(buf));
ATF_CHECK_EQ(result, ISC_R_SUCCESS);
ATF_CHECK_STREQ(buf, "/absolute/file-XXXXXXXX");
result = isc_file_template("relative/path", "file-XXXXXXXX",
buf, sizeof(buf));
ATF_CHECK_EQ(result, ISC_R_SUCCESS);
ATF_CHECK_STREQ(buf, "relative/file-XXXXXXXX");
result = isc_file_template("/trailing/slash/", "file-XXXXXXXX",
buf, sizeof(buf));
ATF_CHECK_EQ(result, ISC_R_SUCCESS);
ATF_CHECK_STREQ(buf, "/trailing/slash/file-XXXXXXXX");
result = isc_file_template("relative/trailing/slash/", "file-XXXXXXXX",
buf, sizeof(buf));
ATF_CHECK_EQ(result, ISC_R_SUCCESS);
ATF_CHECK_STREQ(buf, "relative/trailing/slash/file-XXXXXXXX");
result = isc_file_template("/", "file-XXXXXXXX", buf, sizeof(buf));
ATF_CHECK_EQ(result, ISC_R_SUCCESS);
ATF_CHECK_STREQ(buf, "/file-XXXXXXXX");
result = isc_file_template("noslash", "file-XXXXXXXX",
buf, sizeof(buf));
ATF_CHECK_EQ(result, ISC_R_SUCCESS);
ATF_CHECK_STREQ(buf, "file-XXXXXXXX");
result = isc_file_template(NULL, "file-XXXXXXXX", buf, sizeof(buf));
ATF_CHECK_EQ(result, ISC_R_SUCCESS);
ATF_CHECK_STREQ(buf, "file-XXXXXXXX");
}
/*
* Main
*/
ATF_TP_ADD_TCS(tp) {
ATF_TP_ADD_TC(tp, isc_file_sanitize);
ATF_TP_ADD_TC(tp, isc_file_template);
return (atf_no_error());
}
......@@ -221,10 +221,12 @@ isc_file_template(const char *path, const char *templet, char *buf,
{
const char *s;
REQUIRE(path != NULL);
REQUIRE(templet != NULL);
REQUIRE(buf != NULL);
if (path == NULL)
path = "";
s = strrchr(templet, '/');
if (s != NULL)
templet = s + 1;
......
......@@ -324,6 +324,9 @@ isc_file_template(const char *path, const char *templet, char *buf,
REQUIRE(templet != NULL);
REQUIRE(buf != NULL);
if (path == NULL)
path = "";
s = strrchr(templet, '\\');
if (s != NULL)
templet = s + 1;
......
......@@ -1896,6 +1896,7 @@ view_clauses[] = {
{ "min-roots", &cfg_type_uint32, CFG_CLAUSEFLAG_NOTIMP },
{ "minimal-any", &cfg_type_boolean, 0 },
{ "minimal-responses", &cfg_type_minimal, 0 },
{ "new-zones-directory", &cfg_type_qstring, 0 },
{ "nta-recheck", &cfg_type_ttlval, 0 },
{ "nta-lifetime", &cfg_type_ttlval, 0 },
{ "nxdomain-redirect", &cfg_type_astring, 0 },
......
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