Implement FTP auth through proxy

BUG=11227

Review URL: https://codereview.chromium.org/12582012

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@192872 0039d316-1c4b-4281-b951-d872f2087c98
parent 1e7565ad
...@@ -42,7 +42,9 @@ bool IsEnclosingPath(const std::string& container, const std::string& path) { ...@@ -42,7 +42,9 @@ bool IsEnclosingPath(const std::string& container, const std::string& path) {
// Debug helper to check that |origin| arguments are properly formed. // Debug helper to check that |origin| arguments are properly formed.
void CheckOriginIsValid(const GURL& origin) { void CheckOriginIsValid(const GURL& origin) {
DCHECK(origin.is_valid()); DCHECK(origin.is_valid());
DCHECK(origin.SchemeIs("http") || origin.SchemeIs("https")); // Note that the scheme may be FTP when we're using a HTTP proxy.
DCHECK(origin.SchemeIs("http") || origin.SchemeIs("https") ||
origin.SchemeIs("ftp"));
DCHECK(origin.GetOrigin() == origin); DCHECK(origin.GetOrigin() == origin);
} }
......
...@@ -30,7 +30,7 @@ URLRequestFtpJob::URLRequestFtpJob( ...@@ -30,7 +30,7 @@ URLRequestFtpJob::URLRequestFtpJob(
: URLRequestJob(request, network_delegate), : URLRequestJob(request, network_delegate),
priority_(DEFAULT_PRIORITY), priority_(DEFAULT_PRIORITY),
pac_request_(NULL), pac_request_(NULL),
response_info_(NULL), http_response_info_(NULL),
read_in_progress_(false), read_in_progress_(false),
ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)), ALLOW_THIS_IN_INITIALIZER_LIST(weak_factory_(this)),
ftp_transaction_factory_(ftp_transaction_factory), ftp_transaction_factory_(ftp_transaction_factory),
...@@ -85,8 +85,8 @@ bool URLRequestFtpJob::GetMimeType(std::string* mime_type) const { ...@@ -85,8 +85,8 @@ bool URLRequestFtpJob::GetMimeType(std::string* mime_type) const {
} }
void URLRequestFtpJob::GetResponseInfo(HttpResponseInfo* info) { void URLRequestFtpJob::GetResponseInfo(HttpResponseInfo* info) {
if (response_info_) if (http_response_info_)
*info = *response_info_; *info = *http_response_info_;
} }
HostPortPair URLRequestFtpJob::GetSocketAddress() const { HostPortPair URLRequestFtpJob::GetSocketAddress() const {
...@@ -194,11 +194,9 @@ void URLRequestFtpJob::StartHttpTransaction() { ...@@ -194,11 +194,9 @@ void URLRequestFtpJob::StartHttpTransaction() {
DCHECK(!http_transaction_); DCHECK(!http_transaction_);
// Do not cache FTP responses sent through HTTP proxy. // Do not cache FTP responses sent through HTTP proxy.
// Do not send HTTP auth data because this is really FTP.
request_->set_load_flags(request_->load_flags() | request_->set_load_flags(request_->load_flags() |
LOAD_DISABLE_CACHE | LOAD_DISABLE_CACHE |
LOAD_DO_NOT_SAVE_COOKIES | LOAD_DO_NOT_SAVE_COOKIES |
LOAD_DO_NOT_SEND_AUTH_DATA |
LOAD_DO_NOT_SEND_COOKIES); LOAD_DO_NOT_SEND_COOKIES);
http_request_info_.url = request_->url(); http_request_info_.url = request_->url();
...@@ -235,27 +233,20 @@ void URLRequestFtpJob::OnStartCompleted(int result) { ...@@ -235,27 +233,20 @@ void URLRequestFtpJob::OnStartCompleted(int result) {
} }
if (result == OK) { if (result == OK) {
if (http_transaction_) if (http_transaction_) {
response_info_ = http_transaction_->GetResponseInfo(); http_response_info_ = http_transaction_->GetResponseInfo();
if (http_response_info_->headers->response_code() == 401 ||
http_response_info_->headers->response_code() == 407) {
HandleAuthNeededResponse();
return;
}
}
NotifyHeadersComplete(); NotifyHeadersComplete();
} else if (ftp_transaction_ && } else if (ftp_transaction_ &&
ftp_transaction_->GetResponseInfo()->needs_auth) { ftp_transaction_->GetResponseInfo()->needs_auth) {
GURL origin = request_->url().GetOrigin(); HandleAuthNeededResponse();
if (server_auth_ && server_auth_->state == AUTH_STATE_HAVE_AUTH) { return;
ftp_auth_cache_->Remove(origin, server_auth_->credentials);
} else if (!server_auth_) {
server_auth_ = new AuthData();
}
server_auth_->state = AUTH_STATE_NEED_AUTH;
FtpAuthCache::Entry* cached_auth = ftp_auth_cache_->Lookup(origin);
if (cached_auth) {
// Retry using cached auth data.
SetAuth(cached_auth->credentials);
} else {
// Prompt for a username/password.
NotifyHeadersComplete();
}
} else { } else {
NotifyDone(URLRequestStatus(URLRequestStatus::FAILED, result)); NotifyDone(URLRequestStatus(URLRequestStatus::FAILED, result));
} }
...@@ -282,17 +273,24 @@ void URLRequestFtpJob::OnReadCompleted(int result) { ...@@ -282,17 +273,24 @@ void URLRequestFtpJob::OnReadCompleted(int result) {
} }
void URLRequestFtpJob::RestartTransactionWithAuth() { void URLRequestFtpJob::RestartTransactionWithAuth() {
DCHECK(ftp_transaction_); DCHECK(auth_data_ && auth_data_->state == AUTH_STATE_HAVE_AUTH);
DCHECK(server_auth_ && server_auth_->state == AUTH_STATE_HAVE_AUTH);
// No matter what, we want to report our status as IO pending since we will // No matter what, we want to report our status as IO pending since we will
// be notifying our consumer asynchronously via OnStartCompleted. // be notifying our consumer asynchronously via OnStartCompleted.
SetStatus(URLRequestStatus(URLRequestStatus::IO_PENDING, 0)); SetStatus(URLRequestStatus(URLRequestStatus::IO_PENDING, 0));
int rv = ftp_transaction_->RestartWithAuth( int rv;
server_auth_->credentials, if (proxy_info_.is_direct()) {
base::Bind(&URLRequestFtpJob::OnStartCompleted, rv = ftp_transaction_->RestartWithAuth(
base::Unretained(this))); auth_data_->credentials,
base::Bind(&URLRequestFtpJob::OnStartCompleted,
base::Unretained(this)));
} else {
rv = http_transaction_->RestartWithAuth(
auth_data_->credentials,
base::Bind(&URLRequestFtpJob::OnStartCompleted,
base::Unretained(this)));
}
if (rv == ERR_IO_PENDING) if (rv == ERR_IO_PENDING)
return; return;
...@@ -310,21 +308,18 @@ LoadState URLRequestFtpJob::GetLoadState() const { ...@@ -310,21 +308,18 @@ LoadState URLRequestFtpJob::GetLoadState() const {
} }
bool URLRequestFtpJob::NeedsAuth() { bool URLRequestFtpJob::NeedsAuth() {
// TODO(phajdan.jr): Implement proxy auth, http://crbug.com/171497 . return auth_data_ && auth_data_->state == AUTH_STATE_NEED_AUTH;
if (!ftp_transaction_)
return false;
// Note that we only have to worry about cases where an actual FTP server
// requires auth (and not a proxy), because connecting to FTP via proxy
// effectively means the browser communicates via HTTP, and uses HTTP's
// Proxy-Authenticate protocol when proxy servers require auth.
return server_auth_ && server_auth_->state == AUTH_STATE_NEED_AUTH;
} }
void URLRequestFtpJob::GetAuthChallengeInfo( void URLRequestFtpJob::GetAuthChallengeInfo(
scoped_refptr<AuthChallengeInfo>* result) { scoped_refptr<AuthChallengeInfo>* result) {
DCHECK((server_auth_ != NULL) && DCHECK(NeedsAuth());
(server_auth_->state == AUTH_STATE_NEED_AUTH));
if (http_response_info_) {
*result = http_response_info_->auth_challenge;
return;
}
scoped_refptr<AuthChallengeInfo> auth_info(new AuthChallengeInfo); scoped_refptr<AuthChallengeInfo> auth_info(new AuthChallengeInfo);
auth_info->is_proxy = false; auth_info->is_proxy = false;
auth_info->challenger = HostPortPair::FromURL(request_->url()); auth_info->challenger = HostPortPair::FromURL(request_->url());
...@@ -335,20 +330,25 @@ void URLRequestFtpJob::GetAuthChallengeInfo( ...@@ -335,20 +330,25 @@ void URLRequestFtpJob::GetAuthChallengeInfo(
} }
void URLRequestFtpJob::SetAuth(const AuthCredentials& credentials) { void URLRequestFtpJob::SetAuth(const AuthCredentials& credentials) {
DCHECK(ftp_transaction_); DCHECK(ftp_transaction_ || http_transaction_);
DCHECK(NeedsAuth()); DCHECK(NeedsAuth());
server_auth_->state = AUTH_STATE_HAVE_AUTH;
server_auth_->credentials = credentials;
ftp_auth_cache_->Add(request_->url().GetOrigin(), server_auth_->credentials); auth_data_->state = AUTH_STATE_HAVE_AUTH;
auth_data_->credentials = credentials;
if (ftp_transaction_) {
ftp_auth_cache_->Add(request_->url().GetOrigin(),
auth_data_->credentials);
}
RestartTransactionWithAuth(); RestartTransactionWithAuth();
} }
void URLRequestFtpJob::CancelAuth() { void URLRequestFtpJob::CancelAuth() {
DCHECK(ftp_transaction_); DCHECK(ftp_transaction_ || http_transaction_);
DCHECK(NeedsAuth()); DCHECK(NeedsAuth());
server_auth_->state = AUTH_STATE_CANCELED;
auth_data_->state = AUTH_STATE_CANCELED;
// Once the auth is cancelled, we proceed with the request as though // Once the auth is cancelled, we proceed with the request as though
// there were no auth. Schedule this for later so that we don't cause // there were no auth. Schedule this for later so that we don't cause
...@@ -392,4 +392,32 @@ bool URLRequestFtpJob::ReadRawData(IOBuffer* buf, ...@@ -392,4 +392,32 @@ bool URLRequestFtpJob::ReadRawData(IOBuffer* buf,
return false; return false;
} }
void URLRequestFtpJob::HandleAuthNeededResponse() {
GURL origin = request_->url().GetOrigin();
if (auth_data_) {
if (auth_data_->state == AUTH_STATE_CANCELED) {
NotifyHeadersComplete();
return;
}
if (ftp_transaction_ && auth_data_->state == AUTH_STATE_HAVE_AUTH)
ftp_auth_cache_->Remove(origin, auth_data_->credentials);
} else {
auth_data_ = new AuthData;
}
auth_data_->state = AUTH_STATE_NEED_AUTH;
FtpAuthCache::Entry* cached_auth = NULL;
if (ftp_transaction_ && ftp_transaction_->GetResponseInfo()->needs_auth)
cached_auth = ftp_auth_cache_->Lookup(origin);
if (cached_auth) {
// Retry using cached auth data.
SetAuth(cached_auth->credentials);
} else {
// Prompt for a username/password.
NotifyHeadersComplete();
}
}
} // namespace net } // namespace net
...@@ -80,6 +80,8 @@ class NET_EXPORT_PRIVATE URLRequestFtpJob : public URLRequestJob { ...@@ -80,6 +80,8 @@ class NET_EXPORT_PRIVATE URLRequestFtpJob : public URLRequestJob {
int buf_size, int buf_size,
int *bytes_read) OVERRIDE; int *bytes_read) OVERRIDE;
void HandleAuthNeededResponse();
RequestPriority priority_; RequestPriority priority_;
ProxyInfo proxy_info_; ProxyInfo proxy_info_;
...@@ -90,11 +92,11 @@ class NET_EXPORT_PRIVATE URLRequestFtpJob : public URLRequestJob { ...@@ -90,11 +92,11 @@ class NET_EXPORT_PRIVATE URLRequestFtpJob : public URLRequestJob {
HttpRequestInfo http_request_info_; HttpRequestInfo http_request_info_;
scoped_ptr<HttpTransaction> http_transaction_; scoped_ptr<HttpTransaction> http_transaction_;
const HttpResponseInfo* response_info_; const HttpResponseInfo* http_response_info_;
bool read_in_progress_; bool read_in_progress_;
scoped_refptr<AuthData> server_auth_; scoped_refptr<AuthData> auth_data_;
base::WeakPtrFactory<URLRequestFtpJob> weak_factory_; base::WeakPtrFactory<URLRequestFtpJob> weak_factory_;
......
...@@ -242,7 +242,7 @@ TEST_F(URLRequestFtpJobTest, FtpProxyRequest) { ...@@ -242,7 +242,7 @@ TEST_F(URLRequestFtpJobTest, FtpProxyRequest) {
EXPECT_EQ("test.html", request_delegate.data_received()); EXPECT_EQ("test.html", request_delegate.data_received());
} }
TEST_F(URLRequestFtpJobTest, FtpProxyRequestNeedAuth) { TEST_F(URLRequestFtpJobTest, FtpProxyRequestNeedProxyAuthNoCredentials) {
MockWrite writes[] = { MockWrite writes[] = {
MockWrite(ASYNC, 0, "GET ftp://ftp.example.com/ HTTP/1.1\r\n" MockWrite(ASYNC, 0, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
"Host: ftp.example.com\r\n" "Host: ftp.example.com\r\n"
...@@ -271,10 +271,201 @@ TEST_F(URLRequestFtpJobTest, FtpProxyRequestNeedAuth) { ...@@ -271,10 +271,201 @@ TEST_F(URLRequestFtpJobTest, FtpProxyRequestNeedAuth) {
EXPECT_TRUE(url_request.status().is_success()); EXPECT_TRUE(url_request.status().is_success());
EXPECT_EQ(1, network_delegate()->completed_requests()); EXPECT_EQ(1, network_delegate()->completed_requests());
EXPECT_EQ(0, network_delegate()->error_count()); EXPECT_EQ(0, network_delegate()->error_count());
EXPECT_FALSE(request_delegate.auth_required_called()); EXPECT_TRUE(request_delegate.auth_required_called());
EXPECT_EQ("test.html", request_delegate.data_received());
}
TEST_F(URLRequestFtpJobTest, FtpProxyRequestNeedProxyAuthWithCredentials) {
MockWrite writes[] = {
MockWrite(ASYNC, 0, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
"Host: ftp.example.com\r\n"
"Proxy-Connection: keep-alive\r\n\r\n"),
MockWrite(ASYNC, 5, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
"Host: ftp.example.com\r\n"
"Proxy-Connection: keep-alive\r\n"
"Proxy-Authorization: Basic bXl1c2VyOm15cGFzcw==\r\n\r\n"),
};
MockRead reads[] = {
// No credentials.
MockRead(ASYNC, 1, "HTTP/1.1 407 Proxy Authentication Required\r\n"),
MockRead(ASYNC, 2, "Proxy-Authenticate: Basic "
"realm=\"MyRealm1\"\r\n"),
MockRead(ASYNC, 3, "Content-Length: 9\r\n\r\n"),
MockRead(ASYNC, 4, "test.html"),
// Second response.
MockRead(ASYNC, 6, "HTTP/1.1 200 OK\r\n"),
MockRead(ASYNC, 7, "Content-Length: 10\r\n\r\n"),
MockRead(ASYNC, 8, "test2.html"),
};
AddSocket(reads, arraysize(reads), writes, arraysize(writes));
TestDelegate request_delegate;
request_delegate.set_credentials(
AuthCredentials(ASCIIToUTF16("myuser"), ASCIIToUTF16("mypass")));
URLRequest url_request(GURL("ftp://ftp.example.com/"),
&request_delegate,
request_context(),
network_delegate());
url_request.Start();
ASSERT_TRUE(url_request.is_pending());
socket_data(0)->RunFor(9);
EXPECT_TRUE(url_request.status().is_success());
EXPECT_EQ(1, network_delegate()->completed_requests());
EXPECT_EQ(0, network_delegate()->error_count());
EXPECT_TRUE(request_delegate.auth_required_called());
EXPECT_EQ("test2.html", request_delegate.data_received());
}
TEST_F(URLRequestFtpJobTest, FtpProxyRequestNeedServerAuthNoCredentials) {
MockWrite writes[] = {
MockWrite(ASYNC, 0, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
"Host: ftp.example.com\r\n"
"Proxy-Connection: keep-alive\r\n\r\n"),
};
MockRead reads[] = {
// No credentials.
MockRead(ASYNC, 1, "HTTP/1.1 401 Unauthorized\r\n"),
MockRead(ASYNC, 2, "WWW-Authenticate: Basic "
"realm=\"MyRealm1\"\r\n"),
MockRead(ASYNC, 3, "Content-Length: 9\r\n\r\n"),
MockRead(ASYNC, 4, "test.html"),
};
AddSocket(reads, arraysize(reads), writes, arraysize(writes));
TestDelegate request_delegate;
URLRequest url_request(GURL("ftp://ftp.example.com/"),
&request_delegate,
request_context(),
network_delegate());
url_request.Start();
ASSERT_TRUE(url_request.is_pending());
socket_data(0)->RunFor(5);
EXPECT_TRUE(url_request.status().is_success());
EXPECT_EQ(1, network_delegate()->completed_requests());
EXPECT_EQ(0, network_delegate()->error_count());
EXPECT_TRUE(request_delegate.auth_required_called());
EXPECT_EQ("test.html", request_delegate.data_received()); EXPECT_EQ("test.html", request_delegate.data_received());
} }
TEST_F(URLRequestFtpJobTest, FtpProxyRequestNeedServerAuthWithCredentials) {
MockWrite writes[] = {
MockWrite(ASYNC, 0, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
"Host: ftp.example.com\r\n"
"Proxy-Connection: keep-alive\r\n\r\n"),
MockWrite(ASYNC, 5, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
"Host: ftp.example.com\r\n"
"Proxy-Connection: keep-alive\r\n"
"Authorization: Basic bXl1c2VyOm15cGFzcw==\r\n\r\n"),
};
MockRead reads[] = {
// No credentials.
MockRead(ASYNC, 1, "HTTP/1.1 401 Unauthorized\r\n"),
MockRead(ASYNC, 2, "WWW-Authenticate: Basic "
"realm=\"MyRealm1\"\r\n"),
MockRead(ASYNC, 3, "Content-Length: 9\r\n\r\n"),
MockRead(ASYNC, 4, "test.html"),
// Second response.
MockRead(ASYNC, 6, "HTTP/1.1 200 OK\r\n"),
MockRead(ASYNC, 7, "Content-Length: 10\r\n\r\n"),
MockRead(ASYNC, 8, "test2.html"),
};
AddSocket(reads, arraysize(reads), writes, arraysize(writes));
TestDelegate request_delegate;
request_delegate.set_credentials(
AuthCredentials(ASCIIToUTF16("myuser"), ASCIIToUTF16("mypass")));
URLRequest url_request(GURL("ftp://ftp.example.com/"),
&request_delegate,
request_context(),
network_delegate());
url_request.Start();
ASSERT_TRUE(url_request.is_pending());
socket_data(0)->RunFor(9);
EXPECT_TRUE(url_request.status().is_success());
EXPECT_EQ(1, network_delegate()->completed_requests());
EXPECT_EQ(0, network_delegate()->error_count());
EXPECT_TRUE(request_delegate.auth_required_called());
EXPECT_EQ("test2.html", request_delegate.data_received());
}
TEST_F(URLRequestFtpJobTest, FtpProxyRequestNeedProxyAndServerAuth) {
MockWrite writes[] = {
MockWrite(ASYNC, 0, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
"Host: ftp.example.com\r\n"
"Proxy-Connection: keep-alive\r\n\r\n"),
MockWrite(ASYNC, 5, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
"Host: ftp.example.com\r\n"
"Proxy-Connection: keep-alive\r\n"
"Proxy-Authorization: Basic "
"cHJveHl1c2VyOnByb3h5cGFzcw==\r\n\r\n"),
MockWrite(ASYNC, 10, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
"Host: ftp.example.com\r\n"
"Proxy-Connection: keep-alive\r\n"
"Proxy-Authorization: Basic "
"cHJveHl1c2VyOnByb3h5cGFzcw==\r\n"
"Authorization: Basic bXl1c2VyOm15cGFzcw==\r\n\r\n"),
};
MockRead reads[] = {
// No credentials.
MockRead(ASYNC, 1, "HTTP/1.1 407 Proxy Authentication Required\r\n"),
MockRead(ASYNC, 2, "Proxy-Authenticate: Basic "
"realm=\"MyRealm1\"\r\n"),
MockRead(ASYNC, 3, "Content-Length: 9\r\n\r\n"),
MockRead(ASYNC, 4, "test.html"),
// Second response.
MockRead(ASYNC, 6, "HTTP/1.1 401 Unauthorized\r\n"),
MockRead(ASYNC, 7, "WWW-Authenticate: Basic "
"realm=\"MyRealm1\"\r\n"),
MockRead(ASYNC, 8, "Content-Length: 9\r\n\r\n"),
MockRead(ASYNC, 9, "test.html"),
// Third response.
MockRead(ASYNC, 11, "HTTP/1.1 200 OK\r\n"),
MockRead(ASYNC, 12, "Content-Length: 10\r\n\r\n"),
MockRead(ASYNC, 13, "test2.html"),
};
AddSocket(reads, arraysize(reads), writes, arraysize(writes));
GURL url("ftp://ftp.example.com");
// Make sure cached FTP credentials are not used for proxy authentication.
request_context()->ftp_auth_cache()->Add(
url.GetOrigin(),
AuthCredentials(ASCIIToUTF16("userdonotuse"),
ASCIIToUTF16("passworddonotuse")));
TestDelegate request_delegate;
request_delegate.set_credentials(
AuthCredentials(ASCIIToUTF16("proxyuser"), ASCIIToUTF16("proxypass")));
URLRequest url_request(url,
&request_delegate,
request_context(),
network_delegate());
url_request.Start();
ASSERT_TRUE(url_request.is_pending());
socket_data(0)->RunFor(5);
request_delegate.set_credentials(
AuthCredentials(ASCIIToUTF16("myuser"), ASCIIToUTF16("mypass")));
socket_data(0)->RunFor(9);
EXPECT_TRUE(url_request.status().is_success());
EXPECT_EQ(1, network_delegate()->completed_requests());
EXPECT_EQ(0, network_delegate()->error_count());
EXPECT_TRUE(request_delegate.auth_required_called());
EXPECT_EQ("test2.html", request_delegate.data_received());
}
TEST_F(URLRequestFtpJobTest, FtpProxyRequestDoNotSaveCookies) { TEST_F(URLRequestFtpJobTest, FtpProxyRequestDoNotSaveCookies) {
MockWrite writes[] = { MockWrite writes[] = {
MockWrite(ASYNC, 0, "GET ftp://ftp.example.com/ HTTP/1.1\r\n" MockWrite(ASYNC, 0, "GET ftp://ftp.example.com/ HTTP/1.1\r\n"
......
...@@ -505,6 +505,7 @@ NetworkDelegate::AuthRequiredResponse TestNetworkDelegate::OnAuthRequired( ...@@ -505,6 +505,7 @@ NetworkDelegate::AuthRequiredResponse TestNetworkDelegate::OnAuthRequired(
EXPECT_TRUE(next_states_[req_id] & kStageAuthRequired) << EXPECT_TRUE(next_states_[req_id] & kStageAuthRequired) <<
event_order_[req_id]; event_order_[req_id];
next_states_[req_id] = kStageBeforeSendHeaders | next_states_[req_id] = kStageBeforeSendHeaders |
kStageAuthRequired | // For example, proxy auth followed by server auth.
kStageHeadersReceived | // Request canceled by delegate simulates empty kStageHeadersReceived | // Request canceled by delegate simulates empty
// response. // response.
kStageResponseStarted | // data: URLs do not trigger sending headers kStageResponseStarted | // data: URLs do not trigger sending headers
......
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