Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
LEFEBVREJP email
rsm
Commits
7edeedfc
Commit
7edeedfc
authored
Jan 27, 2020
by
LEFEBVREJP email
Browse files
Working authentication and command submission. Started on interactive authentication.
parent
2d792e1e
Changes
6
Hide whitespace changes
Inline
Side-by-side
rsmcore/sessioncontroller.cc
View file @
7edeedfc
...
@@ -51,6 +51,11 @@ SessionController::SessionController(QObject* parent)
...
@@ -51,6 +51,11 @@ SessionController::SessionController(QObject* parent)
&
SessionController
::
connectionFailed
);
&
SessionController
::
connectionFailed
);
QObject
::
connect
(
p
->
worker
,
&
SessionWorker
::
connectionSuccessful
,
this
,
QObject
::
connect
(
p
->
worker
,
&
SessionWorker
::
connectionSuccessful
,
this
,
&
SessionController
::
connectionSuccessful
);
&
SessionController
::
connectionSuccessful
);
QObject
::
connect
(
p
->
worker
,
&
SessionWorker
::
disconnectSuccessful
,
this
,
&
SessionController
::
disconnectSuccessful
);
QObject
::
connect
(
p
->
worker
,
&
SessionWorker
::
interactiveAuthenticationRequested
,
this
,
&
SessionController
::
interactiveAuthenticationRequested
);
QObject
::
connect
(
p
->
worker
,
&
SessionWorker
::
verifyKnownHostSuccessful
,
this
,
QObject
::
connect
(
p
->
worker
,
&
SessionWorker
::
verifyKnownHostSuccessful
,
this
,
&
SessionController
::
verifyKnownHostSuccessful
);
&
SessionController
::
verifyKnownHostSuccessful
);
QObject
::
connect
(
p
->
worker
,
&
SessionWorker
::
getServerPublicKeyFailed
,
this
,
QObject
::
connect
(
p
->
worker
,
&
SessionWorker
::
getServerPublicKeyFailed
,
this
,
...
...
rsmcore/sessioncontroller.hh
View file @
7edeedfc
...
@@ -88,7 +88,11 @@ class RSM_PUBLIC SessionController : public QObject
...
@@ -88,7 +88,11 @@ class RSM_PUBLIC SessionController : public QObject
void
connectionFailed
(
QString
message
);
void
connectionFailed
(
QString
message
);
void
connectionSuccessful
();
void
connectionSuccessful
();
void
disconnectSuccessful
();
void
verifyKnownHostSuccessful
();
void
verifyKnownHostSuccessful
();
void
interactiveAuthenticationRequested
(
QString
instruction
,
QString
name
,
QStringList
prompts
);
/**
/**
* @brief getServerPublicKeyFailed
* @brief getServerPublicKeyFailed
* Signal is emitted when the remote host does not have or provide a public
* Signal is emitted when the remote host does not have or provide a public
...
...
rsmcore/sessionworker.cc
View file @
7edeedfc
...
@@ -86,6 +86,9 @@ void SessionWorker::connect()
...
@@ -86,6 +86,9 @@ void SessionWorker::connect()
{
{
assert_ssh_session
(
p
->
session
,
"connect() -- Session is not allocated."
);
assert_ssh_session
(
p
->
session
,
"connect() -- Session is not allocated."
);
if
(
ssh_is_connected
(
p
->
session
)
==
0
)
if
(
ssh_is_connected
(
p
->
session
)
==
0
)
{
ssh_disconnect
(
p
->
session
);
}
{
{
// attempt a connection
// attempt a connection
int
rc
=
ssh_connect
(
p
->
session
);
int
rc
=
ssh_connect
(
p
->
session
);
...
@@ -111,10 +114,12 @@ void SessionWorker::connect()
...
@@ -111,10 +114,12 @@ void SessionWorker::connect()
void
SessionWorker
::
disconnect
()
void
SessionWorker
::
disconnect
()
{
{
assert_ssh_session
(
p
->
session
,
"disconnect() -- Session is not allocated."
);
assert_ssh_session
(
p
->
session
,
"disconnect() -- Session is not allocated."
);
radix_tagged_line
(
"disconnect()"
);
if
(
ssh_is_connected
(
p
->
session
)
!=
0
)
if
(
ssh_is_connected
(
p
->
session
)
!=
0
)
{
{
radix_tagged_line
(
"Disconnecting session."
);
radix_tagged_line
(
"Disconnecting session."
);
ssh_disconnect
(
p
->
session
);
ssh_disconnect
(
p
->
session
);
emit
disconnectSuccessful
();
}
}
}
}
...
@@ -129,6 +134,7 @@ void SessionWorker::verifyKnownHost()
...
@@ -129,6 +134,7 @@ void SessionWorker::verifyKnownHost()
size_t
hlen
;
size_t
hlen
;
QString
qhexa
;
QString
qhexa
;
radix_tagged_line
(
"verifyKnownHost()"
);
int
rc
=
ssh_get_server_publickey
(
p
->
session
,
&
server_public_key
);
int
rc
=
ssh_get_server_publickey
(
p
->
session
,
&
server_public_key
);
if
(
rc
<
0
)
if
(
rc
<
0
)
{
{
...
@@ -191,6 +197,7 @@ void SessionWorker::acceptHostPublicKeyUpdate()
...
@@ -191,6 +197,7 @@ void SessionWorker::acceptHostPublicKeyUpdate()
{
{
assert_ssh_session
(
assert_ssh_session
(
p
->
session
,
"acceptHostPublicKeyUpdate() -- Session is not allocated."
);
p
->
session
,
"acceptHostPublicKeyUpdate() -- Session is not allocated."
);
radix_tagged_line
(
"authenticateHostPublicKeyUpdate()"
);
int
rc
=
ssh_session_update_known_hosts
(
p
->
session
);
int
rc
=
ssh_session_update_known_hosts
(
p
->
session
);
if
(
rc
!=
SSH_OK
)
if
(
rc
!=
SSH_OK
)
{
{
...
@@ -202,6 +209,7 @@ void SessionWorker::authenticate()
...
@@ -202,6 +209,7 @@ void SessionWorker::authenticate()
{
{
assert_ssh_session
(
p
->
session
,
"authenticate() -- Session is not allocated."
);
assert_ssh_session
(
p
->
session
,
"authenticate() -- Session is not allocated."
);
radix_tagged_line
(
"Authenticate."
);
// Try authenticating with no credentials - rarely works
// Try authenticating with no credentials - rarely works
int
rc
=
ssh_userauth_none
(
p
->
session
,
nullptr
);
int
rc
=
ssh_userauth_none
(
p
->
session
,
nullptr
);
if
(
rc
==
SSH_AUTH_ERROR
)
if
(
rc
==
SSH_AUTH_ERROR
)
...
@@ -232,6 +240,34 @@ void SessionWorker::authenticate()
...
@@ -232,6 +240,34 @@ void SessionWorker::authenticate()
}
}
radix_tagged_line
(
"SSH_AUTH_METHOD_PUBLICKEY didn't work."
);
radix_tagged_line
(
"SSH_AUTH_METHOD_PUBLICKEY didn't work."
);
}
// public key authentication
}
// public key authentication
// Try to authenticate with keyboard interactive";
if
(
method
&
SSH_AUTH_METHOD_INTERACTIVE
)
{
int
err
;
err
=
ssh_userauth_kbdint
(
p
->
session
,
nullptr
,
nullptr
);
while
(
err
==
SSH_AUTH_INFO
)
{
QString
instruction
=
ssh_userauth_kbdint_getname
(
p
->
session
);
QString
name
=
ssh_userauth_kbdint_getinstruction
(
p
->
session
);
int
num_prompts
=
ssh_userauth_kbdint_getnprompts
(
p
->
session
);
// build list of prompts
QStringList
prompts
;
char
echo
;
for
(
int
i
=
0
;
i
<
num_prompts
;
++
i
)
{
QString
prompt
=
ssh_userauth_kbdint_getprompt
(
p
->
session
,
static_cast
<
unsigned
int
>
(
i
),
&
echo
);
radix_tagged_line
(
i
<<
". "
<<
prompt
.
toStdString
());
if
(
prompt
.
isEmpty
())
break
;
prompts
<<
prompt
;
}
emit
interactiveAuthenticationRequested
(
instruction
,
name
,
prompts
);
return
;
}
}
// Try to authenticate with password
// Try to authenticate with password
if
(
method
&
SSH_AUTH_METHOD_PASSWORD
)
if
(
method
&
SSH_AUTH_METHOD_PASSWORD
)
{
{
...
@@ -245,6 +281,7 @@ void SessionWorker::authenticateWithPassword(QString pswd)
...
@@ -245,6 +281,7 @@ void SessionWorker::authenticateWithPassword(QString pswd)
{
{
assert_ssh_session
(
p
->
session
,
assert_ssh_session
(
p
->
session
,
"authenticateWithPassword() -- Session is not allocated."
);
"authenticateWithPassword() -- Session is not allocated."
);
radix_tagged_line
(
"Authenticate with password."
);
int
rc
=
int
rc
=
ssh_userauth_password
(
p
->
session
,
nullptr
,
pswd
.
toStdString
().
c_str
());
ssh_userauth_password
(
p
->
session
,
nullptr
,
pswd
.
toStdString
().
c_str
());
if
(
rc
==
SSH_AUTH_ERROR
)
if
(
rc
==
SSH_AUTH_ERROR
)
...
@@ -266,6 +303,36 @@ void SessionWorker::authenticateWithPassword(QString pswd)
...
@@ -266,6 +303,36 @@ void SessionWorker::authenticateWithPassword(QString pswd)
}
}
}
}
void
SessionWorker
::
authenticatePrompts
(
QStringList
responses
)
{
int
err
;
for
(
int
i
=
0
;
i
<
responses
.
size
();
++
i
)
{
const
char
*
answer
=
responses
.
at
(
i
).
toStdString
().
c_str
();
err
=
ssh_userauth_kbdint_setanswer
(
p
->
session
,
static_cast
<
unsigned
int
>
(
i
),
answer
);
if
(
err
<
0
)
{
emit
authenticationError
(
"Failed to authenticate prompts."
);
ssh_disconnect
(
p
->
session
);
return
;
}
}
// check status
err
=
ssh_userauth_kbdint
(
p
->
session
,
nullptr
,
nullptr
);
if
(
err
==
SSH_AUTH_DENIED
)
{
emit
authenticationError
(
"Authentication denied."
);
ssh_disconnect
(
p
->
session
);
return
;
}
else
if
(
err
==
SSH_AUTH_SUCCESS
)
{
emit
authenticationSucceeded
();
return
;
}
}
void
SessionWorker
::
requestExec
(
QString
command
)
void
SessionWorker
::
requestExec
(
QString
command
)
{
{
assert_ssh_session
(
p
->
session
,
"request_exec() -- Session is not allocated."
);
assert_ssh_session
(
p
->
session
,
"request_exec() -- Session is not allocated."
);
...
@@ -303,9 +370,9 @@ void SessionWorker::requestExec(QString command)
...
@@ -303,9 +370,9 @@ void SessionWorker::requestExec(QString command)
while
(
nbytes
>
0
)
while
(
nbytes
>
0
)
{
{
p
->
output_buffer
.
append
(
buffer
,
nbytes
);
p
->
output_buffer
.
append
(
buffer
,
nbytes
);
emit
execOutputReady
();
nbytes
=
ssh_channel_read
(
channel
,
buffer
,
sizeof
(
buffer
),
0
);
nbytes
=
ssh_channel_read
(
channel
,
buffer
,
sizeof
(
buffer
),
0
);
}
}
emit
execOutputReady
();
radix_tagged_line
(
"nbytes="
<<
nbytes
);
radix_tagged_line
(
"nbytes="
<<
nbytes
);
radix_tagged_line
(
"Finished reading response
\n
"
<<
p
->
output_buffer
.
data
());
radix_tagged_line
(
"Finished reading response
\n
"
<<
p
->
output_buffer
.
data
());
...
...
rsmcore/sessionworker.hh
View file @
7edeedfc
...
@@ -85,6 +85,8 @@ class RSM_PUBLIC SessionWorker : public QObject
...
@@ -85,6 +85,8 @@ class RSM_PUBLIC SessionWorker : public QObject
*/
*/
void
authenticateWithPassword
(
QString
pswd
);
void
authenticateWithPassword
(
QString
pswd
);
void
authenticatePrompts
(
QStringList
responses
);
/**
/**
* Requests remote execution of command
* Requests remote execution of command
* @param command - QString command to execute
* @param command - QString command to execute
...
@@ -94,7 +96,11 @@ class RSM_PUBLIC SessionWorker : public QObject
...
@@ -94,7 +96,11 @@ class RSM_PUBLIC SessionWorker : public QObject
signals:
signals:
void
connectionFailed
(
QString
message
);
void
connectionFailed
(
QString
message
);
void
connectionSuccessful
();
void
connectionSuccessful
();
void
disconnectSuccessful
();
void
verifyKnownHostSuccessful
();
void
verifyKnownHostSuccessful
();
void
interactiveAuthenticationRequested
(
QString
instruction
,
QString
name
,
QStringList
prompts
);
/**
/**
* @brief getServerPublicKeyFailed
* @brief getServerPublicKeyFailed
* Signal is emitted when the remote host does not have or provide a public
* Signal is emitted when the remote host does not have or provide a public
...
...
rsmwidgets/examples/rsmportalexample.cc
View file @
7edeedfc
...
@@ -9,7 +9,9 @@
...
@@ -9,7 +9,9 @@
#include <QApplication>
#include <QApplication>
#include <QGridLayout>
#include <QGridLayout>
#include <QHeaderView>
#include <QHeaderView>
#include <QInputDialog>
#include <QLabel>
#include <QLabel>
#include <QMessageBox>
#include "radixbug/bug.hh"
#include "radixbug/bug.hh"
using
namespace
rsm
;
using
namespace
rsm
;
...
@@ -45,18 +47,26 @@ ExamplePortalWidget::ExamplePortalWidget(QWidget *parent)
...
@@ -45,18 +47,26 @@ ExamplePortalWidget::ExamplePortalWidget(QWidget *parent)
connect
(
mConnectButton
,
&
QPushButton
::
pressed
,
this
,
connect
(
mConnectButton
,
&
QPushButton
::
pressed
,
this
,
&
ExamplePortalWidget
::
connectToHost
);
&
ExamplePortalWidget
::
connectToHost
);
connect
(
mCommandSubmitButton
,
&
QPushButton
::
pressed
,
this
,
&
ExamplePortalWidget
::
submitCommandToHost
);
mSession
=
new
SessionController
();
mSession
=
new
SessionController
();
connect
(
mSession
,
&
SessionController
::
connectionFailed
,
this
,
connect
(
mSession
,
&
SessionController
::
connectionFailed
,
this
,
&
ExamplePortalWidget
::
connectionFailed
);
&
ExamplePortalWidget
::
connectionFailed
);
connect
(
mSession
,
&
SessionController
::
connectionSuccessful
,
this
,
connect
(
mSession
,
&
SessionController
::
connectionSuccessful
,
this
,
&
ExamplePortalWidget
::
connectionSuccessful
);
&
ExamplePortalWidget
::
connectionSuccessful
);
connect
(
mSession
,
&
SessionController
::
disconnectSuccessful
,
this
,
&
ExamplePortalWidget
::
disconnectSuccessful
);
connect
(
mSession
,
&
SessionController
::
interactiveAuthenticationRequested
,
this
,
&
ExamplePortalWidget
::
interactiveAuthenticationRequested
);
connect
(
mSession
,
&
SessionController
::
getServerPublicKeyFailed
,
this
,
connect
(
mSession
,
&
SessionController
::
getServerPublicKeyFailed
,
this
,
&
ExamplePortalWidget
::
getServerPublicKeyFailed
);
&
ExamplePortalWidget
::
getServerPublicKeyFailed
);
connect
(
mSession
,
&
SessionController
::
hostUnknown
,
this
,
connect
(
mSession
,
&
SessionController
::
hostUnknown
,
this
,
&
ExamplePortalWidget
::
hostUnknown
);
&
ExamplePortalWidget
::
hostUnknown
);
connect
(
mSession
,
&
SessionController
::
hostPublicKeyChanged
,
this
,
connect
(
mSession
,
&
SessionController
::
hostPublicKeyChanged
,
this
,
&
ExamplePortalWidget
::
hostPublicKeyChanged
);
&
ExamplePortalWidget
::
hostPublicKeyChanged
);
connect
(
mSession
,
&
SessionController
::
verifyKnownHostSuccessful
,
this
,
&
ExamplePortalWidget
::
verifyKnownHostSuccessful
);
connect
(
mSession
,
&
SessionController
::
knownHostError
,
this
,
connect
(
mSession
,
&
SessionController
::
knownHostError
,
this
,
&
ExamplePortalWidget
::
knownHostError
);
&
ExamplePortalWidget
::
knownHostError
);
connect
(
mSession
,
&
SessionController
::
authenticationError
,
this
,
connect
(
mSession
,
&
SessionController
::
authenticationError
,
this
,
...
@@ -79,13 +89,26 @@ void ExamplePortalWidget::connectToHost()
...
@@ -79,13 +89,26 @@ void ExamplePortalWidget::connectToHost()
{
{
radix_tagged_line
(
"Host:"
<<
mHostEdit
->
text
().
toStdString
());
radix_tagged_line
(
"Host:"
<<
mHostEdit
->
text
().
toStdString
());
mSession
->
setHost
(
mHostEdit
->
text
());
if
(
mConnectButton
->
text
().
compare
(
"Disconnect"
)
==
0
)
mSession
->
setPort
(
mPortEdit
->
text
().
toInt
());
{
mSession
->
setUser
(
mUserNameEdit
->
text
());
mSession
->
disconnect
();
mSession
->
connect
();
mConnectButton
->
setText
(
"Connect"
);
}
else
{
mSession
->
setHost
(
mHostEdit
->
text
());
mSession
->
setPort
(
mPortEdit
->
text
().
toInt
());
mSession
->
setUser
(
mUserNameEdit
->
text
());
mSession
->
connect
();
}
}
}
void
ExamplePortalWidget
::
disconnectFromHost
()
{
mSession
->
disconnect
();
}
void
ExamplePortalWidget
::
submitCommandToHost
()
{
radix_tagged_line
(
"submitCommandToHost()"
);
QString
command
=
mCommandEdit
->
text
();
mSession
->
requestExec
(
command
);
}
void
ExamplePortalWidget
::
slotExecOutputReady
()
void
ExamplePortalWidget
::
slotExecOutputReady
()
{
{
...
@@ -105,12 +128,31 @@ void ExamplePortalWidget::connectionSuccessful()
...
@@ -105,12 +128,31 @@ void ExamplePortalWidget::connectionSuccessful()
mSession
->
verifyKnownHost
();
mSession
->
verifyKnownHost
();
}
}
void
ExamplePortalWidget
::
verifyKnownHostSuccesful
()
void
ExamplePortalWidget
::
disconnectSuccessful
()
{
mTextEdit
->
append
(
"Disconnected.
\n
"
);
}
void
ExamplePortalWidget
::
verifyKnownHostSuccessful
()
{
{
mTextEdit
->
append
(
"Verification successful.\Authenticating...
\n
"
);
mTextEdit
->
append
(
"Verification successful.
\
n
Authenticating...
\n
"
);
mSession
->
authenticate
();
mSession
->
authenticate
();
}
}
void
ExamplePortalWidget
::
interactiveAuthenticationRequested
(
QString
instruction
,
QString
name
,
QStringList
prompts
)
{
mTextEdit
->
append
(
"Interactive authentication requested."
);
mTextEdit
->
append
(
"Instruction: "
);
mTextEdit
->
append
(
instruction
);
mTextEdit
->
append
(
"Name:"
);
mTextEdit
->
append
(
name
);
for
(
int
i
=
0
;
i
<
prompts
.
size
();
++
i
)
{
mTextEdit
->
append
(
prompts
.
at
(
i
));
}
}
void
ExamplePortalWidget
::
getServerPublicKeyFailed
()
void
ExamplePortalWidget
::
getServerPublicKeyFailed
()
{
{
mTextEdit
->
append
(
"Retrieval of host's public key failed.
\n
"
);
mTextEdit
->
append
(
"Retrieval of host's public key failed.
\n
"
);
...
@@ -119,9 +161,21 @@ void ExamplePortalWidget::getServerPublicKeyFailed()
...
@@ -119,9 +161,21 @@ void ExamplePortalWidget::getServerPublicKeyFailed()
void
ExamplePortalWidget
::
hostUnknown
(
QString
host_hash
)
void
ExamplePortalWidget
::
hostUnknown
(
QString
host_hash
)
{
{
mTextEdit
->
append
(
"Host key unknown.
\n
Do you accept:"
);
int
ret
=
QMessageBox
::
warning
(
mTextEdit
->
append
(
host_hash
);
this
,
tr
(
"Host Unknown"
),
mTextEdit
->
append
(
"
\n
"
);
QString
(
"The remote host is unknown.
\n
"
)
.
append
(
"Are you sure you want to continue connecting?
\n
"
)
.
append
(
"Remote host key:"
)
.
append
(
host_hash
),
QMessageBox
::
Yes
|
QMessageBox
::
Cancel
);
if
(
ret
==
QMessageBox
::
Yes
)
{
mSession
->
acceptHostPublicKeyUpdate
();
}
else
{
mSession
->
disconnect
();
}
}
}
void
ExamplePortalWidget
::
hostPublicKeyChanged
(
QString
host_hash
)
void
ExamplePortalWidget
::
hostPublicKeyChanged
(
QString
host_hash
)
...
@@ -159,17 +213,33 @@ void ExamplePortalWidget::authenticationSucceeded()
...
@@ -159,17 +213,33 @@ void ExamplePortalWidget::authenticationSucceeded()
{
{
mTextEdit
->
append
(
"Authentication succeeded."
);
mTextEdit
->
append
(
"Authentication succeeded."
);
mTextEdit
->
append
(
"
\n
"
);
mTextEdit
->
append
(
"
\n
"
);
mConnectButton
->
setText
(
"Disconnect"
);
}
}
void
ExamplePortalWidget
::
passwordRequested
()
void
ExamplePortalWidget
::
passwordRequested
()
{
{
mTextEdit
->
append
(
"Password requested."
);
mTextEdit
->
append
(
"Password requested."
);
mTextEdit
->
append
(
"
\n
"
);
mTextEdit
->
append
(
"
\n
"
);
// TODO: Prompt password dialog
QString
text
=
QInputDialog
::
getText
(
this
,
"Authentication"
,
"Password:"
,
QLineEdit
::
Password
);
if
(
text
.
isEmpty
())
{
mSession
->
disconnect
();
}
else
{
mSession
->
authenticateWithPassword
(
text
);
}
}
}
void
ExamplePortalWidget
::
loginBannerIssued
(
QString
message
)
void
ExamplePortalWidget
::
loginBannerIssued
(
QString
message
)
{
{
int
ret
=
QMessageBox
::
warning
(
this
,
tr
(
"Host Login Banner"
),
message
,
QMessageBox
::
Ok
|
QMessageBox
::
Cancel
);
if
(
ret
!=
QMessageBox
::
Ok
)
{
mSession
->
disconnect
();
}
mTextEdit
->
append
(
message
);
mTextEdit
->
append
(
message
);
mTextEdit
->
append
(
"
\n
"
);
mTextEdit
->
append
(
"
\n
"
);
}
}
...
...
rsmwidgets/examples/rsmportalexample.hh
View file @
7edeedfc
...
@@ -33,12 +33,16 @@ class ExamplePortalWidget : public QWidget
...
@@ -33,12 +33,16 @@ class ExamplePortalWidget : public QWidget
public
slots
:
public
slots
:
void
connectToHost
();
void
connectToHost
();
void
disconnectFrom
Host
();
void
submitCommandTo
Host
();
void
slotExecOutputReady
();
void
slotExecOutputReady
();
void
connectionFailed
(
QString
message
);
void
connectionFailed
(
QString
message
);
void
connectionSuccessful
();
void
connectionSuccessful
();
void
verifyKnownHostSuccesful
();
void
disconnectSuccessful
();
void
verifyKnownHostSuccessful
();
void
interactiveAuthenticationRequested
(
QString
instruction
,
QString
name
,
QStringList
prompts
);
void
getServerPublicKeyFailed
();
void
getServerPublicKeyFailed
();
void
hostUnknown
(
QString
host_hash
);
void
hostUnknown
(
QString
host_hash
);
void
hostPublicKeyChanged
(
QString
host_hash
);
void
hostPublicKeyChanged
(
QString
host_hash
);
...
...
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