Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
S
StackExchange.Redis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
tsai
StackExchange.Redis
Commits
b816bf29
Commit
b816bf29
authored
Jul 11, 2018
by
Marc Gravell
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
add new helper API to make it easy to trust issuers; support env-based issuer paths
parent
42e44b92
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
164 additions
and
17 deletions
+164
-17
start-all.sh
RedisConfigs/start-all.sh
+9
-10
SSL.cs
StackExchange.Redis.Tests/SSL.cs
+16
-1
StackExchange.Redis.Tests.csproj
StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj
+6
-0
redislabs_ca.pem
StackExchange.Redis.Tests/redislabs_ca.pem
+77
-0
ConfigurationOptions.cs
...xchange.Redis/StackExchange/Redis/ConfigurationOptions.cs
+33
-0
PhysicalConnection.cs
...kExchange.Redis/StackExchange/Redis/PhysicalConnection.cs
+23
-6
No files found.
RedisConfigs/start-all.sh
View file @
b816bf29
INDENT
=
' '
echo
Starting Redis servers
for
testing...
echo
"Starting Redis servers for testing..."
#Basic Servers
#Basic Servers
echo
"Starting Basic: 6379-6382"
echo
Starting Basic: 6379-6382
pushd
Basic
>
/dev/null
pushd
Basic
>
/dev/null
echo
"
${
INDENT
}
Master: 6379"
echo
Master: 6379
redis-server master-6379.conf &>/dev/null &
redis-server master-6379.conf &>/dev/null &
echo
"
${
INDENT
}
Slave: 6380"
echo
Slave: 6380
redis-server slave-6380.conf &>/dev/null &
redis-server slave-6380.conf &>/dev/null &
echo
"
${
INDENT
}
Secure: 6381"
echo
Secure: 6381
redis-server secure-6381.conf &>/dev/null &
redis-server secure-6381.conf &>/dev/null &
popd
>
/dev/null
popd
>
/dev/null
#Failover Servers
#Failover Servers
echo
Starting Failover: 6382-6383
echo
Starting Failover: 6382-6383
pushd
Failover
>
/dev/null
pushd
Failover
>
/dev/null
echo
"
${
INDENT
}
Master: 6382"
echo
Master: 6382
redis-server master-6382.conf &>/dev/null &
redis-server master-6382.conf &>/dev/null &
echo
"
${
INDENT
}
Slave: 6383"
echo
Slave: 6383
redis-server slave-6383.conf &>/dev/null &
redis-server slave-6383.conf &>/dev/null &
popd
>
/dev/null
popd
>
/dev/null
...
@@ -35,10 +34,10 @@ popd > /dev/null
...
@@ -35,10 +34,10 @@ popd > /dev/null
#Sentinel Servers
#Sentinel Servers
echo
Starting Sentinel: 7010-7011,26379-26380
echo
Starting Sentinel: 7010-7011,26379-26380
pushd
Sentinel
>
/dev/null
pushd
Sentinel
>
/dev/null
echo
"
${
INDENT
}
Targets: 7010-7011"
echo
Targets: 7010-7011
redis-server redis-7010.conf &>/dev/null &
redis-server redis-7010.conf &>/dev/null &
redis-server redis-7011.conf &>/dev/null &
redis-server redis-7011.conf &>/dev/null &
echo
"
${
INDENT
}
Monitors: 26379-26380"
echo
Monitors: 26379-26380
redis-server sentinel-26379.conf
--sentinel
&>/dev/null &
redis-server sentinel-26379.conf
--sentinel
&>/dev/null &
redis-server sentinel-26380.conf
--sentinel
&>/dev/null &
redis-server sentinel-26380.conf
--sentinel
&>/dev/null &
popd
>
/dev/null
popd
>
/dev/null
...
...
StackExchange.Redis.Tests/SSL.cs
View file @
b816bf29
...
@@ -173,6 +173,11 @@ public void RedisLabsSSL()
...
@@ -173,6 +173,11 @@ public void RedisLabsSSL()
Skip
.
IfNoConfig
(
nameof
(
TestConfig
.
Config
.
RedisLabsSslServer
),
TestConfig
.
Current
.
RedisLabsSslServer
);
Skip
.
IfNoConfig
(
nameof
(
TestConfig
.
Config
.
RedisLabsSslServer
),
TestConfig
.
Current
.
RedisLabsSslServer
);
Skip
.
IfNoConfig
(
nameof
(
TestConfig
.
Config
.
RedisLabsPfxPath
),
TestConfig
.
Current
.
RedisLabsPfxPath
);
Skip
.
IfNoConfig
(
nameof
(
TestConfig
.
Config
.
RedisLabsPfxPath
),
TestConfig
.
Current
.
RedisLabsPfxPath
);
var
cert
=
new
X509Certificate2
(
TestConfig
.
Current
.
RedisLabsPfxPath
,
""
);
Assert
.
NotNull
(
cert
);
Writer
.
WriteLine
(
"Thumbprint: "
+
cert
.
Thumbprint
);
int
timeout
=
5000
;
int
timeout
=
5000
;
if
(
Debugger
.
IsAttached
)
timeout
*=
100
;
if
(
Debugger
.
IsAttached
)
timeout
*=
100
;
var
options
=
new
ConfigurationOptions
var
options
=
new
ConfigurationOptions
...
@@ -184,6 +189,11 @@ public void RedisLabsSSL()
...
@@ -184,6 +189,11 @@ public void RedisLabsSSL()
"subscribe"
,
"unsubscribe"
,
"cluster"
"subscribe"
,
"unsubscribe"
,
"cluster"
},
false
)
},
false
)
};
};
options
.
CertificateValidation
+=
ConfigurationOptions
.
TrustIssuer
(
"redislabs_ca.pem"
);
if
(!
Directory
.
Exists
(
Me
()))
Directory
.
CreateDirectory
(
Me
());
if
(!
Directory
.
Exists
(
Me
()))
Directory
.
CreateDirectory
(
Me
());
#if LOGOUTPUT
#if LOGOUTPUT
ConnectionMultiplexer
.
EchoPath
=
Me
();
ConnectionMultiplexer
.
EchoPath
=
Me
();
...
@@ -191,7 +201,7 @@ public void RedisLabsSSL()
...
@@ -191,7 +201,7 @@ public void RedisLabsSSL()
options
.
Ssl
=
true
;
options
.
Ssl
=
true
;
options
.
CertificateSelection
+=
delegate
options
.
CertificateSelection
+=
delegate
{
{
return
new
X509Certificate2
(
TestConfig
.
Current
.
RedisLabsPfxPath
,
""
)
;
return
cert
;
};
};
RedisKey
key
=
Me
();
RedisKey
key
=
Me
();
using
(
var
conn
=
ConnectionMultiplexer
.
Connect
(
options
))
using
(
var
conn
=
ConnectionMultiplexer
.
Connect
(
options
))
...
@@ -227,6 +237,10 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv)
...
@@ -227,6 +237,10 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv)
if
(
setEnv
)
if
(
setEnv
)
{
{
Environment
.
SetEnvironmentVariable
(
"SERedis_ClientCertPfxPath"
,
TestConfig
.
Current
.
RedisLabsPfxPath
);
Environment
.
SetEnvironmentVariable
(
"SERedis_ClientCertPfxPath"
,
TestConfig
.
Current
.
RedisLabsPfxPath
);
Environment
.
SetEnvironmentVariable
(
"SERedis_IssuerCertPath"
,
"redislabs_ca.pem"
);
// check env worked
Assert
.
Equal
(
TestConfig
.
Current
.
RedisLabsPfxPath
,
Environment
.
GetEnvironmentVariable
(
"SERedis_ClientCertPfxPath"
));
Assert
.
Equal
(
"redislabs_ca.pem"
,
Environment
.
GetEnvironmentVariable
(
"SERedis_IssuerCertPath"
));
}
}
int
timeout
=
5000
;
int
timeout
=
5000
;
if
(
Debugger
.
IsAttached
)
timeout
*=
100
;
if
(
Debugger
.
IsAttached
)
timeout
*=
100
;
...
@@ -239,6 +253,7 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv)
...
@@ -239,6 +253,7 @@ public void RedisLabsEnvironmentVariableClientCertificate(bool setEnv)
"subscribe"
,
"unsubscribe"
,
"cluster"
"subscribe"
,
"unsubscribe"
,
"cluster"
},
false
)
},
false
)
};
};
if
(!
Directory
.
Exists
(
Me
()))
Directory
.
CreateDirectory
(
Me
());
if
(!
Directory
.
Exists
(
Me
()))
Directory
.
CreateDirectory
(
Me
());
#if LOGOUTPUT
#if LOGOUTPUT
ConnectionMultiplexer
.
EchoPath
=
Me
();
ConnectionMultiplexer
.
EchoPath
=
Me
();
...
...
StackExchange.Redis.Tests/StackExchange.Redis.Tests.csproj
View file @
b816bf29
...
@@ -29,4 +29,10 @@
...
@@ -29,4 +29,10 @@
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
<ItemGroup Condition=" '$(TargetFramework)' == 'net462' ">
<Reference Include="Microsoft.CSharp" />
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
</ItemGroup>
<ItemGroup>
<None Update="redislabs_ca.pem">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
</Project>
StackExchange.Redis.Tests/redislabs_ca.pem
0 → 100644
View file @
b816bf29
Certificate:
Data:
Version: 3 (0x2)
Serial Number: 11859567854091286320 (0xa495a620ecc0b730)
Signature Algorithm: sha1WithRSAEncryption
Issuer: O=Garantia Data, CN=SSL Certification Authority
Validity
Not Before: Oct 1 12:14:55 2013 GMT
Not After : Sep 29 12:14:55 2023 GMT
Subject: O=Garantia Data, CN=SSL Certification Authority
Subject Public Key Info:
Public Key Algorithm: rsaEncryption
Public-Key: (2048 bit)
Modulus:
00:b6:6a:92:1f:c3:73:35:8f:26:7c:67:1c:b4:3b:
40:bd:13:e0:1e:02:0c:a5:81:28:27:22:b2:b8:86:
6c:0e:99:78:f5:95:36:8e:21:7c:a4:02:e8:9a:f3:
7d:1f:b4:f3:53:5e:0f:a5:5c:59:48:b3:ae:67:7e:
8e:d3:e1:21:8e:1c:f9:65:50:62:6e:4f:29:a3:7a:
0d:3d:62:99:87:71:43:0e:da:a8:ee:63:d8:a5:02:
12:1f:dc:ce:7a:4b:c5:e4:87:a1:3c:65:47:7e:04:
43:01:76:f1:69:77:7a:0d:af:73:97:2d:f0:b8:d4:
dd:ea:33:59:59:37:81:be:da:97:1f:66:48:0d:92:
82:6b:97:e6:51:10:6b:09:7e:fa:b4:a3:b0:14:ad:
7a:66:36:04:3c:0e:a4:03:17:22:b7:44:c8:ff:dc:
56:7f:26:92:f8:bf:04:3b:39:33:91:be:d3:d8:f4:
81:f8:72:0b:34:56:31:0e:c7:9f:bd:6e:d5:ea:25:
47:1c:15:c6:08:b7:4c:c9:fe:fe:f4:da:15:2a:b1:
2a:38:1c:93:ac:ee:01:88:c1:44:f6:87:7b:ba:8b:
c4:73:6b:d5:2a:3f:31:cf:67:3f:2f:b7:c0:77:9b:
17:06:c8:72:75:28:8f:06:e9:e2:77:2d:91:66:e3:
6f:67
Exponent: 65537 (0x10001)
X509v3 extensions:
X509v3 Subject Key Identifier:
FD:70:86:D7:2B:C9:D9:96:DD:92:5E:B9:2A:0A:64:82:A3:CD:ED:F0
X509v3 Authority Key Identifier:
keyid:FD:70:86:D7:2B:C9:D9:96:DD:92:5E:B9:2A:0A:64:82:A3:CD:ED:F0
X509v3 Basic Constraints:
CA:TRUE
Signature Algorithm: sha1WithRSAEncryption
6d:9e:ad:78:70:44:06:bb:f9:93:81:b3:40:7a:5f:9e:c7:c3:
27:75:47:89:1f:99:77:2c:d2:bb:5a:95:b3:e9:be:05:0b:4a:
20:7e:4c:26:df:dc:46:e1:26:71:c6:ca:f7:42:63:5b:6f:95:
f7:cb:8d:d0:3b:1c:9d:0f:08:e9:fe:61:82:c1:03:4a:53:53:
f7:72:be:b3:7a:4a:ef:0d:b9:2e:72:b9:b9:ed:f6:66:f5:de:
70:c6:62:8d:6b:9e:dd:18:45:fc:4d:fb:c0:cc:dd:f5:c8:56:
bd:37:f0:0d:f4:52:53:d7:d8:eb:b5:13:11:49:4f:43:19:b8:
52:98:e9:9b:cb:74:8e:bf:d5:c6:e0:9a:0b:8c:94:08:4c:f8:
38:4a:c9:5e:92:af:9e:bd:f4:b3:37:ce:a7:88:f3:5e:a9:66:
69:51:10:44:d8:90:6a:fd:d6:ae:e4:06:95:c9:bb:f7:6d:1d:
a1:b1:83:56:46:bb:ac:3f:3c:2b:18:19:47:04:09:61:0d:60:
3e:15:40:f7:7c:37:7d:89:8c:e7:ee:ea:f1:20:a0:40:30:7c:
f3:fe:de:81:a9:67:89:b7:7b:00:02:71:63:80:7a:7a:9f:95:
bf:9c:41:80:b8:3e:c1:7b:a9:b5:c3:99:16:96:ad:b2:a7:b4:
e9:59:de:7d
-----BEGIN CERTIFICATE-----
MIIDTzCCAjegAwIBAgIJAKSVpiDswLcwMA0GCSqGSIb3DQEBBQUAMD4xFjAUBgNV
BAoMDUdhcmFudGlhIERhdGExJDAiBgNVBAMMG1NTTCBDZXJ0aWZpY2F0aW9uIEF1
dGhvcml0eTAeFw0xMzEwMDExMjE0NTVaFw0yMzA5MjkxMjE0NTVaMD4xFjAUBgNV
BAoMDUdhcmFudGlhIERhdGExJDAiBgNVBAMMG1NTTCBDZXJ0aWZpY2F0aW9uIEF1
dGhvcml0eTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALZqkh/DczWP
JnxnHLQ7QL0T4B4CDKWBKCcisriGbA6ZePWVNo4hfKQC6JrzfR+081NeD6VcWUiz
rmd+jtPhIY4c+WVQYm5PKaN6DT1imYdxQw7aqO5j2KUCEh/cznpLxeSHoTxlR34E
QwF28Wl3eg2vc5ct8LjU3eozWVk3gb7alx9mSA2SgmuX5lEQawl++rSjsBStemY2
BDwOpAMXIrdEyP/cVn8mkvi/BDs5M5G+09j0gfhyCzRWMQ7Hn71u1eolRxwVxgi3
TMn+/vTaFSqxKjgck6zuAYjBRPaHe7qLxHNr1So/Mc9nPy+3wHebFwbIcnUojwbp
4nctkWbjb2cCAwEAAaNQME4wHQYDVR0OBBYEFP1whtcrydmW3ZJeuSoKZIKjze3w
MB8GA1UdIwQYMBaAFP1whtcrydmW3ZJeuSoKZIKjze3wMAwGA1UdEwQFMAMBAf8w
DQYJKoZIhvcNAQEFBQADggEBAG2erXhwRAa7+ZOBs0B6X57Hwyd1R4kfmXcs0rta
lbPpvgULSiB+TCbf3EbhJnHGyvdCY1tvlffLjdA7HJ0PCOn+YYLBA0pTU/dyvrN6
Su8NuS5yubnt9mb13nDGYo1rnt0YRfxN+8DM3fXIVr038A30UlPX2Ou1ExFJT0MZ
uFKY6ZvLdI6/1cbgmguMlAhM+DhKyV6Sr5699LM3zqeI816pZmlREETYkGr91q7k
BpXJu/dtHaGxg1ZGu6w/PCsYGUcECWENYD4VQPd8N32JjOfu6vEgoEAwfPP+3oGp
Z4m3ewACcWOAenqflb+cQYC4PsF7qbXDmRaWrbKntOlZ3n0=
-----END CERTIFICATE-----
StackExchange.Redis/StackExchange/Redis/ConfigurationOptions.cs
View file @
b816bf29
...
@@ -6,6 +6,7 @@
...
@@ -6,6 +6,7 @@
using
System.Net
;
using
System.Net
;
using
System.Net.Security
;
using
System.Net.Security
;
using
System.Security.Authentication
;
using
System.Security.Authentication
;
using
System.Security.Cryptography.X509Certificates
;
using
System.Text
;
using
System.Text
;
using
System.Threading.Tasks
;
using
System.Threading.Tasks
;
...
@@ -175,6 +176,38 @@ public static string TryNormalize(string value)
...
@@ -175,6 +176,38 @@ public static string TryNormalize(string value)
/// </summary>
/// </summary>
public
RedisChannel
ChannelPrefix
{
get
;
set
;
}
public
RedisChannel
ChannelPrefix
{
get
;
set
;
}
/// <summary>
/// Create a certificate validation check that checks against the supplied issuer even if not known by the machine
/// </summary>
public
static
RemoteCertificateValidationCallback
TrustIssuer
(
string
issuerCertificatePath
)
=>
TrustIssuer
(
new
X509Certificate2
(
issuerCertificatePath
));
/// <summary>
/// Create a certificate validation check that checks against the supplied issuer even if not known by the machine
/// </summary>
public
static
RemoteCertificateValidationCallback
TrustIssuer
(
X509Certificate2
issuer
)
{
if
(
issuer
==
null
)
throw
new
ArgumentNullException
(
nameof
(
issuer
));
return
(
object
sender
,
X509Certificate
certificate
,
X509Chain
chain
,
SslPolicyErrors
sslPolicyError
)
=>
sslPolicyError
==
SslPolicyErrors
.
RemoteCertificateChainErrors
&&
certificate
is
X509Certificate2
v2
&&
CheckTrustedIssuer
(
v2
,
issuer
);
}
static
bool
CheckTrustedIssuer
(
X509Certificate2
certificateToValidate
,
X509Certificate2
authority
)
{
// reference: https://stackoverflow.com/questions/6497040/how-do-i-validate-that-a-certificate-was-created-by-a-particular-certification-a
X509Chain
chain
=
new
X509Chain
();
chain
.
ChainPolicy
.
RevocationMode
=
X509RevocationMode
.
NoCheck
;
chain
.
ChainPolicy
.
RevocationFlag
=
X509RevocationFlag
.
ExcludeRoot
;
chain
.
ChainPolicy
.
VerificationFlags
=
X509VerificationFlags
.
AllowUnknownCertificateAuthority
;
chain
.
ChainPolicy
.
VerificationTime
=
DateTime
.
Now
;
chain
.
ChainPolicy
.
UrlRetrievalTimeout
=
new
TimeSpan
(
0
,
0
,
0
);
chain
.
ChainPolicy
.
ExtraStore
.
Add
(
authority
);
return
chain
.
Build
(
certificateToValidate
);
}
/// <summary>
/// <summary>
/// The client name to use for all connections
/// The client name to use for all connections
/// </summary>
/// </summary>
...
...
StackExchange.Redis/StackExchange/Redis/PhysicalConnection.cs
View file @
b816bf29
...
@@ -944,7 +944,20 @@ private static void WriteUnified(PipeWriter writer, long value)
...
@@ -944,7 +944,20 @@ private static void WriteUnified(PipeWriter writer, long value)
internal
int
GetAvailableInboundBytes
()
=>
_socket
?.
Available
??
0
;
internal
int
GetAvailableInboundBytes
()
=>
_socket
?.
Available
??
0
;
private
static
LocalCertificateSelectionCallback
GetAmbientCertificateCallback
()
private
RemoteCertificateValidationCallback
GetAmbientIssuerCertificateCallback
()
{
try
{
var
issuerPath
=
Environment
.
GetEnvironmentVariable
(
"SERedis_IssuerCertPath"
);
if
(!
string
.
IsNullOrEmpty
(
issuerPath
))
return
ConfigurationOptions
.
TrustIssuer
(
issuerPath
);
}
catch
(
Exception
ex
)
{
Debug
.
WriteLine
(
ex
.
Message
);
}
return
null
;
}
private
static
LocalCertificateSelectionCallback
GetAmbientClientCertificateCallback
()
{
{
try
try
{
{
...
@@ -963,8 +976,10 @@ private static LocalCertificateSelectionCallback GetAmbientCertificateCallback()
...
@@ -963,8 +976,10 @@ private static LocalCertificateSelectionCallback GetAmbientCertificateCallback()
return
delegate
{
return
new
X509Certificate2
(
pfxPath
,
pfxPassword
??
""
,
flags
??
X509KeyStorageFlags
.
DefaultKeySet
);
};
return
delegate
{
return
new
X509Certificate2
(
pfxPath
,
pfxPassword
??
""
,
flags
??
X509KeyStorageFlags
.
DefaultKeySet
);
};
}
}
}
}
catch
catch
(
Exception
ex
)
{
}
{
Debug
.
WriteLine
(
ex
.
Message
);
}
return
null
;
return
null
;
}
}
...
@@ -987,8 +1002,9 @@ internal async ValueTask<bool> ConnectedAsync(Socket socket, TextWriter log, Soc
...
@@ -987,8 +1002,9 @@ internal async ValueTask<bool> ConnectedAsync(Socket socket, TextWriter log, Soc
var
host
=
config
.
SslHost
;
var
host
=
config
.
SslHost
;
if
(
string
.
IsNullOrWhiteSpace
(
host
))
host
=
Format
.
ToStringHostOnly
(
Bridge
.
ServerEndPoint
.
EndPoint
);
if
(
string
.
IsNullOrWhiteSpace
(
host
))
host
=
Format
.
ToStringHostOnly
(
Bridge
.
ServerEndPoint
.
EndPoint
);
var
ssl
=
new
SslStream
(
new
NetworkStream
(
socket
),
false
,
config
.
CertificateValidationCallback
,
var
ssl
=
new
SslStream
(
new
NetworkStream
(
socket
),
false
,
config
.
CertificateSelectionCallback
??
GetAmbientCertificateCallback
(),
config
.
CertificateValidationCallback
??
GetAmbientIssuerCertificateCallback
(),
config
.
CertificateSelectionCallback
??
GetAmbientClientCertificateCallback
(),
EncryptionPolicy
.
RequireEncryption
);
EncryptionPolicy
.
RequireEncryption
);
try
try
{
{
...
@@ -996,8 +1012,9 @@ internal async ValueTask<bool> ConnectedAsync(Socket socket, TextWriter log, Soc
...
@@ -996,8 +1012,9 @@ internal async ValueTask<bool> ConnectedAsync(Socket socket, TextWriter log, Soc
{
{
ssl
.
AuthenticateAsClient
(
host
,
config
.
SslProtocols
);
ssl
.
AuthenticateAsClient
(
host
,
config
.
SslProtocols
);
}
}
catch
catch
(
Exception
ex
)
{
{
Debug
.
WriteLine
(
ex
.
Message
);
Multiplexer
?.
SetAuthSuspect
();
Multiplexer
?.
SetAuthSuspect
();
throw
;
throw
;
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment