| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "extensions/browser/api/display_source/display_source_apitestbase.h" |
| |
| #include <list> |
| #include <map> |
| #include <utility> |
| |
| #include "base/memory/ptr_util.h" |
| #include "net/base/net_errors.h" |
| #include "net/udp/udp_socket.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| namespace extensions { |
| |
| using api::display_source::SinkInfo; |
| using api::display_source::SinkState; |
| using api::display_source::AuthenticationMethod; |
| using api::display_source::SINK_STATE_DISCONNECTED; |
| using api::display_source::SINK_STATE_CONNECTING; |
| using api::display_source::SINK_STATE_CONNECTED; |
| using api::display_source::AUTHENTICATION_METHOD_PBC; |
| using api::display_source::AUTHENTICATION_METHOD_PIN; |
| using content::BrowserThread; |
| |
| class MockDisplaySourceConnectionDelegate |
| : public DisplaySourceConnectionDelegate, |
| public DisplaySourceConnectionDelegate::Connection { |
| public: |
| MockDisplaySourceConnectionDelegate(); |
| |
| const DisplaySourceSinkInfoList& last_found_sinks() const override; |
| |
| DisplaySourceConnectionDelegate::Connection* connection() |
| override { |
| return (active_sink_ && active_sink_->state == SINK_STATE_CONNECTED) |
| ? this |
| : nullptr; |
| } |
| |
| void GetAvailableSinks(const SinkInfoListCallback& sinks_callback, |
| const StringCallback& failure_callback) override; |
| |
| void RequestAuthentication(int sink_id, |
| const AuthInfoCallback& auth_info_callback, |
| const StringCallback& failure_callback) override; |
| |
| void Connect(int sink_id, |
| const DisplaySourceAuthInfo& auth_info, |
| const StringCallback& failure_callback) override; |
| |
| void Disconnect(const StringCallback& failure_callback) override; |
| |
| void StartWatchingAvailableSinks() override; |
| |
| // DisplaySourceConnectionDelegate::Connection overrides |
| const DisplaySourceSinkInfo& GetConnectedSink() const override; |
| |
| void StopWatchingAvailableSinks() override; |
| |
| std::string GetLocalAddress() const override; |
| |
| std::string GetSinkAddress() const override; |
| |
| void SendMessage(const std::string& message) override; |
| |
| void SetMessageReceivedCallback( |
| const StringCallback& callback) override; |
| |
| private: |
| void AddSink(DisplaySourceSinkInfo sink, |
| AuthenticationMethod auth_method, |
| const std::string& pin_value); |
| |
| void OnSinkConnected(); |
| |
| void NotifySinksUpdated(); |
| |
| void EnqueueSinkMessage(std::string message); |
| |
| void CheckSourceMessageContent(std::string pattern, |
| const std::string& message); |
| |
| void BindToUdpSocket(); |
| |
| void ReceiveMediaPacket(); |
| |
| void OnMediaPacketReceived(int net_result); |
| |
| DisplaySourceSinkInfoList sinks_; |
| DisplaySourceSinkInfo* active_sink_; |
| std::map<int, std::pair<AuthenticationMethod, std::string>> auth_infos_; |
| StringCallback message_received_cb_; |
| |
| struct Message { |
| enum Direction { |
| SourceToSink, |
| SinkToSource |
| }; |
| std::string data; |
| Direction direction; |
| |
| bool is_from_sink() const { return direction == SinkToSource; } |
| Message(const std::string& message_data, Direction direction) |
| : data(message_data), direction(direction) {} |
| }; |
| |
| std::list<Message> messages_list_; |
| std::string session_id_; |
| |
| std::unique_ptr<net::UDPSocket, |
| content::BrowserThread::DeleteOnIOThread> socket_; |
| scoped_refptr<net::IOBuffer> recvfrom_buffer_; |
| net::IPEndPoint end_point_; |
| std::string udp_port_; |
| }; |
| |
| namespace { |
| |
| const size_t kSessionIdLength = 8; |
| const size_t kUdpPortLength = 5; |
| const char kClientPortKey[] = "client_port="; |
| const char kLocalHost[] = "127.0.0.1"; |
| const char kSessionKey[] = "Session: "; |
| const char kUnicastKey[] = "unicast "; |
| const int kPortStart = 10000; |
| const int kPortEnd = 65535; |
| |
| DisplaySourceSinkInfo CreateSinkInfo(int id, const std::string& name) { |
| DisplaySourceSinkInfo ptr; |
| ptr.id = id; |
| ptr.name = name; |
| ptr.state = SINK_STATE_DISCONNECTED; |
| |
| return ptr; |
| } |
| |
| std::unique_ptr<KeyedService> CreateMockDelegate( |
| content::BrowserContext* profile) { |
| return base::WrapUnique<KeyedService>( |
| new MockDisplaySourceConnectionDelegate()); |
| } |
| |
| void AdaptMessagePattern(std::size_t key_pos, |
| const char *key, |
| std::size_t substr_len, |
| const std::string& replace_with, |
| std::string& pattern) { |
| const std::size_t position = key_pos + |
| std::char_traits<char>::length(key); |
| pattern.replace(position, substr_len, replace_with); |
| } |
| |
| } // namespace |
| |
| void InitMockDisplaySourceConnectionDelegate(content::BrowserContext* profile) { |
| DisplaySourceConnectionDelegateFactory::GetInstance()->SetTestingFactory( |
| profile, &CreateMockDelegate); |
| } |
| namespace { |
| |
| // WiFi Display session RTSP messages patterns. |
| |
| const char kM1Message[] = "OPTIONS * RTSP/1.0\r\n" |
| "CSeq: 1\r\n" |
| "Require: org.wfa.wfd1.0\r\n\r\n"; |
| |
| const char kM1MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq:1\r\n" |
| "Public: org.wfa.wfd1.0, " |
| "GET_PARAMETER, SET_PARAMETER\r\n\r\n"; |
| |
| const char kM2Message[] = "OPTIONS * RTSP/1.0\r\n" |
| "CSeq: 2\r\n" |
| "Require: org.wfa.wfd1.0\r\n\r\n"; |
| |
| const char kM2MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq: 2\r\n" |
| "Public: org.wfa.wfd1.0, " |
| "GET_PARAMETER, SET_PARAMETER, PLAY, PAUSE, " |
| "SETUP, TEARDOWN\r\n\r\n"; |
| |
| const char kM3Message[] = "GET_PARAMETER rtsp://localhost/wfd1.0 RTSP/1.0\r\n" |
| "CSeq: 2\r\n" |
| "Content-Type: text/parameters\r\n" |
| "Content-Length: 41\r\n\r\n" |
| "wfd_video_formats\r\n" |
| "wfd_client_rtp_ports\r\n"; |
| |
| const char kM3MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq: 2\r\n" |
| "Content-Type: text/parameters\r\n" |
| "Content-Length: 145\r\n\r\n" |
| "wfd_video_formats: " |
| "00 00 01 01 0001FFFF 1FFFFFFF 00000FFF 00 0000 " |
| "0000 00 none none\r\n" |
| "wfd_client_rtp_ports: RTP/AVP/UDP;" |
| "unicast 00000 0 mode=play\r\n"; |
| |
| const char kM4Message[] = "SET_PARAMETER rtsp://localhost/wfd1.0 RTSP/1.0\r\n" |
| "CSeq: 3\r\n" |
| "Content-Type: text/parameters\r\n" |
| "Content-Length: 209\r\n\r\n" |
| "wfd_client_rtp_ports: " |
| "RTP/AVP/UDP;unicast 00000 0 mode=play\r\n" |
| "wfd_presentation_URL: " |
| "rtsp://127.0.0.1/wfd1.0/streamid=0 none\r\n" |
| "wfd_video_formats: " |
| "00 00 01 01 00000001 00000000 00000000 00 0000 0000 " |
| "00 none none\r\n"; |
| |
| const char kM4MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq: 3\r\n\r\n"; |
| |
| const char kM5Message[] = "SET_PARAMETER rtsp://localhost/wfd1.0 RTSP/1.0\r\n" |
| "CSeq: 4\r\n" |
| "Content-Type: text/parameters\r\n" |
| "Content-Length: 27\r\n\r\n" |
| "wfd_trigger_method: SETUP\r\n"; |
| |
| const char kM5MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq: 4\r\n\r\n"; |
| |
| const char kM6Message[] = "SETUP rtsp://localhost/wfd1.0/streamid=0 " |
| "RTSP/1.0\r\n" |
| "CSeq: 3\r\n" |
| "Transport: RTP/AVP/UDP;unicast;" |
| "client_port=00000\r\n\r\n"; |
| |
| const char kM6MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq: 3\r\n" |
| "Session: 00000000;timeout=60\r\n" |
| "Transport: RTP/AVP/UDP;unicast;" |
| "client_port=00000\r\n\r\n"; |
| |
| const char kM7Message[] = "PLAY rtsp://localhost/wfd1.0/streamid=0 RTSP/1.0\r\n" |
| "CSeq: 4\r\n" |
| "Session: 00000000\r\n\r\n"; |
| |
| const char kM7MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq: 4\r\n\r\n"; |
| |
| const char kM8Message[] = "GET_PARAMETER rtsp://localhost/wfd1.0 RTSP/1.0\r\n" |
| "CSeq: 5\r\n\r\n"; |
| |
| const char kM8MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq: 5\r\n\r\n"; |
| |
| const char kM9Message[] = "GET_PARAMETER rtsp://localhost/wfd1.0 RTSP/1.0\r\n" |
| "CSeq: 6\r\n\r\n"; |
| |
| const char kM9MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq: 6\r\n\r\n"; |
| |
| const char kM10Message[] = "GET_PARAMETER rtsp://localhost/wfd1.0 RTSP/1.0\r\n" |
| "CSeq: 7\r\n\r\n"; |
| |
| const char kM10MessageReply[] = "RTSP/1.0 200 OK\r\n" |
| "CSeq: 7\r\n\r\n"; |
| |
| } // namespace |
| MockDisplaySourceConnectionDelegate::MockDisplaySourceConnectionDelegate() |
| : active_sink_(nullptr) { |
| messages_list_.push_back(Message(kM1Message, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM1MessageReply, Message::SinkToSource)); |
| messages_list_.push_back(Message(kM2Message, Message::SinkToSource)); |
| messages_list_.push_back(Message(kM2MessageReply, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM3Message, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM3MessageReply, Message::SinkToSource)); |
| messages_list_.push_back(Message(kM4Message, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM4MessageReply, Message::SinkToSource)); |
| messages_list_.push_back(Message(kM5Message, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM5MessageReply, Message::SinkToSource)); |
| messages_list_.push_back(Message(kM6Message, Message::SinkToSource)); |
| messages_list_.push_back(Message(kM6MessageReply, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM7Message, Message::SinkToSource)); |
| messages_list_.push_back(Message(kM7MessageReply, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM8Message, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM8MessageReply, Message::SinkToSource)); |
| messages_list_.push_back(Message(kM9Message, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM9MessageReply, Message::SinkToSource)); |
| messages_list_.push_back(Message(kM10Message, Message::SourceToSink)); |
| messages_list_.push_back(Message(kM10MessageReply, Message::SinkToSource)); |
| |
| AddSink(CreateSinkInfo(1, "sink 1"), AUTHENTICATION_METHOD_PIN, "1234"); |
| } |
| |
| const DisplaySourceSinkInfoList& |
| MockDisplaySourceConnectionDelegate::last_found_sinks() const { |
| return sinks_; |
| } |
| |
| void MockDisplaySourceConnectionDelegate::GetAvailableSinks( |
| const SinkInfoListCallback& sinks_callback, |
| const StringCallback& failure_callback) { |
| sinks_callback.Run(sinks_); |
| } |
| |
| void MockDisplaySourceConnectionDelegate::RequestAuthentication( |
| int sink_id, |
| const AuthInfoCallback& auth_info_callback, |
| const StringCallback& failure_callback) { |
| DisplaySourceAuthInfo info; |
| auto it = auth_infos_.find(sink_id); |
| ASSERT_NE(it, auth_infos_.end()); |
| |
| info.method = it->second.first; |
| auth_info_callback.Run(info); |
| } |
| |
| void MockDisplaySourceConnectionDelegate::Connect( |
| int sink_id, |
| const DisplaySourceAuthInfo& auth_info, |
| const StringCallback& failure_callback) { |
| auto it = auth_infos_.find(sink_id); |
| ASSERT_NE(it, auth_infos_.end()); |
| ASSERT_EQ(it->second.first, auth_info.method); |
| ASSERT_STREQ(it->second.second.c_str(), auth_info.data->c_str()); |
| |
| auto found = std::find_if(sinks_.begin(), sinks_.end(), |
| [sink_id](const DisplaySourceSinkInfo& sink) { |
| return sink.id == sink_id; |
| }); |
| |
| ASSERT_NE(found, sinks_.end()); |
| active_sink_ = sinks_.data() + (found - sinks_.begin()); |
| active_sink_->state = SINK_STATE_CONNECTING; |
| NotifySinksUpdated(); |
| |
| // Bind sink to udp socket at this stage |
| // And store udp port to udp_port_ string in order to be used |
| // In a message exchange. Then make a BrowserThread::PostTask |
| // on UI thread and call OnSinkConnected() to proceed with the test |
| BrowserThread::PostTask( |
| BrowserThread::IO, FROM_HERE, |
| base::Bind(&MockDisplaySourceConnectionDelegate::BindToUdpSocket, |
| base::Unretained(this))); |
| } |
| |
| void MockDisplaySourceConnectionDelegate::Disconnect( |
| const StringCallback& failure_callback) { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(active_sink_); |
| ASSERT_EQ(active_sink_->state, SINK_STATE_CONNECTED); |
| active_sink_->state = SINK_STATE_DISCONNECTED; |
| active_sink_ = nullptr; |
| NotifySinksUpdated(); |
| } |
| |
| void MockDisplaySourceConnectionDelegate::StartWatchingAvailableSinks() { |
| AddSink(CreateSinkInfo(2, "sink 2"), AUTHENTICATION_METHOD_PBC, ""); |
| } |
| |
| const DisplaySourceSinkInfo& |
| MockDisplaySourceConnectionDelegate::GetConnectedSink() const { |
| CHECK(active_sink_); |
| return *active_sink_; |
| } |
| |
| void MockDisplaySourceConnectionDelegate::StopWatchingAvailableSinks() {} |
| |
| std::string MockDisplaySourceConnectionDelegate::GetLocalAddress() const { |
| return "127.0.0.1"; |
| } |
| |
| std::string MockDisplaySourceConnectionDelegate::GetSinkAddress() const { |
| return "127.0.0.1"; |
| } |
| |
| void MockDisplaySourceConnectionDelegate::SendMessage( |
| const std::string& message) { |
| ASSERT_FALSE(messages_list_.empty()); |
| ASSERT_FALSE(messages_list_.front().is_from_sink()); |
| |
| CheckSourceMessageContent(messages_list_.front().data, message); |
| messages_list_.pop_front(); |
| |
| while (!messages_list_.empty() && messages_list_.front().is_from_sink()) { |
| EnqueueSinkMessage(messages_list_.front().data); |
| messages_list_.pop_front(); |
| } |
| } |
| |
| void MockDisplaySourceConnectionDelegate::SetMessageReceivedCallback( |
| const StringCallback& callback) { |
| message_received_cb_ = callback; |
| } |
| |
| void MockDisplaySourceConnectionDelegate::AddSink( |
| DisplaySourceSinkInfo sink, |
| AuthenticationMethod auth_method, |
| const std::string& pin_value) { |
| auth_infos_[sink.id] = {auth_method, pin_value}; |
| sinks_.push_back(std::move(sink)); |
| NotifySinksUpdated(); |
| } |
| |
| void MockDisplaySourceConnectionDelegate::OnSinkConnected() { |
| DCHECK_CURRENTLY_ON(BrowserThread::UI); |
| CHECK(active_sink_); |
| active_sink_->state = SINK_STATE_CONNECTED; |
| NotifySinksUpdated(); |
| } |
| |
| void MockDisplaySourceConnectionDelegate::NotifySinksUpdated() { |
| FOR_EACH_OBSERVER(DisplaySourceConnectionDelegate::Observer, observers_, |
| OnSinksUpdated(sinks_)); |
| } |
| |
| void MockDisplaySourceConnectionDelegate:: |
| EnqueueSinkMessage(std::string message) { |
| const std::size_t found_session_key = message.find(kSessionKey); |
| if (found_session_key != std::string::npos) |
| AdaptMessagePattern(found_session_key, kSessionKey, kSessionIdLength, |
| session_id_, message); |
| |
| const std::size_t found_unicast_key = message.find(kUnicastKey); |
| if (found_unicast_key != std::string::npos) |
| AdaptMessagePattern(found_unicast_key, kUnicastKey, kUdpPortLength, |
| udp_port_, message); |
| |
| const std::size_t found_clientport_key = message.find(kClientPortKey); |
| if (found_clientport_key != std::string::npos) |
| AdaptMessagePattern(found_clientport_key, kClientPortKey, kUdpPortLength, |
| udp_port_, message); |
| |
| BrowserThread::PostTask(BrowserThread::UI, FROM_HERE, |
| base::Bind(message_received_cb_, message)); |
| } |
| |
| void MockDisplaySourceConnectionDelegate:: |
| CheckSourceMessageContent(std::string pattern, |
| const std::string& message) { |
| // Message M6_reply from Source to Sink has a unique and random session id |
| // generated by Source. The id cannot be predicted and the session id should |
| // be extracted and added to the message pattern for assertion. |
| // The following code checks if messages include "Session" string. |
| // If not, assert the message normally. |
| // If yes, find the session id, add it to the pattern and to the sink message |
| // that has Session: substring inside. |
| const std::size_t found_session_key = message.find(kSessionKey); |
| if (found_session_key != std::string::npos) { |
| const std::size_t session_id_pos = found_session_key + |
| std::char_traits<char>::length(kSessionKey); |
| session_id_ = message.substr(session_id_pos, kSessionIdLength); |
| AdaptMessagePattern(found_session_key, kSessionKey, kSessionIdLength, |
| session_id_, pattern); |
| } |
| |
| const std::size_t found_unicast_key = message.find(kUnicastKey); |
| if (found_unicast_key != std::string::npos) |
| AdaptMessagePattern(found_unicast_key, kUnicastKey, kUdpPortLength, |
| udp_port_, pattern); |
| |
| const std::size_t found_clientport_key = message.find(kClientPortKey); |
| if (found_clientport_key != std::string::npos) |
| AdaptMessagePattern(found_clientport_key, kClientPortKey, kUdpPortLength, |
| udp_port_, pattern); |
| |
| ASSERT_EQ(pattern, message); |
| } |
| |
| void MockDisplaySourceConnectionDelegate::BindToUdpSocket() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| |
| socket_.reset(new net::UDPSocket( |
| net::DatagramSocket::DEFAULT_BIND, net::RandIntCallback(), nullptr, |
| net::NetLog::Source())); |
| |
| net::IPAddress address; |
| ASSERT_TRUE(address.AssignFromIPLiteral(kLocalHost)); |
| |
| int net_result; |
| net_result = socket_->Open(net::ADDRESS_FAMILY_IPV4); |
| ASSERT_EQ(net_result, net::OK); |
| |
| for (uint16_t port = kPortStart; port < kPortEnd; ++port) { |
| net::IPEndPoint local_point(address, port); |
| net_result = socket_->Bind(local_point); |
| if (net_result == net::OK) { |
| udp_port_ = std::to_string(port); |
| // When we got an udp socket established and udp port is known |
| // Change sink's status to connected and proceed with the test. |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(&MockDisplaySourceConnectionDelegate::OnSinkConnected, |
| base::Unretained(this))); |
| break; |
| } |
| } |
| |
| ReceiveMediaPacket(); |
| } |
| |
| void MockDisplaySourceConnectionDelegate::ReceiveMediaPacket() { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(socket_.get()); |
| const int kBufferSize = 512; |
| |
| recvfrom_buffer_ = new net::IOBuffer(kBufferSize); |
| |
| int net_result = socket_->RecvFrom( |
| recvfrom_buffer_.get(), kBufferSize, &end_point_, |
| base::Bind(&MockDisplaySourceConnectionDelegate::OnMediaPacketReceived, |
| base::Unretained(this))); |
| |
| if (net_result != net::ERR_IO_PENDING) |
| OnMediaPacketReceived(net_result); |
| } |
| |
| void MockDisplaySourceConnectionDelegate::OnMediaPacketReceived( |
| int net_result) { |
| DCHECK_CURRENTLY_ON(BrowserThread::IO); |
| DCHECK(recvfrom_buffer_.get()); |
| recvfrom_buffer_ = NULL; |
| |
| if (net_result > 0) { |
| // We received at least one media packet. |
| // Test is completed. |
| socket_->Close(); |
| BrowserThread::PostTask( |
| BrowserThread::UI, FROM_HERE, |
| base::Bind(&MockDisplaySourceConnectionDelegate::Disconnect, |
| base::Unretained(this), StringCallback())); |
| return; |
| } |
| |
| DCHECK(socket_.get()); |
| ReceiveMediaPacket(); |
| } |
| |
| } // namespace extensions |