| /* |
| * libjingle |
| * Copyright 2004--2006, Google Inc. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright notice, |
| * this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * 3. The name of the author may not be used to endorse or promote products |
| * derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO |
| * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; |
| * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
| * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include "talk/session/fileshare/fileshare.h" |
| |
| #include "talk/base/httpcommon-inl.h" |
| |
| #include "talk/base/fileutils.h" |
| #include "talk/base/streamutils.h" |
| #include "talk/base/event.h" |
| #include "talk/base/helpers.h" |
| #include "talk/base/httpclient.h" |
| #include "talk/base/httpserver.h" |
| #include "talk/base/pathutils.h" |
| #include "talk/base/socketstream.h" |
| #include "talk/base/stringdigest.h" |
| #include "talk/base/stringencode.h" |
| #include "talk/base/stringutils.h" |
| #include "talk/base/tarstream.h" |
| #include "talk/base/thread.h" |
| #include "talk/session/tunnel/pseudotcpchannel.h" |
| #include "talk/session/tunnel/tunnelsessionclient.h" |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // <description xmlns="http://www.google.com/session/share"> |
| // <manifest> |
| // <file size='341'> |
| // <name>foo.txt</name> |
| // </file> |
| // <file size='51321'> |
| // <name>foo.jpg</name> |
| // <image width='480' height='320'/> |
| // </file> |
| // <folder> |
| // <name>stuff</name> |
| // </folder> |
| // </manifest> |
| // <protocol> |
| // <http> |
| // <url name='source-path'>/temporary/23A53F01/</url> |
| // <url name='preview-path'>/temporary/90266EA1/</url> |
| // </http> |
| // <raw/> |
| // </protocol> |
| // </description> |
| // <p:transport xmns:p="p2p"/> |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // Constants and private functions |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| const std::string NS_GOOGLE_SHARE("http://www.google.com/session/share"); |
| |
| namespace { |
| |
| const buzz::QName QN_SHARE_DESCRIPTION(true, NS_GOOGLE_SHARE, "description"); |
| const buzz::QName QN_SHARE_MANIFEST(true, NS_GOOGLE_SHARE, "manifest"); |
| const buzz::QName QN_SHARE_FOLDER(true, NS_GOOGLE_SHARE, "folder"); |
| const buzz::QName QN_SHARE_FILE(true, NS_GOOGLE_SHARE, "file"); |
| const buzz::QName QN_SHARE_NAME(true, NS_GOOGLE_SHARE, "name"); |
| const buzz::QName QN_SHARE_IMAGE(true, NS_GOOGLE_SHARE, "image"); |
| const buzz::QName QN_SHARE_PROTOCOL(true, NS_GOOGLE_SHARE, "protocol"); |
| const buzz::QName QN_SHARE_HTTP(true, NS_GOOGLE_SHARE, "http"); |
| const buzz::QName QN_SHARE_URL(true, NS_GOOGLE_SHARE, "url"); |
| const buzz::QName QN_SHARE_CHANNEL(true, NS_GOOGLE_SHARE, "channel"); |
| const buzz::QName QN_SHARE_COMPLETE(true, NS_GOOGLE_SHARE, "complete"); |
| |
| const buzz::QName QN_SIZE(true, buzz::STR_EMPTY, "size"); |
| const buzz::QName QN_WIDTH(true, buzz::STR_EMPTY, "width"); |
| const buzz::QName QN_HEIGHT(true, buzz::STR_EMPTY, "height"); |
| |
| const std::string kHttpSourcePath("source-path"); |
| const std::string kHttpPreviewPath("preview-path"); |
| |
| const size_t kMinImageSize = 16U; |
| const size_t kMaxImageSize = 0x8000U; // (32k) |
| const size_t kMaxPreviewSize = 1024; |
| // Wait 10 seconds to see if any new proxies get established |
| const uint32 kProxyWait = 10000; |
| |
| const int MSG_RETRY = 1; |
| const uint32 kFileTransferEnableRetryMs = 1000 * 60 * 4; // 4 minutes |
| |
| const std::string MIME_OCTET_STREAM("application/octet-stream"); |
| |
| enum { |
| MSG_PROXY_WAIT, |
| }; |
| |
| bool AllowedImageDimensions(size_t width, size_t height) { |
| return (width >= kMinImageSize) && (width <= kMaxImageSize) |
| && (height >= kMinImageSize) && (height <= kMaxImageSize); |
| } |
| |
| } // anon namespace |
| |
| namespace cricket { |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // FileShareManifest |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| void |
| FileShareManifest::AddFile(const std::string& name, size_t size) { |
| Item i = { T_FILE, name, size }; |
| items_.push_back(i); |
| } |
| |
| void |
| FileShareManifest::AddImage(const std::string& name, size_t size, |
| size_t width, size_t height) { |
| Item i = { T_IMAGE, name, size, width, height }; |
| items_.push_back(i); |
| } |
| |
| void |
| FileShareManifest::AddFolder(const std::string& name, size_t size) { |
| Item i = { T_FOLDER, name, size }; |
| items_.push_back(i); |
| } |
| |
| size_t |
| FileShareManifest::GetItemCount(Type t) const { |
| size_t count = 0; |
| for (size_t i=0; i<items_.size(); ++i) { |
| if (items_[i].type == t) |
| ++count; |
| } |
| return count; |
| } |
| |
| /////////////////////////////////////////////////////////////////////////////// |
| // FileShareSession |
| /////////////////////////////////////////////////////////////////////////////// |
| |
| FileShareSession::FileShareSession(cricket::Session* session, const std::string &user_agent) |
| : session_(session), state_(FS_NONE), |
| is_closed_(false), |
| is_sender_(false), manifest_(NULL), pool_(this), http_client_(NULL), |
| http_server_(NULL), |
| transfer_connection_id_(talk_base::HTTP_INVALID_CONNECTION_ID), |
| counter_(NULL), item_transferring_(0), bytes_transferred_(0), |
| local_cancel_(false), local_listener_(NULL), remote_listener_(NULL), |
| next_channel_id_(1), user_agent_(user_agent) { |
| session_->SignalState.connect(this, &FileShareSession::OnSessionState); |
| session_->SignalInfoMessage.connect(this, |
| &FileShareSession::OnSessionInfoMessage); |
| session_->SignalChannelGone.connect(this, |
| &FileShareSession::OnSessionChannelGone); |
| } |
| |
| FileShareSession::~FileShareSession() { |
| ASSERT(FS_NONE != state_); |
| // If we haven't closed, do cleanup now. |
| if (!IsClosed()) { |
| if (!IsComplete()) { |
| state_ = FS_FAILURE; |
| } |
| DoClose(true); |
| } |
| if (session_) { |
| // Make sure we don't get future state changes on this session. |
| session_->SignalState.disconnect(this); |
| session_->SignalInfoMessage.disconnect(this); |
| session_ = NULL; |
| } |
| |
| for (TransactionList::const_iterator trans_it = transactions_.begin(); |
| trans_it != transactions_.end(); ++trans_it) { |
| (*trans_it)->response()->set_error(talk_base::HC_NOT_FOUND); |
| http_server_->Respond(*trans_it); |
| } |
| |
| delete http_client_; |
| delete http_server_; |
| delete manifest_; |
| delete local_listener_; |
| delete remote_listener_; |
| } |
| |
| bool |
| FileShareSession::IsComplete() const { |
| return (state_ >= FS_COMPLETE); |
| } |
| |
| bool |
| FileShareSession::IsClosed() const { |
| return is_closed_; |
| } |
| |
| FileShareState |
| FileShareSession::state() const { |
| return state_; |
| } |
| |
| bool |
| FileShareSession::is_sender() const { |
| ASSERT(FS_NONE != state_); |
| return is_sender_; |
| } |
| |
| const buzz::Jid& |
| FileShareSession::jid() const { |
| ASSERT(FS_NONE != state_); |
| return jid_; |
| } |
| |
| const FileShareManifest* |
| FileShareSession::manifest() const { |
| ASSERT(FS_NONE != state_); |
| return manifest_; |
| } |
| |
| const std::string& |
| FileShareSession::local_folder() const { |
| ASSERT(!local_folder_.empty()); |
| return local_folder_; |
| } |
| |
| void |
| FileShareSession::Share(const buzz::Jid& jid, FileShareManifest* manifest) { |
| ASSERT(FS_NONE == state_); |
| ASSERT(NULL != session_); |
| |
| http_server_ = new talk_base::HttpServer; |
| http_server_->SignalHttpRequest.connect(this, |
| &FileShareSession::OnHttpRequest); |
| http_server_->SignalHttpRequestComplete.connect(this, |
| &FileShareSession::OnHttpRequestComplete); |
| http_server_->SignalConnectionClosed.connect(this, |
| &FileShareSession::OnHttpConnectionClosed); |
| |
| FileShareDescription* desc = new FileShareDescription; |
| desc->supports_http = true; |
| desc->manifest = *manifest; |
| GenerateTemporaryPrefix(&desc->source_path); |
| GenerateTemporaryPrefix(&desc->preview_path); |
| session_->Initiate(jid.Str(), NULL, desc); |
| |
| delete manifest; |
| } |
| |
| void |
| FileShareSession::Accept() { |
| ASSERT(FS_OFFER == state_); |
| ASSERT(NULL != session_); |
| ASSERT(NULL != manifest_); |
| |
| ASSERT(!http_client_); |
| ASSERT(item_transferring_ == 0); |
| http_client_ = new talk_base::HttpClient(user_agent_, |
| &pool_); |
| http_client_->SignalHttpClientComplete.connect(this, |
| &FileShareSession::OnHttpClientComplete); |
| http_client_->SignalHttpClientClosed.connect(this, |
| &FileShareSession::OnHttpClientClosed); |
| |
| // The receiver now has a need for the http_server_, when previewing already |
| // downloaded content. |
| http_server_ = new talk_base::HttpServer; |
| http_server_->SignalHttpRequest.connect(this, |
| &FileShareSession::OnHttpRequest); |
| http_server_->SignalHttpRequestComplete.connect(this, |
| &FileShareSession::OnHttpRequestComplete); |
| http_server_->SignalConnectionClosed.connect(this, |
| &FileShareSession::OnHttpConnectionClosed); |
| |
| FileShareDescription* desc = new FileShareDescription; |
| desc->supports_http = description()->supports_http; |
| session_->Accept(desc); |
| |
| SetState(FS_TRANSFER, false); |
| NextDownload(); |
| } |
| |
| void |
| FileShareSession::Decline() { |
| ASSERT(FS_OFFER == state_); |
| ASSERT(NULL != session_); |
| local_cancel_ = true; |
| session_->Reject(); |
| } |
| |
| void |
| FileShareSession::Cancel() { |
| ASSERT(!IsComplete()); |
| ASSERT(NULL != session_); |
| local_cancel_ = true; |
| session_->Terminate(); |
| } |
| |
| bool |
| FileShareSession::GetItemUrl(size_t index, std::string* url) { |
| return GetItemBaseUrl(index, false, url); |
| } |
| |
| bool FileShareSession::GetImagePreviewUrl(size_t index, size_t width, |
| size_t height, std::string* url) { |
| if (!GetItemBaseUrl(index, true, url)) |
| return false; |
| |
| if (FileShareManifest::T_IMAGE != manifest_->item(index).type) { |
| ASSERT(false); |
| return false; |
| } |
| |
| char query[256]; |
| talk_base::sprintfn(query, ARRAY_SIZE(query), "?width=%u&height=%u", |
| width, height); |
| url->append(query); |
| return true; |
| } |
| |
| void FileShareSession::ResampleComplete(talk_base::StreamInterface *i, talk_base::HttpTransaction *trans, bool success) { |
| bool found = false; |
| for (TransactionList::const_iterator trans_it = transactions_.begin(); |
| trans_it != transactions_.end(); ++trans_it) { |
| if (*trans_it == trans) { |
| found = true; |
| break; |
| } |
| } |
| |
| if (!found) |
| return; |
| |
| transactions_.remove(trans); |
| |
| if (success) { |
| trans->response()->set_success(MIME_OCTET_STREAM, i); |
| http_server_->Respond(trans); |
| |
| } |
| trans->response()->set_error(talk_base::HC_NOT_FOUND); |
| http_server_->Respond(trans); |
| } |
| |
| bool FileShareSession::GetProgress(size_t& bytes) const { |
| bool known = true; |
| bytes = bytes_transferred_; |
| if (counter_) { |
| size_t current_size = manifest_->item(item_transferring_).size; |
| size_t current_pos = counter_->GetByteCount(); |
| if (current_size == FileShareManifest::SIZE_UNKNOWN) { |
| known = false; |
| } else if (current_pos > current_size) { |
| // Don't allow the size of a 'known' item to be reported as larger than |
| // it claimed to be. |
| ASSERT(false); |
| current_pos = current_size; |
| } |
| bytes += current_pos; |
| } |
| return known; |
| } |
| |
| bool FileShareSession::GetTotalSize(size_t& bytes) const { |
| bool known = true; |
| bytes = 0; |
| for (size_t i=0; i<manifest_->size(); ++i) { |
| if (manifest_->item(i).size == FileShareManifest::SIZE_UNKNOWN) { |
| // We make files of unknown length worth a single byte. |
| known = false; |
| bytes += 1; |
| } else { |
| bytes += manifest_->item(i).size; |
| } |
| } |
| return known; |
| } |
| |
| bool FileShareSession::GetCurrentItemName(std::string* name) { |
| if (FS_TRANSFER != state_) { |
| name->clear(); |
| return false; |
| } |
| ASSERT(item_transferring_ < manifest_->size()); |
| if (transfer_name_.empty()) { |
| const FileShareManifest::Item& item = manifest_->item(item_transferring_); |
| *name = item.name; |
| } else { |
| *name = transfer_name_; |
| } |
| return !name->empty(); |
| } |
| |
| // StreamPool Implementation |
| |
| talk_base::StreamInterface* FileShareSession::RequestConnectedStream( |
| const talk_base::SocketAddress& remote, int* err) { |
| ASSERT(remote.IPAsString() == jid_.Str()); |
| ASSERT(!IsClosed()); |
| ASSERT(NULL != session_); |
| if (!session_) { |
| if (err) |
| *err = -1; |
| return NULL; |
| } |
| |
| char channel_name[64]; |
| talk_base::sprintfn(channel_name, ARRAY_SIZE(channel_name), |
| "private-%u", next_channel_id_++); |
| if (err) |
| *err = 0; |
| return CreateChannel(channel_name); |
| } |
| |
| void FileShareSession::ReturnConnectedStream( |
| talk_base::StreamInterface* stream) { |
| talk_base::Thread::Current()->Dispose(stream); |
| } |
| |
| // MessageHandler Implementation |
| |
| void FileShareSession::OnMessage(talk_base::Message* msg) { |
| if (MSG_PROXY_WAIT == msg->message_id) { |
| LOG_F(LS_INFO) << "MSG_PROXY_WAIT"; |
| if (proxies_.empty() && IsComplete() && !IsClosed()) { |
| DoClose(true); |
| } |
| } |
| } |
| |
| // Session Signals |
| |
| void FileShareSession::OnSessionState(cricket::Session* session, |
| cricket::Session::State state) { |
| // Once we are complete, state changes are meaningless. |
| if (!IsComplete()) { |
| switch (state) { |
| case cricket::Session::STATE_SENTINITIATE: |
| case cricket::Session::STATE_RECEIVEDINITIATE: |
| OnInitiate(); |
| break; |
| case cricket::Session::STATE_SENTACCEPT: |
| case cricket::Session::STATE_RECEIVEDACCEPT: |
| case cricket::Session::STATE_INPROGRESS: |
| SetState(FS_TRANSFER, false); |
| break; |
| case cricket::Session::STATE_SENTREJECT: |
| case cricket::Session::STATE_SENTTERMINATE: |
| case cricket::Session::STATE_DEINIT: |
| if (local_cancel_) { |
| SetState(FS_LOCAL_CANCEL, false); |
| } else { |
| SetState(FS_REMOTE_CANCEL, false); |
| } |
| break; |
| case cricket::Session::STATE_RECEIVEDTERMINATE: |
| if (is_sender()) { |
| // If we are the sender, and the receiver downloaded the correct number |
| // of bytes, then we assume the transfer was successful. We've |
| // introduced support for explicit completion notification |
| // (QN_SHARE_COMPLETE), but it's not mandatory at this point, so we need |
| // this as a fallback. |
| size_t total_bytes; |
| GetTotalSize(total_bytes); |
| if (bytes_transferred_ >= total_bytes) { |
| SetState(FS_COMPLETE, false); |
| break; |
| } |
| } |
| // Fall through |
| case cricket::Session::STATE_RECEIVEDREJECT: |
| SetState(FS_REMOTE_CANCEL, false); |
| break; |
| case cricket::Session::STATE_INIT: |
| case cricket::Session::STATE_SENTMODIFY: |
| case cricket::Session::STATE_RECEIVEDMODIFY: |
| case cricket::Session::STATE_SENTREDIRECT: |
| default: |
| // These states should not occur. |
| ASSERT(false); |
| break; |
| } |
| } |
| |
| if (state == cricket::Session::STATE_DEINIT) { |
| if (!IsClosed()) { |
| DoClose(false); |
| } |
| session_ = NULL; |
| } |
| } |
| |
| void FileShareSession::OnSessionInfoMessage(cricket::Session* session, |
| const cricket::Session::XmlElements& els) { |
| if (IsClosed()) |
| return; |
| ASSERT(NULL != session_); |
| for (size_t i=0; i<els.size(); ++i) { |
| if (is_sender() && (els[i]->Name() == QN_SHARE_CHANNEL)) { |
| if (els[i]->HasAttr(buzz::QN_NAME)) { |
| cricket::PseudoTcpChannel* channel = |
| new cricket::PseudoTcpChannel(talk_base::Thread::Current(), session_); |
| VERIFY(channel->Connect(els[i]->Attr(buzz::QN_NAME))); |
| talk_base::StreamInterface* stream = channel->GetStream(); |
| http_server_->HandleConnection(stream); |
| } |
| } else if (is_sender() && (els[i]->Name() == QN_SHARE_COMPLETE)) { |
| // Normal file transfer has completed, but receiver may still be getting |
| // previews. |
| if (!IsComplete()) { |
| SetState(FS_COMPLETE, true); |
| } |
| } else { |
| LOG(LS_WARNING) << "Unknown FileShareSession info message: " |
| << els[i]->Name().Merged(); |
| } |
| } |
| } |
| |
| void FileShareSession::OnSessionChannelGone(cricket::Session* session, |
| const std::string& name) { |
| LOG_F(LS_WARNING) << "(" << name << ")"; |
| ASSERT(session == session_); |
| if (cricket::TransportChannel* channel = session->GetChannel(name)) { |
| session->DestroyChannel(channel); |
| } |
| } |
| |
| // HttpClient Signals |
| |
| void FileShareSession::OnHttpClientComplete(talk_base::HttpClient* http, |
| int err) { |
| LOG_F(LS_INFO) << "(" << err << ", " << http->response().scode << ")"; |
| ASSERT(http == http_client_); |
| ASSERT(NULL != session_); |
| |
| transfer_name_.clear(); |
| counter_ = NULL; // counter_ is deleted by HttpClient |
| http->response().document.reset(); |
| bool success = (err == 0) && (http->response().scode == talk_base::HC_OK); |
| |
| const FileShareManifest::Item& item = manifest_->item(item_transferring_); |
| talk_base::Pathname local_name; |
| local_name.SetFilename(item.name); |
| local_name.SetFolder(local_folder_); |
| |
| if (local_name.pathname() != transfer_path_) { |
| const bool is_folder = (item.type == FileShareManifest::T_FOLDER); |
| if (success && !talk_base::CreateUniqueFile(local_name, false)) { |
| LOG(LS_ERROR) << "Couldn't rename downloaded file: " |
| << local_name.pathname(); |
| success = false; |
| } |
| |
| talk_base::Pathname temp_name(transfer_path_); |
| if (is_folder) { |
| // The folder we want is a subdirectory of the transfer_path_. |
| temp_name.AppendFolder(item.name); |
| } |
| |
| if (!talk_base::Filesystem::MoveFile(temp_name.pathname(), local_name.pathname())) { |
| success = false; |
| LOG(LS_ERROR) << "Couldn't move downloaded file from '" |
| << temp_name.pathname() << "' to '" |
| << local_name.pathname(); |
| } |
| |
| if (success && is_folder) { |
| talk_base::Filesystem::DeleteFile(transfer_path_); |
| } |
| } |
| |
| if (!success) { |
| if (!talk_base::Filesystem::DeleteFile(transfer_path_)) { |
| LOG(LS_ERROR) << "Couldn't delete downloaded file: " << transfer_path_; |
| } |
| if (!IsComplete()) { |
| SetState(FS_FAILURE, false); |
| } |
| return; |
| } |
| |
| // We may have skipped over some items (if they are directories, or otherwise |
| // failed. resize ensures that we populate the skipped entries with empty |
| // strings. |
| stored_location_.resize(item_transferring_ + 1); |
| stored_location_[item_transferring_] = local_name.pathname(); |
| |
| // bytes_transferred_ represents the size of items which have completely |
| // transferred, and is added to the progress of the currently transferring |
| // items. |
| if (item.size == FileShareManifest::SIZE_UNKNOWN) { |
| bytes_transferred_ += 1; |
| } else { |
| bytes_transferred_ += item.size; |
| } |
| item_transferring_ += 1; |
| NextDownload(); |
| } |
| |
| void FileShareSession::OnHttpClientClosed(talk_base::HttpClient* http, |
| int err) { |
| LOG_F(LS_INFO) << "(" << err << ")"; |
| } |
| |
| // HttpServer Signals |
| |
| void FileShareSession::OnHttpRequest(talk_base::HttpServer* server, |
| talk_base::HttpTransaction* transaction) { |
| LOG_F(LS_INFO) << "(" << transaction->request()->path << ")"; |
| ASSERT(server == http_server_); |
| |
| std::string path, query; |
| size_t query_start = transaction->request()->path.find('?'); |
| if (query_start != std::string::npos) { |
| path = transaction->request()->path.substr(0, query_start); |
| query = transaction->request()->path.substr(query_start + 1); |
| } else { |
| path = transaction->request()->path; |
| } |
| |
| talk_base::Pathname remote_name(path); |
| bool preview = (preview_path_ == remote_name.folder()); |
| bool original = (source_path_ == remote_name.folder()); |
| |
| std::string requested_file(remote_name.filename()); |
| talk_base::transform(requested_file, requested_file.size(), requested_file, |
| talk_base::url_decode); |
| |
| size_t item_index; |
| const FileShareManifest::Item* item = NULL; |
| if (preview || original) { |
| for (size_t i=0; i<manifest_->size(); ++i) { |
| LOG(LS_INFO) << "++++ " << manifest_->item(i).name + " " << requested_file; |
| if (manifest_->item(i).name == requested_file) { |
| item_index = i; |
| item = &manifest_->item(item_index); |
| break; |
| } |
| } |
| } |
| |
| talk_base::StreamInterface* stream = NULL; |
| std::string mime_type(MIME_OCTET_STREAM); |
| |
| if (!item) { |
| // Fall through |
| } else if (preview) { |
| // Only image previews allowed |
| unsigned int width = 0, height = 0; |
| if ((item->type == FileShareManifest::T_IMAGE) |
| && !query.empty() |
| && (sscanf(query.c_str(), "width=%u&height=%u", |
| &width, &height) == 2)) { |
| width = talk_base::_max<unsigned int>(1, talk_base::_min(width, kMaxPreviewSize)); |
| height = talk_base::_max<unsigned int>(1, talk_base::_min(height, kMaxPreviewSize)); |
| std::string pathname; |
| if (is_sender_) { |
| talk_base::Pathname local_path; |
| local_path.SetFolder(local_folder_); |
| local_path.SetFilename(item->name); |
| pathname = local_path.pathname(); |
| } else if ((item_index < stored_location_.size()) |
| && !stored_location_[item_index].empty()) { |
| pathname = stored_location_[item_index]; |
| } |
| if (!pathname.empty()) { |
| transactions_.push_back(transaction); |
| SignalResampleImage(pathname, width, height, transaction); |
| } |
| } |
| } else if (item->type == FileShareManifest::T_FOLDER) { |
| talk_base::Pathname local_path; |
| local_path.SetFolder(local_folder_); |
| local_path.AppendFolder(item->name); |
| talk_base::TarStream* tar = new talk_base::TarStream; |
| VERIFY(tar->AddFilter(local_path.folder_name())); |
| if (tar->Open(local_path.parent_folder(), true)) { |
| stream = tar; |
| tar->SignalNextEntry.connect(this, &FileShareSession::OnNextEntry); |
| mime_type = "application/x-tar"; |
| } else { |
| delete tar; |
| } |
| } else if ((item->type == FileShareManifest::T_FILE) |
| || (item->type == FileShareManifest::T_IMAGE)) { |
| talk_base::Pathname local_path; |
| local_path.SetFolder(local_folder_); |
| local_path.SetFilename(item->name); |
| talk_base::FileStream* file = new talk_base::FileStream; |
| LOG(LS_INFO) << "opening file " << local_path.pathname(); |
| if (file->Open(local_path.pathname().c_str(), "rb")) { |
| LOG(LS_INFO) << "File opened"; |
| stream = file; |
| } else { |
| delete file; |
| } |
| } |
| |
| if (!stream) { |
| transaction->response()->set_error(talk_base::HC_NOT_FOUND); |
| } else if (original) { |
| // We should never have more than one original request pending at a time |
| ASSERT(NULL == counter_); |
| StreamCounter* counter = new StreamCounter(stream); |
| counter->SignalUpdateByteCount.connect(this, &FileShareSession::OnUpdateBytes); |
| transaction->response()->set_success(mime_type.c_str(), counter); |
| transfer_connection_id_ = transaction->connection_id(); |
| item_transferring_ = item_index; |
| counter_ = counter; |
| } else { |
| // Note: in the preview case, we don't set counter_, so the transferred |
| // bytes won't be shown as progress, and won't trigger a state change. |
| transaction->response()->set_success(mime_type.c_str(), stream); |
| } |
| |
| LOG_F(LS_INFO) << "Result: " << transaction->response()->scode; |
| http_server_->Respond(transaction); |
| } |
| |
| void FileShareSession::OnHttpRequestComplete(talk_base::HttpServer* server, |
| talk_base::HttpTransaction* transaction, int err) { |
| LOG_F(LS_INFO) << "(" << transaction->request()->path << ", " << err << ")"; |
| ASSERT(server == http_server_); |
| |
| // We only care about transferred originals |
| if (transfer_connection_id_ != transaction->connection_id()) |
| return; |
| |
| ASSERT(item_transferring_ < manifest_->size()); |
| ASSERT(NULL != counter_); |
| |
| transfer_connection_id_ = talk_base::HTTP_INVALID_CONNECTION_ID; |
| transfer_name_.clear(); |
| counter_ = NULL; |
| |
| if (err == 0) { |
| const FileShareManifest::Item& item = manifest_->item(item_transferring_); |
| if (item.size == FileShareManifest::SIZE_UNKNOWN) { |
| bytes_transferred_ += 1; |
| } else { |
| bytes_transferred_ += item.size; |
| } |
| } |
| } |
| |
| void FileShareSession::OnHttpConnectionClosed(talk_base::HttpServer* server, |
| int err, talk_base::StreamInterface* stream) { |
| LOG_F(LS_INFO) << "(" << err << ")"; |
| talk_base::Thread::Current()->Dispose(stream); |
| } |
| |
| // TarStream Signals |
| |
| void FileShareSession::OnNextEntry(const std::string& name, size_t size) { |
| LOG_F(LS_VERBOSE) << "(" << name << ", " << size << ")"; |
| transfer_name_ = name; |
| SignalNextFile(this); |
| } |
| |
| // Socket Signals |
| |
| void FileShareSession::OnProxyAccept(talk_base::AsyncSocket* socket) { |
| bool is_remote; |
| if (socket == remote_listener_) { |
| is_remote = true; |
| ASSERT(NULL != session_); |
| } else if (socket == local_listener_) { |
| is_remote = false; |
| } else { |
| ASSERT(false); |
| return; |
| } |
| |
| while (talk_base::AsyncSocket* accepted = |
| static_cast<talk_base::AsyncSocket*>(socket->Accept(NULL))) { |
| |
| // Check if connection is from localhost. |
| if (accepted->GetRemoteAddress().ip() != 0x7F000001) { |
| delete accepted; |
| continue; |
| } |
| |
| LOG_F(LS_VERBOSE) << (is_remote ? "[remote]" : "[local]"); |
| |
| if (is_remote) { |
| char channel_name[64]; |
| talk_base::sprintfn(channel_name, ARRAY_SIZE(channel_name), |
| "proxy-%u", next_channel_id_++); |
| talk_base::StreamInterface* remote = |
| (NULL != session_) ? CreateChannel(channel_name) : NULL; |
| if (!remote) { |
| LOG_F(LS_WARNING) << "CreateChannel(" << channel_name << ") failed"; |
| delete accepted; |
| continue; |
| } |
| |
| talk_base::StreamInterface* local = new talk_base::SocketStream(accepted); |
| StreamRelay* proxy = new StreamRelay(local, remote, 64 * 1024); |
| proxy->SignalClosed.connect(this, &FileShareSession::OnProxyClosed); |
| proxies_.push_back(proxy); |
| proxy->Circulate(); |
| talk_base::Thread::Current()->Clear(this, MSG_PROXY_WAIT); |
| } else { |
| talk_base::StreamInterface* local = new talk_base::SocketStream(accepted); |
| http_server_->HandleConnection(local); |
| } |
| } |
| } |
| |
| void FileShareSession::OnProxyClosed(StreamRelay* proxy, int error) { |
| ProxyList::iterator it = std::find(proxies_.begin(), proxies_.end(), proxy); |
| if (it == proxies_.end()) { |
| ASSERT(false); |
| return; |
| } |
| |
| LOG_F(LS_VERBOSE) << "(" << error << ")"; |
| |
| proxies_.erase(it); |
| talk_base::Thread::Current()->Dispose(proxy); |
| |
| if (proxies_.empty() && IsComplete() && !IsClosed()) { |
| talk_base::Thread::Current()->PostDelayed(kProxyWait, this, MSG_PROXY_WAIT); |
| } |
| } |
| |
| |
| void FileShareSession::OnUpdateBytes(size_t count) { |
| SignalUpdateProgress(this); |
| } |
| |
| // Internal Helpers |
| |
| void FileShareSession::GenerateTemporaryPrefix(std::string* prefix) { |
| std::string data = cricket::CreateRandomString(32); |
| ASSERT(NULL != prefix); |
| prefix->assign("/temporary/"); |
| prefix->append(talk_base::MD5(data)); |
| prefix->append("/"); |
| } |
| |
| void FileShareSession::GetItemNetworkPath(size_t index, bool preview, |
| std::string* path) { |
| ASSERT(index < manifest_->size()); |
| ASSERT(NULL != path); |
| |
| // preview_path_ and source_path_ are url path segments, which are composed |
| // with the address of the localhost p2p proxy to provide a url which IE can |
| // use. |
| |
| std::string ue_name; |
| const std::string& name = manifest_->item(index).name; |
| talk_base::transform(ue_name, name.length() * 3, name, talk_base::url_encode); |
| |
| talk_base::Pathname pathname; |
| pathname.SetFolder(preview ? preview_path_ : source_path_); |
| pathname.SetFilename(ue_name); |
| *path = pathname.pathname(); |
| } |
| |
| bool FileShareSession::GetItemBaseUrl(size_t index, bool preview, |
| std::string* url) { |
| // This function composes a URL to the referenced item. It may be a local |
| // file url (file:///...), or a remote peer url relayed through localhost |
| // (http://...) |
| |
| ASSERT(NULL != url); |
| if (index >= manifest_->size()) { |
| ASSERT(false); |
| return false; |
| } |
| |
| const FileShareManifest::Item& item = manifest_->item(index); |
| |
| bool is_remote; |
| if (is_sender_) { |
| if (!preview) { |
| talk_base::Pathname path(local_folder_); |
| path.SetFilename(item.name); |
| *url = path.url(); |
| return true; |
| } |
| is_remote = false; |
| } else { |
| if ((index < stored_location_.size()) && !stored_location_[index].empty()) { |
| if (!preview) { |
| *url = talk_base::Pathname(stored_location_[index]).url(); |
| return true; |
| } |
| // Note: Using the local downloaded files as a source for previews is |
| // desireable, because it means that previews can be regenerated if IE's |
| // cached versions get flushed for some reason, and the remote side is |
| // not available. However, it has the downside that IE _must_ regenerate |
| // the preview locally, which takes time, memory and CPU. Eventually, |
| // we will unify the remote and local cached copy through some sort of |
| // smart http proxying. In the meantime, always use the remote url, to |
| // eliminate the annoying transition from remote to local caching. |
| //is_remote = false; |
| is_remote = true; |
| } else { |
| is_remote = true; |
| } |
| } |
| |
| talk_base::SocketAddress address; |
| if (!GetProxyAddress(address, is_remote)) |
| return false; |
| |
| std::string path; |
| GetItemNetworkPath(index, preview, &path); |
| talk_base::Url<char> make_url(path.c_str(), |
| address.IPAsString().c_str(), |
| address.port()); |
| *url = make_url.url(); |
| return true; |
| } |
| |
| bool FileShareSession::GetProxyAddress(talk_base::SocketAddress& address, |
| bool is_remote) { |
| talk_base::AsyncSocket*& proxy_listener = |
| is_remote ? remote_listener_ : local_listener_; |
| |
| if (!proxy_listener) { |
| talk_base::AsyncSocket* listener = |
| talk_base::Thread::Current()->socketserver() |
| ->CreateAsyncSocket(SOCK_STREAM); |
| if (!listener) |
| return false; |
| |
| talk_base::SocketAddress bind_address("127.0.0.1", 0); |
| |
| if ((listener->Bind(bind_address) != 0) |
| || (listener->Listen(5) != 0)) { |
| delete listener; |
| return false; |
| } |
| |
| LOG(LS_INFO) << "Proxy listener available @ " |
| << listener->GetLocalAddress().ToString(); |
| |
| listener->SignalReadEvent.connect(this, &FileShareSession::OnProxyAccept); |
| proxy_listener = listener; |
| } |
| |
| if (proxy_listener->GetState() == talk_base::Socket::CS_CLOSED) { |
| if (is_remote) { |
| address = remote_listener_address_; |
| return true; |
| } |
| return false; |
| } |
| |
| address = proxy_listener->GetLocalAddress(); |
| return !address.IsAny(); |
| } |
| |
| talk_base::StreamInterface* FileShareSession::CreateChannel( |
| const std::string& channel_name) { |
| ASSERT(NULL != session_); |
| |
| // Send a heads-up for our new channel |
| cricket::Session::XmlElements els; |
| buzz::XmlElement* xel_channel = new buzz::XmlElement(QN_SHARE_CHANNEL, true); |
| xel_channel->AddAttr(buzz::QN_NAME, channel_name); |
| els.push_back(xel_channel); |
| session_->SendInfoMessage(els); |
| |
| cricket::PseudoTcpChannel* channel = |
| new cricket::PseudoTcpChannel(talk_base::Thread::Current(), session_); |
| VERIFY(channel->Connect(channel_name)); |
| return channel->GetStream(); |
| } |
| |
| void FileShareSession::SetState(FileShareState state, bool prevent_close) { |
| if (state == state_) |
| return; |
| |
| if (IsComplete()) { |
| // Entering a completion state is permanent. |
| ASSERT(false); |
| return; |
| } |
| |
| state_ = state; |
| if (IsComplete()) { |
| // All completion states auto-close except for FS_COMPLETE |
| bool close = (state_ > FS_COMPLETE) || !prevent_close; |
| if (close) { |
| DoClose(true); |
| } |
| } |
| |
| SignalState(state_); |
| } |
| |
| void FileShareSession::OnInitiate() { |
| // Cache the variables we will need, in case session_ goes away |
| is_sender_ = session_->initiator(); |
| jid_ = buzz::Jid(session_->remote_name()); |
| manifest_ = new FileShareManifest(description()->manifest); |
| source_path_ = description()->source_path; |
| preview_path_ = description()->preview_path; |
| |
| if (local_folder_.empty()) { |
| LOG(LS_ERROR) << "FileShareSession - no local folder, using temp"; |
| talk_base::Pathname temp_folder; |
| talk_base::Filesystem::GetTemporaryFolder(temp_folder, true, NULL); |
| local_folder_ = temp_folder.pathname(); |
| } |
| LOG(LS_INFO) << session_->state(); |
| SetState(FS_OFFER, false); |
| } |
| |
| void FileShareSession::NextDownload() { |
| if (FS_TRANSFER != state_) |
| return; |
| |
| if (item_transferring_ >= manifest_->size()) { |
| // Notify the other side that transfer has completed |
| cricket::Session::XmlElements els; |
| els.push_back(new buzz::XmlElement(QN_SHARE_COMPLETE, true)); |
| session_->SendInfoMessage(els); |
| SetState(FS_COMPLETE, !proxies_.empty()); |
| return; |
| } |
| |
| const FileShareManifest::Item& item = manifest_->item(item_transferring_); |
| if ((item.type != FileShareManifest::T_FILE) |
| && (item.type != FileShareManifest::T_IMAGE) |
| && (item.type != FileShareManifest::T_FOLDER)) { |
| item_transferring_ += 1; |
| NextDownload(); |
| return; |
| } |
| |
| const bool is_folder = (item.type == FileShareManifest::T_FOLDER); |
| talk_base::Pathname temp_name; |
| temp_name.SetFilename(item.name); |
| if (!talk_base::CreateUniqueFile(temp_name, !is_folder)) { |
| SetState(FS_FAILURE, false); |
| return; |
| } |
| |
| talk_base::StreamInterface* stream = NULL; |
| if (is_folder) { |
| // Convert unique filename into unique foldername |
| temp_name.AppendFolder(temp_name.filename()); |
| temp_name.SetFilename(""); |
| talk_base::TarStream* tar = new talk_base::TarStream; |
| // Note: the 'target' directory will be a subdirectory of the transfer_path_ |
| talk_base::Pathname target; |
| target.SetFolder(item.name); |
| tar->AddFilter(target.pathname()); |
| if (!tar->Open(temp_name.pathname(), false)) { |
| delete tar; |
| SetState(FS_FAILURE, false); |
| return; |
| } |
| stream = tar; |
| tar->SignalNextEntry.connect(this, &FileShareSession::OnNextEntry); |
| } else { |
| talk_base::FileStream* file = new talk_base::FileStream; |
| if (!file->Open(temp_name.pathname().c_str(), "wb")) { |
| delete file; |
| talk_base::Filesystem::DeleteFile(temp_name); |
| SetState(FS_FAILURE, false); |
| return; |
| } |
| stream = file; |
| } |
| |
| ASSERT(NULL != stream); |
| transfer_path_ = temp_name.pathname(); |
| |
| std::string remote_path; |
| GetItemNetworkPath(item_transferring_, false, &remote_path); |
| |
| StreamCounter* counter = new StreamCounter(stream); |
| counter->SignalUpdateByteCount.connect(this, &FileShareSession::OnUpdateBytes); |
| counter_ = counter; |
| |
| http_client_->reset(); |
| http_client_->set_server(talk_base::SocketAddress(jid_.Str(), 0, false)); |
| http_client_->request().verb = talk_base::HV_GET; |
| http_client_->request().path = remote_path; |
| http_client_->response().document.reset(counter); |
| http_client_->start(); |
| } |
| |
| |
| const FileShareSession::FileShareDescription* FileShareSession::description() |
| const { |
| ASSERT(NULL != session_); |
| const cricket::SessionDescription* desc = |
| session_->initiator() ? session_->description() |
| : session_->remote_description(); |
| return static_cast<const FileShareDescription*>(desc); |
| } |
| |
| void FileShareSession::DoClose(bool terminate) { |
| ASSERT(!is_closed_); |
| ASSERT(IsComplete()); |
| ASSERT(NULL != session_); |
| |
| is_closed_ = true; |
| |
| if (http_client_) { |
| http_client_->reset(); |
| } |
| if (http_server_) { |
| http_server_->CloseAll(true); |
| // Currently, CloseAll doesn't result in OnHttpRequestComplete callback. |
| // If we change that, the following resetting won't be necessary. |
| transfer_connection_id_ = talk_base::HTTP_INVALID_CONNECTION_ID; |
| transfer_name_.clear(); |
| counter_ = NULL; |
| } |
| // 'reset' and 'CloseAll' cause counter_ to clear. |
| ASSERT(NULL == counter_); |
| |
| if (remote_listener_) { |
| // Cache the address for the remote_listener_, so that we can continue to |
| // present a consistent URL for remote previews, which is necessary for IE |
| // to continue using its cached copy. |
| remote_listener_address_ = remote_listener_->GetLocalAddress(); |
| remote_listener_->Close(); |
| LOG(LS_INFO) << "Proxy listener closed @ " |
| << remote_listener_address_.ToString(); |
| } |
| |
| if (terminate) { |
| session_->Terminate(); |
| } |
| } |
| |
| ////////////////////////////// |
| /// FileShareSessionClient // |
| //////////////////////////// |
| |
| void FileShareSessionClient::OnSessionCreate(cricket::Session* session, |
| bool received_initiate) { |
| VERIFY(sessions_.insert(session).second); |
| if (received_initiate) { |
| FileShareSession* share = new FileShareSession(session, user_agent_); |
| SignalFileShareSessionCreate(share); |
| UNUSED(share); // FileShareSession registers itself with the UI |
| } |
| } |
| |
| void FileShareSessionClient::OnSessionDestroy(cricket::Session* session) { |
| VERIFY(1 == sessions_.erase(session)); |
| } |
| |
| const cricket::SessionDescription* FileShareSessionClient::CreateSessionDescription( |
| const buzz::XmlElement* element) { |
| FileShareSession::FileShareDescription* share_desc = |
| new FileShareSession::FileShareDescription; |
| |
| if (element->Name() != QN_SHARE_DESCRIPTION) |
| return share_desc; |
| |
| const buzz::XmlElement* manifest = element->FirstNamed(QN_SHARE_MANIFEST); |
| const buzz::XmlElement* protocol = element->FirstNamed(QN_SHARE_PROTOCOL); |
| |
| if (!manifest || !protocol) |
| return share_desc; |
| |
| for (const buzz::XmlElement* item = manifest->FirstElement(); |
| item != NULL; item = item->NextElement()) { |
| bool is_folder; |
| if (item->Name() == QN_SHARE_FOLDER) { |
| is_folder = true; |
| } else if (item->Name() == QN_SHARE_FILE) { |
| is_folder = false; |
| } else { |
| continue; |
| } |
| std::string name; |
| if (const buzz::XmlElement* el_name = item->FirstNamed(QN_SHARE_NAME)) { |
| name = el_name->BodyText(); |
| } |
| if (name.empty()) { |
| continue; |
| } |
| size_t size = FileShareManifest::SIZE_UNKNOWN; |
| if (item->HasAttr(QN_SIZE)) { |
| size = strtoul(item->Attr(QN_SIZE).c_str(), NULL, 10); |
| } |
| if (is_folder) { |
| share_desc->manifest.AddFolder(name, size); |
| } else { |
| // Check if there is a valid image description for this file. |
| if (const buzz::XmlElement* image = item->FirstNamed(QN_SHARE_IMAGE)) { |
| if (image->HasAttr(QN_WIDTH) && image->HasAttr(QN_HEIGHT)) { |
| size_t width = strtoul(image->Attr(QN_WIDTH).c_str(), NULL, 10); |
| size_t height = strtoul(image->Attr(QN_HEIGHT).c_str(), NULL, 10); |
| if (AllowedImageDimensions(width, height)) { |
| share_desc->manifest.AddImage(name, size, width, height); |
| continue; |
| } |
| } |
| } |
| share_desc->manifest.AddFile(name, size); |
| } |
| } |
| |
| if (const buzz::XmlElement* http = protocol->FirstNamed(QN_SHARE_HTTP)) { |
| share_desc->supports_http = true; |
| for (const buzz::XmlElement* url = http->FirstNamed(QN_SHARE_URL); |
| url != NULL; url = url->NextNamed(QN_SHARE_URL)) { |
| if (url->Attr(buzz::QN_NAME) == kHttpSourcePath) { |
| share_desc->source_path = url->BodyText(); |
| } else if (url->Attr(buzz::QN_NAME) == kHttpPreviewPath) { |
| share_desc->preview_path = url->BodyText(); |
| } |
| } |
| } |
| |
| return share_desc; |
| } |
| |
| buzz::XmlElement* FileShareSessionClient::TranslateSessionDescription( |
| const cricket::SessionDescription* description) { |
| |
| const FileShareSession::FileShareDescription* share_desc = |
| static_cast<const FileShareSession::FileShareDescription*>(description); |
| |
| scoped_ptr<buzz::XmlElement> el(new buzz::XmlElement(QN_SHARE_DESCRIPTION, |
| true)); |
| |
| const FileShareManifest& manifest = share_desc->manifest; |
| el->AddElement(new buzz::XmlElement(QN_SHARE_MANIFEST)); |
| for (size_t i=0; i<manifest.size(); ++i) { |
| const FileShareManifest::Item& item = manifest.item(i); |
| buzz::QName qname; |
| if (item.type == FileShareManifest::T_FOLDER) { |
| qname = QN_SHARE_FOLDER; |
| } else if ((item.type == FileShareManifest::T_FILE) |
| || (item.type == FileShareManifest::T_IMAGE)) { |
| qname = QN_SHARE_FILE; |
| } else { |
| ASSERT(false); |
| continue; |
| } |
| el->AddElement(new buzz::XmlElement(qname), 1); |
| if (item.size != FileShareManifest::SIZE_UNKNOWN) { |
| char buffer[256]; |
| talk_base::sprintfn(buffer, sizeof(buffer), "%lu", item.size); |
| el->AddAttr(QN_SIZE, buffer, 2); |
| } |
| buzz::XmlElement* el_name = new buzz::XmlElement(QN_SHARE_NAME); |
| el_name->SetBodyText(item.name); |
| el->AddElement(el_name, 2); |
| if ((item.type == FileShareManifest::T_IMAGE) |
| && AllowedImageDimensions(item.width, item.height)) { |
| el->AddElement(new buzz::XmlElement(QN_SHARE_IMAGE), 2); |
| char buffer[256]; |
| talk_base::sprintfn(buffer, sizeof(buffer), "%lu", item.width); |
| el->AddAttr(QN_WIDTH, buffer, 3); |
| talk_base::sprintfn(buffer, sizeof(buffer), "%lu", item.height); |
| el->AddAttr(QN_HEIGHT, buffer, 3); |
| } |
| } |
| |
| el->AddElement(new buzz::XmlElement(QN_SHARE_PROTOCOL)); |
| if (share_desc->supports_http) { |
| el->AddElement(new buzz::XmlElement(QN_SHARE_HTTP), 1); |
| if (!share_desc->source_path.empty()) { |
| buzz::XmlElement* url = new buzz::XmlElement(QN_SHARE_URL); |
| url->SetAttr(buzz::QN_NAME, kHttpSourcePath); |
| url->SetBodyText(share_desc->source_path); |
| el->AddElement(url, 2); |
| } |
| if (!share_desc->preview_path.empty()) { |
| buzz::XmlElement* url = new buzz::XmlElement(QN_SHARE_URL); |
| url->SetAttr(buzz::QN_NAME, kHttpPreviewPath); |
| url->SetBodyText(share_desc->preview_path); |
| el->AddElement(url, 2); |
| } |
| } |
| |
| return el.release(); |
| } |
| |
| FileShareSession *FileShareSessionClient::CreateFileShareSession() { |
| cricket::Session* session = sm_->CreateSession(jid_.Str(), |
| NS_GOOGLE_SHARE); |
| FileShareSession* share = new FileShareSession(session, user_agent_); |
| SignalFileShareSessionCreate(share); |
| return share; |
| } |
| |
| |
| } // namespace cricket |